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.
- fastmcp/__init__.py +2 -23
- fastmcp/cli/__init__.py +0 -3
- fastmcp/cli/__main__.py +5 -0
- fastmcp/cli/cli.py +19 -33
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +18 -12
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/install/shared.py +0 -15
- fastmcp/cli/run.py +13 -8
- fastmcp/cli/tasks.py +110 -0
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +123 -225
- fastmcp/client/client.py +697 -95
- fastmcp/client/elicitation.py +11 -5
- fastmcp/client/logging.py +18 -14
- fastmcp/client/messages.py +7 -5
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/roots.py +2 -1
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/tasks.py +614 -0
- fastmcp/client/transports.py +117 -30
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +10 -26
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/dependencies.py +25 -0
- fastmcp/experimental/sampling/handlers/openai.py +3 -3
- fastmcp/experimental/server/openapi/__init__.py +20 -21
- fastmcp/experimental/utilities/openapi/__init__.py +16 -47
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +54 -51
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +43 -21
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +161 -61
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -14
- fastmcp/server/auth/auth.py +197 -46
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1469 -298
- fastmcp/server/auth/oidc_proxy.py +91 -20
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +312 -131
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +86 -29
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +48 -9
- fastmcp/server/auth/providers/in_memory.py +29 -5
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +35 -17
- fastmcp/server/context.py +236 -116
- fastmcp/server/dependencies.py +503 -18
- fastmcp/server/elicitation.py +286 -48
- fastmcp/server/event_store.py +177 -0
- fastmcp/server/http.py +71 -20
- fastmcp/server/low_level.py +165 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +50 -39
- fastmcp/server/middleware/middleware.py +29 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi/__init__.py +35 -0
- fastmcp/{experimental/server → server}/openapi/components.py +15 -10
- fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
- fastmcp/{experimental/server → server}/openapi/server.py +6 -5
- fastmcp/server/proxy.py +72 -48
- fastmcp/server/server.py +1415 -733
- fastmcp/server/tasks/__init__.py +21 -0
- fastmcp/server/tasks/capabilities.py +22 -0
- fastmcp/server/tasks/config.py +89 -0
- fastmcp/server/tasks/converters.py +205 -0
- fastmcp/server/tasks/handlers.py +356 -0
- fastmcp/server/tasks/keys.py +93 -0
- fastmcp/server/tasks/protocol.py +355 -0
- fastmcp/server/tasks/subscriptions.py +205 -0
- fastmcp/settings.py +125 -113
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +138 -55
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -21
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +10 -5
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +8 -8
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_config.py +1 -2
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
- fastmcp/utilities/openapi/__init__.py +63 -0
- fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
- fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
- fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
- fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
- fastmcp/utilities/tests.py +92 -5
- fastmcp/utilities/types.py +86 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
- fastmcp-2.14.0.dist-info/RECORD +156 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/server/auth/providers/bearer.py +0 -25
- fastmcp/server/openapi.py +0 -1083
- fastmcp/utilities/openapi.py +0 -1568
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- fastmcp/{experimental/server → server}/openapi/README.md +0 -0
- fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
- fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/utilities/tests.py
CHANGED
|
@@ -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(
|
|
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")
|
fastmcp/utilities/types.py
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
216
|
-
|
|
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
|
|
366
|
+
"""Helper class for returning file data from tools."""
|
|
297
367
|
|
|
298
368
|
def __init__(
|
|
299
369
|
self,
|