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.
Files changed (149) hide show
  1. {fastmcp-2.2.5 → fastmcp-2.2.6}/PKG-INFO +2 -2
  2. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/clients/client.mdx +71 -64
  3. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/docs.json +2 -1
  4. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/patterns/fastapi.mdx +35 -14
  5. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/patterns/openapi.mdx +97 -27
  6. fastmcp-2.2.6/docs/patterns/testing.mdx +38 -0
  7. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/lights/server.py +25 -13
  8. {fastmcp-2.2.5 → fastmcp-2.2.6}/pyproject.toml +1 -1
  9. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/__init__.py +1 -0
  10. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/client/client.py +12 -8
  11. fastmcp-2.2.6/src/fastmcp/client/logging.py +13 -0
  12. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/client/sampling.py +2 -0
  13. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/client/transports.py +37 -4
  14. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/server/context.py +15 -13
  15. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/server/openapi.py +67 -28
  16. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/server/proxy.py +1 -1
  17. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/server/server.py +18 -12
  18. fastmcp-2.2.6/tests/client/test_logs.py +60 -0
  19. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_openapi.py +203 -5
  20. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_server_interactions.py +3 -40
  21. fastmcp-2.2.6/tests/tools/__init__.py +0 -0
  22. fastmcp-2.2.5/src/fastmcp/client/base.py +0 -1
  23. {fastmcp-2.2.5 → fastmcp-2.2.6}/.cursor/rules/core-mcp-objects.mdc +0 -0
  24. {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  25. {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  26. {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/ISSUE_TEMPLATE/enhancement.yml +0 -0
  27. {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/release.yml +0 -0
  28. {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/workflows/publish.yml +0 -0
  29. {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/workflows/run-static.yml +0 -0
  30. {fastmcp-2.2.5 → fastmcp-2.2.6}/.github/workflows/run-tests.yml +0 -0
  31. {fastmcp-2.2.5 → fastmcp-2.2.6}/.gitignore +0 -0
  32. {fastmcp-2.2.5 → fastmcp-2.2.6}/.pre-commit-config.yaml +0 -0
  33. {fastmcp-2.2.5 → fastmcp-2.2.6}/LICENSE +0 -0
  34. {fastmcp-2.2.5 → fastmcp-2.2.6}/README.md +0 -0
  35. {fastmcp-2.2.5 → fastmcp-2.2.6}/Windows_Notes.md +0 -0
  36. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/assets/demo-inspector.png +0 -0
  37. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/clients/transports.mdx +0 -0
  38. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/getting-started/installation.mdx +0 -0
  39. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/getting-started/quickstart.mdx +0 -0
  40. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/getting-started/welcome.mdx +0 -0
  41. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/patterns/composition.mdx +0 -0
  42. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/patterns/contrib.mdx +0 -0
  43. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/patterns/decorating-methods.mdx +0 -0
  44. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/patterns/proxy.mdx +0 -0
  45. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/servers/context.mdx +0 -0
  46. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/servers/fastmcp.mdx +0 -0
  47. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/servers/prompts.mdx +0 -0
  48. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/servers/resources.mdx +0 -0
  49. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/servers/tools.mdx +0 -0
  50. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/snippets/version-badge.mdx +0 -0
  51. {fastmcp-2.2.5 → fastmcp-2.2.6}/docs/style.css +0 -0
  52. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/complex_inputs.py +0 -0
  53. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/desktop.py +0 -0
  54. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/echo.py +0 -0
  55. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/memory.py +0 -0
  56. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/mount_example.py +0 -0
  57. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/readme-quickstart.py +0 -0
  58. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/sampling.py +0 -0
  59. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/screenshot.py +0 -0
  60. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/simple_echo.py +0 -0
  61. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/README.md +0 -0
  62. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/pyproject.toml +0 -0
  63. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/__init__.py +0 -0
  64. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/__main__.py +0 -0
  65. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/hub.py +0 -0
  66. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/lights/__init__.py +0 -0
  67. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/lights/hue_utils.py +0 -0
  68. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/py.typed +0 -0
  69. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/src/smart_home/settings.py +0 -0
  70. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/smart_home/uv.lock +0 -0
  71. {fastmcp-2.2.5 → fastmcp-2.2.6}/examples/text_me.py +0 -0
  72. {fastmcp-2.2.5 → fastmcp-2.2.6}/justfile +0 -0
  73. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/cli/__init__.py +0 -0
  74. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/cli/claude.py +0 -0
  75. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/cli/cli.py +0 -0
  76. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/client/__init__.py +0 -0
  77. /fastmcp-2.2.5/src/fastmcp/py.typed → /fastmcp-2.2.6/src/fastmcp/client/base.py +0 -0
  78. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/client/roots.py +0 -0
  79. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/README.md +0 -0
  80. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/bulk_tool_caller/README.md +0 -0
  81. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/bulk_tool_caller/__init__.py +0 -0
  82. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py +0 -0
  83. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/bulk_tool_caller/example.py +0 -0
  84. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/mcp_mixin/README.md +0 -0
  85. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/mcp_mixin/__init__.py +0 -0
  86. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/mcp_mixin/example.py +0 -0
  87. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py +0 -0
  88. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/exceptions.py +0 -0
  89. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/prompts/__init__.py +0 -0
  90. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/prompts/prompt.py +0 -0
  91. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/prompts/prompt_manager.py +0 -0
  92. /fastmcp-2.2.5/tests/__init__.py → /fastmcp-2.2.6/src/fastmcp/py.typed +0 -0
  93. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/resources/__init__.py +0 -0
  94. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/resources/resource.py +0 -0
  95. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/resources/resource_manager.py +0 -0
  96. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/resources/template.py +0 -0
  97. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/resources/types.py +0 -0
  98. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/server/__init__.py +0 -0
  99. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/settings.py +0 -0
  100. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/tools/__init__.py +0 -0
  101. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/tools/tool.py +0 -0
  102. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/tools/tool_manager.py +0 -0
  103. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/utilities/__init__.py +0 -0
  104. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/utilities/decorators.py +0 -0
  105. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/utilities/func_metadata.py +0 -0
  106. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/utilities/logging.py +0 -0
  107. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/utilities/openapi.py +0 -0
  108. {fastmcp-2.2.5 → fastmcp-2.2.6}/src/fastmcp/utilities/types.py +0 -0
  109. {fastmcp-2.2.5/tests/prompts → fastmcp-2.2.6/tests}/__init__.py +0 -0
  110. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/cli/test_run.py +0 -0
  111. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/client/__init__.py +0 -0
  112. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/client/test_client.py +0 -0
  113. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/client/test_roots.py +0 -0
  114. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/client/test_sampling.py +0 -0
  115. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/conftest.py +0 -0
  116. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/contrib/__init__.py +0 -0
  117. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/contrib/test_bulk_tool_caller.py +0 -0
  118. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/contrib/test_mcp_mixin.py +0 -0
  119. {fastmcp-2.2.5/tests/resources → fastmcp-2.2.6/tests/prompts}/__init__.py +0 -0
  120. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/prompts/test_base.py +0 -0
  121. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/prompts/test_prompt_manager.py +0 -0
  122. {fastmcp-2.2.5/tests/server → fastmcp-2.2.6/tests/resources}/__init__.py +0 -0
  123. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/resources/test_file_resources.py +0 -0
  124. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/resources/test_function_resources.py +0 -0
  125. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/resources/test_resource_manager.py +0 -0
  126. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/resources/test_resource_template.py +0 -0
  127. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/resources/test_resources.py +0 -0
  128. {fastmcp-2.2.5/tests/tools → fastmcp-2.2.6/tests/server}/__init__.py +0 -0
  129. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_file_server.py +0 -0
  130. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_import_server.py +0 -0
  131. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_lifespan.py +0 -0
  132. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_mount.py +0 -0
  133. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_proxy.py +0 -0
  134. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_run_server.py +0 -0
  135. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/server/test_server.py +0 -0
  136. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/test_servers/fastmcp_server.py +0 -0
  137. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/test_servers/sse.py +0 -0
  138. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/test_servers/stdio.py +0 -0
  139. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/tools/test_tool_manager.py +0 -0
  140. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/__init__.py +0 -0
  141. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/openapi/__init__.py +0 -0
  142. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/openapi/conftest.py +0 -0
  143. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/openapi/test_openapi.py +0 -0
  144. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/openapi/test_openapi_advanced.py +0 -0
  145. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/openapi/test_openapi_fastapi.py +0 -0
  146. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/test_decorated_function.py +0 -0
  147. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/test_func_metadata.py +0 -0
  148. {fastmcp-2.2.5 → fastmcp-2.2.6}/tests/utilities/test_logging.py +0 -0
  149. {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.5
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>=15.0.1
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
- ### Callbacks
152
+ ### Advanced Features
153
153
 
154
- MCP allows servers to make requests *back* to the client for certain capabilities. The `Client` constructor accepts callback functions to handle these server requests:
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
- * **`roots: RootsList | RootsHandler | None`**: Provides the server with a list of root directories the client grants access to. This can be a static list or a function that dynamically determines roots.
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
- # Tell the server the roots might have changed (if needed)
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
- #### LLM Sampling
161
+ The following example uses the `marvin` library to generate a completion:
185
162
 
186
- * **`sampling_handler: SamplingHandler | None`**: Handles `sampling/createMessage` requests from the server. This callback receives messages from the server and should return an LLM completion.
187
- ```python
188
- from fastmcp.client.sampling import SamplingHandler, MessageResult
189
- from mcp.types import SamplingMessage, SamplingParams, TextContent
190
- from mcp.shared.context import RequestContext # For type hint
191
-
192
- async def my_llm_handler(
193
- messages: list[SamplingMessage],
194
- params: SamplingParams,
195
- context: RequestContext
196
- ) -> str | MessageResult:
197
- print(f"Server requested sampling (Request ID: {context.request_id})")
198
- # In a real scenario, call your LLM API here
199
- last_user_message = next((m for m in reversed(messages) if m.role == 'user'), None)
200
- prompt = last_user_message.content.text if last_user_message and isinstance(last_user_message.content, TextContent) else "Default prompt"
201
-
202
- # Simulate LLM response
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
- See `fastmcp.client.sampling` for helpers.
181
+
182
+ client = Client(
183
+ ...,
184
+ sampling_handler=sampling_handler,
185
+ )
186
+ ```
213
187
 
214
188
  #### Logging
215
189
 
216
- * **`log_handler: LoggingFnT | None`**: Receives log messages sent from the server (`ctx.info`, `ctx.error`, etc.).
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
- def my_log_handler(level: LogLevel, message: str, logger_name: str | None):
221
- print(f"[Server Log - {level.upper()}] {logger_name or 'default'}: {message}")
192
+ ```python {4-5, 9}
193
+ from fastmcp import Client
194
+ from fastmcp.client.logging import LogHandler, LogMessage
222
195
 
223
- client_with_logging = Client(
224
- "my_server.py",
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.
@@ -62,7 +62,8 @@
62
62
  "patterns/decorating-methods",
63
63
  "patterns/openapi",
64
64
  "patterns/fastapi",
65
- "patterns/contrib"
65
+ "patterns/contrib",
66
+ "patterns/testing"
66
67
  ]
67
68
  },
68
69
  {
@@ -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 test():
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.list_tools()
104
- resources = await mcp.list_resources()
105
- templates = await mcp.list_resource_templates()
113
+ tools = await mcp.get_tools()
114
+ resources = await mcp.get_resources()
115
+ templates = await mcp.get_resource_templates()
106
116
 
107
- print(f"Generated {len(tools)} tools")
108
- print(f"Generated {len(resources)} resources")
109
- print(f"Generated {len(templates)} templates")
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
- # In a real scenario, you would run the server:
112
- # mcp.run()
127
+ return mcp
113
128
 
114
129
  if __name__ == "__main__":
115
- asyncio.run(test())
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
- ## Complete Example
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
- async def main():
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 = await FastMCP.from_openapi(
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
- # List what components were created
142
- tools = await mcp.list_tools()
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[Annotated[int, Field(ge=0, le=65535)]]
17
- sat: NotRequired[Annotated[int, Field(ge=0, le=254)]]
18
- xy: NotRequired[list[float]]
19
- ct: NotRequired[Annotated[int, Field(ge=153, le=500)]]
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] # deciseconds
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 sid, sinfo in scenes_data.items():
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 group_id, group_details in groups_data.items():
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() # Keep light list sorted
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}"]
@@ -11,7 +11,7 @@ dependencies = [
11
11
  "openapi-pydantic>=0.5.1",
12
12
  "rich>=13.9.4",
13
13
  "typer>=0.15.2",
14
- "websockets>=15.0.1",
14
+ "websockets>=14.0",
15
15
  ]
16
16
  requires-python = ">=3.10"
17
17
  readme = "README.md"
@@ -14,6 +14,7 @@ __all__ = [
14
14
  "FastMCP",
15
15
  "Context",
16
16
  "client",
17
+ "Client",
17
18
  "settings",
18
19
  "Image",
19
20
  ]
@@ -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__ = ["Client", "RootsHandler", "RootsList"]
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: LoggingFnT | None = None,
43
- message_handler: MessageHandlerFnT | None = None,
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"