fastmcp 2.3.3__tar.gz → 2.3.4__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.3.3 → fastmcp-2.3.4}/.github/workflows/run-tests.yml +3 -11
- {fastmcp-2.3.3 → fastmcp-2.3.4}/PKG-INFO +2 -2
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/clients/client.mdx +44 -1
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/servers/resources.mdx +38 -9
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/servers/tools.mdx +34 -12
- {fastmcp-2.3.3 → fastmcp-2.3.4}/pyproject.toml +1 -1
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/client/__init__.py +2 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/client/client.py +84 -21
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/client/transports.py +53 -28
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/exceptions.py +2 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/prompts/prompt.py +12 -6
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/resources/resource_manager.py +22 -1
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/resources/template.py +21 -17
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/resources/types.py +25 -27
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/server/openapi.py +14 -1
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/server/proxy.py +4 -4
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/server/server.py +73 -53
- fastmcp-2.3.4/src/fastmcp/settings.py +131 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/tools/tool.py +45 -45
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/tools/tool_manager.py +27 -2
- fastmcp-2.3.4/src/fastmcp/utilities/exceptions.py +49 -0
- fastmcp-2.3.4/src/fastmcp/utilities/json_schema.py +120 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/utilities/logging.py +11 -6
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/utilities/openapi.py +122 -7
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/client/test_client.py +143 -4
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/client/test_sse.py +56 -2
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/client/test_streamable_http.py +49 -2
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/contrib/test_bulk_tool_caller.py +1 -2
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/resources/test_file_resources.py +3 -2
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/resources/test_function_resources.py +1 -1
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/resources/test_resource_manager.py +85 -1
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/resources/test_resource_template.py +0 -15
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_openapi.py +121 -2
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_proxy.py +4 -5
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_server.py +22 -2
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_server_interactions.py +29 -28
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/test_deprecated.py +11 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/tools/test_tool.py +3 -3
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/tools/test_tool_manager.py +85 -1
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/utilities/openapi/test_openapi_fastapi.py +17 -0
- fastmcp-2.3.4/tests/utilities/test_json_schema.py +304 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/utilities/test_typeadapter.py +2 -2
- {fastmcp-2.3.3 → fastmcp-2.3.4}/uv.lock +4 -4
- fastmcp-2.3.3/src/fastmcp/settings.py +0 -105
- fastmcp-2.3.3/src/fastmcp/utilities/json_schema.py +0 -59
- fastmcp-2.3.3/tests/utilities/test_json_schema.py +0 -110
- {fastmcp-2.3.3 → fastmcp-2.3.4}/.cursor/rules/core-mcp-objects.mdc +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/.github/ISSUE_TEMPLATE/enhancement.yml +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/.github/release.yml +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/.github/workflows/publish.yml +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/.github/workflows/run-static.yml +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/.gitignore +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/.pre-commit-config.yaml +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/LICENSE +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/README.md +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/Windows_Notes.md +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/assets/demo-inspector.png +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/clients/transports.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/deployment/asgi.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/deployment/authentication.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/deployment/cli.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/deployment/running-server.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/docs.json +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/getting-started/installation.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/getting-started/quickstart.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/getting-started/welcome.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/patterns/contrib.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/patterns/decorating-methods.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/patterns/fastapi.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/patterns/http-requests.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/patterns/openapi.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/patterns/testing.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/servers/composition.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/servers/context.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/servers/fastmcp.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/servers/prompts.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/servers/proxy.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/snippets/version-badge.mdx +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/docs/style.css +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/complex_inputs.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/desktop.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/echo.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/memory.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/mount_example.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/sampling.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/screenshot.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/serializer.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/simple_echo.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/smart_home/README.md +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/smart_home/pyproject.toml +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/smart_home/src/smart_home/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/smart_home/src/smart_home/__main__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/smart_home/src/smart_home/hub.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/smart_home/src/smart_home/lights/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/smart_home/src/smart_home/lights/hue_utils.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/smart_home/src/smart_home/lights/server.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/smart_home/src/smart_home/py.typed +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/smart_home/src/smart_home/settings.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/smart_home/uv.lock +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/examples/text_me.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/justfile +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/cli/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/cli/claude.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/cli/cli.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/client/base.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/client/logging.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/client/roots.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/client/sampling.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/contrib/README.md +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/contrib/bulk_tool_caller/README.md +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/contrib/bulk_tool_caller/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/contrib/bulk_tool_caller/example.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/contrib/mcp_mixin/README.md +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/contrib/mcp_mixin/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/contrib/mcp_mixin/example.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/low_level/README.md +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/low_level/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/low_level/sse_server_transport.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/prompts/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/prompts/prompt_manager.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/py.typed +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/resources/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/resources/resource.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/server/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/server/context.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/server/dependencies.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/server/http.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/tools/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/utilities/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/utilities/cache.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/utilities/decorators.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/utilities/tests.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/src/fastmcp/utilities/types.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/cli/test_cli.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/cli/test_run.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/client/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/client/test_logs.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/client/test_roots.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/client/test_sampling.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/conftest.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/contrib/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/contrib/test_mcp_mixin.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/prompts/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/prompts/test_prompt.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/prompts/test_prompt_manager.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/resources/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/resources/test_resources.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_auth_integration.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_context.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_file_server.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_http_dependencies.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_http_middleware.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_import_server.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_lifespan.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_mount.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_run_server.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/server/test_tool_annotations.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/test_examples.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/test_servers/fastmcp_server.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/test_servers/sse.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/test_servers/stdio.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/tools/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/utilities/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/utilities/openapi/__init__.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/utilities/openapi/conftest.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/utilities/openapi/test_openapi.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/utilities/openapi/test_openapi_advanced.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/utilities/test_cache.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/utilities/test_decorated_function.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/utilities/test_logging.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/utilities/test_tests.py +0 -0
- {fastmcp-2.3.3 → fastmcp-2.3.4}/tests/utilities/test_types.py +0 -0
|
@@ -45,18 +45,10 @@ jobs:
|
|
|
45
45
|
with:
|
|
46
46
|
enable-cache: true
|
|
47
47
|
cache-dependency-glob: "uv.lock"
|
|
48
|
-
|
|
49
|
-
- name: Set up Python ${{ matrix.python-version }}
|
|
50
|
-
run: uv python install ${{ matrix.python-version }}
|
|
48
|
+
python-version: ${{ matrix.python-version }}
|
|
51
49
|
|
|
52
50
|
- name: Install FastMCP
|
|
53
|
-
run: uv sync --dev
|
|
54
|
-
|
|
55
|
-
- name: Fix pyreadline on Windows
|
|
56
|
-
if: matrix.os == 'windows-latest'
|
|
57
|
-
run: |
|
|
58
|
-
uv pip uninstall -y pyreadline
|
|
59
|
-
uv pip install pyreadline3
|
|
51
|
+
run: uv sync --dev --locked
|
|
60
52
|
|
|
61
53
|
- name: Run tests
|
|
62
|
-
run: uv run
|
|
54
|
+
run: uv run pytest
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastmcp
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.4
|
|
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
|
|
@@ -19,7 +19,7 @@ Classifier: Typing :: Typed
|
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Requires-Dist: exceptiongroup>=1.2.2
|
|
21
21
|
Requires-Dist: httpx>=0.28.1
|
|
22
|
-
Requires-Dist: mcp<2.0.0,>=1.8.
|
|
22
|
+
Requires-Dist: mcp<2.0.0,>=1.8.1
|
|
23
23
|
Requires-Dist: openapi-pydantic>=0.5.1
|
|
24
24
|
Requires-Dist: python-dotenv>=1.1.0
|
|
25
25
|
Requires-Dist: rich>=13.9.4
|
|
@@ -115,14 +115,18 @@ The standard client methods return user-friendly representations that may change
|
|
|
115
115
|
tools = await client.list_tools()
|
|
116
116
|
# tools -> list[mcp.types.Tool]
|
|
117
117
|
```
|
|
118
|
-
* **`call_tool(name: str, arguments: dict[str, Any] | None = None)`**: Executes a tool on the server.
|
|
118
|
+
* **`call_tool(name: str, arguments: dict[str, Any] | None = None, timeout: float | None = None)`**: Executes a tool on the server.
|
|
119
119
|
```python
|
|
120
120
|
result = await client.call_tool("add", {"a": 5, "b": 3})
|
|
121
121
|
# result -> list[mcp.types.TextContent | mcp.types.ImageContent | ...]
|
|
122
122
|
print(result[0].text) # Assuming TextContent, e.g., '8'
|
|
123
|
+
|
|
124
|
+
# With timeout (aborts if execution takes longer than 2 seconds)
|
|
125
|
+
result = await client.call_tool("long_running_task", {"param": "value"}, timeout=2.0)
|
|
123
126
|
```
|
|
124
127
|
* Arguments are passed as a dictionary. FastMCP servers automatically handle JSON string parsing for complex types if needed.
|
|
125
128
|
* Returns a list of content objects (usually `TextContent` or `ImageContent`).
|
|
129
|
+
* The optional `timeout` parameter limits the maximum execution time (in seconds) for this specific call, overriding any client-level timeout.
|
|
126
130
|
|
|
127
131
|
#### Resource Operations
|
|
128
132
|
|
|
@@ -191,6 +195,45 @@ These methods are especially useful for debugging or when you need to access met
|
|
|
191
195
|
|
|
192
196
|
MCP allows servers to interact with clients in order to provide additional capabilities. The `Client` constructor accepts additional configuration to handle these server requests.
|
|
193
197
|
|
|
198
|
+
#### Timeout Control
|
|
199
|
+
|
|
200
|
+
<VersionBadge version="2.3.4" />
|
|
201
|
+
|
|
202
|
+
You can control request timeouts at both the client level and individual request level:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from fastmcp import Client
|
|
206
|
+
from fastmcp.exceptions import McpError
|
|
207
|
+
|
|
208
|
+
# Client with a global 5-second timeout for all requests
|
|
209
|
+
client = Client(
|
|
210
|
+
my_mcp_server,
|
|
211
|
+
timeout=5.0 # Default timeout in seconds
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
async with client:
|
|
215
|
+
# This uses the global 5-second timeout
|
|
216
|
+
result1 = await client.call_tool("quick_task", {"param": "value"})
|
|
217
|
+
|
|
218
|
+
# This specifies a 10-second timeout for this specific call
|
|
219
|
+
result2 = await client.call_tool("slow_task", {"param": "value"}, timeout=10.0)
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
# This will likely timeout
|
|
223
|
+
result3 = await client.call_tool("medium_task", {"param": "value"}, timeout=0.01)
|
|
224
|
+
except McpError as e:
|
|
225
|
+
# Handle timeout error
|
|
226
|
+
print(f"The task timed out: {e}")
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
<Warning>
|
|
230
|
+
Timeout behavior varies between transport types:
|
|
231
|
+
|
|
232
|
+
- With **SSE** transport, the per-request (tool call) timeout **always** takes precedence, regardless of which is lower.
|
|
233
|
+
- With **HTTP** transport, the **lower** of the two timeouts (client or tool call) takes precedence.
|
|
234
|
+
|
|
235
|
+
For consistent behavior across all transports, we recommend explicitly setting timeouts at the individual tool call level when needed, rather than relying on client-level timeouts.
|
|
236
|
+
</Warning>
|
|
194
237
|
|
|
195
238
|
#### LLM Sampling
|
|
196
239
|
|
|
@@ -147,6 +147,7 @@ async def read_important_log() -> str:
|
|
|
147
147
|
return "Log file not found."
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
|
|
150
151
|
### Resource Classes
|
|
151
152
|
|
|
152
153
|
While `@mcp.resource` is ideal for dynamic content, you can directly register pre-defined resources (like static files or simple text) using `mcp.add_resource()` and concrete `Resource` subclasses.
|
|
@@ -403,17 +404,45 @@ In this stacked decorator pattern:
|
|
|
403
404
|
- Each parameter defaults to `None` when not included in the URI
|
|
404
405
|
- The function logic handles whichever parameter is provided
|
|
405
406
|
|
|
406
|
-
|
|
407
|
+
Templates provide a powerful way to expose parameterized data access points following REST-like principles.
|
|
408
|
+
|
|
409
|
+
## Error Handling
|
|
407
410
|
|
|
408
|
-
|
|
409
|
-
2. **Discovery:** Clients list templates via `resources/listResourceTemplates`.
|
|
410
|
-
3. **Request & Matching:** A client requests a specific URI, e.g., `weather://london/current`. FastMCP matches this to the `weather://{city}/current` template.
|
|
411
|
-
4. **Parameter Extraction:** It extracts the parameter value: `city="london"`.
|
|
412
|
-
5. **Type Conversion & Function Call:** It converts extracted values to the types hinted in the function and calls `get_weather(city="london")`.
|
|
413
|
-
6. **Default Values:** For any function parameters with default values not included in the URI template, FastMCP uses the default values.
|
|
414
|
-
7. **Response:** The function's return value is formatted (e.g., dict to JSON) and sent back as the resource content.
|
|
411
|
+
<VersionBadge version="2.3.4" />
|
|
415
412
|
|
|
416
|
-
|
|
413
|
+
If your resource function encounters an error, you can raise a standard Python exception (`ValueError`, `TypeError`, `FileNotFoundError`, custom exceptions, etc.) or a FastMCP `ResourceError`.
|
|
414
|
+
|
|
415
|
+
For security reasons, most exceptions are wrapped in a generic `ResourceError` before being sent to the client, with internal error details masked. However, if you raise a `ResourceError` directly, its contents **are** included in the response. This allows you to provide informative error messages to the client on an opt-in basis.
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
from fastmcp import FastMCP
|
|
419
|
+
from fastmcp.exceptions import ResourceError
|
|
420
|
+
|
|
421
|
+
mcp = FastMCP(name="DataServer")
|
|
422
|
+
|
|
423
|
+
@mcp.resource("resource://safe-error")
|
|
424
|
+
def fail_with_details() -> str:
|
|
425
|
+
"""This resource provides detailed error information."""
|
|
426
|
+
# ResourceError contents are sent back to clients
|
|
427
|
+
raise ResourceError("Unable to retrieve data: file not found")
|
|
428
|
+
|
|
429
|
+
@mcp.resource("resource://masked-error")
|
|
430
|
+
def fail_with_masked_details() -> str:
|
|
431
|
+
"""This resource masks internal error details."""
|
|
432
|
+
# Other exceptions are converted to ResourceError with generic message
|
|
433
|
+
raise ValueError("Sensitive internal file path: /etc/secrets.conf")
|
|
434
|
+
|
|
435
|
+
@mcp.resource("data://{id}")
|
|
436
|
+
def get_data_by_id(id: str) -> dict:
|
|
437
|
+
"""Template resources also support the same error handling pattern."""
|
|
438
|
+
if id == "secure":
|
|
439
|
+
raise ValueError("Cannot access secure data")
|
|
440
|
+
elif id == "missing":
|
|
441
|
+
raise ResourceError("Data ID 'missing' not found in database")
|
|
442
|
+
return {"id": id, "value": "data"}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
This error handling pattern applies to both regular resources and resource templates.
|
|
417
446
|
|
|
418
447
|
## Server Behavior
|
|
419
448
|
|
|
@@ -248,27 +248,30 @@ def do_nothing() -> None:
|
|
|
248
248
|
|
|
249
249
|
### Error Handling
|
|
250
250
|
|
|
251
|
-
|
|
251
|
+
<VersionBadge version="2.3.4" />
|
|
252
|
+
|
|
253
|
+
If your tool encounters an error, you can raise a standard Python exception (`ValueError`, `TypeError`, `FileNotFoundError`, custom exceptions, etc.) or a FastMCP `ToolError`.
|
|
254
|
+
|
|
255
|
+
In all cases, the exception is logged and converted into an MCP error response to be sent back to the client LLM. For security reasons, the error message is **not** included in the response by default. However, if you raise a `ToolError`, the contents of the exception **are** included in the response. This allows you to provide informative error messages to the client LLM on an opt-in basis, which can help the LLM understand failures and react appropriately.
|
|
256
|
+
|
|
257
|
+
```python {2, 10, 14}
|
|
258
|
+
from fastmcp import FastMCP
|
|
259
|
+
from fastmcp.exceptions import ToolError
|
|
252
260
|
|
|
253
|
-
```python
|
|
254
261
|
@mcp.tool()
|
|
255
262
|
def divide(a: float, b: float) -> float:
|
|
256
263
|
"""Divide a by b."""
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
raise ValueError("Division by zero is not allowed.")
|
|
264
|
+
|
|
265
|
+
# Python exceptions raise errors but the contents are not sent to clients
|
|
260
266
|
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
|
|
261
267
|
raise TypeError("Both arguments must be numbers.")
|
|
268
|
+
|
|
269
|
+
if b == 0:
|
|
270
|
+
# ToolError contents are sent back to clients
|
|
271
|
+
raise ToolError("Division by zero is not allowed.")
|
|
262
272
|
return a / b
|
|
263
273
|
```
|
|
264
274
|
|
|
265
|
-
FastMCP automatically catches exceptions raised within your tool function:
|
|
266
|
-
1. It converts the exception into an MCP error response, typically including the exception type and message.
|
|
267
|
-
2. This error response is sent back to the client/LLM.
|
|
268
|
-
3. The LLM can then inform the user or potentially try the tool again with different arguments.
|
|
269
|
-
|
|
270
|
-
Using informative exceptions helps the LLM understand failures and react appropriately.
|
|
271
|
-
|
|
272
275
|
### Annotations
|
|
273
276
|
|
|
274
277
|
<VersionBadge version="2.2.7" />
|
|
@@ -709,6 +712,25 @@ The duplicate behavior options are:
|
|
|
709
712
|
- `"replace"`: Silently replaces the existing tool with the new one.
|
|
710
713
|
- `"ignore"`: Keeps the original tool and ignores the new registration attempt.
|
|
711
714
|
|
|
715
|
+
### Removing Tools
|
|
716
|
+
|
|
717
|
+
<VersionBadge version="2.3.4" />
|
|
718
|
+
|
|
719
|
+
You can dynamically remove tools from a server using the `remove_tool` method:
|
|
720
|
+
|
|
721
|
+
```python
|
|
722
|
+
from fastmcp import FastMCP
|
|
723
|
+
|
|
724
|
+
mcp = FastMCP(name="DynamicToolServer")
|
|
725
|
+
|
|
726
|
+
@mcp.tool()
|
|
727
|
+
def calculate_sum(a: int, b: int) -> int:
|
|
728
|
+
"""Add two numbers together."""
|
|
729
|
+
return a + b
|
|
730
|
+
|
|
731
|
+
mcp.remove_tool("calculate_sum")
|
|
732
|
+
```
|
|
733
|
+
|
|
712
734
|
### Legacy JSON Parsing
|
|
713
735
|
|
|
714
736
|
<VersionBadge version="2.2.10" />
|
|
@@ -9,6 +9,7 @@ from .transports import (
|
|
|
9
9
|
UvxStdioTransport,
|
|
10
10
|
NpxStdioTransport,
|
|
11
11
|
FastMCPTransport,
|
|
12
|
+
StreamableHttpTransport,
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
@@ -22,4 +23,5 @@ __all__ = [
|
|
|
22
23
|
"UvxStdioTransport",
|
|
23
24
|
"NpxStdioTransport",
|
|
24
25
|
"FastMCPTransport",
|
|
26
|
+
"StreamableHttpTransport",
|
|
25
27
|
]
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import datetime
|
|
2
|
-
from contextlib import
|
|
2
|
+
from contextlib import AsyncExitStack
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Any, cast
|
|
5
5
|
|
|
6
6
|
import mcp.types
|
|
7
|
+
from exceptiongroup import catch
|
|
7
8
|
from mcp import ClientSession
|
|
8
9
|
from pydantic import AnyUrl
|
|
9
10
|
|
|
@@ -14,8 +15,9 @@ from fastmcp.client.roots import (
|
|
|
14
15
|
create_roots_callback,
|
|
15
16
|
)
|
|
16
17
|
from fastmcp.client.sampling import SamplingHandler, create_sampling_callback
|
|
17
|
-
from fastmcp.exceptions import
|
|
18
|
+
from fastmcp.exceptions import ToolError
|
|
18
19
|
from fastmcp.server import FastMCP
|
|
20
|
+
from fastmcp.utilities.exceptions import get_catch_handlers
|
|
19
21
|
|
|
20
22
|
from .transports import ClientTransport, SessionKwargs, infer_transport
|
|
21
23
|
|
|
@@ -33,8 +35,35 @@ class Client:
|
|
|
33
35
|
"""
|
|
34
36
|
MCP client that delegates connection management to a Transport instance.
|
|
35
37
|
|
|
36
|
-
The Client class is
|
|
37
|
-
|
|
38
|
+
The Client class is responsible for MCP protocol logic, while the Transport
|
|
39
|
+
handles connection establishment and management. Client provides methods
|
|
40
|
+
for working with resources, prompts, tools and other MCP capabilities.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
transport: Connection source specification, which can be:
|
|
44
|
+
- ClientTransport: Direct transport instance
|
|
45
|
+
- FastMCP: In-process FastMCP server
|
|
46
|
+
- AnyUrl | str: URL to connect to
|
|
47
|
+
- Path: File path for local socket
|
|
48
|
+
- dict: Transport configuration
|
|
49
|
+
roots: Optional RootsList or RootsHandler for filesystem access
|
|
50
|
+
sampling_handler: Optional handler for sampling requests
|
|
51
|
+
log_handler: Optional handler for log messages
|
|
52
|
+
message_handler: Optional handler for protocol messages
|
|
53
|
+
timeout: Optional timeout for requests (seconds or timedelta)
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
```python
|
|
57
|
+
# Connect to FastMCP server
|
|
58
|
+
client = Client("http://localhost:8080")
|
|
59
|
+
|
|
60
|
+
async with client:
|
|
61
|
+
# List available resources
|
|
62
|
+
resources = await client.list_resources()
|
|
63
|
+
|
|
64
|
+
# Call a tool
|
|
65
|
+
result = await client.call_tool("my_tool", {"param": "value"})
|
|
66
|
+
```
|
|
38
67
|
"""
|
|
39
68
|
|
|
40
69
|
def __init__(
|
|
@@ -45,19 +74,22 @@ class Client:
|
|
|
45
74
|
sampling_handler: SamplingHandler | None = None,
|
|
46
75
|
log_handler: LogHandler | None = None,
|
|
47
76
|
message_handler: MessageHandler | None = None,
|
|
48
|
-
|
|
77
|
+
timeout: datetime.timedelta | float | int | None = None,
|
|
49
78
|
):
|
|
50
79
|
self.transport = infer_transport(transport)
|
|
51
80
|
self._session: ClientSession | None = None
|
|
52
|
-
self.
|
|
81
|
+
self._exit_stack: AsyncExitStack | None = None
|
|
53
82
|
self._nesting_counter: int = 0
|
|
54
83
|
|
|
84
|
+
if isinstance(timeout, int | float):
|
|
85
|
+
timeout = datetime.timedelta(seconds=timeout)
|
|
86
|
+
|
|
55
87
|
self._session_kwargs: SessionKwargs = {
|
|
56
88
|
"sampling_callback": None,
|
|
57
89
|
"list_roots_callback": None,
|
|
58
90
|
"logging_callback": log_handler,
|
|
59
91
|
"message_handler": message_handler,
|
|
60
|
-
"read_timeout_seconds":
|
|
92
|
+
"read_timeout_seconds": timeout,
|
|
61
93
|
}
|
|
62
94
|
|
|
63
95
|
if roots is not None:
|
|
@@ -91,9 +123,23 @@ class Client:
|
|
|
91
123
|
|
|
92
124
|
async def __aenter__(self):
|
|
93
125
|
if self._nesting_counter == 0:
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
126
|
+
# Create exit stack to manage both context managers
|
|
127
|
+
stack = AsyncExitStack()
|
|
128
|
+
await stack.__aenter__()
|
|
129
|
+
|
|
130
|
+
# Add the exception handling context
|
|
131
|
+
stack.enter_context(catch(get_catch_handlers()))
|
|
132
|
+
|
|
133
|
+
# the above catch will only apply once this __aenter__ finishes so
|
|
134
|
+
# we need to wrap the session creation in a new context in case it
|
|
135
|
+
# raises errors itself
|
|
136
|
+
with catch(get_catch_handlers()):
|
|
137
|
+
# Create and enter the transport session using the exit stack
|
|
138
|
+
session_cm = self.transport.connect_session(**self._session_kwargs)
|
|
139
|
+
self._session = await stack.enter_async_context(session_cm)
|
|
140
|
+
|
|
141
|
+
# Store the stack for cleanup in __aexit__
|
|
142
|
+
self._exit_stack = stack
|
|
97
143
|
|
|
98
144
|
self._nesting_counter += 1
|
|
99
145
|
return self
|
|
@@ -101,10 +147,14 @@ class Client:
|
|
|
101
147
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
102
148
|
self._nesting_counter -= 1
|
|
103
149
|
|
|
104
|
-
if self._nesting_counter == 0
|
|
105
|
-
|
|
106
|
-
self.
|
|
107
|
-
|
|
150
|
+
if self._nesting_counter == 0:
|
|
151
|
+
# Exit the stack which will handle cleaning up the session
|
|
152
|
+
if self._exit_stack is not None:
|
|
153
|
+
try:
|
|
154
|
+
await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)
|
|
155
|
+
finally:
|
|
156
|
+
self._exit_stack = None
|
|
157
|
+
self._session = None
|
|
108
158
|
|
|
109
159
|
# --- MCP Client Methods ---
|
|
110
160
|
|
|
@@ -377,7 +427,10 @@ class Client:
|
|
|
377
427
|
# --- Call Tool ---
|
|
378
428
|
|
|
379
429
|
async def call_tool_mcp(
|
|
380
|
-
self,
|
|
430
|
+
self,
|
|
431
|
+
name: str,
|
|
432
|
+
arguments: dict[str, Any],
|
|
433
|
+
timeout: datetime.timedelta | float | int | None = None,
|
|
381
434
|
) -> mcp.types.CallToolResult:
|
|
382
435
|
"""Send a tools/call request and return the complete MCP protocol result.
|
|
383
436
|
|
|
@@ -387,7 +440,7 @@ class Client:
|
|
|
387
440
|
Args:
|
|
388
441
|
name (str): The name of the tool to call.
|
|
389
442
|
arguments (dict[str, Any]): Arguments to pass to the tool.
|
|
390
|
-
|
|
443
|
+
timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
|
|
391
444
|
Returns:
|
|
392
445
|
mcp.types.CallToolResult: The complete response object from the protocol,
|
|
393
446
|
containing the tool result and any additional metadata.
|
|
@@ -395,19 +448,25 @@ class Client:
|
|
|
395
448
|
Raises:
|
|
396
449
|
RuntimeError: If called while the client is not connected.
|
|
397
450
|
"""
|
|
398
|
-
|
|
451
|
+
|
|
452
|
+
if isinstance(timeout, int | float):
|
|
453
|
+
timeout = datetime.timedelta(seconds=timeout)
|
|
454
|
+
result = await self.session.call_tool(
|
|
455
|
+
name=name, arguments=arguments, read_timeout_seconds=timeout
|
|
456
|
+
)
|
|
399
457
|
return result
|
|
400
458
|
|
|
401
459
|
async def call_tool(
|
|
402
460
|
self,
|
|
403
461
|
name: str,
|
|
404
462
|
arguments: dict[str, Any] | None = None,
|
|
463
|
+
timeout: datetime.timedelta | float | int | None = None,
|
|
405
464
|
) -> list[
|
|
406
465
|
mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
|
|
407
466
|
]:
|
|
408
467
|
"""Call a tool on the server.
|
|
409
468
|
|
|
410
|
-
Unlike call_tool_mcp, this method raises a
|
|
469
|
+
Unlike call_tool_mcp, this method raises a ToolError if the tool call results in an error.
|
|
411
470
|
|
|
412
471
|
Args:
|
|
413
472
|
name (str): The name of the tool to call.
|
|
@@ -418,11 +477,15 @@ class Client:
|
|
|
418
477
|
The content returned by the tool.
|
|
419
478
|
|
|
420
479
|
Raises:
|
|
421
|
-
|
|
480
|
+
ToolError: If the tool call results in an error.
|
|
422
481
|
RuntimeError: If called while the client is not connected.
|
|
423
482
|
"""
|
|
424
|
-
result = await self.call_tool_mcp(
|
|
483
|
+
result = await self.call_tool_mcp(
|
|
484
|
+
name=name,
|
|
485
|
+
arguments=arguments or {},
|
|
486
|
+
timeout=timeout,
|
|
487
|
+
)
|
|
425
488
|
if result.isError:
|
|
426
489
|
msg = cast(mcp.types.TextContent, result.content[0]).text
|
|
427
|
-
raise
|
|
490
|
+
raise ToolError(msg)
|
|
428
491
|
return result.content
|
|
@@ -8,10 +8,9 @@ import sys
|
|
|
8
8
|
import warnings
|
|
9
9
|
from collections.abc import AsyncIterator
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any, TypedDict
|
|
11
|
+
from typing import Any, TypedDict, cast
|
|
12
12
|
|
|
13
|
-
from
|
|
14
|
-
from mcp import ClientSession, McpError, StdioServerParameters
|
|
13
|
+
from mcp import ClientSession, StdioServerParameters
|
|
15
14
|
from mcp.client.session import (
|
|
16
15
|
ListRootsFnT,
|
|
17
16
|
LoggingFnT,
|
|
@@ -26,7 +25,6 @@ from mcp.shared.memory import create_connected_server_and_client_session
|
|
|
26
25
|
from pydantic import AnyUrl
|
|
27
26
|
from typing_extensions import Unpack
|
|
28
27
|
|
|
29
|
-
from fastmcp.exceptions import ClientError
|
|
30
28
|
from fastmcp.server import FastMCP as FastMCPServer
|
|
31
29
|
|
|
32
30
|
|
|
@@ -104,7 +102,12 @@ class WSTransport(ClientTransport):
|
|
|
104
102
|
class SSETransport(ClientTransport):
|
|
105
103
|
"""Transport implementation that connects to an MCP server via Server-Sent Events."""
|
|
106
104
|
|
|
107
|
-
def __init__(
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
url: str | AnyUrl,
|
|
108
|
+
headers: dict[str, str] | None = None,
|
|
109
|
+
sse_read_timeout: datetime.timedelta | float | int | None = None,
|
|
110
|
+
):
|
|
108
111
|
if isinstance(url, AnyUrl):
|
|
109
112
|
url = str(url)
|
|
110
113
|
if not isinstance(url, str) or not url.startswith("http"):
|
|
@@ -112,11 +115,28 @@ class SSETransport(ClientTransport):
|
|
|
112
115
|
self.url = url
|
|
113
116
|
self.headers = headers or {}
|
|
114
117
|
|
|
118
|
+
if isinstance(sse_read_timeout, int | float):
|
|
119
|
+
sse_read_timeout = datetime.timedelta(seconds=sse_read_timeout)
|
|
120
|
+
self.sse_read_timeout = sse_read_timeout
|
|
121
|
+
|
|
115
122
|
@contextlib.asynccontextmanager
|
|
116
123
|
async def connect_session(
|
|
117
124
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
118
125
|
) -> AsyncIterator[ClientSession]:
|
|
119
|
-
|
|
126
|
+
client_kwargs = {}
|
|
127
|
+
# sse_read_timeout has a default value set, so we can't pass None without overriding it
|
|
128
|
+
# instead we simply leave the kwarg out if it's not provided
|
|
129
|
+
if self.sse_read_timeout is not None:
|
|
130
|
+
client_kwargs["sse_read_timeout"] = self.sse_read_timeout.total_seconds()
|
|
131
|
+
if session_kwargs.get("read_timeout_seconds", None) is not None:
|
|
132
|
+
read_timeout_seconds = cast(
|
|
133
|
+
datetime.timedelta, session_kwargs.get("read_timeout_seconds")
|
|
134
|
+
)
|
|
135
|
+
client_kwargs["timeout"] = read_timeout_seconds.total_seconds()
|
|
136
|
+
|
|
137
|
+
async with sse_client(
|
|
138
|
+
self.url, headers=self.headers, **client_kwargs
|
|
139
|
+
) as transport:
|
|
120
140
|
read_stream, write_stream = transport
|
|
121
141
|
async with ClientSession(
|
|
122
142
|
read_stream, write_stream, **session_kwargs
|
|
@@ -131,7 +151,12 @@ class SSETransport(ClientTransport):
|
|
|
131
151
|
class StreamableHttpTransport(ClientTransport):
|
|
132
152
|
"""Transport implementation that connects to an MCP server via Streamable HTTP Requests."""
|
|
133
153
|
|
|
134
|
-
def __init__(
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
url: str | AnyUrl,
|
|
157
|
+
headers: dict[str, str] | None = None,
|
|
158
|
+
sse_read_timeout: datetime.timedelta | float | int | None = None,
|
|
159
|
+
):
|
|
135
160
|
if isinstance(url, AnyUrl):
|
|
136
161
|
url = str(url)
|
|
137
162
|
if not isinstance(url, str) or not url.startswith("http"):
|
|
@@ -139,11 +164,25 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
139
164
|
self.url = url
|
|
140
165
|
self.headers = headers or {}
|
|
141
166
|
|
|
167
|
+
if isinstance(sse_read_timeout, int | float):
|
|
168
|
+
sse_read_timeout = datetime.timedelta(seconds=sse_read_timeout)
|
|
169
|
+
self.sse_read_timeout = sse_read_timeout
|
|
170
|
+
|
|
142
171
|
@contextlib.asynccontextmanager
|
|
143
172
|
async def connect_session(
|
|
144
173
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
145
174
|
) -> AsyncIterator[ClientSession]:
|
|
146
|
-
|
|
175
|
+
client_kwargs = {}
|
|
176
|
+
# sse_read_timeout has a default value set, so we can't pass None without overriding it
|
|
177
|
+
# instead we simply leave the kwarg out if it's not provided
|
|
178
|
+
if self.sse_read_timeout is not None:
|
|
179
|
+
client_kwargs["sse_read_timeout"] = self.sse_read_timeout
|
|
180
|
+
if session_kwargs.get("read_timeout_seconds", None) is not None:
|
|
181
|
+
client_kwargs["timeout"] = session_kwargs.get("read_timeout_seconds")
|
|
182
|
+
|
|
183
|
+
async with streamablehttp_client(
|
|
184
|
+
self.url, headers=self.headers, **client_kwargs
|
|
185
|
+
) as transport:
|
|
147
186
|
read_stream, write_stream, _ = transport
|
|
148
187
|
async with ClientSession(
|
|
149
188
|
read_stream, write_stream, **session_kwargs
|
|
@@ -418,26 +457,12 @@ class FastMCPTransport(ClientTransport):
|
|
|
418
457
|
async def connect_session(
|
|
419
458
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
420
459
|
) -> AsyncIterator[ClientSession]:
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
def mcperror_handler(excgroup: BaseExceptionGroup):
|
|
428
|
-
for exc in excgroup.exceptions:
|
|
429
|
-
if isinstance(exc, BaseExceptionGroup):
|
|
430
|
-
mcperror_handler(exc)
|
|
431
|
-
raise ClientError(exc)
|
|
432
|
-
|
|
433
|
-
# backport of 3.11's except* syntax
|
|
434
|
-
with catch({McpError: mcperror_handler, Exception: exception_handler}):
|
|
435
|
-
# create_connected_server_and_client_session manages the session lifecycle itself
|
|
436
|
-
async with create_connected_server_and_client_session(
|
|
437
|
-
server=self._fastmcp._mcp_server,
|
|
438
|
-
**session_kwargs,
|
|
439
|
-
) as session:
|
|
440
|
-
yield session
|
|
460
|
+
# create_connected_server_and_client_session manages the session lifecycle itself
|
|
461
|
+
async with create_connected_server_and_client_session(
|
|
462
|
+
server=self._fastmcp._mcp_server,
|
|
463
|
+
**session_kwargs,
|
|
464
|
+
) as session:
|
|
465
|
+
yield session
|
|
441
466
|
|
|
442
467
|
def __repr__(self) -> str:
|
|
443
468
|
return f"<FastMCP(server='{self._fastmcp.name}')>"
|
|
@@ -13,7 +13,8 @@ from mcp.types import PromptArgument as MCPPromptArgument
|
|
|
13
13
|
from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
|
|
14
14
|
|
|
15
15
|
from fastmcp.server.dependencies import get_context
|
|
16
|
-
from fastmcp.utilities.json_schema import
|
|
16
|
+
from fastmcp.utilities.json_schema import compress_schema
|
|
17
|
+
from fastmcp.utilities.logging import get_logger
|
|
17
18
|
from fastmcp.utilities.types import (
|
|
18
19
|
_convert_set_defaults,
|
|
19
20
|
find_kwarg_by_type,
|
|
@@ -25,6 +26,8 @@ if TYPE_CHECKING:
|
|
|
25
26
|
|
|
26
27
|
CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
|
|
27
28
|
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
28
31
|
|
|
29
32
|
def Message(
|
|
30
33
|
content: str | CONTENT_TYPES, role: Role | None = None, **kwargs: Any
|
|
@@ -112,7 +115,11 @@ class Prompt(BaseModel):
|
|
|
112
115
|
|
|
113
116
|
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
114
117
|
if context_kwarg:
|
|
115
|
-
|
|
118
|
+
prune_params = [context_kwarg]
|
|
119
|
+
else:
|
|
120
|
+
prune_params = None
|
|
121
|
+
|
|
122
|
+
parameters = compress_schema(parameters, prune_params=prune_params)
|
|
116
123
|
|
|
117
124
|
# Convert parameters to PromptArguments
|
|
118
125
|
arguments: list[PromptArgument] = []
|
|
@@ -192,13 +199,12 @@ class Prompt(BaseModel):
|
|
|
192
199
|
)
|
|
193
200
|
)
|
|
194
201
|
except Exception:
|
|
195
|
-
raise ValueError(
|
|
196
|
-
f"Could not convert prompt result to message: {msg}"
|
|
197
|
-
)
|
|
202
|
+
raise ValueError("Could not convert prompt result to message.")
|
|
198
203
|
|
|
199
204
|
return messages
|
|
200
205
|
except Exception as e:
|
|
201
|
-
|
|
206
|
+
logger.exception(f"Error rendering prompt {self.name}: {e}")
|
|
207
|
+
raise ValueError(f"Error rendering prompt {self.name}.")
|
|
202
208
|
|
|
203
209
|
def __eq__(self, other: object) -> bool:
|
|
204
210
|
if not isinstance(other, Prompt):
|