fastmcp 2.2.9__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.9 → fastmcp-2.2.10}/PKG-INFO +5 -1
- {fastmcp-2.2.9 → fastmcp-2.2.10}/README.md +4 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/docs.json +5 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/servers/tools.mdx +10 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/desktop.py +7 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/pyproject.toml +2 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/settings.py +13 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/tools/tool.py +28 -12
- fastmcp-2.2.10/src/fastmcp/utilities/tests.py +41 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/types.py +4 -7
- {fastmcp-2.2.9 → 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.9 → fastmcp-2.2.10}/tests/tools/test_tool.py +85 -2
- {fastmcp-2.2.9 → 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.9 → fastmcp-2.2.10}/uv.lock +91 -0
- fastmcp-2.2.9/examples/readme-quickstart.py +0 -18
- {fastmcp-2.2.9 → fastmcp-2.2.10}/.cursor/rules/core-mcp-objects.mdc +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/ISSUE_TEMPLATE/enhancement.yml +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/release.yml +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/workflows/publish.yml +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/workflows/run-static.yml +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/workflows/run-tests.yml +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/.gitignore +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/.pre-commit-config.yaml +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/LICENSE +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/Windows_Notes.md +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/assets/demo-inspector.png +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/clients/client.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/clients/transports.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/getting-started/installation.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/getting-started/quickstart.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/getting-started/welcome.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/composition.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/contrib.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/decorating-methods.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/fastapi.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/openapi.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/proxy.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/testing.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/servers/context.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/servers/fastmcp.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/servers/prompts.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/servers/resources.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/snippets/version-badge.mdx +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/style.css +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/complex_inputs.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/echo.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/memory.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/mount_example.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/sampling.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/screenshot.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/serializer.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/simple_echo.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/README.md +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/pyproject.toml +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/__main__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/hub.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/lights/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/lights/hue_utils.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/lights/server.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/py.typed +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/settings.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/uv.lock +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/text_me.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/justfile +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/cli/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/cli/claude.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/cli/cli.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/base.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/client.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/logging.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/roots.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/sampling.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/transports.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/README.md +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/README.md +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/example.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/README.md +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/example.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/exceptions.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/prompts/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/prompts/prompt.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/prompts/prompt_manager.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/py.typed +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/resources/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/resources/resource.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/resources/resource_manager.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/resources/template.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/resources/types.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/server/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/server/context.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/server/openapi.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/server/proxy.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/server/server.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/tools/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/tools/tool_manager.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/decorators.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/http.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/json_schema.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/logging.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/openapi.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/cli/test_run.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/client/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/client/test_client.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/client/test_logs.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/client/test_roots.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/client/test_sampling.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/conftest.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/contrib/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/contrib/test_bulk_tool_caller.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/contrib/test_mcp_mixin.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/prompts/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/prompts/test_prompt.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/prompts/test_prompt_manager.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/resources/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/resources/test_file_resources.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/resources/test_function_resources.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/resources/test_resource_manager.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/resources/test_resource_template.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/resources/test_resources.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_auth_integration.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_file_server.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_import_server.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_lifespan.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_mount.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_openapi.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_proxy.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_run_server.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_server.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_tool_annotations.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/test_servers/fastmcp_server.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/test_servers/sse.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/test_servers/stdio.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/tools/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/openapi/__init__.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/openapi/conftest.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/openapi/test_openapi.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/openapi/test_openapi_advanced.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/openapi/test_openapi_fastapi.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/test_decorated_function.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/test_json_schema.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/test_logging.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/test_typeadapter.py +0 -0
- {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/test_types.py +0 -0
|
@@ -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
|
|
|
@@ -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"""
|
|
@@ -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,22 +116,36 @@ 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
|
|
|
130
|
-
type_adapter = get_cached_typeadapter(
|
|
131
|
-
self.fn, config=frozenset([("coerce_numbers_to_str", True)])
|
|
132
|
-
)
|
|
148
|
+
type_adapter = get_cached_typeadapter(self.fn)
|
|
133
149
|
result = type_adapter.validate_python(parsed_args | injected_args)
|
|
134
150
|
if inspect.isawaitable(result):
|
|
135
151
|
result = await result
|
|
@@ -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])
|
|
@@ -6,26 +6,23 @@ from collections.abc import Callable
|
|
|
6
6
|
from functools import lru_cache
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from types import UnionType
|
|
9
|
-
from typing import Annotated,
|
|
9
|
+
from typing import Annotated, TypeVar, Union, get_args, get_origin
|
|
10
10
|
|
|
11
11
|
from mcp.types import ImageContent
|
|
12
|
-
from pydantic import
|
|
12
|
+
from pydantic import TypeAdapter
|
|
13
13
|
|
|
14
14
|
T = TypeVar("T")
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
@lru_cache(maxsize=5000)
|
|
18
|
-
def get_cached_typeadapter(
|
|
19
|
-
cls: T, config: frozenset[tuple[str, Any]] | None = None
|
|
20
|
-
) -> TypeAdapter[T]:
|
|
18
|
+
def get_cached_typeadapter(cls: T) -> TypeAdapter[T]:
|
|
21
19
|
"""
|
|
22
20
|
TypeAdapters are heavy objects, and in an application context we'd typically
|
|
23
21
|
create them once in a global scope and reuse them as often as possible.
|
|
24
22
|
However, this isn't feasible for user-generated functions. Instead, we use a
|
|
25
23
|
cache to minimize the cost of creating them as much as possible.
|
|
26
24
|
"""
|
|
27
|
-
|
|
28
|
-
return TypeAdapter(cls, config=ConfigDict(**config_dict))
|
|
25
|
+
return TypeAdapter(cls)
|
|
29
26
|
|
|
30
27
|
|
|
31
28
|
def issubclass_safe(cls: type, base: type) -> bool:
|
|
@@ -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"
|
|
@@ -14,6 +14,7 @@ from fastmcp import Context, FastMCP, Image
|
|
|
14
14
|
from fastmcp.exceptions import NotFoundError, ToolError
|
|
15
15
|
from fastmcp.tools import ToolManager
|
|
16
16
|
from fastmcp.tools.tool import Tool
|
|
17
|
+
from fastmcp.utilities.tests import temporary_settings
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class TestAddTools:
|
|
@@ -320,14 +321,6 @@ class TestCallTools:
|
|
|
320
321
|
|
|
321
322
|
manager = ToolManager()
|
|
322
323
|
manager.add_tool_from_fn(sum_vals)
|
|
323
|
-
# Try both with plain list and with JSON list
|
|
324
|
-
|
|
325
|
-
result = await manager.call_tool("sum_vals", {"vals": "[1, 2, 3]"})
|
|
326
|
-
assert isinstance(result, list)
|
|
327
|
-
assert len(result) == 1
|
|
328
|
-
assert isinstance(result[0], TextContent)
|
|
329
|
-
assert result[0].text == "6"
|
|
330
|
-
assert json.loads(result[0].text) == 6
|
|
331
324
|
|
|
332
325
|
result = await manager.call_tool("sum_vals", {"vals": [1, 2, 3]})
|
|
333
326
|
assert isinstance(result, list)
|
|
@@ -336,6 +329,24 @@ class TestCallTools:
|
|
|
336
329
|
assert result[0].text == "6"
|
|
337
330
|
assert json.loads(result[0].text) == 6
|
|
338
331
|
|
|
332
|
+
async def test_call_tool_with_list_int_input_legacy_behavior(self):
|
|
333
|
+
"""Legacy behavior -- parse a stringified JSON object"""
|
|
334
|
+
|
|
335
|
+
def sum_vals(vals: list[int]) -> int:
|
|
336
|
+
return sum(vals)
|
|
337
|
+
|
|
338
|
+
manager = ToolManager()
|
|
339
|
+
manager.add_tool_from_fn(sum_vals)
|
|
340
|
+
# Try both with plain list and with JSON list
|
|
341
|
+
|
|
342
|
+
with temporary_settings(tool_attempt_parse_json_args=True):
|
|
343
|
+
result = await manager.call_tool("sum_vals", {"vals": "[1, 2, 3]"})
|
|
344
|
+
assert isinstance(result, list)
|
|
345
|
+
assert len(result) == 1
|
|
346
|
+
assert isinstance(result[0], TextContent)
|
|
347
|
+
assert result[0].text == "6"
|
|
348
|
+
assert json.loads(result[0].text) == 6
|
|
349
|
+
|
|
339
350
|
async def test_call_tool_with_list_str_or_str_input(self):
|
|
340
351
|
def concat_strs(vals: list[str] | str) -> str:
|
|
341
352
|
return vals if isinstance(vals, str) else "".join(vals)
|
|
@@ -350,23 +361,33 @@ class TestCallTools:
|
|
|
350
361
|
assert isinstance(result[0], TextContent)
|
|
351
362
|
assert result[0].text == "abc"
|
|
352
363
|
|
|
353
|
-
result = await manager.call_tool("concat_strs", {"vals": '["a", "b", "c"]'})
|
|
354
|
-
assert isinstance(result, list)
|
|
355
|
-
assert len(result) == 1
|
|
356
|
-
assert isinstance(result[0], TextContent)
|
|
357
|
-
assert result[0].text == "abc"
|
|
358
|
-
|
|
359
364
|
result = await manager.call_tool("concat_strs", {"vals": "a"})
|
|
360
365
|
assert isinstance(result, list)
|
|
361
366
|
assert len(result) == 1
|
|
362
367
|
assert isinstance(result[0], TextContent)
|
|
363
368
|
assert result[0].text == "a"
|
|
364
369
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
+
async def test_call_tool_with_list_str_or_str_input_legacy_behavior(self):
|
|
371
|
+
"""Legacy behavior -- parse a stringified JSON object"""
|
|
372
|
+
|
|
373
|
+
def concat_strs(vals: list[str] | str) -> str:
|
|
374
|
+
return vals if isinstance(vals, str) else "".join(vals)
|
|
375
|
+
|
|
376
|
+
manager = ToolManager()
|
|
377
|
+
manager.add_tool_from_fn(concat_strs)
|
|
378
|
+
|
|
379
|
+
with temporary_settings(tool_attempt_parse_json_args=True):
|
|
380
|
+
result = await manager.call_tool("concat_strs", {"vals": '["a", "b", "c"]'})
|
|
381
|
+
assert isinstance(result, list)
|
|
382
|
+
assert len(result) == 1
|
|
383
|
+
assert isinstance(result[0], TextContent)
|
|
384
|
+
assert result[0].text == "abc"
|
|
385
|
+
|
|
386
|
+
result = await manager.call_tool("concat_strs", {"vals": '"a"'})
|
|
387
|
+
assert isinstance(result, list)
|
|
388
|
+
assert len(result) == 1
|
|
389
|
+
assert isinstance(result[0], TextContent)
|
|
390
|
+
assert result[0].text == "a"
|
|
370
391
|
|
|
371
392
|
async def test_call_tool_with_complex_model(self):
|
|
372
393
|
class MyShrimpTank(BaseModel):
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import fastmcp
|
|
2
|
+
from fastmcp.utilities.tests import temporary_settings
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestTemporarySettings:
|
|
6
|
+
def test_temporary_settings(self):
|
|
7
|
+
with temporary_settings(log_level="DEBUG"):
|
|
8
|
+
assert fastmcp.settings.settings.log_level == "DEBUG"
|
|
9
|
+
assert fastmcp.settings.settings.log_level == "INFO"
|