Repository: jlowin/fastmcp
Pull request #290 (2025-04-30 21:30:07)

use pydantic_core.to_json
Analogue of https://github.com/modelcontextprotocol/python-sdk/pull/545


Patch:
diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py
index 0cf3740..806fe63 100644
--- a/src/fastmcp/prompts/prompt.py
+++ b/src/fastmcp/prompts/prompt.py
@@ -3,7 +3,6 @@
 from __future__ import annotations as _annotations
 
 import inspect
-import json
 from collections.abc import Awaitable, Callable, Sequence
 from typing import TYPE_CHECKING, Annotated, Any, Literal
 
@@ -192,7 +191,9 @@ class Prompt(BaseModel):
                         content = TextContent(type="text", text=msg)
                         messages.append(Message(role="user", content=content))
                     else:
-                        content = json.dumps(pydantic_core.to_jsonable_python(msg))
+                        content = pydantic_core.to_json(
+                            msg, fallback=str, indent=2
+                        ).decode()
                         messages.append(Message(role="user", content=content))
                 except Exception:
                     raise ValueError(
diff --git a/src/fastmcp/resources/types.py b/src/fastmcp/resources/types.py
index f7384a8..c6fda51 100644
--- a/src/fastmcp/resources/types.py
+++ b/src/fastmcp/resources/types.py
@@ -97,15 +97,12 @@ class FunctionResource(Resource):
 
             if isinstance(result, Resource):
                 return await result.read(context=context)
-            if isinstance(result, bytes):
+            elif isinstance(result, bytes):
                 return result
-            if isinstance(result, str):
+            elif isinstance(result, str):
                 return result
-            try:
-                return json.dumps(pydantic_core.to_jsonable_python(result))
-            except (TypeError, pydantic_core.PydanticSerializationError):
-                # If JSON serialization fails, try str()
-                return str(result)
+            else:
+                return pydantic_core.to_json(result, fallback=str, indent=2).decode()
         except Exception as e:
             raise ValueError(f"Error reading resource {self.uri}: {e}")
 
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index 65dd33e..d838570 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -14,7 +14,6 @@ from typing import TYPE_CHECKING, Any, Generic, Literal
 
 import anyio
 import httpx
-import pydantic_core
 import uvicorn
 from mcp.server.lowlevel.helper_types import ReadResourceContents
 from mcp.server.lowlevel.server import LifespanResultT
@@ -27,6 +26,7 @@ from mcp.types import (
     EmbeddedResource,
     GetPromptResult,
     ImageContent,
+    PromptMessage,
     TextContent,
 )
 from mcp.types import Prompt as MCPPrompt
@@ -435,7 +435,12 @@ class FastMCP(Generic[LifespanResultT]):
             messages = await self._prompt_manager.render_prompt(
                 name, arguments=arguments or {}, context=context
             )
-            return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
+
+            return GetPromptResult(
+                messages=[
+                    PromptMessage(role=m.role, content=m.content) for m in messages
+                ]
+            )
         else:
             for server in self._mounted_servers.values():
                 if server.match_prompt(name):
diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py
index 6f67e20..bc0b0a3 100644
--- a/src/fastmcp/tools/tool.py
+++ b/src/fastmcp/tools/tool.py
@@ -1,7 +1,6 @@
 from __future__ import annotations
 
 import inspect
-import json
 from collections.abc import Callable
 from typing import TYPE_CHECKING, Annotated, Any
 
@@ -166,23 +165,7 @@ def _convert_to_content(
 
         return other_content + mcp_types
 
-    # if the result is a bytes object, convert it to a text content object
     if not isinstance(result, str):
-        try:
-            jsonable_result = pydantic_core.to_jsonable_python(result)
-            if jsonable_result is None:
-                return [TextContent(type="text", text="null")]
-            elif isinstance(jsonable_result, bool):
-                return [
-                    TextContent(
-                        type="text", text="true" if jsonable_result else "false"
-                    )
-                ]
-            elif isinstance(jsonable_result, str | int | float):
-                return [TextContent(type="text", text=str(jsonable_result))]
-            else:
-                return [TextContent(type="text", text=json.dumps(jsonable_result))]
-        except Exception:
-            result = str(result)
+        result = pydantic_core.to_json(result, fallback=str, indent=2).decode()
 
     return [TextContent(type="text", text=result)]


Test patch:
diff --git a/tests/resources/test_function_resources.py b/tests/resources/test_function_resources.py
index c179fd2..46ac320 100644
--- a/tests/resources/test_function_resources.py
+++ b/tests/resources/test_function_resources.py
@@ -95,7 +95,7 @@ class TestFunctionResource:
             fn=lambda: MyModel(name="test"),
         )
         content = await resource.read()
-        assert content == '{"name": "test"}'
+        assert content == '{\n  "name": "test"\n}'
 
     async def test_custom_type_conversion(self):
         """Test handling of custom types."""
diff --git a/tests/resources/test_resource_template.py b/tests/resources/test_resource_template.py
index 86fd24d..fab5e4b 100644
--- a/tests/resources/test_resource_template.py
+++ b/tests/resources/test_resource_template.py
@@ -295,7 +295,7 @@ class TestResourceTemplate:
 
         assert isinstance(resource, FunctionResource)
         content = await resource.read()
-        assert content == "hello"
+        assert content == '"hello"'
 
     async def test_wildcard_param_can_create_resource(self):
         """Test that wildcard parameters are valid."""
diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py
index 61d9588..dedb9e8 100644
--- a/tests/server/test_mount.py
+++ b/tests/server/test_mount.py
@@ -227,7 +227,7 @@ class TestResourcesAndTemplates:
         async with Client(main_app) as client:
             resource = await client.read_resource("data+data://users")
             assert isinstance(resource[0], TextResourceContents)
-            assert resource[0].text == '["user1", "user2"]'
+            assert resource[0].text == '[\n  "user1",\n  "user2"\n]'
 
     async def test_mount_with_resource_templates(self):
         """Test mounting a server with resource templates."""
diff --git a/tests/server/test_server_interactions.py b/tests/server/test_server_interactions.py
index 1411494..98025f0 100644
--- a/tests/server/test_server_interactions.py
+++ b/tests/server/test_server_interactions.py
@@ -6,6 +6,7 @@ from enum import Enum
 from pathlib import Path
 from typing import Annotated, Literal
 
+import pydantic_core
 import pytest
 from mcp.types import (
     BlobResourceContents,
@@ -104,7 +105,7 @@ class TestTools:
         async with Client(tool_server) as client:
             result = await client.call_tool("list_tool", {})
             assert isinstance(result[0], TextContent)
-            assert result[0].text == '["x", 2]'
+            assert result[0].text == '[\n  "x",\n  2\n]'
 
 
 class TestToolReturnTypes:
@@ -130,7 +131,7 @@ class TestToolReturnTypes:
         async with Client(mcp) as client:
             result = await client.call_tool("bytes_tool", {})
             assert isinstance(result[0], TextContent)
-            assert result[0].text == "Hello, world!"
+            assert result[0].text == '"Hello, world!"'
 
     async def test_uuid(self):
         mcp = FastMCP()
@@ -144,7 +145,7 @@ class TestToolReturnTypes:
         async with Client(mcp) as client:
             result = await client.call_tool("uuid_tool", {})
             assert isinstance(result[0], TextContent)
-            assert result[0].text == str(test_uuid)
+            assert result[0].text == pydantic_core.to_json(test_uuid).decode()
 
     async def test_path(self):
         mcp = FastMCP()
@@ -158,7 +159,7 @@ class TestToolReturnTypes:
         async with Client(mcp) as client:
             result = await client.call_tool("path_tool", {})
             assert isinstance(result[0], TextContent)
-            assert result[0].text == str(test_path)
+            assert result[0].text == pydantic_core.to_json(test_path).decode()
 
     async def test_datetime(self):
         mcp = FastMCP()
@@ -172,7 +173,7 @@ class TestToolReturnTypes:
         async with Client(mcp) as client:
             result = await client.call_tool("datetime_tool", {})
             assert isinstance(result[0], TextContent)
-            assert result[0].text == dt.isoformat()
+            assert result[0].text == pydantic_core.to_json(dt).decode()
 
     async def test_image(self, tmp_path: Path):
         mcp = FastMCP()
diff --git a/tests/tools/test_tool_manager.py b/tests/tools/test_tool_manager.py
index c58406c..9212b93 100644
--- a/tests/tools/test_tool_manager.py
+++ b/tests/tools/test_tool_manager.py
@@ -387,18 +387,7 @@ class TestCallTools:
         assert isinstance(result, list)
         assert len(result) == 1
         assert isinstance(result[0], TextContent)
-        assert result[0].text == '["rex", "gertrude"]'
-        assert json.loads(result[0].text) == ["rex", "gertrude"]
-
-        result = await manager.call_tool(
-            "name_shrimp",
-            {"tank": '{"x": null, "shrimp": [{"name": "rex"}, {"name": "gertrude"}]}'},
-        )
-        assert isinstance(result, list)
-        assert len(result) == 1
-        assert isinstance(result[0], TextContent)
-        assert result[0].text == '["rex", "gertrude"]'
-        assert json.loads(result[0].text) == ["rex", "gertrude"]
+        assert result[0].text == '[\n  "rex",\n  "gertrude"\n]'
 
 
 class TestToolSchema: