Spaces:
Running
Running
Merge pull request #290 from jlowin/json
Browse files- src/fastmcp/prompts/prompt.py +3 -2
- src/fastmcp/resources/types.py +4 -7
- src/fastmcp/server/server.py +7 -2
- src/fastmcp/tools/tool.py +1 -18
- tests/resources/test_function_resources.py +1 -1
- tests/resources/test_resource_template.py +1 -1
- tests/server/test_mount.py +1 -1
- tests/server/test_server_interactions.py +6 -5
- tests/tools/test_tool_manager.py +1 -12
src/fastmcp/prompts/prompt.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
| 3 |
from __future__ import annotations as _annotations
|
| 4 |
|
| 5 |
import inspect
|
| 6 |
-
import json
|
| 7 |
from collections.abc import Awaitable, Callable, Sequence
|
| 8 |
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
| 9 |
|
|
@@ -195,7 +194,9 @@ class Prompt(BaseModel):
|
|
| 195 |
content = TextContent(type="text", text=msg)
|
| 196 |
messages.append(Message(role="user", content=content))
|
| 197 |
else:
|
| 198 |
-
content =
|
|
|
|
|
|
|
| 199 |
messages.append(Message(role="user", content=content))
|
| 200 |
except Exception:
|
| 201 |
raise ValueError(
|
|
|
|
| 3 |
from __future__ import annotations as _annotations
|
| 4 |
|
| 5 |
import inspect
|
|
|
|
| 6 |
from collections.abc import Awaitable, Callable, Sequence
|
| 7 |
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
| 8 |
|
|
|
|
| 194 |
content = TextContent(type="text", text=msg)
|
| 195 |
messages.append(Message(role="user", content=content))
|
| 196 |
else:
|
| 197 |
+
content = pydantic_core.to_json(
|
| 198 |
+
msg, fallback=str, indent=2
|
| 199 |
+
).decode()
|
| 200 |
messages.append(Message(role="user", content=content))
|
| 201 |
except Exception:
|
| 202 |
raise ValueError(
|
src/fastmcp/resources/types.py
CHANGED
|
@@ -97,15 +97,12 @@ class FunctionResource(Resource):
|
|
| 97 |
|
| 98 |
if isinstance(result, Resource):
|
| 99 |
return await result.read(context=context)
|
| 100 |
-
|
| 101 |
return result
|
| 102 |
-
|
| 103 |
return result
|
| 104 |
-
|
| 105 |
-
return
|
| 106 |
-
except (TypeError, pydantic_core.PydanticSerializationError):
|
| 107 |
-
# If JSON serialization fails, try str()
|
| 108 |
-
return str(result)
|
| 109 |
except Exception as e:
|
| 110 |
raise ValueError(f"Error reading resource {self.uri}: {e}")
|
| 111 |
|
|
|
|
| 97 |
|
| 98 |
if isinstance(result, Resource):
|
| 99 |
return await result.read(context=context)
|
| 100 |
+
elif isinstance(result, bytes):
|
| 101 |
return result
|
| 102 |
+
elif isinstance(result, str):
|
| 103 |
return result
|
| 104 |
+
else:
|
| 105 |
+
return pydantic_core.to_json(result, fallback=str, indent=2).decode()
|
|
|
|
|
|
|
|
|
|
| 106 |
except Exception as e:
|
| 107 |
raise ValueError(f"Error reading resource {self.uri}: {e}")
|
| 108 |
|
src/fastmcp/server/server.py
CHANGED
|
@@ -14,7 +14,6 @@ from typing import TYPE_CHECKING, Any, Generic, Literal
|
|
| 14 |
|
| 15 |
import anyio
|
| 16 |
import httpx
|
| 17 |
-
import pydantic_core
|
| 18 |
import uvicorn
|
| 19 |
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
| 20 |
from mcp.server.lowlevel.server import LifespanResultT
|
|
@@ -27,6 +26,7 @@ from mcp.types import (
|
|
| 27 |
EmbeddedResource,
|
| 28 |
GetPromptResult,
|
| 29 |
ImageContent,
|
|
|
|
| 30 |
TextContent,
|
| 31 |
)
|
| 32 |
from mcp.types import Prompt as MCPPrompt
|
|
@@ -435,7 +435,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 435 |
messages = await self._prompt_manager.render_prompt(
|
| 436 |
name, arguments=arguments or {}, context=context
|
| 437 |
)
|
| 438 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
else:
|
| 440 |
for server in self._mounted_servers.values():
|
| 441 |
if server.match_prompt(name):
|
|
|
|
| 14 |
|
| 15 |
import anyio
|
| 16 |
import httpx
|
|
|
|
| 17 |
import uvicorn
|
| 18 |
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
| 19 |
from mcp.server.lowlevel.server import LifespanResultT
|
|
|
|
| 26 |
EmbeddedResource,
|
| 27 |
GetPromptResult,
|
| 28 |
ImageContent,
|
| 29 |
+
PromptMessage,
|
| 30 |
TextContent,
|
| 31 |
)
|
| 32 |
from mcp.types import Prompt as MCPPrompt
|
|
|
|
| 435 |
messages = await self._prompt_manager.render_prompt(
|
| 436 |
name, arguments=arguments or {}, context=context
|
| 437 |
)
|
| 438 |
+
|
| 439 |
+
return GetPromptResult(
|
| 440 |
+
messages=[
|
| 441 |
+
PromptMessage(role=m.role, content=m.content) for m in messages
|
| 442 |
+
]
|
| 443 |
+
)
|
| 444 |
else:
|
| 445 |
for server in self._mounted_servers.values():
|
| 446 |
if server.match_prompt(name):
|
src/fastmcp/tools/tool.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import inspect
|
| 4 |
-
import json
|
| 5 |
from collections.abc import Callable
|
| 6 |
from typing import TYPE_CHECKING, Annotated, Any
|
| 7 |
|
|
@@ -170,23 +169,7 @@ def _convert_to_content(
|
|
| 170 |
|
| 171 |
return other_content + mcp_types
|
| 172 |
|
| 173 |
-
# if the result is a bytes object, convert it to a text content object
|
| 174 |
if not isinstance(result, str):
|
| 175 |
-
|
| 176 |
-
jsonable_result = pydantic_core.to_jsonable_python(result)
|
| 177 |
-
if jsonable_result is None:
|
| 178 |
-
return [TextContent(type="text", text="null")]
|
| 179 |
-
elif isinstance(jsonable_result, bool):
|
| 180 |
-
return [
|
| 181 |
-
TextContent(
|
| 182 |
-
type="text", text="true" if jsonable_result else "false"
|
| 183 |
-
)
|
| 184 |
-
]
|
| 185 |
-
elif isinstance(jsonable_result, str | int | float):
|
| 186 |
-
return [TextContent(type="text", text=str(jsonable_result))]
|
| 187 |
-
else:
|
| 188 |
-
return [TextContent(type="text", text=json.dumps(jsonable_result))]
|
| 189 |
-
except Exception:
|
| 190 |
-
result = str(result)
|
| 191 |
|
| 192 |
return [TextContent(type="text", text=result)]
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import inspect
|
|
|
|
| 4 |
from collections.abc import Callable
|
| 5 |
from typing import TYPE_CHECKING, Annotated, Any
|
| 6 |
|
|
|
|
| 169 |
|
| 170 |
return other_content + mcp_types
|
| 171 |
|
|
|
|
| 172 |
if not isinstance(result, str):
|
| 173 |
+
result = pydantic_core.to_json(result, fallback=str, indent=2).decode()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
return [TextContent(type="text", text=result)]
|
tests/resources/test_function_resources.py
CHANGED
|
@@ -95,7 +95,7 @@ class TestFunctionResource:
|
|
| 95 |
fn=lambda: MyModel(name="test"),
|
| 96 |
)
|
| 97 |
content = await resource.read()
|
| 98 |
-
assert content == '{"name": "test"}'
|
| 99 |
|
| 100 |
async def test_custom_type_conversion(self):
|
| 101 |
"""Test handling of custom types."""
|
|
|
|
| 95 |
fn=lambda: MyModel(name="test"),
|
| 96 |
)
|
| 97 |
content = await resource.read()
|
| 98 |
+
assert content == '{\n "name": "test"\n}'
|
| 99 |
|
| 100 |
async def test_custom_type_conversion(self):
|
| 101 |
"""Test handling of custom types."""
|
tests/resources/test_resource_template.py
CHANGED
|
@@ -298,7 +298,7 @@ class TestResourceTemplate:
|
|
| 298 |
|
| 299 |
assert isinstance(resource, FunctionResource)
|
| 300 |
content = await resource.read()
|
| 301 |
-
assert content == "hello"
|
| 302 |
|
| 303 |
async def test_wildcard_param_can_create_resource(self):
|
| 304 |
"""Test that wildcard parameters are valid."""
|
|
|
|
| 298 |
|
| 299 |
assert isinstance(resource, FunctionResource)
|
| 300 |
content = await resource.read()
|
| 301 |
+
assert content == '"hello"'
|
| 302 |
|
| 303 |
async def test_wildcard_param_can_create_resource(self):
|
| 304 |
"""Test that wildcard parameters are valid."""
|
tests/server/test_mount.py
CHANGED
|
@@ -227,7 +227,7 @@ class TestResourcesAndTemplates:
|
|
| 227 |
async with Client(main_app) as client:
|
| 228 |
resource = await client.read_resource("data+data://users")
|
| 229 |
assert isinstance(resource[0], TextResourceContents)
|
| 230 |
-
assert resource[0].text == '["user1",
|
| 231 |
|
| 232 |
async def test_mount_with_resource_templates(self):
|
| 233 |
"""Test mounting a server with resource templates."""
|
|
|
|
| 227 |
async with Client(main_app) as client:
|
| 228 |
resource = await client.read_resource("data+data://users")
|
| 229 |
assert isinstance(resource[0], TextResourceContents)
|
| 230 |
+
assert resource[0].text == '[\n "user1",\n "user2"\n]'
|
| 231 |
|
| 232 |
async def test_mount_with_resource_templates(self):
|
| 233 |
"""Test mounting a server with resource templates."""
|
tests/server/test_server_interactions.py
CHANGED
|
@@ -6,6 +6,7 @@ from enum import Enum
|
|
| 6 |
from pathlib import Path
|
| 7 |
from typing import Annotated, Literal
|
| 8 |
|
|
|
|
| 9 |
import pytest
|
| 10 |
from mcp.types import (
|
| 11 |
BlobResourceContents,
|
|
@@ -104,7 +105,7 @@ class TestTools:
|
|
| 104 |
async with Client(tool_server) as client:
|
| 105 |
result = await client.call_tool("list_tool", {})
|
| 106 |
assert isinstance(result[0], TextContent)
|
| 107 |
-
assert result[0].text == '["x",
|
| 108 |
|
| 109 |
|
| 110 |
class TestToolReturnTypes:
|
|
@@ -130,7 +131,7 @@ class TestToolReturnTypes:
|
|
| 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()
|
|
@@ -144,7 +145,7 @@ class TestToolReturnTypes:
|
|
| 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 ==
|
| 148 |
|
| 149 |
async def test_path(self):
|
| 150 |
mcp = FastMCP()
|
|
@@ -158,7 +159,7 @@ class TestToolReturnTypes:
|
|
| 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 ==
|
| 162 |
|
| 163 |
async def test_datetime(self):
|
| 164 |
mcp = FastMCP()
|
|
@@ -172,7 +173,7 @@ class TestToolReturnTypes:
|
|
| 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.
|
| 176 |
|
| 177 |
async def test_image(self, tmp_path: Path):
|
| 178 |
mcp = FastMCP()
|
|
|
|
| 6 |
from pathlib import Path
|
| 7 |
from typing import Annotated, Literal
|
| 8 |
|
| 9 |
+
import pydantic_core
|
| 10 |
import pytest
|
| 11 |
from mcp.types import (
|
| 12 |
BlobResourceContents,
|
|
|
|
| 105 |
async with Client(tool_server) as client:
|
| 106 |
result = await client.call_tool("list_tool", {})
|
| 107 |
assert isinstance(result[0], TextContent)
|
| 108 |
+
assert result[0].text == '[\n "x",\n 2\n]'
|
| 109 |
|
| 110 |
|
| 111 |
class TestToolReturnTypes:
|
|
|
|
| 131 |
async with Client(mcp) as client:
|
| 132 |
result = await client.call_tool("bytes_tool", {})
|
| 133 |
assert isinstance(result[0], TextContent)
|
| 134 |
+
assert result[0].text == '"Hello, world!"'
|
| 135 |
|
| 136 |
async def test_uuid(self):
|
| 137 |
mcp = FastMCP()
|
|
|
|
| 145 |
async with Client(mcp) as client:
|
| 146 |
result = await client.call_tool("uuid_tool", {})
|
| 147 |
assert isinstance(result[0], TextContent)
|
| 148 |
+
assert result[0].text == pydantic_core.to_json(test_uuid).decode()
|
| 149 |
|
| 150 |
async def test_path(self):
|
| 151 |
mcp = FastMCP()
|
|
|
|
| 159 |
async with Client(mcp) as client:
|
| 160 |
result = await client.call_tool("path_tool", {})
|
| 161 |
assert isinstance(result[0], TextContent)
|
| 162 |
+
assert result[0].text == pydantic_core.to_json(test_path).decode()
|
| 163 |
|
| 164 |
async def test_datetime(self):
|
| 165 |
mcp = FastMCP()
|
|
|
|
| 173 |
async with Client(mcp) as client:
|
| 174 |
result = await client.call_tool("datetime_tool", {})
|
| 175 |
assert isinstance(result[0], TextContent)
|
| 176 |
+
assert result[0].text == pydantic_core.to_json(dt).decode()
|
| 177 |
|
| 178 |
async def test_image(self, tmp_path: Path):
|
| 179 |
mcp = FastMCP()
|
tests/tools/test_tool_manager.py
CHANGED
|
@@ -390,18 +390,7 @@ class TestCallTools:
|
|
| 390 |
assert isinstance(result, list)
|
| 391 |
assert len(result) == 1
|
| 392 |
assert isinstance(result[0], TextContent)
|
| 393 |
-
assert result[0].text == '["rex",
|
| 394 |
-
assert json.loads(result[0].text) == ["rex", "gertrude"]
|
| 395 |
-
|
| 396 |
-
result = await manager.call_tool(
|
| 397 |
-
"name_shrimp",
|
| 398 |
-
{"tank": '{"x": null, "shrimp": [{"name": "rex"}, {"name": "gertrude"}]}'},
|
| 399 |
-
)
|
| 400 |
-
assert isinstance(result, list)
|
| 401 |
-
assert len(result) == 1
|
| 402 |
-
assert isinstance(result[0], TextContent)
|
| 403 |
-
assert result[0].text == '["rex", "gertrude"]'
|
| 404 |
-
assert json.loads(result[0].text) == ["rex", "gertrude"]
|
| 405 |
|
| 406 |
|
| 407 |
class TestToolSchema:
|
|
|
|
| 390 |
assert isinstance(result, list)
|
| 391 |
assert len(result) == 1
|
| 392 |
assert isinstance(result[0], TextContent)
|
| 393 |
+
assert result[0].text == '[\n "rex",\n "gertrude"\n]'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
|
| 395 |
|
| 396 |
class TestToolSchema:
|