fastmcp 2.12.5__py3-none-any.whl → 2.14.0__py3-none-any.whl

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 (133) hide show
  1. fastmcp/__init__.py +2 -23
  2. fastmcp/cli/__init__.py +0 -3
  3. fastmcp/cli/__main__.py +5 -0
  4. fastmcp/cli/cli.py +19 -33
  5. fastmcp/cli/install/claude_code.py +6 -6
  6. fastmcp/cli/install/claude_desktop.py +3 -3
  7. fastmcp/cli/install/cursor.py +18 -12
  8. fastmcp/cli/install/gemini_cli.py +3 -3
  9. fastmcp/cli/install/mcp_json.py +3 -3
  10. fastmcp/cli/install/shared.py +0 -15
  11. fastmcp/cli/run.py +13 -8
  12. fastmcp/cli/tasks.py +110 -0
  13. fastmcp/client/__init__.py +9 -9
  14. fastmcp/client/auth/oauth.py +123 -225
  15. fastmcp/client/client.py +697 -95
  16. fastmcp/client/elicitation.py +11 -5
  17. fastmcp/client/logging.py +18 -14
  18. fastmcp/client/messages.py +7 -5
  19. fastmcp/client/oauth_callback.py +85 -171
  20. fastmcp/client/roots.py +2 -1
  21. fastmcp/client/sampling.py +1 -1
  22. fastmcp/client/tasks.py +614 -0
  23. fastmcp/client/transports.py +117 -30
  24. fastmcp/contrib/component_manager/__init__.py +1 -1
  25. fastmcp/contrib/component_manager/component_manager.py +2 -2
  26. fastmcp/contrib/component_manager/component_service.py +10 -26
  27. fastmcp/contrib/mcp_mixin/README.md +32 -1
  28. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  29. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  30. fastmcp/dependencies.py +25 -0
  31. fastmcp/experimental/sampling/handlers/openai.py +3 -3
  32. fastmcp/experimental/server/openapi/__init__.py +20 -21
  33. fastmcp/experimental/utilities/openapi/__init__.py +16 -47
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +54 -51
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +43 -21
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +161 -61
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -14
  45. fastmcp/server/auth/auth.py +197 -46
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1469 -298
  50. fastmcp/server/auth/oidc_proxy.py +91 -20
  51. fastmcp/server/auth/providers/auth0.py +40 -21
  52. fastmcp/server/auth/providers/aws.py +29 -3
  53. fastmcp/server/auth/providers/azure.py +312 -131
  54. fastmcp/server/auth/providers/debug.py +114 -0
  55. fastmcp/server/auth/providers/descope.py +86 -29
  56. fastmcp/server/auth/providers/discord.py +308 -0
  57. fastmcp/server/auth/providers/github.py +29 -8
  58. fastmcp/server/auth/providers/google.py +48 -9
  59. fastmcp/server/auth/providers/in_memory.py +29 -5
  60. fastmcp/server/auth/providers/introspection.py +281 -0
  61. fastmcp/server/auth/providers/jwt.py +48 -31
  62. fastmcp/server/auth/providers/oci.py +233 -0
  63. fastmcp/server/auth/providers/scalekit.py +238 -0
  64. fastmcp/server/auth/providers/supabase.py +188 -0
  65. fastmcp/server/auth/providers/workos.py +35 -17
  66. fastmcp/server/context.py +236 -116
  67. fastmcp/server/dependencies.py +503 -18
  68. fastmcp/server/elicitation.py +286 -48
  69. fastmcp/server/event_store.py +177 -0
  70. fastmcp/server/http.py +71 -20
  71. fastmcp/server/low_level.py +165 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +50 -39
  76. fastmcp/server/middleware/middleware.py +29 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi/__init__.py +35 -0
  80. fastmcp/{experimental/server → server}/openapi/components.py +15 -10
  81. fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
  82. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  83. fastmcp/server/proxy.py +72 -48
  84. fastmcp/server/server.py +1415 -733
  85. fastmcp/server/tasks/__init__.py +21 -0
  86. fastmcp/server/tasks/capabilities.py +22 -0
  87. fastmcp/server/tasks/config.py +89 -0
  88. fastmcp/server/tasks/converters.py +205 -0
  89. fastmcp/server/tasks/handlers.py +356 -0
  90. fastmcp/server/tasks/keys.py +93 -0
  91. fastmcp/server/tasks/protocol.py +355 -0
  92. fastmcp/server/tasks/subscriptions.py +205 -0
  93. fastmcp/settings.py +125 -113
  94. fastmcp/tools/__init__.py +1 -1
  95. fastmcp/tools/tool.py +138 -55
  96. fastmcp/tools/tool_manager.py +30 -112
  97. fastmcp/tools/tool_transform.py +12 -21
  98. fastmcp/utilities/cli.py +67 -28
  99. fastmcp/utilities/components.py +10 -5
  100. fastmcp/utilities/inspect.py +79 -23
  101. fastmcp/utilities/json_schema.py +4 -4
  102. fastmcp/utilities/json_schema_type.py +8 -8
  103. fastmcp/utilities/logging.py +118 -8
  104. fastmcp/utilities/mcp_config.py +1 -2
  105. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  106. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  107. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  108. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
  109. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  110. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  111. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  112. fastmcp/utilities/openapi/__init__.py +63 -0
  113. fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
  114. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  115. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
  116. fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
  117. fastmcp/utilities/tests.py +92 -5
  118. fastmcp/utilities/types.py +86 -16
  119. fastmcp/utilities/ui.py +626 -0
  120. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
  121. fastmcp-2.14.0.dist-info/RECORD +156 -0
  122. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
  123. fastmcp/cli/claude.py +0 -135
  124. fastmcp/server/auth/providers/bearer.py +0 -25
  125. fastmcp/server/openapi.py +0 -1083
  126. fastmcp/utilities/openapi.py +0 -1568
  127. fastmcp/utilities/storage.py +0 -204
  128. fastmcp-2.12.5.dist-info/RECORD +0 -134
  129. fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  130. fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
  131. fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
  132. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
  133. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,13 +5,14 @@ import logging
