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.
Files changed (158) hide show
  1. {fastmcp-2.2.9 → fastmcp-2.2.10}/PKG-INFO +5 -1
  2. {fastmcp-2.2.9 → fastmcp-2.2.10}/README.md +4 -0
  3. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/docs.json +5 -0
  4. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/servers/tools.mdx +10 -0
  5. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/desktop.py +7 -0
  6. {fastmcp-2.2.9 → fastmcp-2.2.10}/pyproject.toml +2 -0
  7. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/settings.py +13 -0
  8. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/tools/tool.py +28 -12
  9. fastmcp-2.2.10/src/fastmcp/utilities/tests.py +41 -0
  10. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/types.py +4 -7
  11. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_server_interactions.py +0 -75
  12. fastmcp-2.2.10/tests/test_examples.py +92 -0
  13. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/tools/test_tool.py +85 -2
  14. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/tools/test_tool_manager.py +40 -19
  15. fastmcp-2.2.10/tests/utilities/test_tests.py +9 -0
  16. {fastmcp-2.2.9 → fastmcp-2.2.10}/uv.lock +91 -0
  17. fastmcp-2.2.9/examples/readme-quickstart.py +0 -18
  18. {fastmcp-2.2.9 → fastmcp-2.2.10}/.cursor/rules/core-mcp-objects.mdc +0 -0
  19. {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  20. {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  21. {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/ISSUE_TEMPLATE/enhancement.yml +0 -0
  22. {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/release.yml +0 -0
  23. {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/workflows/publish.yml +0 -0
  24. {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/workflows/run-static.yml +0 -0
  25. {fastmcp-2.2.9 → fastmcp-2.2.10}/.github/workflows/run-tests.yml +0 -0
  26. {fastmcp-2.2.9 → fastmcp-2.2.10}/.gitignore +0 -0
  27. {fastmcp-2.2.9 → fastmcp-2.2.10}/.pre-commit-config.yaml +0 -0
  28. {fastmcp-2.2.9 → fastmcp-2.2.10}/LICENSE +0 -0
  29. {fastmcp-2.2.9 → fastmcp-2.2.10}/Windows_Notes.md +0 -0
  30. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/assets/demo-inspector.png +0 -0
  31. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/clients/client.mdx +0 -0
  32. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/clients/transports.mdx +0 -0
  33. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/getting-started/installation.mdx +0 -0
  34. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/getting-started/quickstart.mdx +0 -0
  35. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/getting-started/welcome.mdx +0 -0
  36. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/composition.mdx +0 -0
  37. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/contrib.mdx +0 -0
  38. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/decorating-methods.mdx +0 -0
  39. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/fastapi.mdx +0 -0
  40. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/openapi.mdx +0 -0
  41. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/proxy.mdx +0 -0
  42. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/patterns/testing.mdx +0 -0
  43. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/servers/context.mdx +0 -0
  44. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/servers/fastmcp.mdx +0 -0
  45. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/servers/prompts.mdx +0 -0
  46. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/servers/resources.mdx +0 -0
  47. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/snippets/version-badge.mdx +0 -0
  48. {fastmcp-2.2.9 → fastmcp-2.2.10}/docs/style.css +0 -0
  49. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/complex_inputs.py +0 -0
  50. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/echo.py +0 -0
  51. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/memory.py +0 -0
  52. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/mount_example.py +0 -0
  53. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/sampling.py +0 -0
  54. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/screenshot.py +0 -0
  55. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/serializer.py +0 -0
  56. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/simple_echo.py +0 -0
  57. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/README.md +0 -0
  58. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/pyproject.toml +0 -0
  59. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/__init__.py +0 -0
  60. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/__main__.py +0 -0
  61. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/hub.py +0 -0
  62. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/lights/__init__.py +0 -0
  63. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/lights/hue_utils.py +0 -0
  64. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/lights/server.py +0 -0
  65. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/py.typed +0 -0
  66. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/settings.py +0 -0
  67. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/smart_home/uv.lock +0 -0
  68. {fastmcp-2.2.9 → fastmcp-2.2.10}/examples/text_me.py +0 -0
  69. {fastmcp-2.2.9 → fastmcp-2.2.10}/justfile +0 -0
  70. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/__init__.py +0 -0
  71. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/cli/__init__.py +0 -0
  72. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/cli/claude.py +0 -0
  73. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/cli/cli.py +0 -0
  74. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/__init__.py +0 -0
  75. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/base.py +0 -0
  76. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/client.py +0 -0
  77. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/logging.py +0 -0
  78. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/roots.py +0 -0
  79. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/sampling.py +0 -0
  80. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/client/transports.py +0 -0
  81. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/README.md +0 -0
  82. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/README.md +0 -0
  83. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/__init__.py +0 -0
  84. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py +0 -0
  85. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/example.py +0 -0
  86. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/README.md +0 -0
  87. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/__init__.py +0 -0
  88. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/example.py +0 -0
  89. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py +0 -0
  90. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/exceptions.py +0 -0
  91. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/prompts/__init__.py +0 -0
  92. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/prompts/prompt.py +0 -0
  93. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/prompts/prompt_manager.py +0 -0
  94. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/py.typed +0 -0
  95. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/resources/__init__.py +0 -0
  96. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/resources/resource.py +0 -0
  97. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/resources/resource_manager.py +0 -0
  98. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/resources/template.py +0 -0
  99. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/resources/types.py +0 -0
  100. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/server/__init__.py +0 -0
  101. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/server/context.py +0 -0
  102. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/server/openapi.py +0 -0
  103. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/server/proxy.py +0 -0
  104. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/server/server.py +0 -0
  105. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/tools/__init__.py +0 -0
  106. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/tools/tool_manager.py +0 -0
  107. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/__init__.py +0 -0
  108. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/decorators.py +0 -0
  109. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/http.py +0 -0
  110. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/json_schema.py +0 -0
  111. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/logging.py +0 -0
  112. {fastmcp-2.2.9 → fastmcp-2.2.10}/src/fastmcp/utilities/openapi.py +0 -0
  113. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/__init__.py +0 -0
  114. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/cli/test_run.py +0 -0
  115. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/client/__init__.py +0 -0
  116. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/client/test_client.py +0 -0
  117. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/client/test_logs.py +0 -0
  118. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/client/test_roots.py +0 -0
  119. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/client/test_sampling.py +0 -0
  120. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/conftest.py +0 -0
  121. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/contrib/__init__.py +0 -0
  122. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/contrib/test_bulk_tool_caller.py +0 -0
  123. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/contrib/test_mcp_mixin.py +0 -0
  124. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/prompts/__init__.py +0 -0
  125. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/prompts/test_prompt.py +0 -0
  126. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/prompts/test_prompt_manager.py +0 -0
  127. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/resources/__init__.py +0 -0
  128. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/resources/test_file_resources.py +0 -0
  129. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/resources/test_function_resources.py +0 -0
  130. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/resources/test_resource_manager.py +0 -0
  131. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/resources/test_resource_template.py +0 -0
  132. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/resources/test_resources.py +0 -0
  133. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/__init__.py +0 -0
  134. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_auth_integration.py +0 -0
  135. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_file_server.py +0 -0
  136. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_import_server.py +0 -0
  137. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_lifespan.py +0 -0
  138. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_mount.py +0 -0
  139. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_openapi.py +0 -0
  140. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_proxy.py +0 -0
  141. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_run_server.py +0 -0
  142. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_server.py +0 -0
  143. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/server/test_tool_annotations.py +0 -0
  144. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/test_servers/fastmcp_server.py +0 -0
  145. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/test_servers/sse.py +0 -0
  146. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/test_servers/stdio.py +0 -0
  147. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/tools/__init__.py +0 -0
  148. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/__init__.py +0 -0
  149. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/openapi/__init__.py +0 -0
  150. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/openapi/conftest.py +0 -0
  151. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/openapi/test_openapi.py +0 -0
  152. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/openapi/test_openapi_advanced.py +0 -0
  153. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/openapi/test_openapi_fastapi.py +0 -0
  154. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/test_decorated_function.py +0 -0
  155. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/test_json_schema.py +0 -0
  156. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/test_logging.py +0 -0
  157. {fastmcp-2.2.9 → fastmcp-2.2.10}/tests/utilities/test_typeadapter.py +0 -0
  158. {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.9
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
 
@@ -350,6 +350,10 @@ Run tests using pytest:
350
350
  ```bash
351
351
  pytest
352
352
  ```
353
+ or if you want an overview of the code coverage
354
+ ```bash
355
+ uv run pytest --cov=src --cov=examples --cov-report=html
356
+ ```
353
357
 
354
358
  ### Static Checks
355
359
 
@@ -20,6 +20,11 @@
20
20
  "x": "https://x.com/jlowin"
21
21
  }
22
22
  },
23
+ "integrations": {
24
+ "ga4": {
25
+ "measurementId": "G-64R5W1TJXG"
26
+ }
27
+ },
23
28
  "name": "FastMCP",
24
29
  "navbar": {
25
30
  "primary": {
@@ -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"""
@@ -47,7 +47,9 @@ dev = [
47
47
  "pyright>=1.1.389",
48
48
  "pytest>=8.3.3",
49
49
  "pytest-asyncio>=0.23.5",
50
+ "pytest-cov>=6.1.1",
50
51
  "pytest-flakefinder",
52
+ "pytest-report>=0.2.1",
51
53
  "pytest-xdist>=3.6.1",
52
54
  "ruff",
53
55
  ]
@@ -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
- # Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
118
- # being passed in as JSON inside a string rather than an actual list.
119
- #
120
- # Claude desktop is prone to this - in fact it seems incapable of NOT doing
121
- # this. For sub-models, it tends to pass dicts (JSON objects) as JSON strings,
122
- # which can be pre-parsed here.
123
- for param_name in self.parameters["properties"]:
124
- if isinstance(parsed_args.get(param_name, None), str):
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(parsed_args[param_name])
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, Any, TypeVar, Union, get_args, get_origin
9
+ from typing import Annotated, TypeVar, Union, get_args, get_origin
10
10
 
11
11
  from mcp.types import ImageContent
12
- from pydantic import ConfigDict, TypeAdapter
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
- config_dict = dict(config or {})
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 TestToolJsonParsing:
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
- result = await manager.call_tool("concat_strs", {"vals": '"a"'})
366
- assert isinstance(result, list)
367
- assert len(result) == 1
368
- assert isinstance(result[0], TextContent)
369
- assert result[0].text == "a"
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"