fastmcp 2.2.5__tar.gz → 2.2.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastmcp-2.2.5 → fastmcp-2.2.6}/PKG-INFO +2 -2
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/clients/client.mdx +71 -64
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/docs.json +2 -1
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/patterns/fastapi.mdx +35 -14
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/patterns/openapi.mdx +97 -27
- fastmcp-2.2.6/docs/patterns/testing.mdx +38 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/lights/server.py +25 -13
- {fastmcp-2.2.5 → fastmcp-2.2.6}/pyproject.toml +1 -1
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/__init__.py +1 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/client/client.py +12 -8
- fastmcp-2.2.6/src/fastmcp/client/logging.py +13 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/client/sampling.py +2 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/client/transports.py +37 -4
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/server/context.py +15 -13
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/server/openapi.py +67 -28
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/server/proxy.py +1 -1
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/server/server.py +18 -12
- fastmcp-2.2.6/tests/client/test_logs.py +60 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_openapi.py +203 -5
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_server_interactions.py +3 -40
- fastmcp-2.2.6/tests/tools/__init__.py +0 -0
- fastmcp-2.2.5/src/fastmcp/client/base.py +0 -1
- {fastmcp-2.2.5 → fastmcp-2.2.6}/.cursor/rules/core-mcp-objects.mdc +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/ISSUE_TEMPLATE/enhancement.yml +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/release.yml +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/workflows/publish.yml +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/workflows/run-static.yml +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/workflows/run-tests.yml +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/.gitignore +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/.pre-commit-config.yaml +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/LICENSE +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/README.md +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/Windows_Notes.md +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/assets/demo-inspector.png +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/clients/transports.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/getting-started/installation.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/getting-started/quickstart.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/getting-started/welcome.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/patterns/composition.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/patterns/contrib.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/patterns/decorating-methods.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/patterns/proxy.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/servers/context.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/servers/fastmcp.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/servers/prompts.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/servers/resources.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/servers/tools.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/snippets/version-badge.mdx +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/style.css +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/complex_inputs.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/desktop.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/echo.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/memory.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/mount_example.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/readme-quickstart.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/sampling.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/screenshot.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/simple_echo.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/README.md +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/pyproject.toml +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/__main__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/hub.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/lights/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/lights/hue_utils.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/py.typed +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/settings.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/uv.lock +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/text_me.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/justfile +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/cli/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/cli/claude.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/cli/cli.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/client/__init__.py +0 -0
- /fastmcp-2.2.5/src/fastmcp/py.typed → /fastmcp-2.2.6/src/fastmcp/client/base.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/client/roots.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/README.md +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/bulk_tool_caller/README.md +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/bulk_tool_caller/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/bulk_tool_caller/example.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/mcp_mixin/README.md +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/mcp_mixin/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/mcp_mixin/example.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/exceptions.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/prompts/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/prompts/prompt.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/prompts/prompt_manager.py +0 -0
- /fastmcp-2.2.5/tests/__init__.py → /fastmcp-2.2.6/src/fastmcp/py.typed +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/resources/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/resources/resource.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/resources/resource_manager.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/resources/template.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/resources/types.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/server/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/settings.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/tools/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/tools/tool.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/tools/tool_manager.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/utilities/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/utilities/decorators.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/utilities/func_metadata.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/utilities/logging.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/utilities/openapi.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/utilities/types.py +0 -0
- {fastmcp-2.2.5/tests/prompts → fastmcp-2.2.6/tests}/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/cli/test_run.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/client/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/client/test_client.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/client/test_roots.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/client/test_sampling.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/conftest.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/contrib/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/contrib/test_bulk_tool_caller.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/contrib/test_mcp_mixin.py +0 -0
- {fastmcp-2.2.5/tests/resources → fastmcp-2.2.6/tests/prompts}/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/prompts/test_base.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/prompts/test_prompt_manager.py +0 -0
- {fastmcp-2.2.5/tests/server → fastmcp-2.2.6/tests/resources}/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/resources/test_file_resources.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/resources/test_function_resources.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/resources/test_resource_manager.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/resources/test_resource_template.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/resources/test_resources.py +0 -0
- {fastmcp-2.2.5/tests/tools → fastmcp-2.2.6/tests/server}/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_file_server.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_import_server.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_lifespan.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_mount.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_proxy.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_run_server.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_server.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/test_servers/fastmcp_server.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/test_servers/sse.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/test_servers/stdio.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/tools/test_tool_manager.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/openapi/__init__.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/openapi/conftest.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/openapi/test_openapi.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/openapi/test_openapi_advanced.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/openapi/test_openapi_fastapi.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/test_decorated_function.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/test_func_metadata.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/test_logging.py +0 -0
- {fastmcp-2.2.5 → fastmcp-2.2.6}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastmcp
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.6
|
|
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
|
|
@@ -24,7 +24,7 @@ Requires-Dist: openapi-pydantic>=0.5.1
|
|
|
24
24
|
Requires-Dist: python-dotenv>=1.1.0
|
|
25
25
|
Requires-Dist: rich>=13.9.4
|
|
26
26
|
Requires-Dist: typer>=0.15.2
|
|
27
|
-
Requires-Dist: websockets>=
|
|
27
|
+
Requires-Dist: websockets>=14.0
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
30
30
|
<div align="center">
|
|
@@ -149,83 +149,90 @@ The `Client` provides methods corresponding to standard MCP requests:
|
|
|
149
149
|
* **`list_prompts()`**: Retrieves available prompt templates.
|
|
150
150
|
* **`get_prompt(name: str, arguments: dict[str, Any] | None = None)`**: Retrieves a rendered prompt message list.
|
|
151
151
|
|
|
152
|
-
###
|
|
152
|
+
### Advanced Features
|
|
153
153
|
|
|
154
|
-
MCP allows servers to
|
|
154
|
+
MCP allows servers to interact with clients in order to provide additional capabilities. The `Client` constructor accepts additional configuration to handle these server requests.
|
|
155
155
|
|
|
156
|
-
#### Roots
|
|
157
156
|
|
|
158
|
-
|
|
159
|
-
```python
|
|
160
|
-
from pathlib import Path
|
|
161
|
-
from fastmcp.client.roots import RootsHandler, RootsList
|
|
162
|
-
from mcp.shared.context import RequestContext # For type hint
|
|
163
|
-
|
|
164
|
-
# Option 1: Static list
|
|
165
|
-
static_roots: RootsList = [str(Path.home() / "Documents")]
|
|
166
|
-
|
|
167
|
-
# Option 2: Dynamic function
|
|
168
|
-
def dynamic_roots_handler(context: RequestContext) -> RootsList:
|
|
169
|
-
# Logic to determine accessible roots based on context
|
|
170
|
-
print(f"Server requested roots (Request ID: {context.request_id})")
|
|
171
|
-
return [str(Path.home() / "Downloads")]
|
|
172
|
-
|
|
173
|
-
client_with_roots = Client(
|
|
174
|
-
"my_server.py",
|
|
175
|
-
roots=dynamic_roots_handler # or roots=static_roots
|
|
176
|
-
)
|
|
157
|
+
#### LLM Sampling
|
|
177
158
|
|
|
178
|
-
|
|
179
|
-
# async with client_with_roots:
|
|
180
|
-
# await client_with_roots.send_roots_list_changed()
|
|
181
|
-
```
|
|
182
|
-
See `fastmcp.client.roots` for helpers.
|
|
159
|
+
MCP Servers can request LLM completions from clients. The client can provide a `sampling_handler` to handle these requests. The sampling handler receives a list of messages and other parameters from the server, and should return a string completion.
|
|
183
160
|
|
|
184
|
-
|
|
161
|
+
The following example uses the `marvin` library to generate a completion:
|
|
185
162
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
response_text = f"LLM processed: {prompt[:50]}..."
|
|
204
|
-
# Return simple string (becomes TextContent) or a MessageResult object
|
|
205
|
-
return response_text
|
|
206
|
-
|
|
207
|
-
client_with_sampling = Client(
|
|
208
|
-
"my_server.py",
|
|
209
|
-
sampling_handler=my_llm_handler
|
|
163
|
+
```python {8-17, 21}
|
|
164
|
+
import marvin
|
|
165
|
+
from fastmcp import Client
|
|
166
|
+
from fastmcp.client.sampling import (
|
|
167
|
+
SamplingMessage,
|
|
168
|
+
SamplingParams,
|
|
169
|
+
RequestContext,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
async def sampling_handler(
|
|
173
|
+
messages: list[SamplingMessage],
|
|
174
|
+
params: SamplingParams,
|
|
175
|
+
context: RequestContext
|
|
176
|
+
) -> str:
|
|
177
|
+
return await marvin.say_async(
|
|
178
|
+
message=[m.content.text for m in messages],
|
|
179
|
+
instructions=params.systemPrompt,
|
|
210
180
|
)
|
|
211
|
-
|
|
212
|
-
|
|
181
|
+
|
|
182
|
+
client = Client(
|
|
183
|
+
...,
|
|
184
|
+
sampling_handler=sampling_handler,
|
|
185
|
+
)
|
|
186
|
+
```
|
|
213
187
|
|
|
214
188
|
#### Logging
|
|
215
189
|
|
|
216
|
-
|
|
217
|
-
```python
|
|
218
|
-
from mcp.client.session import LoggingFnT, LogLevel
|
|
190
|
+
MCP servers can emit logs to clients. The client can set a logging callback to receive these logs.
|
|
219
191
|
|
|
220
|
-
|
|
221
|
-
|
|
192
|
+
```python {4-5, 9}
|
|
193
|
+
from fastmcp import Client
|
|
194
|
+
from fastmcp.client.logging import LogHandler, LogMessage
|
|
222
195
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
log_handler=my_log_handler
|
|
226
|
-
)
|
|
227
|
-
```
|
|
196
|
+
async def my_log_handler(params: LogMessage):
|
|
197
|
+
print(f"[Server Log - {params.level.upper()}] {params.logger or 'default'}: {params.data}")
|
|
228
198
|
|
|
199
|
+
client_with_logging = Client(
|
|
200
|
+
...,
|
|
201
|
+
log_handler=my_log_handler,
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### Roots
|
|
206
|
+
|
|
207
|
+
Roots are a way for clients to inform servers about the resources they have access to or certain boundaries on their access. The server can use this information to adjust behavior or provide more accurate responses.
|
|
208
|
+
|
|
209
|
+
Servers can request roots from clients, and clients can notify servers when their roots change.
|
|
210
|
+
|
|
211
|
+
To set the roots when creating a client, users can either provide a list of roots (which can be a list of strings) or an async function that returns a list of roots.
|
|
212
|
+
|
|
213
|
+
<CodeGroup>
|
|
214
|
+
```python Static Roots {5}
|
|
215
|
+
from fastmcp import Client
|
|
216
|
+
|
|
217
|
+
client = Client(
|
|
218
|
+
...,
|
|
219
|
+
roots=["/path/to/root1", "/path/to/root2"],
|
|
220
|
+
)
|
|
221
|
+
```
|
|
222
|
+
```python Dynamic Roots Callback {4-6, 10}
|
|
223
|
+
from fastmcp import Client
|
|
224
|
+
from fastmcp.client.roots import RequestContext
|
|
225
|
+
|
|
226
|
+
async def roots_callback(context: RequestContext) -> list[str]:
|
|
227
|
+
print(f"Server requested roots (Request ID: {context.request_id})")
|
|
228
|
+
return ["/path/to/root1", "/path/to/root2"]
|
|
229
|
+
|
|
230
|
+
client = Client(
|
|
231
|
+
...,
|
|
232
|
+
roots=roots_callback,
|
|
233
|
+
)
|
|
234
|
+
```
|
|
235
|
+
</CodeGroup>
|
|
229
236
|
### Utility Methods
|
|
230
237
|
|
|
231
238
|
* **`ping()`**: Sends a ping request to the server to verify connectivity.
|
|
@@ -44,6 +44,19 @@ if __name__ == "__main__":
|
|
|
44
44
|
mcp.run() # Start the MCP server
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
## Configuration Options
|
|
48
|
+
|
|
49
|
+
### Timeout
|
|
50
|
+
|
|
51
|
+
You can set a timeout for all API requests:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# Set a 5 second timeout for all requests
|
|
55
|
+
mcp = FastMCP.from_fastapi(app=app, timeout=5.0)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This timeout is applied to all requests made by tools, resources, and resource templates.
|
|
59
|
+
|
|
47
60
|
## Route Mapping
|
|
48
61
|
|
|
49
62
|
By default, FastMCP will map FastAPI routes to MCP components according to the following rules:
|
|
@@ -60,7 +73,7 @@ For more details on route mapping or custom mapping rules, see the [OpenAPI inte
|
|
|
60
73
|
|
|
61
74
|
Here's a more detailed example with a data model:
|
|
62
75
|
|
|
63
|
-
```python
|
|
76
|
+
```python [expandable]
|
|
64
77
|
import asyncio
|
|
65
78
|
from fastapi import FastAPI, HTTPException
|
|
66
79
|
from pydantic import BaseModel
|
|
@@ -95,24 +108,32 @@ def create_item(item: Item):
|
|
|
95
108
|
return items[item_id]
|
|
96
109
|
|
|
97
110
|
# Test your MCP server with a client
|
|
98
|
-
async def
|
|
99
|
-
# Create MCP server from FastAPI app
|
|
100
|
-
mcp = await FastMCP.from_fastapi(app=app)
|
|
101
|
-
|
|
111
|
+
async def check_mcp(mcp: FastMCP):
|
|
102
112
|
# List the components that were created
|
|
103
|
-
tools = await mcp.
|
|
104
|
-
resources = await mcp.
|
|
105
|
-
templates = await mcp.
|
|
113
|
+
tools = await mcp.get_tools()
|
|
114
|
+
resources = await mcp.get_resources()
|
|
115
|
+
templates = await mcp.get_resource_templates()
|
|
106
116
|
|
|
107
|
-
print(
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
print(
|
|
118
|
+
f"{len(tools)} Tool(s): {', '.join([t.name for t in tools.values()])}"
|
|
119
|
+
)
|
|
120
|
+
print(
|
|
121
|
+
f"{len(resources)} Resource(s): {', '.join([r.name for r in resources.values()])}"
|
|
122
|
+
)
|
|
123
|
+
print(
|
|
124
|
+
f"{len(templates)} Resource Template(s): {', '.join([t.name for t in templates.values()])}"
|
|
125
|
+
)
|
|
110
126
|
|
|
111
|
-
|
|
112
|
-
# mcp.run()
|
|
127
|
+
return mcp
|
|
113
128
|
|
|
114
129
|
if __name__ == "__main__":
|
|
115
|
-
|
|
130
|
+
# Create MCP server from FastAPI app
|
|
131
|
+
mcp = FastMCP.from_fastapi(app=app)
|
|
132
|
+
|
|
133
|
+
asyncio.run(check_mcp(mcp))
|
|
134
|
+
|
|
135
|
+
# In a real scenario, you would run the server:
|
|
136
|
+
mcp.run()
|
|
116
137
|
```
|
|
117
138
|
|
|
118
139
|
## Benefits
|
|
@@ -27,6 +27,23 @@ if __name__ == "__main__":
|
|
|
27
27
|
mcp.run()
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
## Configuration Options
|
|
31
|
+
|
|
32
|
+
### Timeout
|
|
33
|
+
|
|
34
|
+
You can set a timeout for all API requests:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# Set a 5 second timeout for all requests
|
|
38
|
+
mcp = FastMCP.from_openapi(
|
|
39
|
+
openapi_spec=spec,
|
|
40
|
+
client=api_client,
|
|
41
|
+
timeout=5.0
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This timeout is applied to all requests made by tools, resources, and resource templates.
|
|
46
|
+
|
|
30
47
|
## Route Mapping
|
|
31
48
|
|
|
32
49
|
By default, OpenAPI routes are mapped to MCP components based on these rules:
|
|
@@ -89,26 +106,67 @@ mcp = await FastMCP.from_openapi(
|
|
|
89
106
|
- It sends the request through the provided httpx client
|
|
90
107
|
- It translates the HTTP response to the appropriate MCP format
|
|
91
108
|
|
|
92
|
-
|
|
109
|
+
### Request Parameter Handling
|
|
110
|
+
|
|
111
|
+
FastMCP carefully handles different types of parameters in OpenAPI requests:
|
|
112
|
+
|
|
113
|
+
#### Query Parameters
|
|
114
|
+
|
|
115
|
+
By default, FastMCP will only include query parameters that have non-empty values. Parameters with `None` values or empty strings (`""`) are automatically filtered out of requests. This ensures that API servers don't receive unnecessary empty parameters that might cause issues.
|
|
93
116
|
|
|
117
|
+
For example, if you call a tool with these parameters:
|
|
94
118
|
```python
|
|
119
|
+
await client.call_tool("search_products", {
|
|
120
|
+
"category": "electronics", # Will be included
|
|
121
|
+
"min_price": 100, # Will be included
|
|
122
|
+
"max_price": None, # Will be excluded
|
|
123
|
+
"brand": "", # Will be excluded
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The resulting HTTP request will only include `category=electronics&min_price=100`.
|
|
128
|
+
|
|
129
|
+
#### Path Parameters
|
|
130
|
+
|
|
131
|
+
For path parameters, which are typically required by REST APIs, FastMCP filters out `None` values and checks that all required path parameters are provided. If a required path parameter is missing or `None`, an error will be raised.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# This will work
|
|
135
|
+
await client.call_tool("get_product", {"product_id": 123})
|
|
136
|
+
|
|
137
|
+
# This will raise ValueError: "Missing required path parameters: {'product_id'}"
|
|
138
|
+
await client.call_tool("get_product", {"product_id": None})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Complete Example
|
|
142
|
+
|
|
143
|
+
```python [expandable]
|
|
95
144
|
import asyncio
|
|
145
|
+
|
|
96
146
|
import httpx
|
|
147
|
+
|
|
97
148
|
from fastmcp import FastMCP
|
|
98
149
|
|
|
99
150
|
# Sample OpenAPI spec for a Pet Store API
|
|
100
151
|
petstore_spec = {
|
|
101
152
|
"openapi": "3.0.0",
|
|
153
|
+
"info": {
|
|
154
|
+
"title": "Pet Store API",
|
|
155
|
+
"version": "1.0.0",
|
|
156
|
+
"description": "A sample API for managing pets",
|
|
157
|
+
},
|
|
102
158
|
"paths": {
|
|
103
159
|
"/pets": {
|
|
104
160
|
"get": {
|
|
105
161
|
"operationId": "listPets",
|
|
106
|
-
"summary": "List all pets"
|
|
162
|
+
"summary": "List all pets",
|
|
163
|
+
"responses": {"200": {"description": "A list of pets"}},
|
|
107
164
|
},
|
|
108
165
|
"post": {
|
|
109
166
|
"operationId": "createPet",
|
|
110
|
-
"summary": "Create a new pet"
|
|
111
|
-
|
|
167
|
+
"summary": "Create a new pet",
|
|
168
|
+
"responses": {"201": {"description": "Pet created successfully"}},
|
|
169
|
+
},
|
|
112
170
|
},
|
|
113
171
|
"/pets/{petId}": {
|
|
114
172
|
"get": {
|
|
@@ -119,38 +177,50 @@ petstore_spec = {
|
|
|
119
177
|
"name": "petId",
|
|
120
178
|
"in": "path",
|
|
121
179
|
"required": True,
|
|
122
|
-
"schema": {"type": "string"}
|
|
180
|
+
"schema": {"type": "string"},
|
|
123
181
|
}
|
|
124
|
-
]
|
|
182
|
+
],
|
|
183
|
+
"responses": {
|
|
184
|
+
"200": {"description": "Pet details"},
|
|
185
|
+
"404": {"description": "Pet not found"},
|
|
186
|
+
},
|
|
125
187
|
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
188
|
+
},
|
|
189
|
+
},
|
|
128
190
|
}
|
|
129
191
|
|
|
130
|
-
|
|
192
|
+
|
|
193
|
+
async def check_mcp(mcp: FastMCP):
|
|
194
|
+
# List what components were created
|
|
195
|
+
tools = await mcp.get_tools()
|
|
196
|
+
resources = await mcp.get_resources()
|
|
197
|
+
templates = await mcp.get_resource_templates()
|
|
198
|
+
|
|
199
|
+
print(
|
|
200
|
+
f"{len(tools)} Tool(s): {', '.join([t.name for t in tools.values()])}"
|
|
201
|
+
) # Should include createPet
|
|
202
|
+
print(
|
|
203
|
+
f"{len(resources)} Resource(s): {', '.join([r.name for r in resources.values()])}"
|
|
204
|
+
) # Should include listPets
|
|
205
|
+
print(
|
|
206
|
+
f"{len(templates)} Resource Template(s): {', '.join([t.name for t in templates.values()])}"
|
|
207
|
+
) # Should include getPet
|
|
208
|
+
|
|
209
|
+
return mcp
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
131
213
|
# Client for the Pet Store API
|
|
132
214
|
client = httpx.AsyncClient(base_url="https://petstore.example.com/api")
|
|
133
|
-
|
|
215
|
+
|
|
134
216
|
# Create the MCP server
|
|
135
|
-
mcp =
|
|
136
|
-
openapi_spec=petstore_spec,
|
|
137
|
-
client=client,
|
|
138
|
-
name="PetStore"
|
|
217
|
+
mcp = FastMCP.from_openapi(
|
|
218
|
+
openapi_spec=petstore_spec, client=client, name="PetStore"
|
|
139
219
|
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
resources = await mcp.list_resources()
|
|
144
|
-
templates = await mcp.list_resource_templates()
|
|
145
|
-
|
|
146
|
-
print(f"Tools: {len(tools)}") # Should include createPet
|
|
147
|
-
print(f"Resources: {len(resources)}") # Should include listPets
|
|
148
|
-
print(f"Templates: {len(templates)}") # Should include getPet
|
|
149
|
-
|
|
220
|
+
|
|
221
|
+
asyncio.run(check_mcp(mcp))
|
|
222
|
+
|
|
150
223
|
# Start the MCP server
|
|
151
224
|
mcp.run()
|
|
152
|
-
|
|
153
|
-
if __name__ == "__main__":
|
|
154
|
-
asyncio.run(main())
|
|
155
225
|
```
|
|
156
226
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Testing MCP Servers
|
|
3
|
+
sidebarTitle: Testing
|
|
4
|
+
description: Learn how to test your FastMCP servers effectively
|
|
5
|
+
icon: vial
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Testing your MCP servers thoroughly is essential for ensuring they work correctly when deployed. FastMCP makes this easy through a variety of testing patterns.
|
|
10
|
+
|
|
11
|
+
## In-Memory Testing
|
|
12
|
+
|
|
13
|
+
The most efficient way to test an MCP server is to pass your FastMCP server instance directly to a Client. This enables in-memory testing without having to start a separate server process, which is particularly useful because managing an MCP server programmatically can be challenging.
|
|
14
|
+
|
|
15
|
+
Here is an example of using a `Client` to test a server with pytest:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import pytest
|
|
19
|
+
from fastmcp import FastMCP, Client
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def mcp_server():
|
|
23
|
+
server = FastMCP("TestServer")
|
|
24
|
+
|
|
25
|
+
@server.tool()
|
|
26
|
+
def greet(name: str) -> str:
|
|
27
|
+
return f"Hello, {name}!"
|
|
28
|
+
|
|
29
|
+
return server
|
|
30
|
+
|
|
31
|
+
async def test_tool_functionality(mcp_server):
|
|
32
|
+
# Pass the server directly to the Client constructor
|
|
33
|
+
async with Client(mcp_server) as client:
|
|
34
|
+
result = await client.call_tool("greet", {"name": "World"})
|
|
35
|
+
assert "Hello, World!" in str(result[0])
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This pattern creates a direct connection between the client and server, allowing you to test your server's functionality efficiently.
|
|
@@ -11,15 +11,28 @@ from smart_home.lights.hue_utils import _get_bridge, handle_phue_error
|
|
|
11
11
|
class HueAttributes(TypedDict, total=False):
|
|
12
12
|
"""TypedDict for optional light attributes."""
|
|
13
13
|
|
|
14
|
-
on: NotRequired[bool]
|
|
15
|
-
bri: NotRequired[Annotated[int, Field(ge=0, le=254)]]
|
|
16
|
-
hue: NotRequired[
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
on: NotRequired[Annotated[bool, Field(description="on/off state")]]
|
|
15
|
+
bri: NotRequired[Annotated[int, Field(ge=0, le=254, description="brightness")]]
|
|
16
|
+
hue: NotRequired[
|
|
17
|
+
Annotated[
|
|
18
|
+
int,
|
|
19
|
+
Field(
|
|
20
|
+
ge=0,
|
|
21
|
+
le=254,
|
|
22
|
+
description="saturation",
|
|
23
|
+
),
|
|
24
|
+
]
|
|
25
|
+
]
|
|
26
|
+
xy: NotRequired[Annotated[list[float], Field(description="xy color coordinates")]]
|
|
27
|
+
ct: NotRequired[
|
|
28
|
+
Annotated[
|
|
29
|
+
int,
|
|
30
|
+
Field(ge=153, le=500, description="color temperature"),
|
|
31
|
+
]
|
|
32
|
+
]
|
|
20
33
|
alert: NotRequired[Literal["none", "select", "lselect"]]
|
|
21
34
|
effect: NotRequired[Literal["none", "colorloop"]]
|
|
22
|
-
transitiontime: NotRequired[int
|
|
35
|
+
transitiontime: NotRequired[Annotated[int, Field(description="deciseconds")]]
|
|
23
36
|
|
|
24
37
|
|
|
25
38
|
lights_mcp = FastMCP(
|
|
@@ -161,7 +174,7 @@ def activate_scene(group_name: str, scene_name: str) -> dict[str, Any]:
|
|
|
161
174
|
scenes_data = bridge.get_scene()
|
|
162
175
|
scene_found = False
|
|
163
176
|
scene_in_correct_group = False
|
|
164
|
-
for
|
|
177
|
+
for sinfo in scenes_data.values():
|
|
165
178
|
if sinfo.get("name") == scene_name:
|
|
166
179
|
scene_found = True
|
|
167
180
|
# Check if this scene is associated with the target group ID
|
|
@@ -217,7 +230,7 @@ def set_light_attributes(light_name: str, attributes: HueAttributes) -> dict[str
|
|
|
217
230
|
}
|
|
218
231
|
|
|
219
232
|
try:
|
|
220
|
-
result = bridge.set_light(light_name, attributes)
|
|
233
|
+
result = bridge.set_light(light_name, dict(attributes))
|
|
221
234
|
return {
|
|
222
235
|
"light": light_name,
|
|
223
236
|
"set_attributes": attributes,
|
|
@@ -243,7 +256,7 @@ def set_group_attributes(group_name: str, attributes: HueAttributes) -> dict[str
|
|
|
243
256
|
}
|
|
244
257
|
|
|
245
258
|
try:
|
|
246
|
-
result = bridge.set_group(group_name, attributes)
|
|
259
|
+
result = bridge.set_group(group_name, dict(attributes))
|
|
247
260
|
return {
|
|
248
261
|
"group": group_name,
|
|
249
262
|
"set_attributes": attributes,
|
|
@@ -269,7 +282,7 @@ def list_lights_by_group() -> dict[str, list[str]] | list[str]:
|
|
|
269
282
|
lights_data = bridge.get_light_objects("id") # dict {light_id: {details}}
|
|
270
283
|
|
|
271
284
|
lights_by_group: dict[str, list[str]] = {}
|
|
272
|
-
for
|
|
285
|
+
for group_details in groups_data.values():
|
|
273
286
|
group_name = group_details.get("name")
|
|
274
287
|
light_ids = group_details.get("lights", [])
|
|
275
288
|
if group_name and light_ids:
|
|
@@ -282,11 +295,10 @@ def list_lights_by_group() -> dict[str, list[str]] | list[str]:
|
|
|
282
295
|
if light_name:
|
|
283
296
|
light_names.append(light_name)
|
|
284
297
|
if light_names:
|
|
285
|
-
light_names.sort()
|
|
298
|
+
light_names.sort()
|
|
286
299
|
lights_by_group[group_name] = light_names
|
|
287
300
|
|
|
288
301
|
return lights_by_group
|
|
289
302
|
|
|
290
303
|
except (PhueException, Exception) as e:
|
|
291
|
-
# Return error as list
|
|
292
304
|
return [f"Error listing lights by group: {e}"]
|
|
@@ -5,12 +5,9 @@ from typing import Any, Literal, cast, overload
|
|
|
5
5
|
|
|
6
6
|
import mcp.types
|
|
7
7
|
from mcp import ClientSession
|
|
8
|
-
from mcp.client.session import (
|
|
9
|
-
LoggingFnT,
|
|
10
|
-
MessageHandlerFnT,
|
|
11
|
-
)
|
|
12
8
|
from pydantic import AnyUrl
|
|
13
9
|
|
|
10
|
+
from fastmcp.client.logging import LogHandler, MessageHandler
|
|
14
11
|
from fastmcp.client.roots import (
|
|
15
12
|
RootsHandler,
|
|
16
13
|
RootsList,
|
|
@@ -22,7 +19,14 @@ from fastmcp.server import FastMCP
|
|
|
22
19
|
|
|
23
20
|
from .transports import ClientTransport, SessionKwargs, infer_transport
|
|
24
21
|
|
|
25
|
-
__all__ = [
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Client",
|
|
24
|
+
"RootsHandler",
|
|
25
|
+
"RootsList",
|
|
26
|
+
"LogHandler",
|
|
27
|
+
"MessageHandler",
|
|
28
|
+
"SamplingHandler",
|
|
29
|
+
]
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
class Client:
|
|
@@ -35,12 +39,12 @@ class Client:
|
|
|
35
39
|
|
|
36
40
|
def __init__(
|
|
37
41
|
self,
|
|
38
|
-
transport: ClientTransport | FastMCP | AnyUrl | Path | str,
|
|
42
|
+
transport: ClientTransport | FastMCP | AnyUrl | Path | dict[str, Any] | str,
|
|
39
43
|
# Common args
|
|
40
44
|
roots: RootsList | RootsHandler | None = None,
|
|
41
45
|
sampling_handler: SamplingHandler | None = None,
|
|
42
|
-
log_handler:
|
|
43
|
-
message_handler:
|
|
46
|
+
log_handler: LogHandler | None = None,
|
|
47
|
+
message_handler: MessageHandler | None = None,
|
|
44
48
|
read_timeout_seconds: datetime.timedelta | None = None,
|
|
45
49
|
):
|
|
46
50
|
self.transport = infer_transport(transport)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import TypeAlias
|
|
2
|
+
|
|
3
|
+
from mcp.client.session import (
|
|
4
|
+
LoggingFnT,
|
|
5
|
+
MessageHandlerFnT,
|
|
6
|
+
)
|
|
7
|
+
from mcp.types import LoggingMessageNotificationParams
|
|
8
|
+
|
|
9
|
+
LogMessage: TypeAlias = LoggingMessageNotificationParams
|
|
10
|
+
LogHandler: TypeAlias = LoggingFnT
|
|
11
|
+
MessageHandler: TypeAlias = MessageHandlerFnT
|
|
12
|
+
|
|
13
|
+
__all__ = ["LogMessage", "LogHandler", "MessageHandler"]
|
|
@@ -9,6 +9,8 @@ from mcp.shared.context import LifespanContextT, RequestContext
|
|
|
9
9
|
from mcp.types import CreateMessageRequestParams as SamplingParams
|
|
10
10
|
from mcp.types import SamplingMessage
|
|
11
11
|
|
|
12
|
+
__all__ = ["SamplingMessage", "SamplingParams", "MessageResult", "SamplingHandler"]
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
class MessageResult(CreateMessageResult):
|
|
14
16
|
role: mcp.types.Role = "assistant"
|