fastmcp 2.2.8__tar.gz → 2.2.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/ISSUE_TEMPLATE/bug.yml +7 -2
- {fastmcp-2.2.8 → fastmcp-2.2.10}/PKG-INFO +5 -1
- {fastmcp-2.2.8 → fastmcp-2.2.10}/README.md +4 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/clients/client.mdx +2 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/docs.json +5 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/getting-started/quickstart.mdx +2 -1
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/servers/tools.mdx +10 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/desktop.py +7 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/pyproject.toml +2 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/server/server.py +15 -1
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/settings.py +13 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/tools/tool.py +27 -9
- fastmcp-2.2.10/src/fastmcp/utilities/tests.py +41 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_import_server.py +23 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_mount.py +20 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_server_interactions.py +0 -75
- fastmcp-2.2.10/tests/test_examples.py +92 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/tools/test_tool.py +85 -2
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/tools/test_tool_manager.py +40 -19
- fastmcp-2.2.10/tests/utilities/test_tests.py +9 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/uv.lock +91 -0
- fastmcp-2.2.8/examples/readme-quickstart.py +0 -18
- {fastmcp-2.2.8 → fastmcp-2.2.10}/.cursor/rules/core-mcp-objects.mdc +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/ISSUE_TEMPLATE/enhancement.yml +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/release.yml +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/workflows/publish.yml +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/workflows/run-static.yml +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/workflows/run-tests.yml +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/.gitignore +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/.pre-commit-config.yaml +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/LICENSE +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/Windows_Notes.md +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/assets/demo-inspector.png +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/clients/transports.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/getting-started/installation.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/getting-started/welcome.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/composition.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/contrib.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/decorating-methods.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/fastapi.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/openapi.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/proxy.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/testing.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/servers/context.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/servers/fastmcp.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/servers/prompts.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/servers/resources.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/snippets/version-badge.mdx +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/style.css +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/complex_inputs.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/echo.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/memory.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/mount_example.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/sampling.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/screenshot.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/serializer.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/simple_echo.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/README.md +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/pyproject.toml +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/__main__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/hub.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/lights/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/lights/hue_utils.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/lights/server.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/py.typed +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/settings.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/uv.lock +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/text_me.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/justfile +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/cli/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/cli/claude.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/cli/cli.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/base.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/client.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/logging.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/roots.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/sampling.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/transports.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/README.md +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/README.md +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/example.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/README.md +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/example.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/exceptions.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/prompts/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/prompts/prompt.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/prompts/prompt_manager.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/py.typed +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/resources/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/resources/resource.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/resources/resource_manager.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/resources/template.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/resources/types.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/server/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/server/context.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/server/openapi.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/server/proxy.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/tools/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/tools/tool_manager.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/decorators.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/http.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/json_schema.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/logging.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/openapi.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/types.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/cli/test_run.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/client/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/client/test_client.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/client/test_logs.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/client/test_roots.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/client/test_sampling.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/conftest.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/contrib/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/contrib/test_bulk_tool_caller.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/contrib/test_mcp_mixin.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/prompts/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/prompts/test_prompt.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/prompts/test_prompt_manager.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/resources/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/resources/test_file_resources.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/resources/test_function_resources.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/resources/test_resource_manager.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/resources/test_resource_template.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/resources/test_resources.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_auth_integration.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_file_server.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_lifespan.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_openapi.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_proxy.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_run_server.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_server.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_tool_annotations.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/test_servers/fastmcp_server.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/test_servers/sse.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/test_servers/stdio.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/tools/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/openapi/__init__.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/openapi/conftest.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/openapi/test_openapi.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/openapi/test_openapi_advanced.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/openapi/test_openapi_fastapi.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/test_decorated_function.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/test_json_schema.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/test_logging.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/test_typeadapter.py +0 -0
- {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/test_types.py +0 -0
|
@@ -27,10 +27,15 @@ body:
|
|
|
27
27
|
[minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example)
|
|
28
28
|
demonstrating the bug.
|
|
29
29
|
|
|
30
|
+
If possible, your example should be a single-file script. Instead of `.run()`-ing an MCP server, use a `Client` to directly interact with it (`async with Client(mcp) as client: ...`) and demonstrate the issue.
|
|
31
|
+
|
|
30
32
|
placeholder: |
|
|
31
|
-
from fastmcp import FastMCP
|
|
33
|
+
from fastmcp import FastMCP, Client
|
|
34
|
+
|
|
35
|
+
mcp = FastMCP()
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
async with Client(mcp) as client:
|
|
38
|
+
...
|
|
34
39
|
render: Python
|
|
35
40
|
|
|
36
41
|
- type: textarea
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastmcp
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.10
|
|
4
4
|
Summary: The fast, Pythonic way to build MCP servers.
|
|
5
5
|
Project-URL: Homepage, https://gofastmcp.com
|
|
6
6
|
Project-URL: Repository, https://github.com/jlowin/fastmcp
|
|
@@ -379,6 +379,10 @@ Run tests using pytest:
|
|
|
379
379
|
```bash
|
|
380
380
|
pytest
|
|
381
381
|
```
|
|
382
|
+
or if you want an overview of the code coverage
|
|
383
|
+
```bash
|
|
384
|
+
uv run pytest --cov=src --cov=examples --cov-report=html
|
|
385
|
+
```
|
|
382
386
|
|
|
383
387
|
### Static Checks
|
|
384
388
|
|
|
@@ -155,6 +155,8 @@ The standard client methods return user-friendly representations that may change
|
|
|
155
155
|
|
|
156
156
|
### Raw MCP Protocol Objects
|
|
157
157
|
|
|
158
|
+
<VersionBadge version="2.2.7" />
|
|
159
|
+
|
|
158
160
|
The FastMCP client attempts to provide a "friendly" interface to the MCP protocol, but sometimes you may need access to the raw MCP protocol objects. Each of the main client methods that returns data has a corresponding `*_mcp` method that returns the raw MCP protocol objects directly.
|
|
159
161
|
|
|
160
162
|
<Warning>
|
|
@@ -43,7 +43,8 @@ def greet(name: str) -> str:
|
|
|
43
43
|
|
|
44
44
|
To test the server, create a FastMCP client and point it at the server object.
|
|
45
45
|
|
|
46
|
-
```python my_server.py {1,
|
|
46
|
+
```python my_server.py {1-2, 10-17}
|
|
47
|
+
import asyncio
|
|
47
48
|
from fastmcp import FastMCP, Client
|
|
48
49
|
|
|
49
50
|
mcp = FastMCP("My MCP Server")
|
|
@@ -708,3 +708,13 @@ The duplicate behavior options are:
|
|
|
708
708
|
- `"error"`: Raises a `ValueError`, preventing the duplicate registration.
|
|
709
709
|
- `"replace"`: Silently replaces the existing tool with the new one.
|
|
710
710
|
- `"ignore"`: Keeps the original tool and ignores the new registration attempt.
|
|
711
|
+
|
|
712
|
+
### Legacy JSON Parsing
|
|
713
|
+
|
|
714
|
+
<VersionBadge version="2.2.10" />
|
|
715
|
+
|
|
716
|
+
FastMCP 1.0 and < 2.2.10 relied on a crutch that attempted to work around LLM limitations by automatically parsing stringified JSON in tool arguments (e.g., converting `"[1,2,3]"` to `[1,2,3]`). As of FastMCP 2.2.10, this behavior is disabled by default because it circumvents type validation and can lead to unexpected type coercion issues (e.g. parsing "true" as a bool and attempting to call a tool that expected a string, which would fail type validation).
|
|
717
|
+
|
|
718
|
+
Most modern LLMs correctly format JSON, but if working with models that unnecessarily stringify JSON (as was the case with Claude Desktop in late 2024), you can re-enable this behavior on your server by setting the environment variable `FASTMCP_TOOL_ATTEMPT_PARSE_JSON_ARGS=1`.
|
|
719
|
+
|
|
720
|
+
We strongly recommend leaving this disabled unless necessary.
|
|
@@ -19,6 +19,13 @@ def desktop() -> list[str]:
|
|
|
19
19
|
return [str(f) for f in desktop.iterdir()]
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
# Add a dynamic greeting resource
|
|
23
|
+
@mcp.resource("greeting://{name}")
|
|
24
|
+
def get_greeting(name: str) -> str:
|
|
25
|
+
"""Get a personalized greeting"""
|
|
26
|
+
return f"Hello, {name}!"
|
|
27
|
+
|
|
28
|
+
|
|
22
29
|
@mcp.tool()
|
|
23
30
|
def add(a: int, b: int) -> int:
|
|
24
31
|
"""Add two numbers"""
|
|
@@ -14,6 +14,7 @@ from typing import TYPE_CHECKING, Any, Generic, Literal
|
|
|
14
14
|
|
|
15
15
|
import anyio
|
|
16
16
|
import httpx
|
|
17
|
+
import pydantic
|
|
17
18
|
import uvicorn
|
|
18
19
|
from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
|
|
19
20
|
from mcp.server.auth.middleware.bearer_auth import (
|
|
@@ -39,7 +40,7 @@ from mcp.types import Prompt as MCPPrompt
|
|
|
39
40
|
from mcp.types import Resource as MCPResource
|
|
40
41
|
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
41
42
|
from mcp.types import Tool as MCPTool
|
|
42
|
-
from pydantic
|
|
43
|
+
from pydantic import AnyUrl
|
|
43
44
|
from starlette.applications import Starlette
|
|
44
45
|
from starlette.middleware import Middleware
|
|
45
46
|
from starlette.middleware.authentication import AuthenticationMiddleware
|
|
@@ -88,6 +89,8 @@ class MountedServer:
|
|
|
88
89
|
if prompt_separator is None:
|
|
89
90
|
prompt_separator = "_"
|
|
90
91
|
|
|
92
|
+
_validate_resource_prefix(f"{prefix}{resource_separator}")
|
|
93
|
+
|
|
91
94
|
self.server = server
|
|
92
95
|
self.prefix = prefix
|
|
93
96
|
self.tool_separator = tool_separator
|
|
@@ -1074,6 +1077,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1074
1077
|
|
|
1075
1078
|
# Import resources and templates from the mounted server
|
|
1076
1079
|
resource_prefix = f"{prefix}{resource_separator}"
|
|
1080
|
+
_validate_resource_prefix(resource_prefix)
|
|
1077
1081
|
for key, resource in (await server.get_resources()).items():
|
|
1078
1082
|
self._resource_manager.add_resource(resource, key=f"{resource_prefix}{key}")
|
|
1079
1083
|
for key, template in (await server.get_resource_templates()).items():
|
|
@@ -1131,3 +1135,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1131
1135
|
from fastmcp.server.proxy import FastMCPProxy
|
|
1132
1136
|
|
|
1133
1137
|
return FastMCPProxy(client=client, **settings)
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
def _validate_resource_prefix(prefix: str) -> None:
|
|
1141
|
+
valid_resource = "resource://path/to/resource"
|
|
1142
|
+
try:
|
|
1143
|
+
AnyUrl(f"{prefix}{valid_resource}")
|
|
1144
|
+
except pydantic.ValidationError as e:
|
|
1145
|
+
raise ValueError(
|
|
1146
|
+
f"Resource prefix or separator would result in an invalid resource URI: {e}"
|
|
1147
|
+
)
|
|
@@ -27,6 +27,16 @@ class Settings(BaseSettings):
|
|
|
27
27
|
|
|
28
28
|
test_mode: bool = False
|
|
29
29
|
log_level: LOG_LEVEL = "INFO"
|
|
30
|
+
tool_attempt_parse_json_args: bool = Field(
|
|
31
|
+
default=False,
|
|
32
|
+
description="""
|
|
33
|
+
Note: this enables a legacy behavior. If True, will attempt to parse
|
|
34
|
+
stringified JSON lists and objects strings in tool arguments before
|
|
35
|
+
passing them to the tool. This is an old behavior that can create
|
|
36
|
+
unexpected type coercion issues, but may be helpful for less powerful
|
|
37
|
+
LLMs that stringify JSON instead of passing actual lists and objects.
|
|
38
|
+
Defaults to False.""",
|
|
39
|
+
)
|
|
30
40
|
|
|
31
41
|
|
|
32
42
|
class ServerSettings(BaseSettings):
|
|
@@ -83,3 +93,6 @@ class ClientSettings(BaseSettings):
|
|
|
83
93
|
)
|
|
84
94
|
|
|
85
95
|
log_level: LOG_LEVEL = Field(default_factory=lambda: Settings().log_level)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
settings = Settings()
|
|
@@ -10,6 +10,7 @@ from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotatio
|
|
|
10
10
|
from mcp.types import Tool as MCPTool
|
|
11
11
|
from pydantic import BaseModel, BeforeValidator, Field
|
|
12
12
|
|
|
13
|
+
import fastmcp
|
|
13
14
|
from fastmcp.exceptions import ToolError
|
|
14
15
|
from fastmcp.utilities.json_schema import prune_params
|
|
15
16
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -107,6 +108,7 @@ class Tool(BaseModel):
|
|
|
107
108
|
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
108
109
|
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
109
110
|
"""Run the tool with arguments."""
|
|
111
|
+
|
|
110
112
|
try:
|
|
111
113
|
injected_args = (
|
|
112
114
|
{self.context_kwarg: context} if self.context_kwarg is not None else {}
|
|
@@ -114,16 +116,32 @@ class Tool(BaseModel):
|
|
|
114
116
|
|
|
115
117
|
parsed_args = arguments.copy()
|
|
116
118
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
if fastmcp.settings.settings.tool_attempt_parse_json_args:
|
|
120
|
+
# Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
|
|
121
|
+
# being passed in as JSON inside a string rather than an actual list.
|
|
122
|
+
#
|
|
123
|
+
# Claude desktop is prone to this - in fact it seems incapable of NOT doing
|
|
124
|
+
# this. For sub-models, it tends to pass dicts (JSON objects) as JSON strings,
|
|
125
|
+
# which can be pre-parsed here.
|
|
126
|
+
signature = inspect.signature(self.fn)
|
|
127
|
+
for param_name in self.parameters["properties"]:
|
|
128
|
+
arg = parsed_args.get(param_name, None)
|
|
129
|
+
# if not in signature, we won't have annotations, so skip logic
|
|
130
|
+
if param_name not in signature.parameters:
|
|
131
|
+
continue
|
|
132
|
+
# if not a string, we won't have a JSON to parse, so skip logic
|
|
133
|
+
if not isinstance(arg, str):
|
|
134
|
+
continue
|
|
135
|
+
# skip if the type is a simple type (int, float, bool)
|
|
136
|
+
if signature.parameters[param_name].annotation in (
|
|
137
|
+
int,
|
|
138
|
+
float,
|
|
139
|
+
bool,
|
|
140
|
+
):
|
|
141
|
+
continue
|
|
125
142
|
try:
|
|
126
|
-
parsed_args[param_name] = json.loads(
|
|
143
|
+
parsed_args[param_name] = json.loads(arg)
|
|
144
|
+
|
|
127
145
|
except json.JSONDecodeError:
|
|
128
146
|
pass
|
|
129
147
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastmcp.settings import settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@contextmanager
|
|
9
|
+
def temporary_settings(**kwargs: Any):
|
|
10
|
+
"""
|
|
11
|
+
Temporarily override ControlFlow setting values.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
**kwargs: The settings to override, including nested settings.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
Temporarily override a setting:
|
|
18
|
+
```python
|
|
19
|
+
import fastmcp
|
|
20
|
+
from fastmcp.utilities.tests import temporary_settings
|
|
21
|
+
|
|
22
|
+
with temporary_settings(log_level='DEBUG'):
|
|
23
|
+
assert fastmcp.settings.settings.log_level == 'DEBUG'
|
|
24
|
+
assert fastmcp.settings.settings.log_level == 'INFO'
|
|
25
|
+
```
|
|
26
|
+
"""
|
|
27
|
+
old_settings = copy.deepcopy(settings.model_dump())
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
# apply the new settings
|
|
31
|
+
for attr, value in kwargs.items():
|
|
32
|
+
if not hasattr(settings, attr):
|
|
33
|
+
raise AttributeError(f"Setting {attr} does not exist.")
|
|
34
|
+
setattr(settings, attr, value)
|
|
35
|
+
yield
|
|
36
|
+
|
|
37
|
+
finally:
|
|
38
|
+
# restore the old settings
|
|
39
|
+
for attr in kwargs:
|
|
40
|
+
if hasattr(settings, attr):
|
|
41
|
+
setattr(settings, attr, old_settings[attr])
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from urllib.parse import quote
|
|
3
3
|
|
|
4
|
+
import pytest
|
|
4
5
|
from mcp.types import TextContent, TextResourceContents
|
|
5
6
|
|
|
6
7
|
from fastmcp.client.client import Client
|
|
@@ -391,3 +392,25 @@ async def test_import_with_proxy_resource_templates():
|
|
|
391
392
|
user_data = json.loads(result[0].text)
|
|
392
393
|
assert user_data["name"] == "John Doe"
|
|
393
394
|
assert user_data["email"] == "john@example.com"
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
async def test_import_invalid_resource_prefix():
|
|
398
|
+
main_app = FastMCP("MainApp")
|
|
399
|
+
api_app = FastMCP("APIApp")
|
|
400
|
+
|
|
401
|
+
with pytest.raises(
|
|
402
|
+
ValueError,
|
|
403
|
+
match="Resource prefix or separator would result in an invalid resource URI",
|
|
404
|
+
):
|
|
405
|
+
await main_app.import_server("api_sub", api_app)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
async def test_import_invalid_resource_separator():
|
|
409
|
+
main_app = FastMCP("MainApp")
|
|
410
|
+
api_app = FastMCP("APIApp")
|
|
411
|
+
|
|
412
|
+
with pytest.raises(
|
|
413
|
+
ValueError,
|
|
414
|
+
match="Resource prefix or separator would result in an invalid resource URI",
|
|
415
|
+
):
|
|
416
|
+
await main_app.import_server("api", api_app, resource_separator="_")
|
|
@@ -59,6 +59,26 @@ class TestBasicMount:
|
|
|
59
59
|
assert isinstance(result[0], TextContent)
|
|
60
60
|
assert result[0].text == "Hello, World!"
|
|
61
61
|
|
|
62
|
+
async def test_mount_invalid_resource_prefix(self):
|
|
63
|
+
main_app = FastMCP("MainApp")
|
|
64
|
+
api_app = FastMCP("APIApp")
|
|
65
|
+
|
|
66
|
+
with pytest.raises(
|
|
67
|
+
ValueError,
|
|
68
|
+
match="Resource prefix or separator would result in an invalid resource URI",
|
|
69
|
+
):
|
|
70
|
+
main_app.mount("api_sub", api_app)
|
|
71
|
+
|
|
72
|
+
async def test_mount_invalid_resource_separator(self):
|
|
73
|
+
main_app = FastMCP("MainApp")
|
|
74
|
+
api_app = FastMCP("APIApp")
|
|
75
|
+
|
|
76
|
+
with pytest.raises(
|
|
77
|
+
ValueError,
|
|
78
|
+
match="Resource prefix or separator would result in an invalid resource URI",
|
|
79
|
+
):
|
|
80
|
+
main_app.mount("api", api_app, resource_separator="_")
|
|
81
|
+
|
|
62
82
|
async def test_unmount_server(self):
|
|
63
83
|
"""Test unmounting a server removes access to its tools."""
|
|
64
84
|
main_app = FastMCP("MainApp")
|
|
@@ -349,81 +349,6 @@ class TestToolParameters:
|
|
|
349
349
|
assert isinstance(result[0], TextContent)
|
|
350
350
|
assert result[0].text == "true"
|
|
351
351
|
|
|
352
|
-
async def test_tool_list_coercion(self):
|
|
353
|
-
"""Test JSON string to collection type coercion."""
|
|
354
|
-
mcp = FastMCP()
|
|
355
|
-
|
|
356
|
-
@mcp.tool()
|
|
357
|
-
def process_list(items: list[int]) -> int:
|
|
358
|
-
return sum(items)
|
|
359
|
-
|
|
360
|
-
async with Client(mcp) as client:
|
|
361
|
-
# JSON array string should be coerced to list
|
|
362
|
-
result = await client.call_tool(
|
|
363
|
-
"process_list", {"items": "[1, 2, 3, 4, 5]"}
|
|
364
|
-
)
|
|
365
|
-
assert isinstance(result[0], TextContent)
|
|
366
|
-
assert result[0].text == "15"
|
|
367
|
-
|
|
368
|
-
async def test_tool_list_coercion_error(self):
|
|
369
|
-
"""Test that a list coercion error is raised if the input is not a valid list."""
|
|
370
|
-
mcp = FastMCP()
|
|
371
|
-
|
|
372
|
-
@mcp.tool()
|
|
373
|
-
def process_list(items: list[int]) -> int:
|
|
374
|
-
return sum(items)
|
|
375
|
-
|
|
376
|
-
async with Client(mcp) as client:
|
|
377
|
-
with pytest.raises(
|
|
378
|
-
ClientError,
|
|
379
|
-
match="Input should be a valid list",
|
|
380
|
-
):
|
|
381
|
-
await client.call_tool("process_list", {"items": "['a', 'b', 3]"})
|
|
382
|
-
|
|
383
|
-
async def test_tool_dict_coercion(self):
|
|
384
|
-
"""Test JSON string to dict type coercion."""
|
|
385
|
-
mcp = FastMCP()
|
|
386
|
-
|
|
387
|
-
@mcp.tool()
|
|
388
|
-
def process_dict(data: dict[str, int]) -> int:
|
|
389
|
-
return sum(data.values())
|
|
390
|
-
|
|
391
|
-
async with Client(mcp) as client:
|
|
392
|
-
# JSON object string should be coerced to dict
|
|
393
|
-
result = await client.call_tool(
|
|
394
|
-
"process_dict", {"data": '{"a": 1, "b": "2", "c": 3}'}
|
|
395
|
-
)
|
|
396
|
-
assert isinstance(result[0], TextContent)
|
|
397
|
-
assert result[0].text == "6"
|
|
398
|
-
|
|
399
|
-
async def test_tool_set_coercion(self):
|
|
400
|
-
"""Test JSON string to set type coercion."""
|
|
401
|
-
mcp = FastMCP()
|
|
402
|
-
|
|
403
|
-
@mcp.tool()
|
|
404
|
-
def process_set(items: set[int]) -> int:
|
|
405
|
-
assert isinstance(items, set)
|
|
406
|
-
return sum(items)
|
|
407
|
-
|
|
408
|
-
async with Client(mcp) as client:
|
|
409
|
-
result = await client.call_tool("process_set", {"items": "[1, 2, 3, 4, 5]"})
|
|
410
|
-
assert isinstance(result[0], TextContent)
|
|
411
|
-
assert result[0].text == "15"
|
|
412
|
-
|
|
413
|
-
async def test_tool_tuple_coercion(self):
|
|
414
|
-
"""Test JSON string to tuple type coercion."""
|
|
415
|
-
mcp = FastMCP()
|
|
416
|
-
|
|
417
|
-
@mcp.tool()
|
|
418
|
-
def process_tuple(items: tuple[int, str]) -> int:
|
|
419
|
-
assert isinstance(items, tuple)
|
|
420
|
-
return items[0] + len(items[1])
|
|
421
|
-
|
|
422
|
-
async with Client(mcp) as client:
|
|
423
|
-
result = await client.call_tool("process_tuple", {"items": '["1", "two"]'})
|
|
424
|
-
assert isinstance(result[0], TextContent)
|
|
425
|
-
assert result[0].text == "4"
|
|
426
|
-
|
|
427
352
|
async def test_annotated_field_validation(self):
|
|
428
353
|
mcp = FastMCP()
|
|
429
354
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Tests for example servers"""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from mcp.types import (
|
|
5
|
+
PromptMessage,
|
|
6
|
+
TextContent,
|
|
7
|
+
TextResourceContents,
|
|
8
|
+
)
|
|
9
|
+
from pydantic import AnyUrl
|
|
10
|
+
|
|
11
|
+
from fastmcp import Client
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.anyio
|
|
15
|
+
async def test_simple_echo():
|
|
16
|
+
"""Test the simple echo server"""
|
|
17
|
+
from examples.simple_echo import mcp
|
|
18
|
+
|
|
19
|
+
async with Client(mcp) as client:
|
|
20
|
+
result = await client.call_tool("echo", {"text": "hello"})
|
|
21
|
+
assert len(result) == 1
|
|
22
|
+
assert isinstance(result[0], TextContent)
|
|
23
|
+
assert result[0].text == "hello"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.anyio
|
|
27
|
+
async def test_complex_inputs():
|
|
28
|
+
"""Test the complex inputs server"""
|
|
29
|
+
from examples.complex_inputs import mcp
|
|
30
|
+
|
|
31
|
+
async with Client(mcp) as client:
|
|
32
|
+
tank = {"shrimp": [{"name": "bob"}, {"name": "alice"}]}
|
|
33
|
+
result = await client.call_tool(
|
|
34
|
+
"name_shrimp", {"tank": tank, "extra_names": ["charlie"]}
|
|
35
|
+
)
|
|
36
|
+
assert len(result) == 1
|
|
37
|
+
assert isinstance(result[0], TextContent)
|
|
38
|
+
assert result[0].text == '[\n "bob",\n "alice",\n "charlie"\n]'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.anyio
|
|
42
|
+
async def test_desktop(monkeypatch):
|
|
43
|
+
"""Test the desktop server"""
|
|
44
|
+
from examples.desktop import mcp
|
|
45
|
+
|
|
46
|
+
async with Client(mcp) as client:
|
|
47
|
+
# Test the add function
|
|
48
|
+
result = await client.call_tool("add", {"a": 1, "b": 2})
|
|
49
|
+
assert len(result) == 1
|
|
50
|
+
assert isinstance(result[0], TextContent)
|
|
51
|
+
assert result[0].text == "3"
|
|
52
|
+
|
|
53
|
+
async with Client(mcp) as client:
|
|
54
|
+
result = await client.read_resource(AnyUrl("greeting://rooter12"))
|
|
55
|
+
assert len(result) == 1
|
|
56
|
+
assert isinstance(result[0], TextResourceContents)
|
|
57
|
+
assert isinstance(result[0].text, str)
|
|
58
|
+
assert result[0].text == "Hello, rooter12!"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.anyio
|
|
62
|
+
async def test_echo():
|
|
63
|
+
"""Test the echo server"""
|
|
64
|
+
from examples.echo import mcp
|
|
65
|
+
|
|
66
|
+
async with Client(mcp) as client:
|
|
67
|
+
result = await client.call_tool("echo_tool", {"text": "hello"})
|
|
68
|
+
assert len(result) == 1
|
|
69
|
+
assert isinstance(result[0], TextContent)
|
|
70
|
+
assert result[0].text == "hello"
|
|
71
|
+
|
|
72
|
+
async with Client(mcp) as client:
|
|
73
|
+
result = await client.read_resource(AnyUrl("echo://static"))
|
|
74
|
+
assert len(result) == 1
|
|
75
|
+
assert isinstance(result[0], TextResourceContents)
|
|
76
|
+
assert isinstance(result[0].text, str)
|
|
77
|
+
assert result[0].text == "Echo!"
|
|
78
|
+
|
|
79
|
+
async with Client(mcp) as client:
|
|
80
|
+
result = await client.read_resource(AnyUrl("echo://server42"))
|
|
81
|
+
assert len(result) == 1
|
|
82
|
+
assert isinstance(result[0], TextResourceContents)
|
|
83
|
+
assert isinstance(result[0].text, str)
|
|
84
|
+
assert result[0].text == "Echo: server42"
|
|
85
|
+
|
|
86
|
+
async with Client(mcp) as client:
|
|
87
|
+
result = await client.get_prompt("echo", {"text": "hello"})
|
|
88
|
+
assert len(result.messages) == 1
|
|
89
|
+
assert isinstance(result.messages[0], PromptMessage)
|
|
90
|
+
assert isinstance(result.messages[0].content, TextContent)
|
|
91
|
+
assert isinstance(result.messages[0].content.text, str)
|
|
92
|
+
assert result.messages[0].content.text == "hello"
|
|
@@ -2,8 +2,11 @@ import pytest
|
|
|
2
2
|
from mcp.types import ImageContent, TextContent
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
|
-
from fastmcp import Image
|
|
5
|
+
from fastmcp import FastMCP, Image
|
|
6
|
+
from fastmcp.client import Client
|
|
7
|
+
from fastmcp.exceptions import ClientError
|
|
6
8
|
from fastmcp.tools.tool import Tool
|
|
9
|
+
from fastmcp.utilities.tests import temporary_settings
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
class TestToolFromFunction:
|
|
@@ -150,9 +153,14 @@ class TestToolFromFunction:
|
|
|
150
153
|
x: int = 10
|
|
151
154
|
|
|
152
155
|
|
|
153
|
-
class
|
|
156
|
+
class TestLegacyToolJsonParsing:
|
|
154
157
|
"""Tests for Tool's JSON pre-parsing functionality."""
|
|
155
158
|
|
|
159
|
+
@pytest.fixture(autouse=True)
|
|
160
|
+
def enable_legacy_json_parsing(self):
|
|
161
|
+
with temporary_settings(tool_attempt_parse_json_args=True):
|
|
162
|
+
yield
|
|
163
|
+
|
|
156
164
|
async def test_json_string_arguments(self):
|
|
157
165
|
"""Test that JSON string arguments are parsed and validated correctly"""
|
|
158
166
|
|
|
@@ -264,3 +272,78 @@ class TestToolJsonParsing:
|
|
|
264
272
|
invalid_json = '{"x": 1, "y": {"invalid": "hello"}}'
|
|
265
273
|
with pytest.raises(Exception):
|
|
266
274
|
await tool.run({"data": invalid_json})
|
|
275
|
+
|
|
276
|
+
async def test_tool_list_coercion(self):
|
|
277
|
+
"""Test JSON string to collection type coercion."""
|
|
278
|
+
mcp = FastMCP()
|
|
279
|
+
|
|
280
|
+
@mcp.tool()
|
|
281
|
+
def process_list(items: list[int]) -> int:
|
|
282
|
+
return sum(items)
|
|
283
|
+
|
|
284
|
+
async with Client(mcp) as client:
|
|
285
|
+
# JSON array string should be coerced to list
|
|
286
|
+
result = await client.call_tool(
|
|
287
|
+
"process_list", {"items": "[1, 2, 3, 4, 5]"}
|
|
288
|
+
)
|
|
289
|
+
assert isinstance(result[0], TextContent)
|
|
290
|
+
assert result[0].text == "15"
|
|
291
|
+
|
|
292
|
+
async def test_tool_list_coercion_error(self):
|
|
293
|
+
"""Test that a list coercion error is raised if the input is not a valid list."""
|
|
294
|
+
mcp = FastMCP()
|
|
295
|
+
|
|
296
|
+
@mcp.tool()
|
|
297
|
+
def process_list(items: list[int]) -> int:
|
|
298
|
+
return sum(items)
|
|
299
|
+
|
|
300
|
+
async with Client(mcp) as client:
|
|
301
|
+
with pytest.raises(
|
|
302
|
+
ClientError,
|
|
303
|
+
match="Input should be a valid list",
|
|
304
|
+
):
|
|
305
|
+
await client.call_tool("process_list", {"items": "['a', 'b', 3]"})
|
|
306
|
+
|
|
307
|
+
async def test_tool_dict_coercion(self):
|
|
308
|
+
"""Test JSON string to dict type coercion."""
|
|
309
|
+
mcp = FastMCP()
|
|
310
|
+
|
|
311
|
+
@mcp.tool()
|
|
312
|
+
def process_dict(data: dict[str, int]) -> int:
|
|
313
|
+
return sum(data.values())
|
|
314
|
+
|
|
315
|
+
async with Client(mcp) as client:
|
|
316
|
+
# JSON object string should be coerced to dict
|
|
317
|
+
result = await client.call_tool(
|
|
318
|
+
"process_dict", {"data": '{"a": 1, "b": "2", "c": 3}'}
|
|
319
|
+
)
|
|
320
|
+
assert isinstance(result[0], TextContent)
|
|
321
|
+
assert result[0].text == "6"
|
|
322
|
+
|
|
323
|
+
async def test_tool_set_coercion(self):
|
|
324
|
+
"""Test JSON string to set type coercion."""
|
|
325
|
+
mcp = FastMCP()
|
|
326
|
+
|
|
327
|
+
@mcp.tool()
|
|
328
|
+
def process_set(items: set[int]) -> int:
|
|
329
|
+
assert isinstance(items, set)
|
|
330
|
+
return sum(items)
|
|
331
|
+
|
|
332
|
+
async with Client(mcp) as client:
|
|
333
|
+
result = await client.call_tool("process_set", {"items": "[1, 2, 3, 4, 5]"})
|
|
334
|
+
assert isinstance(result[0], TextContent)
|
|
335
|
+
assert result[0].text == "15"
|
|
336
|
+
|
|
337
|
+
async def test_tool_tuple_coercion(self):
|
|
338
|
+
"""Test JSON string to tuple type coercion."""
|
|
339
|
+
mcp = FastMCP()
|
|
340
|
+
|
|
341
|
+
@mcp.tool()
|
|
342
|
+
def process_tuple(items: tuple[int, str]) -> int:
|
|
343
|
+
assert isinstance(items, tuple)
|
|
344
|
+
return items[0] + len(items[1])
|
|
345
|
+
|
|
346
|
+
async with Client(mcp) as client:
|
|
347
|
+
result = await client.call_tool("process_tuple", {"items": '["1", "two"]'})
|
|
348
|
+
assert isinstance(result[0], TextContent)
|
|
349
|
+
assert result[0].text == "4"
|