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.
Files changed (158) hide show
  1. {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/ISSUE_TEMPLATE/bug.yml +7 -2
  2. {fastmcp-2.2.8 → fastmcp-2.2.10}/PKG-INFO +5 -1
  3. {fastmcp-2.2.8 → fastmcp-2.2.10}/README.md +4 -0
  4. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/clients/client.mdx +2 -0
  5. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/docs.json +5 -0
  6. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/getting-started/quickstart.mdx +2 -1
  7. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/servers/tools.mdx +10 -0
  8. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/desktop.py +7 -0
  9. {fastmcp-2.2.8 → fastmcp-2.2.10}/pyproject.toml +2 -0
  10. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/server/server.py +15 -1
  11. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/settings.py +13 -0
  12. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/tools/tool.py +27 -9
  13. fastmcp-2.2.10/src/fastmcp/utilities/tests.py +41 -0
  14. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_import_server.py +23 -0
  15. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_mount.py +20 -0
  16. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_server_interactions.py +0 -75
  17. fastmcp-2.2.10/tests/test_examples.py +92 -0
  18. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/tools/test_tool.py +85 -2
  19. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/tools/test_tool_manager.py +40 -19
  20. fastmcp-2.2.10/tests/utilities/test_tests.py +9 -0
  21. {fastmcp-2.2.8 → fastmcp-2.2.10}/uv.lock +91 -0
  22. fastmcp-2.2.8/examples/readme-quickstart.py +0 -18
  23. {fastmcp-2.2.8 → fastmcp-2.2.10}/.cursor/rules/core-mcp-objects.mdc +0 -0
  24. {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  25. {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/ISSUE_TEMPLATE/enhancement.yml +0 -0
  26. {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/release.yml +0 -0
  27. {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/workflows/publish.yml +0 -0
  28. {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/workflows/run-static.yml +0 -0
  29. {fastmcp-2.2.8 → fastmcp-2.2.10}/.github/workflows/run-tests.yml +0 -0
  30. {fastmcp-2.2.8 → fastmcp-2.2.10}/.gitignore +0 -0
  31. {fastmcp-2.2.8 → fastmcp-2.2.10}/.pre-commit-config.yaml +0 -0
  32. {fastmcp-2.2.8 → fastmcp-2.2.10}/LICENSE +0 -0
  33. {fastmcp-2.2.8 → fastmcp-2.2.10}/Windows_Notes.md +0 -0
  34. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/assets/demo-inspector.png +0 -0
  35. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/clients/transports.mdx +0 -0
  36. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/getting-started/installation.mdx +0 -0
  37. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/getting-started/welcome.mdx +0 -0
  38. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/composition.mdx +0 -0
  39. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/contrib.mdx +0 -0
  40. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/decorating-methods.mdx +0 -0
  41. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/fastapi.mdx +0 -0
  42. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/openapi.mdx +0 -0
  43. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/proxy.mdx +0 -0
  44. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/patterns/testing.mdx +0 -0
  45. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/servers/context.mdx +0 -0
  46. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/servers/fastmcp.mdx +0 -0
  47. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/servers/prompts.mdx +0 -0
  48. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/servers/resources.mdx +0 -0
  49. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/snippets/version-badge.mdx +0 -0
  50. {fastmcp-2.2.8 → fastmcp-2.2.10}/docs/style.css +0 -0
  51. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/complex_inputs.py +0 -0
  52. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/echo.py +0 -0
  53. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/memory.py +0 -0
  54. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/mount_example.py +0 -0
  55. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/sampling.py +0 -0
  56. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/screenshot.py +0 -0
  57. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/serializer.py +0 -0
  58. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/simple_echo.py +0 -0
  59. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/README.md +0 -0
  60. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/pyproject.toml +0 -0
  61. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/__init__.py +0 -0
  62. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/__main__.py +0 -0
  63. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/hub.py +0 -0
  64. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/lights/__init__.py +0 -0
  65. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/lights/hue_utils.py +0 -0
  66. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/lights/server.py +0 -0
  67. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/py.typed +0 -0
  68. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/src/smart_home/settings.py +0 -0
  69. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/smart_home/uv.lock +0 -0
  70. {fastmcp-2.2.8 → fastmcp-2.2.10}/examples/text_me.py +0 -0
  71. {fastmcp-2.2.8 → fastmcp-2.2.10}/justfile +0 -0
  72. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/__init__.py +0 -0
  73. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/cli/__init__.py +0 -0
  74. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/cli/claude.py +0 -0
  75. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/cli/cli.py +0 -0
  76. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/__init__.py +0 -0
  77. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/base.py +0 -0
  78. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/client.py +0 -0
  79. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/logging.py +0 -0
  80. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/roots.py +0 -0
  81. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/sampling.py +0 -0
  82. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/client/transports.py +0 -0
  83. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/README.md +0 -0
  84. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/README.md +0 -0
  85. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/__init__.py +0 -0
  86. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py +0 -0
  87. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/bulk_tool_caller/example.py +0 -0
  88. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/README.md +0 -0
  89. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/__init__.py +0 -0
  90. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/example.py +0 -0
  91. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py +0 -0
  92. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/exceptions.py +0 -0
  93. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/prompts/__init__.py +0 -0
  94. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/prompts/prompt.py +0 -0
  95. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/prompts/prompt_manager.py +0 -0
  96. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/py.typed +0 -0
  97. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/resources/__init__.py +0 -0
  98. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/resources/resource.py +0 -0
  99. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/resources/resource_manager.py +0 -0
  100. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/resources/template.py +0 -0
  101. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/resources/types.py +0 -0
  102. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/server/__init__.py +0 -0
  103. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/server/context.py +0 -0
  104. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/server/openapi.py +0 -0
  105. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/server/proxy.py +0 -0
  106. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/tools/__init__.py +0 -0
  107. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/tools/tool_manager.py +0 -0
  108. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/__init__.py +0 -0
  109. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/decorators.py +0 -0
  110. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/http.py +0 -0
  111. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/json_schema.py +0 -0
  112. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/logging.py +0 -0
  113. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/openapi.py +0 -0
  114. {fastmcp-2.2.8 → fastmcp-2.2.10}/src/fastmcp/utilities/types.py +0 -0
  115. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/__init__.py +0 -0
  116. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/cli/test_run.py +0 -0
  117. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/client/__init__.py +0 -0
  118. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/client/test_client.py +0 -0
  119. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/client/test_logs.py +0 -0
  120. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/client/test_roots.py +0 -0
  121. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/client/test_sampling.py +0 -0
  122. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/conftest.py +0 -0
  123. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/contrib/__init__.py +0 -0
  124. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/contrib/test_bulk_tool_caller.py +0 -0
  125. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/contrib/test_mcp_mixin.py +0 -0
  126. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/prompts/__init__.py +0 -0
  127. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/prompts/test_prompt.py +0 -0
  128. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/prompts/test_prompt_manager.py +0 -0
  129. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/resources/__init__.py +0 -0
  130. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/resources/test_file_resources.py +0 -0
  131. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/resources/test_function_resources.py +0 -0
  132. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/resources/test_resource_manager.py +0 -0
  133. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/resources/test_resource_template.py +0 -0
  134. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/resources/test_resources.py +0 -0
  135. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/__init__.py +0 -0
  136. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_auth_integration.py +0 -0
  137. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_file_server.py +0 -0
  138. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_lifespan.py +0 -0
  139. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_openapi.py +0 -0
  140. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_proxy.py +0 -0
  141. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_run_server.py +0 -0
  142. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_server.py +0 -0
  143. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/server/test_tool_annotations.py +0 -0
  144. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/test_servers/fastmcp_server.py +0 -0
  145. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/test_servers/sse.py +0 -0
  146. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/test_servers/stdio.py +0 -0
  147. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/tools/__init__.py +0 -0
  148. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/__init__.py +0 -0
  149. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/openapi/__init__.py +0 -0
  150. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/openapi/conftest.py +0 -0
  151. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/openapi/test_openapi.py +0 -0
  152. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/openapi/test_openapi_advanced.py +0 -0
  153. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/openapi/test_openapi_fastapi.py +0 -0
  154. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/test_decorated_function.py +0 -0
  155. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/test_json_schema.py +0 -0
  156. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/test_logging.py +0 -0
  157. {fastmcp-2.2.8 → fastmcp-2.2.10}/tests/utilities/test_typeadapter.py +0 -0
  158. {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.8
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
 
@@ -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>
@@ -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": {
@@ -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, 9-16}
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"""
@@ -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
  ]
@@ -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.networks import AnyUrl
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
- # 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
 
@@ -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 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"