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/server/http.py
CHANGED
|
@@ -5,10 +5,12 @@ from contextlib import asynccontextmanager, contextmanager
|
|
|
5
5
|
from contextvars import ContextVar
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
-
from mcp.server.auth.
|
|
8
|
+
from mcp.server.auth.routes import build_resource_metadata_url
|
|
9
9
|
from mcp.server.lowlevel.server import LifespanResultT
|
|
10
10
|
from mcp.server.sse import SseServerTransport
|
|
11
|
-
from mcp.server.streamable_http import
|
|
11
|
+
from mcp.server.streamable_http import (
|
|
12
|
+
EventStore,
|
|
13
|
+
)
|
|
12
14
|
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
13
15
|
from starlette.applications import Starlette
|
|
14
16
|
from starlette.middleware import Middleware
|
|
@@ -18,6 +20,8 @@ from starlette.routing import BaseRoute, Mount, Route
|
|
|
18
20
|
from starlette.types import Lifespan, Receive, Scope, Send
|
|
19
21
|
|
|
20
22
|
from fastmcp.server.auth import AuthProvider
|
|
23
|
+
from fastmcp.server.auth.middleware import RequireAuthMiddleware
|
|
24
|
+
from fastmcp.server.tasks.capabilities import get_task_capabilities
|
|
21
25
|
from fastmcp.utilities.logging import get_logger
|
|
22
26
|
|
|
23
27
|
if TYPE_CHECKING:
|
|
@@ -113,7 +117,8 @@ def create_base_app(
|
|
|
113
117
|
A Starlette application
|
|
114
118
|
"""
|
|
115
119
|
# Always add RequestContextMiddleware as the outermost middleware
|
|
116
|
-
|
|
120
|
+
# TODO(ty): remove type ignore when ty supports Starlette Middleware typing
|
|
121
|
+
middleware.insert(0, Middleware(RequestContextMiddleware)) # type: ignore[arg-type]
|
|
117
122
|
|
|
118
123
|
return StarletteWithLifespan(
|
|
119
124
|
routes=routes,
|
|
@@ -155,10 +160,15 @@ def create_sse_app(
|
|
|
155
160
|
# Create handler for SSE connections
|
|
156
161
|
async def handle_sse(scope: Scope, receive: Receive, send: Send) -> Response:
|
|
157
162
|
async with sse.connect_sse(scope, receive, send) as streams:
|
|
163
|
+
# Build experimental capabilities
|
|
164
|
+
experimental_capabilities = get_task_capabilities()
|
|
165
|
+
|
|
158
166
|
await server._mcp_server.run(
|
|
159
167
|
streams[0],
|
|
160
168
|
streams[1],
|
|
161
|
-
server._mcp_server.create_initialization_options(
|
|
169
|
+
server._mcp_server.create_initialization_options(
|
|
170
|
+
experimental_capabilities=experimental_capabilities
|
|
171
|
+
),
|
|
162
172
|
)
|
|
163
173
|
return Response()
|
|
164
174
|
|
|
@@ -167,23 +177,38 @@ def create_sse_app(
|
|
|
167
177
|
# Get auth middleware from the provider
|
|
168
178
|
auth_middleware = auth.get_middleware()
|
|
169
179
|
|
|
170
|
-
# Get auth routes
|
|
171
|
-
auth_routes = auth.get_routes(
|
|
172
|
-
mcp_path=sse_path,
|
|
173
|
-
mcp_endpoint=handle_sse,
|
|
174
|
-
)
|
|
175
|
-
|
|
180
|
+
# Get auth provider's own routes (OAuth endpoints, metadata, etc)
|
|
181
|
+
auth_routes = auth.get_routes(mcp_path=sse_path)
|
|
176
182
|
server_routes.extend(auth_routes)
|
|
177
183
|
server_middleware.extend(auth_middleware)
|
|
178
184
|
|
|
179
|
-
#
|
|
185
|
+
# Build RFC 9728-compliant metadata URL
|
|
186
|
+
resource_url = auth._get_resource_url(sse_path)
|
|
187
|
+
resource_metadata_url = (
|
|
188
|
+
build_resource_metadata_url(resource_url) if resource_url else None
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Create protected SSE endpoint route
|
|
192
|
+
server_routes.append(
|
|
193
|
+
Route(
|
|
194
|
+
sse_path,
|
|
195
|
+
endpoint=RequireAuthMiddleware(
|
|
196
|
+
handle_sse,
|
|
197
|
+
auth.required_scopes,
|
|
198
|
+
resource_metadata_url,
|
|
199
|
+
),
|
|
200
|
+
methods=["GET"],
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Wrap the SSE message endpoint with RequireAuthMiddleware
|
|
180
205
|
server_routes.append(
|
|
181
206
|
Mount(
|
|
182
207
|
message_path,
|
|
183
208
|
app=RequireAuthMiddleware(
|
|
184
209
|
sse.handle_post_message,
|
|
185
210
|
auth.required_scopes,
|
|
186
|
-
|
|
211
|
+
resource_metadata_url,
|
|
187
212
|
),
|
|
188
213
|
)
|
|
189
214
|
)
|
|
@@ -215,11 +240,17 @@ def create_sse_app(
|
|
|
215
240
|
if middleware:
|
|
216
241
|
server_middleware.extend(middleware)
|
|
217
242
|
|
|
243
|
+
@asynccontextmanager
|
|
244
|
+
async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
|
|
245
|
+
async with server._lifespan_manager():
|
|
246
|
+
yield
|
|
247
|
+
|
|
218
248
|
# Create and return the app
|
|
219
249
|
app = create_base_app(
|
|
220
250
|
routes=server_routes,
|
|
221
251
|
middleware=server_middleware,
|
|
222
252
|
debug=debug,
|
|
253
|
+
lifespan=lifespan,
|
|
223
254
|
)
|
|
224
255
|
# Store the FastMCP server instance on the Starlette app state
|
|
225
256
|
app.state.fastmcp_server = server
|
|
@@ -232,6 +263,7 @@ def create_streamable_http_app(
|
|
|
232
263
|
server: FastMCP[LifespanResultT],
|
|
233
264
|
streamable_http_path: str,
|
|
234
265
|
event_store: EventStore | None = None,
|
|
266
|
+
retry_interval: int | None = None,
|
|
235
267
|
auth: AuthProvider | None = None,
|
|
236
268
|
json_response: bool = False,
|
|
237
269
|
stateless_http: bool = False,
|
|
@@ -244,7 +276,10 @@ def create_streamable_http_app(
|
|
|
244
276
|
Args:
|
|
245
277
|
server: The FastMCP server instance
|
|
246
278
|
streamable_http_path: Path for StreamableHTTP connections
|
|
247
|
-
event_store: Optional event store for
|
|
279
|
+
event_store: Optional event store for SSE polling/resumability
|
|
280
|
+
retry_interval: Optional retry interval in milliseconds for SSE polling.
|
|
281
|
+
Controls how quickly clients should reconnect after server-initiated
|
|
282
|
+
disconnections. Requires event_store to be set. Defaults to SDK default.
|
|
248
283
|
auth: Optional authentication provider (AuthProvider)
|
|
249
284
|
json_response: Whether to use JSON response format
|
|
250
285
|
stateless_http: Whether to use stateless mode (new transport per request)
|
|
@@ -262,6 +297,7 @@ def create_streamable_http_app(
|
|
|
262
297
|
session_manager = StreamableHTTPSessionManager(
|
|
263
298
|
app=server._mcp_server,
|
|
264
299
|
event_store=event_store,
|
|
300
|
+
retry_interval=retry_interval,
|
|
265
301
|
json_response=json_response,
|
|
266
302
|
stateless=stateless_http,
|
|
267
303
|
)
|
|
@@ -274,14 +310,29 @@ def create_streamable_http_app(
|
|
|
274
310
|
# Get auth middleware from the provider
|
|
275
311
|
auth_middleware = auth.get_middleware()
|
|
276
312
|
|
|
277
|
-
# Get auth routes
|
|
278
|
-
auth_routes = auth.get_routes(
|
|
279
|
-
mcp_path=streamable_http_path,
|
|
280
|
-
mcp_endpoint=streamable_http_app,
|
|
281
|
-
)
|
|
282
|
-
|
|
313
|
+
# Get auth provider's own routes (OAuth endpoints, metadata, etc)
|
|
314
|
+
auth_routes = auth.get_routes(mcp_path=streamable_http_path)
|
|
283
315
|
server_routes.extend(auth_routes)
|
|
284
316
|
server_middleware.extend(auth_middleware)
|
|
317
|
+
|
|
318
|
+
# Build RFC 9728-compliant metadata URL
|
|
319
|
+
resource_url = auth._get_resource_url(streamable_http_path)
|
|
320
|
+
resource_metadata_url = (
|
|
321
|
+
build_resource_metadata_url(resource_url) if resource_url else None
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Create protected HTTP endpoint route
|
|
325
|
+
server_routes.append(
|
|
326
|
+
Route(
|
|
327
|
+
streamable_http_path,
|
|
328
|
+
endpoint=RequireAuthMiddleware(
|
|
329
|
+
streamable_http_app,
|
|
330
|
+
auth.required_scopes,
|
|
331
|
+
resource_metadata_url,
|
|
332
|
+
),
|
|
333
|
+
methods=["GET", "POST", "DELETE"],
|
|
334
|
+
)
|
|
335
|
+
)
|
|
285
336
|
else:
|
|
286
337
|
# No auth required
|
|
287
338
|
server_routes.append(
|
|
@@ -303,7 +354,7 @@ def create_streamable_http_app(
|
|
|
303
354
|
# Create a lifespan manager to start and stop the session manager
|
|
304
355
|
@asynccontextmanager
|
|
305
356
|
async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
|
|
306
|
-
async with session_manager.run():
|
|
357
|
+
async with server._lifespan_manager(), session_manager.run():
|
|
307
358
|
yield
|
|
308
359
|
|
|
309
360
|
# Create and return the app with lifespan
|
fastmcp/server/low_level.py
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import weakref
|
|
4
|
+
from contextlib import AsyncExitStack
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import anyio
|
|
8
|
+
import mcp.types
|
|
9
|
+
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
|
10
|
+
from mcp import McpError
|
|
3
11
|
from mcp.server.lowlevel.server import (
|
|
4
12
|
LifespanResultT,
|
|
5
13
|
NotificationOptions,
|
|
@@ -9,11 +17,122 @@ from mcp.server.lowlevel.server import (
|
|
|
9
17
|
Server as _Server,
|
|
10
18
|
)
|
|
11
19
|
from mcp.server.models import InitializationOptions
|
|
20
|
+
from mcp.server.session import ServerSession
|
|
21
|
+
from mcp.server.stdio import stdio_server as stdio_server
|
|
22
|
+
from mcp.shared.message import SessionMessage
|
|
23
|
+
from mcp.shared.session import RequestResponder
|
|
24
|
+
|
|
25
|
+
from fastmcp.utilities.logging import get_logger
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from fastmcp.server.server import FastMCP
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MiddlewareServerSession(ServerSession):
|
|
34
|
+
"""ServerSession that routes initialization requests through FastMCP middleware."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, fastmcp: FastMCP, *args, **kwargs):
|
|
37
|
+
super().__init__(*args, **kwargs)
|
|
38
|
+
self._fastmcp_ref: weakref.ref[FastMCP] = weakref.ref(fastmcp)
|
|
39
|
+
# Task group for subscription tasks (set during session run)
|
|
40
|
+
self._subscription_task_group: anyio.TaskGroup | None = None # type: ignore[valid-type]
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def fastmcp(self) -> FastMCP:
|
|
44
|
+
"""Get the FastMCP instance."""
|
|
45
|
+
fastmcp = self._fastmcp_ref()
|
|
46
|
+
if fastmcp is None:
|
|
47
|
+
raise RuntimeError("FastMCP instance is no longer available")
|
|
48
|
+
return fastmcp
|
|
49
|
+
|
|
50
|
+
async def _received_request(
|
|
51
|
+
self,
|
|
52
|
+
responder: RequestResponder[mcp.types.ClientRequest, mcp.types.ServerResult],
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Override the _received_request method to route special requests
|
|
56
|
+
through FastMCP middleware.
|
|
57
|
+
|
|
58
|
+
Handles initialization requests and SEP-1686 task methods.
|
|
59
|
+
"""
|
|
60
|
+
import fastmcp.server.context
|
|
61
|
+
from fastmcp.server.middleware.middleware import MiddlewareContext
|
|
62
|
+
|
|
63
|
+
if isinstance(responder.request.root, mcp.types.InitializeRequest):
|
|
64
|
+
# The MCP SDK's ServerSession._received_request() handles the
|
|
65
|
+
# initialize request internally by calling responder.respond()
|
|
66
|
+
# to send the InitializeResult directly to the write stream, then
|
|
67
|
+
# returning None. This bypasses the middleware return path entirely,
|
|
68
|
+
# so middleware would only see the request, never the response.
|
|
69
|
+
#
|
|
70
|
+
# To expose the response to middleware (e.g., for logging server
|
|
71
|
+
# capabilities), we wrap responder.respond() to capture the
|
|
72
|
+
# InitializeResult before it's sent, then return it from
|
|
73
|
+
# call_original_handler so it flows back through the middleware chain.
|
|
74
|
+
captured_response: mcp.types.ServerResult | None = None
|
|
75
|
+
original_respond = responder.respond
|
|
76
|
+
|
|
77
|
+
async def capturing_respond(
|
|
78
|
+
response: mcp.types.ServerResult,
|
|
79
|
+
) -> None:
|
|
80
|
+
nonlocal captured_response
|
|
81
|
+
captured_response = response
|
|
82
|
+
return await original_respond(response)
|
|
83
|
+
|
|
84
|
+
responder.respond = capturing_respond # type: ignore[method-assign]
|
|
85
|
+
|
|
86
|
+
async def call_original_handler(
|
|
87
|
+
ctx: MiddlewareContext,
|
|
88
|
+
) -> mcp.types.InitializeResult | None:
|
|
89
|
+
await super(MiddlewareServerSession, self)._received_request(responder)
|
|
90
|
+
if captured_response is not None and isinstance(
|
|
91
|
+
captured_response.root, mcp.types.InitializeResult
|
|
92
|
+
):
|
|
93
|
+
return captured_response.root
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
async with fastmcp.server.context.Context(
|
|
97
|
+
fastmcp=self.fastmcp
|
|
98
|
+
) as fastmcp_ctx:
|
|
99
|
+
# Create the middleware context.
|
|
100
|
+
mw_context = MiddlewareContext(
|
|
101
|
+
message=responder.request.root,
|
|
102
|
+
source="client",
|
|
103
|
+
type="request",
|
|
104
|
+
method="initialize",
|
|
105
|
+
fastmcp_context=fastmcp_ctx,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
return await self.fastmcp._apply_middleware(
|
|
110
|
+
mw_context, call_original_handler
|
|
111
|
+
)
|
|
112
|
+
except McpError as e:
|
|
113
|
+
# McpError can be thrown from middleware in `on_initialize`
|
|
114
|
+
# send the error to responder.
|
|
115
|
+
if not responder._completed:
|
|
116
|
+
with responder:
|
|
117
|
+
await responder.respond(e.error)
|
|
118
|
+
else:
|
|
119
|
+
# Don't re-raise: prevents responding to initialize request twice
|
|
120
|
+
logger.warning(
|
|
121
|
+
"Received McpError but responder is already completed. "
|
|
122
|
+
"Cannot send error response as response was already sent.",
|
|
123
|
+
exc_info=e,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Fall through to default handling (task methods now handled via registered handlers)
|
|
127
|
+
return await super()._received_request(responder)
|
|
12
128
|
|
|
13
129
|
|
|
14
130
|
class LowLevelServer(_Server[LifespanResultT, RequestT]):
|
|
15
|
-
def __init__(self, *args, **kwargs):
|
|
131
|
+
def __init__(self, fastmcp: FastMCP, *args: Any, **kwargs: Any):
|
|
16
132
|
super().__init__(*args, **kwargs)
|
|
133
|
+
# Store a weak reference to FastMCP to avoid circular references
|
|
134
|
+
self._fastmcp_ref: weakref.ref[FastMCP] = weakref.ref(fastmcp)
|
|
135
|
+
|
|
17
136
|
# FastMCP servers support notifications for all components
|
|
18
137
|
self.notification_options = NotificationOptions(
|
|
19
138
|
prompts_changed=True,
|
|
@@ -21,6 +140,14 @@ class LowLevelServer(_Server[LifespanResultT, RequestT]):
|
|
|
21
140
|
tools_changed=True,
|
|
22
141
|
)
|
|
23
142
|
|
|
143
|
+
@property
|
|
144
|
+
def fastmcp(self) -> FastMCP:
|
|
145
|
+
"""Get the FastMCP instance."""
|
|
146
|
+
fastmcp = self._fastmcp_ref()
|
|
147
|
+
if fastmcp is None:
|
|
148
|
+
raise RuntimeError("FastMCP instance is no longer available")
|
|
149
|
+
return fastmcp
|
|
150
|
+
|
|
24
151
|
def create_initialization_options(
|
|
25
152
|
self,
|
|
26
153
|
notification_options: NotificationOptions | None = None,
|
|
@@ -35,3 +162,39 @@ class LowLevelServer(_Server[LifespanResultT, RequestT]):
|
|
|
35
162
|
experimental_capabilities=experimental_capabilities,
|
|
36
163
|
**kwargs,
|
|
37
164
|
)
|
|
165
|
+
|
|
166
|
+
async def run(
|
|
167
|
+
self,
|
|
168
|
+
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
|
|
169
|
+
write_stream: MemoryObjectSendStream[SessionMessage],
|
|
170
|
+
initialization_options: InitializationOptions,
|
|
171
|
+
raise_exceptions: bool = False,
|
|
172
|
+
stateless: bool = False,
|
|
173
|
+
):
|
|
174
|
+
"""
|
|
175
|
+
Overrides the run method to use the MiddlewareServerSession.
|
|
176
|
+
"""
|
|
177
|
+
async with AsyncExitStack() as stack:
|
|
178
|
+
lifespan_context = await stack.enter_async_context(self.lifespan(self))
|
|
179
|
+
session = await stack.enter_async_context(
|
|
180
|
+
MiddlewareServerSession(
|
|
181
|
+
self.fastmcp,
|
|
182
|
+
read_stream,
|
|
183
|
+
write_stream,
|
|
184
|
+
initialization_options,
|
|
185
|
+
stateless=stateless,
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
async with anyio.create_task_group() as tg:
|
|
190
|
+
# Store task group on session for subscription tasks (SEP-1686)
|
|
191
|
+
session._subscription_task_group = tg
|
|
192
|
+
|
|
193
|
+
async for message in session.incoming_messages:
|
|
194
|
+
tg.start_soon(
|
|
195
|
+
self._handle_message,
|
|
196
|
+
message,
|
|
197
|
+
session,
|
|
198
|
+
lifespan_context,
|
|
199
|
+
raise_exceptions,
|
|
200
|
+
)
|