Repository: jlowin/fastmcp
Pull request #262 (2025-04-26 00:06:35)

Add tests for tool return types; improve serialization behavior


Patch:
diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx
index 71c24df..2c4fb73 100644
--- a/docs/servers/tools.mdx
+++ b/docs/servers/tools.mdx
@@ -205,6 +205,10 @@ FastMCP automatically converts the value returned by your function into the appr
 
 FastMCP will attempt to serialize other types to a string if possible.
 
+<Tip>
+At this time, FastMCP responds only to your tool's return *value*, not its return *annotation*.
+</Tip>
+
 ```python
 from fastmcp import FastMCP, Image
 import io
diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py
index e4af474..4b8ccee 100644
--- a/src/fastmcp/tools/tool.py
+++ b/src/fastmcp/tools/tool.py
@@ -163,9 +163,22 @@ 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:
-            result = json.dumps(pydantic_core.to_jsonable_python(result))
+            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)
 


Test patch:
diff --git a/tests/server/test_server_interactions.py b/tests/server/test_server_interactions.py
index 94f7ebd..506f1a6 100644
--- a/tests/server/test_server_interactions.py
+++ b/tests/server/test_server_interactions.py
@@ -106,12 +106,86 @@ class TestTools:
             assert isinstance(result[0], TextContent)
             assert result[0].text == '["x", 2]'
 
-    async def test_tool_image_helper(self, tool_server: FastMCP, tmp_path: Path):
+
+class TestToolReturnTypes:
+    async def test_string(self):
+        mcp = FastMCP()
+
+        @mcp.tool()
+        def string_tool() -> str:
+            return "Hello, world!"
+
+        async with Client(mcp) as client:
+            result = await client.call_tool("string_tool", {})
+            assert isinstance(result[0], TextContent)
+            assert result[0].text == "Hello, world!"
+
+    async def test_bytes(self, tmp_path: Path):
+        mcp = FastMCP()
+
+        @mcp.tool()
+        def bytes_tool() -> bytes:
+            return b"Hello, world!"
+
+        async with Client(mcp) as client:
+            result = await client.call_tool("bytes_tool", {})
+            assert isinstance(result[0], TextContent)
+            assert result[0].text == "Hello, world!"
+
+    async def test_uuid(self):
+        mcp = FastMCP()
+
+        test_uuid = uuid.uuid4()
+
+        @mcp.tool()
+        def uuid_tool() -> uuid.UUID:
+            return test_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)
+
+    async def test_path(self):
+        mcp = FastMCP()
+
+        test_path = Path("/tmp/test.txt")
+
+        @mcp.tool()
+        def path_tool() -> Path:
+            return test_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)
+
+    async def test_datetime(self):
+        mcp = FastMCP()
+
+        dt = datetime.datetime(2025, 4, 25, 1, 2, 3)
+
+        @mcp.tool()
+        def datetime_tool() -> datetime.datetime:
+            return dt
+
+        async with Client(mcp) as client:
+            result = await client.call_tool("datetime_tool", {})
+            assert isinstance(result[0], TextContent)
+            assert result[0].text == dt.isoformat()
+
+    async def test_image(self, tmp_path: Path):
+        mcp = FastMCP()
+
+        @mcp.tool()
+        def image_tool(path: str) -> Image:
+            return Image(path)
+
         # Create a test image
         image_path = tmp_path / "test.png"
         image_path.write_bytes(b"fake png data")
 
-        async with Client(tool_server) as client:
+        async with Client(mcp) as client:
             result = await client.call_tool("image_tool", {"path": str(image_path)})
             content = result[0]
             assert isinstance(content, ImageContent)