Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/fastmcp/prompts/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -192,7 +191,9 @@ async def render(
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(
Expand Down
11 changes: 4 additions & 7 deletions src/fastmcp/resources/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,12 @@ async def read(

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}")

Expand Down
9 changes: 7 additions & 2 deletions src/fastmcp/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

import anyio
import httpx
import pydantic_core
import uvicorn
from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.server.lowlevel.server import LifespanResultT
Expand All @@ -27,6 +26,7 @@
EmbeddedResource,
GetPromptResult,
ImageContent,
PromptMessage,
TextContent,
)
from mcp.types import Prompt as MCPPrompt
Expand Down Expand Up @@ -435,7 +435,12 @@ async def _mcp_get_prompt(
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):
Expand Down
19 changes: 1 addition & 18 deletions src/fastmcp/tools/tool.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import inspect
import json
from collections.abc import Callable
from typing import TYPE_CHECKING, Annotated, Any

Expand Down Expand Up @@ -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()
Copy link

Copilot AI Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the fallback is supplied, the removal of the try/except block means any unexpected serialization error will now propagate. Consider reintroducing error handling or logging around this conversion to help diagnose issues in production scenarios.

Copilot uses AI. Check for mistakes.

return [TextContent(type="text", text=result)]
2 changes: 1 addition & 1 deletion tests/resources/test_function_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class MyModel(BaseModel):
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."""
Expand Down
2 changes: 1 addition & 1 deletion tests/resources/test_resource_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ def get_data(value: str) -> CustomData:

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."""
Expand Down
2 changes: 1 addition & 1 deletion tests/server/test_mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ async def get_users():
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."""
Expand Down
11 changes: 6 additions & 5 deletions tests/server/test_server_interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pathlib import Path
from typing import Annotated, Literal

import pydantic_core
import pytest
from mcp.types import (
BlobResourceContents,
Expand Down Expand Up @@ -104,7 +105,7 @@ async def test_tool_returns_list(self, tool_server: FastMCP):
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:
Expand All @@ -130,7 +131,7 @@ def bytes_tool() -> bytes:
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()
Expand All @@ -144,7 +145,7 @@ def uuid_tool() -> uuid.UUID:
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()
Expand All @@ -158,7 +159,7 @@ def path_tool() -> Path:
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()
Expand All @@ -172,7 +173,7 @@ def datetime_tool() -> datetime.datetime:
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()
Expand Down
13 changes: 1 addition & 12 deletions tests/tools/test_tool_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,18 +387,7 @@ def name_shrimp(tank: MyShrimpTank, ctx: Context) -> list[str]:
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:
Expand Down