5
5
  import multiprocessing
6
6
  import socket
7
7
  import time
8
- from collections.abc import Callable, Generator
9
- from contextlib import contextmanager
8
+ from collections.abc import AsyncGenerator, Callable, Generator
9
+ from contextlib import asynccontextmanager, contextmanager, suppress
10
10
  from typing import TYPE_CHECKING, Any, Literal
11
11
  from urllib.parse import parse_qs, urlparse
12
12
 
13
13
  import httpx
14
14
  import uvicorn
15
+ from pytest import LogCaptureFixture
15
16
 
16
17
  from fastmcp import settings
17
18
  from fastmcp.client.auth.oauth import OAuth
@@ -66,6 +67,7 @@ def _run_server(mcp_server: FastMCP, transport: Literal["sse"], port: int) -> No
66
67
  host="127.0.0.1",
67
68
  port=port,
68
69
  log_level="error",
70
+ ws="websockets-sansio",
69
71
  )
70
72
  )
71
73
  uvicorn_server.run()
@@ -74,11 +76,11 @@ def _run_server(mcp_server: FastMCP, transport: Literal["sse"], port: int) -> No
74
76
  @contextmanager
75
77
  def run_server_in_process(
76
78
  server_fn: Callable[..., None],
77
- *args,
79
+ *args: Any,
78
80
  provide_host_and_port: bool = True,
79
81
  host: str = "127.0.0.1",
80
82
  port: int | None = None,
81
- **kwargs,
83
+ **kwargs: Any,
82
84
  ) -> Generator[str, None, None]:
