Jeremiah Lowin commited on
Commit
a348840
·
unverified ·
2 Parent(s): 0a9315c70b949f

Merge pull request #262 from jlowin/return-values

Browse files

Add tests for tool return types; improve serialization behavior

docs/servers/tools.mdx CHANGED
@@ -205,6 +205,10 @@ FastMCP automatically converts the value returned by your function into the appr
205
 
206
  FastMCP will attempt to serialize other types to a string if possible.
207
 
 
 
 
 
208
  ```python
209
  from fastmcp import FastMCP, Image
210
  import io
 
205
 
206
  FastMCP will attempt to serialize other types to a string if possible.
207
 
208
+ <Tip>
209
+ At this time, FastMCP responds only to your tool's return *value*, not its return *annotation*.
210
+ </Tip>
211
+
212
  ```python
213
  from fastmcp import FastMCP, Image
214
  import io
src/fastmcp/tools/tool.py CHANGED
@@ -163,9 +163,22 @@ def _convert_to_content(
163
 
164
  return other_content + mcp_types
165
 
 
166
  if not isinstance(result, str):
167
  try:
168
- result = json.dumps(pydantic_core.to_jsonable_python(result))
 
 
 
 
 
 
 
 
 
 
 
 
169
  except Exception:
170
  result = str(result)
171
 
 
163
 
164
  return other_content + mcp_types
165
 
166
+ # if the result is a bytes object, convert it to a text content object
167
  if not isinstance(result, str):
168
  try:
169
+ jsonable_result = pydantic_core.to_jsonable_python(result)
170
+ if jsonable_result is None:
171
+ return [TextContent(type="text", text="null")]
172
+ elif isinstance(jsonable_result, bool):
173
+ return [
174
+ TextContent(
175
+ type="text", text="true" if jsonable_result else "false"
176
+ )
177
+ ]
178
+ elif isinstance(jsonable_result, str | int | float):
179
+ return [TextContent(type="text", text=str(jsonable_result))]
180
+ else:
181
+ return [TextContent(type="text", text=json.dumps(jsonable_result))]
182
  except Exception:
183
  result = str(result)
184
 
tests/server/test_server_interactions.py CHANGED
@@ -106,12 +106,86 @@ class TestTools:
106
  assert isinstance(result[0], TextContent)
107
  assert result[0].text == '["x", 2]'
108
 
109
- async def test_tool_image_helper(self, tool_server: FastMCP, tmp_path: Path):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  # Create a test image
111
  image_path = tmp_path / "test.png"
112
  image_path.write_bytes(b"fake png data")
113
 
114
- async with Client(tool_server) as client:
115
  result = await client.call_tool("image_tool", {"path": str(image_path)})
116
  content = result[0]
117
  assert isinstance(content, ImageContent)
 
106
  assert isinstance(result[0], TextContent)
107
  assert result[0].text == '["x", 2]'
108
 
109
+
110
+ class TestToolReturnTypes:
111
+ async def test_string(self):
112
+ mcp = FastMCP()
113
+
114
+ @mcp.tool()
115
+ def string_tool() -> str:
116
+ return "Hello, world!"
117
+
118
+ async with Client(mcp) as client:
119
+ result = await client.call_tool("string_tool", {})
120
+ assert isinstance(result[0], TextContent)
121
+ assert result[0].text == "Hello, world!"
122
+
123
+ async def test_bytes(self, tmp_path: Path):
124
+ mcp = FastMCP()
125
+
126
+ @mcp.tool()
127
+ def bytes_tool() -> bytes:
128
+ return b"Hello, world!"
129
+
130
+ async with Client(mcp) as client:
131
+ result = await client.call_tool("bytes_tool", {})
132
+ assert isinstance(result[0], TextContent)
133
+ assert result[0].text == "Hello, world!"
134
+
135
+ async def test_uuid(self):
136
+ mcp = FastMCP()
137
+
138
+ test_uuid = uuid.uuid4()
139
+
140
+ @mcp.tool()
141
+ def uuid_tool() -> uuid.UUID:
142
+ return test_uuid
143
+
144
+ async with Client(mcp) as client:
145
+ result = await client.call_tool("uuid_tool", {})
146
+ assert isinstance(result[0], TextContent)
147
+ assert result[0].text == str(test_uuid)
148
+
149
+ async def test_path(self):
150
+ mcp = FastMCP()
151
+
152
+ test_path = Path("/tmp/test.txt")
153
+
154
+ @mcp.tool()
155
+ def path_tool() -> Path:
156
+ return test_path
157
+
158
+ async with Client(mcp) as client:
159
+ result = await client.call_tool("path_tool", {})
160
+ assert isinstance(result[0], TextContent)
161
+ assert result[0].text == str(test_path)
162
+
163
+ async def test_datetime(self):
164
+ mcp = FastMCP()
165
+
166
+ dt = datetime.datetime(2025, 4, 25, 1, 2, 3)
167
+
168
+ @mcp.tool()
169
+ def datetime_tool() -> datetime.datetime:
170
+ return dt
171
+
172
+ async with Client(mcp) as client:
173
+ result = await client.call_tool("datetime_tool", {})
174
+ assert isinstance(result[0], TextContent)
175
+ assert result[0].text == dt.isoformat()
176
+
177
+ async def test_image(self, tmp_path: Path):
178
+ mcp = FastMCP()
179
+
180
+ @mcp.tool()
181
+ def image_tool(path: str) -> Image:
182
+ return Image(path)
183
+
184
  # Create a test image
185
  image_path = tmp_path / "test.png"
186
  image_path.write_bytes(b"fake png data")
187
 
188
+ async with Client(mcp) as client:
189
  result = await client.call_tool("image_tool", {"path": str(image_path)})
190
  content = result[0]
191
  assert isinstance(content, ImageContent)