fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__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/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/elicitation.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import Any, Generic, Literal, get_origin
|
|
5
|
+
from typing import Any, Generic, Literal, cast, get_origin
|
|
6
6
|
|
|
7
7
|
from mcp.server.elicitation import (
|
|
8
8
|
CancelledElicitation,
|
|
@@ -47,10 +47,10 @@ class ElicitationJsonSchema(GenerateJsonSchema):
|
|
|
47
47
|
if schema["type"] == "enum":
|
|
48
48
|
# Directly call our custom enum_schema without going through handler
|
|
49
49
|
# This prevents the ref/defs mechanism from being invoked
|
|
50
|
-
return self.enum_schema(schema)
|
|
50
|
+
return self.enum_schema(schema)
|
|
51
51
|
# For list schemas, check if items are enums
|
|
52
52
|
if schema["type"] == "list":
|
|
53
|
-
return self.list_schema(schema)
|
|
53
|
+
return self.list_schema(schema)
|
|
54
54
|
# For all other types, use the default implementation
|
|
55
55
|
return super().generate_inner(schema)
|
|
56
56
|
|
|
@@ -94,7 +94,7 @@ class ElicitationJsonSchema(GenerateJsonSchema):
|
|
|
94
94
|
def enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue:
|
|
95
95
|
"""Generate inline enum schema.
|
|
96
96
|
|
|
97
|
-
Always generates enum pattern: {"enum": [value, ...]}
|
|
97
|
+
Always generates enum pattern: `{"enum": [value, ...]}`
|
|
98
98
|
Titled enums are handled separately via dict-based syntax in ctx.elicit().
|
|
99
99
|
"""
|
|
100
100
|
# Get the base schema from parent - always use simple enum pattern
|
|
@@ -134,12 +134,12 @@ def parse_elicit_response_type(response_type: Any) -> ElicitConfig:
|
|
|
134
134
|
|
|
135
135
|
Supports multiple syntaxes:
|
|
136
136
|
- None: Empty object schema, expect empty response
|
|
137
|
-
- dict: {"low": {"title": "..."}} -> single-select titled enum
|
|
137
|
+
- dict: `{"low": {"title": "..."}}` -> single-select titled enum
|
|
138
138
|
- list patterns:
|
|
139
|
-
- [["a", "b"]] -> multi-select untitled
|
|
140
|
-
- [{"low": {...}}] -> multi-select titled
|
|
141
|
-
- ["a", "b"] -> single-select untitled
|
|
142
|
-
- list[X] type annotation: multi-select with type
|
|
139
|
+
- `[["a", "b"]]` -> multi-select untitled
|
|
140
|
+
- `[{"low": {...}}]` -> multi-select titled
|
|
141
|
+
- `["a", "b"]` -> single-select untitled
|
|
142
|
+
- `list[X]` type annotation: multi-select with type
|
|
143
143
|
- Scalar types (bool, int, float, str, Literal, Enum): single value
|
|
144
144
|
- Other types (dataclass, BaseModel): use directly
|
|
145
145
|
"""
|
|
@@ -229,11 +229,13 @@ def _parse_list_syntax(lst: list[Any]) -> ElicitConfig:
|
|
|
229
229
|
|
|
230
230
|
# ["a", "b", "c"] -> single-select untitled
|
|
231
231
|
if lst and all(isinstance(item, str) for item in lst):
|
|
232
|
-
|
|
232
|
+
# Construct Literal type from tuple - use cast since we can't construct Literal dynamically
|
|
233
|
+
# but we know the values are all strings
|
|
234
|
+
choice_literal: type[Any] = cast(type[Any], Literal[tuple(lst)]) # type: ignore[valid-type]
|
|
233
235
|
wrapped = ScalarElicitationType[choice_literal] # type: ignore[valid-type]
|
|
234
236
|
return ElicitConfig(
|
|
235
|
-
schema=get_elicitation_schema(wrapped),
|
|
236
|
-
response_type=wrapped,
|
|
237
|
+
schema=get_elicitation_schema(wrapped),
|
|
238
|
+
response_type=wrapped,
|
|
237
239
|
is_raw=False,
|
|
238
240
|
)
|
|
239
241
|
|
|
@@ -242,20 +244,20 @@ def _parse_list_syntax(lst: list[Any]) -> ElicitConfig:
|
|
|
242
244
|
|
|
243
245
|
def _parse_generic_list(response_type: Any) -> ElicitConfig:
|
|
244
246
|
"""Parse list[X] type annotation -> multi-select."""
|
|
245
|
-
wrapped = ScalarElicitationType[response_type]
|
|
247
|
+
wrapped = ScalarElicitationType[response_type]
|
|
246
248
|
return ElicitConfig(
|
|
247
|
-
schema=get_elicitation_schema(wrapped),
|
|
248
|
-
response_type=wrapped,
|
|
249
|
+
schema=get_elicitation_schema(wrapped),
|
|
250
|
+
response_type=wrapped,
|
|
249
251
|
is_raw=False,
|
|
250
252
|
)
|
|
251
253
|
|
|
252
254
|
|
|
253
255
|
def _parse_scalar_type(response_type: Any) -> ElicitConfig:
|
|
254
256
|
"""Parse scalar types (bool, int, float, str, Literal, Enum)."""
|
|
255
|
-
wrapped = ScalarElicitationType[response_type]
|
|
257
|
+
wrapped = ScalarElicitationType[response_type]
|
|
256
258
|
return ElicitConfig(
|
|
257
|
-
schema=get_elicitation_schema(wrapped),
|
|
258
|
-
response_type=wrapped,
|
|
259
|
+
schema=get_elicitation_schema(wrapped),
|
|
260
|
+
response_type=wrapped,
|
|
259
261
|
is_raw=False,
|
|
260
262
|
)
|
|
261
263
|
|
fastmcp/server/event_store.py
CHANGED
|
@@ -16,14 +16,14 @@ from key_value.aio.stores.memory import MemoryStore
|
|
|
16
16
|
from mcp.server.streamable_http import EventCallback, EventId, EventMessage, StreamId
|
|
17
17
|
from mcp.server.streamable_http import EventStore as SDKEventStore
|
|
18
18
|
from mcp.types import JSONRPCMessage
|
|
19
|
-
from pydantic import BaseModel
|
|
20
19
|
|
|
21
20
|
from fastmcp.utilities.logging import get_logger
|
|
21
|
+
from fastmcp.utilities.types import FastMCPBaseModel
|
|
22
22
|
|
|
23
23
|
logger = get_logger(__name__)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class EventEntry(
|
|
26
|
+
class EventEntry(FastMCPBaseModel):
|
|
27
27
|
"""Stored event entry."""
|
|
28
28
|
|
|
29
29
|
event_id: str
|
|
@@ -31,7 +31,7 @@ class EventEntry(BaseModel):
|
|
|
31
31
|
message: dict | None # JSONRPCMessage serialized to dict
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
class StreamEventList(
|
|
34
|
+
class StreamEventList(FastMCPBaseModel):
|
|
35
35
|
"""List of event IDs for a stream."""
|
|
36
36
|
|
|
37
37
|
event_ids: list[str]
|
fastmcp/server/http.py
CHANGED
|
@@ -61,7 +61,7 @@ class StreamableHTTPASGIApp:
|
|
|
61
61
|
raise
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
_current_http_request: ContextVar[Request | None] = ContextVar(
|
|
64
|
+
_current_http_request: ContextVar[Request | None] = ContextVar(
|
|
65
65
|
"http_request",
|
|
66
66
|
default=None,
|
|
67
67
|
)
|
|
@@ -84,7 +84,7 @@ def set_http_request(request: Request) -> Generator[Request, None, None]:
|
|
|
84
84
|
|
|
85
85
|
class RequestContextMiddleware:
|
|
86
86
|
"""
|
|
87
|
-
Middleware that stores each request in a ContextVar
|
|
87
|
+
Middleware that stores each request in a ContextVar and sets transport type.
|
|
88
88
|
"""
|
|
89
89
|
|
|
90
90
|
def __init__(self, app):
|
|
@@ -92,8 +92,17 @@ class RequestContextMiddleware:
|
|
|
92
92
|
|
|
93
93
|
async def __call__(self, scope, receive, send):
|
|
94
94
|
if scope["type"] == "http":
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
from fastmcp.server.context import reset_transport, set_transport
|
|
96
|
+
|
|
97
|
+
# Get transport type from app state (set during app creation)
|
|
98
|
+
transport_type = getattr(scope["app"].state, "transport_type", None)
|
|
99
|
+
transport_token = set_transport(transport_type) if transport_type else None
|
|
100
|
+
try:
|
|
101
|
+
with set_http_request(Request(scope)):
|
|
102
|
+
await self.app(scope, receive, send)
|
|
103
|
+
finally:
|
|
104
|
+
if transport_token is not None:
|
|
105
|
+
reset_transport(transport_token)
|
|
97
106
|
else:
|
|
98
107
|
await self.app(scope, receive, send)
|
|
99
108
|
|
|
@@ -209,7 +218,7 @@ def create_sse_app(
|
|
|
209
218
|
else:
|
|
210
219
|
# No auth required
|
|
211
220
|
async def sse_endpoint(request: Request) -> Response:
|
|
212
|
-
return await handle_sse(request.scope, request.receive, request._send)
|
|
221
|
+
return await handle_sse(request.scope, request.receive, request._send)
|
|
213
222
|
|
|
214
223
|
server_routes.append(
|
|
215
224
|
Route(
|
|
@@ -249,6 +258,7 @@ def create_sse_app(
|
|
|
249
258
|
# Store the FastMCP server instance on the Starlette app state
|
|
250
259
|
app.state.fastmcp_server = server
|
|
251
260
|
app.state.path = sse_path
|
|
261
|
+
app.state.transport_type = "sse"
|
|
252
262
|
|
|
253
263
|
return app
|
|
254
264
|
|
|
@@ -360,7 +370,7 @@ def create_streamable_http_app(
|
|
|
360
370
|
)
|
|
361
371
|
# Store the FastMCP server instance on the Starlette app state
|
|
362
372
|
app.state.fastmcp_server = server
|
|
363
|
-
|
|
364
373
|
app.state.path = streamable_http_path
|
|
374
|
+
app.state.transport_type = "streamable-http"
|
|
365
375
|
|
|
366
376
|
return app
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Composable lifespans for FastMCP servers.
|
|
2
|
+
|
|
3
|
+
This module provides a `@lifespan` decorator for creating composable server lifespans
|
|
4
|
+
that can be combined using the `|` operator.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
```python
|
|
8
|
+
from fastmcp import FastMCP
|
|
9
|
+
from fastmcp.server.lifespan import lifespan
|
|
10
|
+
|
|
11
|
+
@lifespan
|
|
12
|
+
async def db_lifespan(server):
|
|
13
|
+
conn = await connect_db()
|
|
14
|
+
yield {"db": conn}
|
|
15
|
+
await conn.close()
|
|
16
|
+
|
|
17
|
+
@lifespan
|
|
18
|
+
async def cache_lifespan(server):
|
|
19
|
+
cache = await connect_cache()
|
|
20
|
+
yield {"cache": cache}
|
|
21
|
+
await cache.close()
|
|
22
|
+
|
|
23
|
+
mcp = FastMCP("server", lifespan=db_lifespan | cache_lifespan)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
To compose with existing `@asynccontextmanager` lifespans, wrap them explicitly:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from contextlib import asynccontextmanager
|
|
30
|
+
from fastmcp.server.lifespan import lifespan, ContextManagerLifespan
|
|
31
|
+
|
|
32
|
+
@asynccontextmanager
|
|
33
|
+
async def legacy_lifespan(server):
|
|
34
|
+
yield {"legacy": True}
|
|
35
|
+
|
|
36
|
+
@lifespan
|
|
37
|
+
async def new_lifespan(server):
|
|
38
|
+
yield {"new": True}
|
|
39
|
+
|
|
40
|
+
# Wrap the legacy lifespan explicitly
|
|
41
|
+
combined = ContextManagerLifespan(legacy_lifespan) | new_lifespan
|
|
42
|
+
```
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
from collections.abc import AsyncIterator, Callable
|
|
48
|
+
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
|
49
|
+
from typing import TYPE_CHECKING, Any
|
|
50
|
+
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
from fastmcp.server.server import FastMCP
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
LifespanFn = Callable[["FastMCP[Any]"], AsyncIterator[dict[str, Any] | None]]
|
|
56
|
+
LifespanContextManagerFn = Callable[
|
|
57
|
+
["FastMCP[Any]"], AbstractAsyncContextManager[dict[str, Any] | None]
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Lifespan:
|
|
62
|
+
"""Composable lifespan wrapper.
|
|
63
|
+
|
|
64
|
+
Wraps an async generator function and enables composition via the `|` operator.
|
|
65
|
+
The wrapped function should yield a dict that becomes part of the lifespan context.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, fn: LifespanFn) -> None:
|
|
69
|
+
"""Initialize a Lifespan wrapper.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
fn: An async generator function that takes a FastMCP server and yields
|
|
73
|
+
a dict for the lifespan context.
|
|
74
|
+
"""
|
|
75
|
+
self._fn = fn
|
|
76
|
+
|
|
77
|
+
@asynccontextmanager
|
|
78
|
+
async def __call__(self, server: FastMCP[Any]) -> AsyncIterator[dict[str, Any]]:
|
|
79
|
+
"""Execute the lifespan as an async context manager.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
server: The FastMCP server instance.
|
|
83
|
+
|
|
84
|
+
Yields:
|
|
85
|
+
The lifespan context dict.
|
|
86
|
+
"""
|
|
87
|
+
async with asynccontextmanager(self._fn)(server) as result:
|
|
88
|
+
yield result if result is not None else {}
|
|
89
|
+
|
|
90
|
+
def __or__(self, other: Lifespan) -> ComposedLifespan:
|
|
91
|
+
"""Compose with another lifespan using the | operator.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
other: Another Lifespan instance.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
A ComposedLifespan that runs both lifespans.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
TypeError: If other is not a Lifespan instance.
|
|
101
|
+
"""
|
|
102
|
+
if not isinstance(other, Lifespan):
|
|
103
|
+
raise TypeError(
|
|
104
|
+
f"Cannot compose Lifespan with {type(other).__name__}. "
|
|
105
|
+
f"Use @lifespan decorator or wrap with ContextManagerLifespan()."
|
|
106
|
+
)
|
|
107
|
+
return ComposedLifespan(self, other)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ContextManagerLifespan(Lifespan):
|
|
111
|
+
"""Lifespan wrapper for already-wrapped context manager functions.
|
|
112
|
+
|
|
113
|
+
Use this for functions already decorated with @asynccontextmanager.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
_fn: LifespanContextManagerFn # Override type for this subclass
|
|
117
|
+
|
|
118
|
+
def __init__(self, fn: LifespanContextManagerFn) -> None:
|
|
119
|
+
"""Initialize with a context manager factory function."""
|
|
120
|
+
self._fn = fn
|
|
121
|
+
|
|
122
|
+
@asynccontextmanager
|
|
123
|
+
async def __call__(self, server: FastMCP[Any]) -> AsyncIterator[dict[str, Any]]:
|
|
124
|
+
"""Execute the lifespan as an async context manager.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
server: The FastMCP server instance.
|
|
128
|
+
|
|
129
|
+
Yields:
|
|
130
|
+
The lifespan context dict.
|
|
131
|
+
"""
|
|
132
|
+
# self._fn is already a context manager factory, just call it
|
|
133
|
+
async with self._fn(server) as result:
|
|
134
|
+
yield result if result is not None else {}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ComposedLifespan(Lifespan):
|
|
138
|
+
"""Two lifespans composed together.
|
|
139
|
+
|
|
140
|
+
Enters the left lifespan first, then the right. Exits in reverse order.
|
|
141
|
+
Results are shallow-merged into a single dict.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(self, left: Lifespan, right: Lifespan) -> None:
|
|
145
|
+
"""Initialize a composed lifespan.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
left: The first lifespan to enter.
|
|
149
|
+
right: The second lifespan to enter.
|
|
150
|
+
"""
|
|
151
|
+
# Don't call super().__init__ since we override __call__
|
|
152
|
+
self._left = left
|
|
153
|
+
self._right = right
|
|
154
|
+
|
|
155
|
+
@asynccontextmanager
|
|
156
|
+
async def __call__(self, server: FastMCP[Any]) -> AsyncIterator[dict[str, Any]]:
|
|
157
|
+
"""Execute both lifespans, merging their results.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
server: The FastMCP server instance.
|
|
161
|
+
|
|
162
|
+
Yields:
|
|
163
|
+
The merged lifespan context dict from both lifespans.
|
|
164
|
+
"""
|
|
165
|
+
async with (
|
|
166
|
+
self._left(server) as left_result,
|
|
167
|
+
self._right(server) as right_result,
|
|
168
|
+
):
|
|
169
|
+
yield {**left_result, **right_result}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def lifespan(fn: LifespanFn) -> Lifespan:
|
|
173
|
+
"""Decorator to create a composable lifespan.
|
|
174
|
+
|
|
175
|
+
Use this decorator on an async generator function to make it composable
|
|
176
|
+
with other lifespans using the `|` operator.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
```python
|
|
180
|
+
@lifespan
|
|
181
|
+
async def my_lifespan(server):
|
|
182
|
+
# Setup
|
|
183
|
+
resource = await create_resource()
|
|
184
|
+
yield {"resource": resource}
|
|
185
|
+
# Teardown
|
|
186
|
+
await resource.close()
|
|
187
|
+
|
|
188
|
+
mcp = FastMCP("server", lifespan=my_lifespan | other_lifespan)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
fn: An async generator function that takes a FastMCP server and yields
|
|
193
|
+
a dict for the lifespan context.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
A composable Lifespan wrapper.
|
|
197
|
+
"""
|
|
198
|
+
return Lifespan(fn)
|
fastmcp/server/low_level.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import weakref
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
4
5
|
from contextlib import AsyncExitStack
|
|
5
6
|
from typing import TYPE_CHECKING, Any
|
|
6
7
|
|
|
@@ -21,6 +22,7 @@ from mcp.server.session import ServerSession
|
|
|
21
22
|
from mcp.server.stdio import stdio_server as stdio_server
|
|
22
23
|
from mcp.shared.message import SessionMessage
|
|
23
24
|
from mcp.shared.session import RequestResponder
|
|
25
|
+
from pydantic import AnyUrl
|
|
24
26
|
|
|
25
27
|
from fastmcp.utilities.logging import get_logger
|
|
26
28
|
|
|
@@ -94,7 +96,7 @@ class MiddlewareServerSession(ServerSession):
|
|
|
94
96
|
return None
|
|
95
97
|
|
|
96
98
|
async with fastmcp.server.context.Context(
|
|
97
|
-
fastmcp=self.fastmcp
|
|
99
|
+
fastmcp=self.fastmcp, session=self
|
|
98
100
|
) as fastmcp_ctx:
|
|
99
101
|
# Create the middleware context.
|
|
100
102
|
mw_context = MiddlewareContext(
|
|
@@ -106,7 +108,7 @@ class MiddlewareServerSession(ServerSession):
|
|
|
106
108
|
)
|
|
107
109
|
|
|
108
110
|
try:
|
|
109
|
-
return await self.fastmcp.
|
|
111
|
+
return await self.fastmcp._run_middleware(
|
|
110
112
|
mw_context, call_original_handler
|
|
111
113
|
)
|
|
112
114
|
except McpError as e:
|
|
@@ -223,3 +225,91 @@ class LowLevelServer(_Server[LifespanResultT, RequestT]):
|
|
|
223
225
|
lifespan_context,
|
|
224
226
|
raise_exceptions,
|
|
225
227
|
)
|
|
228
|
+
|
|
229
|
+
def read_resource(
|
|
230
|
+
self,
|
|
231
|
+
) -> Callable[
|
|
232
|
+
[
|
|
233
|
+
Callable[
|
|
234
|
+
[AnyUrl],
|
|
235
|
+
Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult],
|
|
236
|
+
]
|
|
237
|
+
],
|
|
238
|
+
Callable[
|
|
239
|
+
[AnyUrl],
|
|
240
|
+
Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult],
|
|
241
|
+
],
|
|
242
|
+
]:
|
|
243
|
+
"""
|
|
244
|
+
Decorator for registering a read_resource handler with CreateTaskResult support.
|
|
245
|
+
|
|
246
|
+
The MCP SDK's read_resource decorator does not support returning CreateTaskResult
|
|
247
|
+
for background task execution. This decorator wraps the result in ServerResult.
|
|
248
|
+
|
|
249
|
+
This decorator can be removed once the MCP SDK adds native CreateTaskResult support
|
|
250
|
+
for resources.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def decorator(
|
|
254
|
+
func: Callable[
|
|
255
|
+
[AnyUrl],
|
|
256
|
+
Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult],
|
|
257
|
+
],
|
|
258
|
+
) -> Callable[
|
|
259
|
+
[AnyUrl],
|
|
260
|
+
Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult],
|
|
261
|
+
]:
|
|
262
|
+
async def handler(
|
|
263
|
+
req: mcp.types.ReadResourceRequest,
|
|
264
|
+
) -> mcp.types.ServerResult:
|
|
265
|
+
result = await func(req.params.uri)
|
|
266
|
+
return mcp.types.ServerResult(result)
|
|
267
|
+
|
|
268
|
+
self.request_handlers[mcp.types.ReadResourceRequest] = handler
|
|
269
|
+
return func
|
|
270
|
+
|
|
271
|
+
return decorator
|
|
272
|
+
|
|
273
|
+
def get_prompt(
|
|
274
|
+
self,
|
|
275
|
+
) -> Callable[
|
|
276
|
+
[
|
|
277
|
+
Callable[
|
|
278
|
+
[str, dict[str, Any] | None],
|
|
279
|
+
Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult],
|
|
280
|
+
]
|
|
281
|
+
],
|
|
282
|
+
Callable[
|
|
283
|
+
[str, dict[str, Any] | None],
|
|
284
|
+
Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult],
|
|
285
|
+
],
|
|
286
|
+
]:
|
|
287
|
+
"""
|
|
288
|
+
Decorator for registering a get_prompt handler with CreateTaskResult support.
|
|
289
|
+
|
|
290
|
+
The MCP SDK's get_prompt decorator does not support returning CreateTaskResult
|
|
291
|
+
for background task execution. This decorator wraps the result in ServerResult.
|
|
292
|
+
|
|
293
|
+
This decorator can be removed once the MCP SDK adds native CreateTaskResult support
|
|
294
|
+
for prompts.
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
def decorator(
|
|
298
|
+
func: Callable[
|
|
299
|
+
[str, dict[str, Any] | None],
|
|
300
|
+
Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult],
|
|
301
|
+
],
|
|
302
|
+
) -> Callable[
|
|
303
|
+
[str, dict[str, Any] | None],
|
|
304
|
+
Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult],
|
|
305
|
+
]:
|
|
306
|
+
async def handler(
|
|
307
|
+
req: mcp.types.GetPromptRequest,
|
|
308
|
+
) -> mcp.types.ServerResult:
|
|
309
|
+
result = await func(req.params.name, req.params.arguments)
|
|
310
|
+
return mcp.types.ServerResult(result)
|
|
311
|
+
|
|
312
|
+
self.request_handlers[mcp.types.GetPromptRequest] = handler
|
|
313
|
+
return func
|
|
314
|
+
|
|
315
|
+
return decorator
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
from .authorization import AuthMiddleware
|
|
1
2
|
from .middleware import (
|
|
3
|
+
CallNext,
|
|
2
4
|
Middleware,
|
|
3
5
|
MiddlewareContext,
|
|
4
|
-
CallNext,
|
|
5
6
|
)
|
|
7
|
+
from .ping import PingMiddleware
|
|
6
8
|
|
|
7
9
|
__all__ = [
|
|
10
|
+
"AuthMiddleware",
|
|
8
11
|
"CallNext",
|
|
9
12
|
"Middleware",
|
|
10
13
|
"MiddlewareContext",
|
|
14
|
+
"PingMiddleware",
|
|
11
15
|
]
|