Spaces:
Running
Running
Merge pull request #262 from jlowin/return-values
Browse filesAdd tests for tool return types; improve serialization behavior
- docs/servers/tools.mdx +4 -0
- src/fastmcp/tools/tool.py +14 -1
- tests/server/test_server_interactions.py +76 -2
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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)
|