83
85
  """
84
86
  Context manager that runs a FastMCP server in a separate process and
@@ -139,8 +141,93 @@ def run_server_in_process(
139
141
  raise RuntimeError("Server process failed to terminate even after kill")
140
142
 
141
143
 
144
+ @asynccontextmanager
145
+ async def run_server_async(
146
+ server: FastMCP,
147
+ port: int | None = None,
148
+ transport: Literal["http", "streamable-http", "sse"] = "http",
149
+ path: str = "/mcp",
150
+ host: str = "127.0.0.1",
151
+ ) -> AsyncGenerator[str, None]:
152
+ """
153
+ Start a FastMCP server as an asyncio task for in-process async testing.
154
+
155
+ This is the recommended way to test FastMCP servers. It runs the server
156
+ as an async task in the same process, eliminating subprocess coordination,
157
+ sleeps, and cleanup issues.
158
+
159
+ Args:
160
+ server: FastMCP server instance
161
+ port: Port to bind to (default: find available port)
162
+ transport: Transport type ("http", "streamable-http", or "sse")
163
+ path: URL path for the server (default: "/mcp")
164
+ host: Host to bind to (default: "127.0.0.1")
165
+
166
+ Yields:
167
+ Server URL string
168
+
169
+ Example:
170
+ ```python
171
+ import pytest
172
+ from fastmcp import FastMCP, Client
173
+ from fastmcp.client.transports import StreamableHttpTransport
174
+ from fastmcp.utilities.tests import run_server_async
175
+
176
+ @pytest.fixture
177
+ async def server():
178
+ mcp = FastMCP("test")
179
+
180
+ @mcp.tool()
181
+ def greet(name: str) -> str:
182
+ return f"Hello, {name}!"
183
+
184
+ async with run_server_async(mcp) as url:
185
+ yield url
186
+
187
+ async def test_greet(server: str):
188
+ async with Client(StreamableHttpTransport(server)) as client:
189
+ result = await client.call_tool("greet", {"name": "World"})
190
+ assert result.content[0].text == "Hello, World!"
191
+ ```
192
+ """
193
+ import asyncio
194
+
195
+ if port is None:
196
+ port = find_available_port()
197
+
198
+ # Wait a tiny bit for the port to be released if it was just used
199
+ await asyncio.sleep(0.01)
200
+
201
+ # Start server as a background task
202
+ server_task = asyncio.create_task(
203
+ server.run_http_async(
204
+ host=host,
205
+ port=port,
206
+ transport=transport,
207
+ path=path,
208
+ show_banner=False,
209
+ )
210
+ )
211
+
212
+ # Wait for server lifespan to be ready
213
+ await server._started.wait()
214
+
215
+ # Give uvicorn a moment to bind the port after lifespan is ready
216
+ await asyncio.sleep(0.1)
217
+
218
+ try:
219
+ yield f"http://{host}:{port}{path}"
220
+ finally:
221
+ # Cleanup: cancel the task with timeout to avoid hanging on Windows
222
+ server_task.cancel()
223
+ with suppress(asyncio.CancelledError, asyncio.TimeoutError):
224
+ await asyncio.wait_for(server_task, timeout=2.0)
225
+
226
+
142
227
  @contextmanager
143
- def caplog_for_fastmcp(caplog):
228
+ def caplog_for_fastmcp(
229
+ caplog: LogCaptureFixture,
230
+ ) -> Generator[LogCaptureFixture, None, None]:
144
231
  """Context manager to capture logs from FastMCP loggers even when propagation is disabled."""
145
232
  caplog.clear()
146
233
  logger = logging.getLogger("fastmcp")
@@ -175,6 +175,63 @@ def find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None:
175
175
  return None
176
176
 
177
177
 
178
+ def create_function_without_params(
179
+ fn: Callable[..., Any], exclude_params: list[str]
180
+ ) -> Callable[..., Any]:
181
+ """
182
+ Create a new function with the same code but without the specified parameters in annotations.
183
+
184
+ This is used to exclude parameters from type adapter processing when they can't be serialized.
185
+ The excluded parameters are removed from the function's __annotations__ dictionary.
186
+ """
187
+ import types
188
+
189
+ if inspect.ismethod(fn):
190
+ actual_func = fn.__func__
191
+ code = actual_func.__code__ # ty: ignore[unresolved-attribute]
192
+ globals_dict = actual_func.__globals__ # ty: ignore[unresolved-attribute]
193
+ name = actual_func.__name__ # ty: ignore[unresolved-attribute]
194
+ defaults = actual_func.__defaults__ # ty: ignore[unresolved-attribute]
195
+ closure = actual_func.__closure__ # ty: ignore[unresolved-attribute]
196
+ else:
197
+ code = fn.__code__ # ty: ignore[unresolved-attribute]
198
+ globals_dict = fn.__globals__ # ty: ignore[unresolved-attribute]
199
+ name = fn.__name__ # ty: ignore[unresolved-attribute]
200
+ defaults = fn.__defaults__ # ty: ignore[unresolved-attribute]
201
+ closure = fn.__closure__ # ty: ignore[unresolved-attribute]
202
+
203
+ # Create a copy of annotations without the excluded parameters
204
+ original_annotations = getattr(fn, "__annotations__", {})
205
+ new_annotations = {
206
+ k: v for k, v in original_annotations.items() if k not in exclude_params
207
+ }
208
+
209
+ # Create new signature without the excluded parameters
210
+ sig = inspect.signature(fn)
211
+ new_params = [
212
+ param for name, param in sig.parameters.items() if name not in exclude_params
213
+ ]
214
+ new_sig = inspect.Signature(new_params, return_annotation=sig.return_annotation)
215
+
216
+ new_func = types.FunctionType(
217
+ code,
218
+ globals_dict,
219
+ name,
220
+ defaults,
221
+ closure,
222
+ )
223
+ new_func.__dict__.update(fn.__dict__)
224
+ new_func.__module__ = fn.__module__
225
+ new_func.__qualname__ = getattr(fn, "__qualname__", fn.__name__) # ty: ignore[unresolved-attribute]
226
+ new_func.__annotations__ = new_annotations
227
+ new_func.__signature__ = new_sig # type: ignore[attr-defined]
228
+
229
+ if inspect.ismethod(fn):
230
+ return types.MethodType(new_func, fn.__self__)
231
+ else:
232
+ return new_func
233
+
234
+
178
235
  class Image:
179
236
  """Helper class for returning images from tools."""
180
237
 
@@ -190,34 +247,33 @@ class Image:
190
247
  if path is not None and data is not None:
191
248
  raise ValueError("Only one of path or data can be provided")
192
249
 
193
- self.path = Path(os.path.expandvars(str(path))).expanduser() if path else None
250
+ self.path = self._get_expanded_path(path)
194
251
  self.data = data
195
252
  self._format = format
196
253
  self._mime_type = self._get_mime_type()
197
254
  self.annotations = annotations
198
255
 
256
+ @staticmethod
257
+ def _get_expanded_path(path: str | Path | None) -> Path | None:
258
+ """Expand environment variables and user home in path."""
259
+ return Path(os.path.expandvars(str(path))).expanduser() if path else None
260
+
199
261
  def _get_mime_type(self) -> str:
200
262
  """Get MIME type from format or guess from file extension."""
201
263
  if self._format:
202
264
  return f"image/{self._format.lower()}"
203
265
 
204
266
  if self.path:
205
- suffix = self.path.suffix.lower()
206
- return {
207
- ".png": "image/png",
208
- ".jpg": "image/jpeg",
209
- ".jpeg": "image/jpeg",
210
- ".gif": "image/gif",
211
- ".webp": "image/webp",
212
- }.get(suffix, "application/octet-stream")
267
+ # Workaround for WEBP in Py3.10
268
+ mimetypes.add_type("image/webp", ".webp")
269
+ resp = mimetypes.guess_type(self.path, strict=False)
270
+ if resp and resp[0] is not None:
271
+ return resp[0]
272
+ return "application/octet-stream"
213
273
  return "image/png" # default for raw binary data
214
274
 
215
- def to_image_content(
216
- self,
217
- mime_type: str | None = None,
218
- annotations: Annotations | None = None,
219
- ) -> mcp.types.ImageContent:
220
- """Convert to MCP ImageContent."""
275
+ def _get_data(self) -> str:
276
+ """Get raw image data as base64-encoded string."""
221
277
  if self.path:
222
278
  with open(self.path, "rb") as f:
223
279
  data = base64.b64encode(f.read()).decode()
@@ -225,6 +281,15 @@ class Image:
225
281
  data = base64.b64encode(self.data).decode()
226
282
  else:
227
283
  raise ValueError("No image data available")
284
+ return data
285
+
286
+ def to_image_content(
287
+ self,
288
+ mime_type: str | None = None,
289
+ annotations: Annotations | None = None,
290
+ ) -> mcp.types.ImageContent:
291
+ """Convert to MCP ImageContent."""
292
+ data = self._get_data()
228
293
 
229
294
  return mcp.types.ImageContent(
230
295
  type="image",
@@ -233,6 +298,11 @@ class Image:
233
298
  annotations=annotations or self.annotations,
234
299
  )
235
300
 
301
+ def to_data_uri(self, mime_type: str | None = None) -> str:
302
+ """Get image as a data URI."""
303
+ data = self._get_data()
304
+ return f"data:{mime_type or self._mime_type};base64,{data}"
305
+
236
306
 
237
307
  class Audio:
238
308
  """Helper class for returning audio from tools."""
@@ -293,7 +363,7 @@ class Audio:
293
363
 
294
364
 
295
365
  class File:
296
- """Helper class for returning audio from tools."""
366
+ """Helper class for returning file data from tools."""
297
367
 
298
368
  def __init__(
299
369
  self,