From a9253d311121367faa8ebd5a729994cd5f9f1bc1 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:29:38 -0400 Subject: [PATCH] use pydantic_core.to_json --- src/fastmcp/prompts/prompt.py | 5 +++-- src/fastmcp/resources/types.py | 11 ++++------- src/fastmcp/server/server.py | 9 +++++++-- src/fastmcp/tools/tool.py | 19 +------------------ tests/resources/test_function_resources.py | 2 +- tests/resources/test_resource_template.py | 2 +- tests/server/test_mount.py | 2 +- tests/server/test_server_interactions.py | 10 +++++----- tests/tools/test_tool_manager.py | 13 +------------ 9 files changed, 24 insertions(+), 49 deletions(-) diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index 0cf37403d..806fe6395 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 f7384a85a..c6fda510a 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 65dd33ea4..d838570ae 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 6f67e20a6..bc0b0a3d3 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)] diff --git a/tests/resources/test_function_resources.py b/tests/resources/test_function_resources.py index c179fd26c..46ac320ae 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 86fd24df4..fab5e4b0f 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 61d958868..dedb9e802 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 141149441..f0ae19209 100644 --- a/tests/server/test_server_interactions.py +++ b/tests/server/test_server_interactions.py @@ -104,7 +104,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 +130,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 +144,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 == f'"{test_uuid}"' async def test_path(self): mcp = FastMCP() @@ -158,7 +158,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 == f'"{test_path}"' async def test_datetime(self): mcp = FastMCP() @@ -172,7 +172,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 == f'"{dt.isoformat()}"' 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 c58406c6d..9212b931a 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: