fastmcp 2.3.0rc1__py3-none-any.whl → 2.3.2__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/cli/cli.py +7 -8
- fastmcp/low_level/README.md +1 -0
- fastmcp/low_level/__init__.py +0 -0
- fastmcp/low_level/sse_server_transport.py +104 -0
- fastmcp/server/http.py +64 -43
- fastmcp/server/proxy.py +1 -1
- fastmcp/server/server.py +148 -47
- fastmcp/tools/tool.py +1 -1
- fastmcp/utilities/tests.py +1 -1
- {fastmcp-2.3.0rc1.dist-info → fastmcp-2.3.2.dist-info}/METADATA +43 -47
- {fastmcp-2.3.0rc1.dist-info → fastmcp-2.3.2.dist-info}/RECORD +14 -12
- fastmcp/server/streamable_http_manager.py +0 -241
- {fastmcp-2.3.0rc1.dist-info → fastmcp-2.3.2.dist-info}/WHEEL +0 -0
- {fastmcp-2.3.0rc1.dist-info → fastmcp-2.3.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.3.0rc1.dist-info → fastmcp-2.3.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/cli.py
CHANGED
|
@@ -327,14 +327,14 @@ def run(
|
|
|
327
327
|
typer.Option(
|
|
328
328
|
"--transport",
|
|
329
329
|
"-t",
|
|
330
|
-
help="Transport protocol to use (stdio or sse)",
|
|
330
|
+
help="Transport protocol to use (stdio, streamable-http, or sse)",
|
|
331
331
|
),
|
|
332
332
|
] = None,
|
|
333
333
|
host: Annotated[
|
|
334
334
|
str | None,
|
|
335
335
|
typer.Option(
|
|
336
336
|
"--host",
|
|
337
|
-
help="Host to bind to when using
|
|
337
|
+
help="Host to bind to when using http transport (default: 127.0.0.1)",
|
|
338
338
|
),
|
|
339
339
|
] = None,
|
|
340
340
|
port: Annotated[
|
|
@@ -342,7 +342,7 @@ def run(
|
|
|
342
342
|
typer.Option(
|
|
343
343
|
"--port",
|
|
344
344
|
"-p",
|
|
345
|
-
help="Port to bind to when using
|
|
345
|
+
help="Port to bind to when using http transport (default: 8000)",
|
|
346
346
|
),
|
|
347
347
|
] = None,
|
|
348
348
|
log_level: Annotated[
|
|
@@ -350,20 +350,19 @@ def run(
|
|
|
350
350
|
typer.Option(
|
|
351
351
|
"--log-level",
|
|
352
352
|
"-l",
|
|
353
|
-
help="Log level
|
|
353
|
+
help="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
|
|
354
354
|
),
|
|
355
355
|
] = None,
|
|
356
356
|
) -> None:
|
|
357
357
|
"""Run a MCP server.
|
|
358
358
|
|
|
359
|
-
The server can be specified in two ways
|
|
359
|
+
The server can be specified in two ways:
|
|
360
360
|
1. Module approach: server.py - runs the module directly, expecting a server.run() call.\n
|
|
361
361
|
2. Import approach: server.py:app - imports and runs the specified server object.\n\n
|
|
362
362
|
|
|
363
363
|
Note: This command runs the server directly. You are responsible for ensuring
|
|
364
|
-
all dependencies are available
|
|
365
|
-
|
|
366
|
-
""" # noqa: E501
|
|
364
|
+
all dependencies are available.
|
|
365
|
+
"""
|
|
367
366
|
file, server_object = _parse_file_path(file_spec)
|
|
368
367
|
|
|
369
368
|
logger.debug(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Patched low-level objects. When possible, we prefer the official SDK, but we patch bugs here if necessary.
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
import anyio
|
|
8
|
+
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
|
9
|
+
from mcp.server.sse import SseServerTransport as LowLevelSSEServerTransport
|
|
10
|
+
from mcp.shared.message import SessionMessage
|
|
11
|
+
from sse_starlette import EventSourceResponse
|
|
12
|
+
from starlette.types import Receive, Scope, Send
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SseServerTransport(LowLevelSSEServerTransport):
|
|
18
|
+
"""
|
|
19
|
+
Patched SSE server transport
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@asynccontextmanager
|
|
23
|
+
async def connect_sse(self, scope: Scope, receive: Receive, send: Send):
|
|
24
|
+
"""
|
|
25
|
+
See https://github.com/modelcontextprotocol/python-sdk/pull/659/
|
|
26
|
+
"""
|
|
27
|
+
if scope["type"] != "http":
|
|
28
|
+
logger.error("connect_sse received non-HTTP request")
|
|
29
|
+
raise ValueError("connect_sse can only handle HTTP requests")
|
|
30
|
+
|
|
31
|
+
logger.debug("Setting up SSE connection")
|
|
32
|
+
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
|
|
33
|
+
read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]
|
|
34
|
+
|
|
35
|
+
write_stream: MemoryObjectSendStream[SessionMessage]
|
|
36
|
+
write_stream_reader: MemoryObjectReceiveStream[SessionMessage]
|
|
37
|
+
|
|
38
|
+
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
|
|
39
|
+
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
|
|
40
|
+
|
|
41
|
+
session_id = uuid4()
|
|
42
|
+
self._read_stream_writers[session_id] = read_stream_writer
|
|
43
|
+
logger.debug(f"Created new session with ID: {session_id}")
|
|
44
|
+
|
|
45
|
+
# Determine the full path for the message endpoint to be sent to the client.
|
|
46
|
+
# scope['root_path'] is the prefix where the current Starlette app
|
|
47
|
+
# instance is mounted.
|
|
48
|
+
# e.g., "" if top-level, or "/api_prefix" if mounted under "/api_prefix".
|
|
49
|
+
root_path = scope.get("root_path", "")
|
|
50
|
+
|
|
51
|
+
# self._endpoint is the path *within* this app, e.g., "/messages".
|
|
52
|
+
# Concatenating them gives the full absolute path from the server root.
|
|
53
|
+
# e.g., "" + "/messages" -> "/messages"
|
|
54
|
+
# e.g., "/api_prefix" + "/messages" -> "/api_prefix/messages"
|
|
55
|
+
full_message_path_for_client = root_path.rstrip("/") + self._endpoint
|
|
56
|
+
|
|
57
|
+
# This is the URI (path + query) the client will use to POST messages.
|
|
58
|
+
client_post_uri_data = (
|
|
59
|
+
f"{quote(full_message_path_for_client)}?session_id={session_id.hex}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[
|
|
63
|
+
dict[str, Any]
|
|
64
|
+
](0)
|
|
65
|
+
|
|
66
|
+
async def sse_writer():
|
|
67
|
+
logger.debug("Starting SSE writer")
|
|
68
|
+
async with sse_stream_writer, write_stream_reader:
|
|
69
|
+
await sse_stream_writer.send(
|
|
70
|
+
{"event": "endpoint", "data": client_post_uri_data}
|
|
71
|
+
)
|
|
72
|
+
logger.debug(f"Sent endpoint event: {client_post_uri_data}")
|
|
73
|
+
|
|
74
|
+
async for session_message in write_stream_reader:
|
|
75
|
+
logger.debug(f"Sending message via SSE: {session_message}")
|
|
76
|
+
await sse_stream_writer.send(
|
|
77
|
+
{
|
|
78
|
+
"event": "message",
|
|
79
|
+
"data": session_message.message.model_dump_json(
|
|
80
|
+
by_alias=True, exclude_none=True
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
async with anyio.create_task_group() as tg:
|
|
86
|
+
|
|
87
|
+
async def response_wrapper(scope: Scope, receive: Receive, send: Send):
|
|
88
|
+
"""
|
|
89
|
+
The EventSourceResponse returning signals a client close / disconnect.
|
|
90
|
+
In this case we close our side of the streams to signal the client that
|
|
91
|
+
the connection has been closed.
|
|
92
|
+
"""
|
|
93
|
+
await EventSourceResponse(
|
|
94
|
+
content=sse_stream_reader, data_sender_callable=sse_writer
|
|
95
|
+
)(scope, receive, send)
|
|
96
|
+
await read_stream_writer.aclose()
|
|
97
|
+
await write_stream_reader.aclose()
|
|
98
|
+
logging.debug(f"Client session disconnected {session_id}")
|
|
99
|
+
|
|
100
|
+
logger.debug("Starting SSE response task")
|
|
101
|
+
tg.start_soon(response_wrapper, scope, receive, send)
|
|
102
|
+
|
|
103
|
+
logger.debug("Yielding read and write streams")
|
|
104
|
+
yield (read_stream, write_stream)
|
fastmcp/server/http.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import AsyncGenerator, Callable, Generator
|
|
4
4
|
from contextlib import asynccontextmanager, contextmanager
|
|
5
5
|
from contextvars import ContextVar
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
|
|
9
9
|
from mcp.server.auth.middleware.bearer_auth import (
|
|
@@ -13,17 +13,16 @@ from mcp.server.auth.middleware.bearer_auth import (
|
|
|
13
13
|
from mcp.server.auth.provider import OAuthAuthorizationServerProvider
|
|
14
14
|
from mcp.server.auth.routes import create_auth_routes
|
|
15
15
|
from mcp.server.auth.settings import AuthSettings
|
|
16
|
-
from mcp.server.
|
|
16
|
+
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
17
17
|
from starlette.applications import Starlette
|
|
18
18
|
from starlette.middleware import Middleware
|
|
19
19
|
from starlette.middleware.authentication import AuthenticationMiddleware
|
|
20
20
|
from starlette.requests import Request
|
|
21
21
|
from starlette.responses import Response
|
|
22
|
-
from starlette.routing import Mount, Route
|
|
22
|
+
from starlette.routing import BaseRoute, Mount, Route
|
|
23
23
|
from starlette.types import Receive, Scope, Send
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
from fastmcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
25
|
+
from fastmcp.low_level.sse_server_transport import SseServerTransport
|
|
27
26
|
from fastmcp.utilities.logging import get_logger
|
|
28
27
|
|
|
29
28
|
if TYPE_CHECKING:
|
|
@@ -65,7 +64,7 @@ class RequestContextMiddleware:
|
|
|
65
64
|
def setup_auth_middleware_and_routes(
|
|
66
65
|
auth_server_provider: OAuthAuthorizationServerProvider | None,
|
|
67
66
|
auth_settings: AuthSettings | None,
|
|
68
|
-
) -> tuple[list[Middleware], list[
|
|
67
|
+
) -> tuple[list[Middleware], list[BaseRoute], list[str]]:
|
|
69
68
|
"""Set up authentication middleware and routes if auth is enabled.
|
|
70
69
|
|
|
71
70
|
Args:
|
|
@@ -76,7 +75,7 @@ def setup_auth_middleware_and_routes(
|
|
|
76
75
|
Tuple of (middleware, auth_routes, required_scopes)
|
|
77
76
|
"""
|
|
78
77
|
middleware: list[Middleware] = []
|
|
79
|
-
auth_routes: list[
|
|
78
|
+
auth_routes: list[BaseRoute] = []
|
|
80
79
|
required_scopes: list[str] = []
|
|
81
80
|
|
|
82
81
|
if auth_server_provider:
|
|
@@ -109,9 +108,9 @@ def setup_auth_middleware_and_routes(
|
|
|
109
108
|
|
|
110
109
|
|
|
111
110
|
def create_base_app(
|
|
112
|
-
routes: list[
|
|
111
|
+
routes: list[BaseRoute],
|
|
113
112
|
middleware: list[Middleware],
|
|
114
|
-
debug: bool,
|
|
113
|
+
debug: bool = False,
|
|
115
114
|
lifespan: Callable | None = None,
|
|
116
115
|
) -> Starlette:
|
|
117
116
|
"""Create a base Starlette app with common middleware and routes.
|
|
@@ -128,17 +127,12 @@ def create_base_app(
|
|
|
128
127
|
# Always add RequestContextMiddleware as the outermost middleware
|
|
129
128
|
middleware.append(Middleware(RequestContextMiddleware))
|
|
130
129
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if lifespan:
|
|
139
|
-
app_kwargs["lifespan"] = lifespan
|
|
140
|
-
|
|
141
|
-
return Starlette(**app_kwargs)
|
|
130
|
+
return Starlette(
|
|
131
|
+
routes=routes,
|
|
132
|
+
middleware=middleware,
|
|
133
|
+
debug=debug,
|
|
134
|
+
lifespan=lifespan,
|
|
135
|
+
)
|
|
142
136
|
|
|
143
137
|
|
|
144
138
|
def create_sse_app(
|
|
@@ -148,7 +142,8 @@ def create_sse_app(
|
|
|
148
142
|
auth_server_provider: OAuthAuthorizationServerProvider | None = None,
|
|
149
143
|
auth_settings: AuthSettings | None = None,
|
|
150
144
|
debug: bool = False,
|
|
151
|
-
|
|
145
|
+
routes: list[BaseRoute] | None = None,
|
|
146
|
+
middleware: list[Middleware] | None = None,
|
|
152
147
|
) -> Starlette:
|
|
153
148
|
"""Return an instance of the SSE server app.
|
|
154
149
|
|
|
@@ -159,11 +154,15 @@ def create_sse_app(
|
|
|
159
154
|
auth_server_provider: Optional auth provider
|
|
160
155
|
auth_settings: Optional auth settings
|
|
161
156
|
debug: Whether to enable debug mode
|
|
162
|
-
|
|
163
|
-
|
|
157
|
+
routes: Optional list of custom routes
|
|
158
|
+
middleware: Optional list of middleware
|
|
164
159
|
Returns:
|
|
165
160
|
A Starlette application with RequestContextMiddleware
|
|
166
161
|
"""
|
|
162
|
+
|
|
163
|
+
server_routes: list[BaseRoute] = []
|
|
164
|
+
server_middleware: list[Middleware] = []
|
|
165
|
+
|
|
167
166
|
# Set up SSE transport
|
|
168
167
|
sse = SseServerTransport(message_path)
|
|
169
168
|
|
|
@@ -178,24 +177,24 @@ def create_sse_app(
|
|
|
178
177
|
return Response()
|
|
179
178
|
|
|
180
179
|
# Get auth middleware and routes
|
|
181
|
-
|
|
180
|
+
auth_middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
|
|
182
181
|
auth_server_provider, auth_settings
|
|
183
182
|
)
|
|
184
183
|
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
server_routes.extend(auth_routes)
|
|
185
|
+
server_middleware.extend(auth_middleware)
|
|
187
186
|
|
|
188
187
|
# Add SSE routes with or without auth
|
|
189
188
|
if auth_server_provider:
|
|
190
189
|
# Auth is enabled, wrap endpoints with RequireAuthMiddleware
|
|
191
|
-
|
|
190
|
+
server_routes.append(
|
|
192
191
|
Route(
|
|
193
192
|
sse_path,
|
|
194
193
|
endpoint=RequireAuthMiddleware(handle_sse, required_scopes),
|
|
195
194
|
methods=["GET"],
|
|
196
195
|
)
|
|
197
196
|
)
|
|
198
|
-
|
|
197
|
+
server_routes.append(
|
|
199
198
|
Mount(
|
|
200
199
|
message_path,
|
|
201
200
|
app=RequireAuthMiddleware(sse.handle_post_message, required_scopes),
|
|
@@ -206,14 +205,14 @@ def create_sse_app(
|
|
|
206
205
|
async def sse_endpoint(request: Request) -> Response:
|
|
207
206
|
return await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
|
|
208
207
|
|
|
209
|
-
|
|
208
|
+
server_routes.append(
|
|
210
209
|
Route(
|
|
211
210
|
sse_path,
|
|
212
211
|
endpoint=sse_endpoint,
|
|
213
212
|
methods=["GET"],
|
|
214
213
|
)
|
|
215
214
|
)
|
|
216
|
-
|
|
215
|
+
server_routes.append(
|
|
217
216
|
Mount(
|
|
218
217
|
message_path,
|
|
219
218
|
app=sse.handle_post_message,
|
|
@@ -221,11 +220,19 @@ def create_sse_app(
|
|
|
221
220
|
)
|
|
222
221
|
|
|
223
222
|
# Add custom routes with lowest precedence
|
|
224
|
-
if
|
|
225
|
-
|
|
223
|
+
if routes:
|
|
224
|
+
server_routes.extend(routes)
|
|
225
|
+
|
|
226
|
+
# Add middleware
|
|
227
|
+
if middleware:
|
|
228
|
+
server_middleware.extend(middleware)
|
|
226
229
|
|
|
227
230
|
# Create and return the app
|
|
228
|
-
return create_base_app(
|
|
231
|
+
return create_base_app(
|
|
232
|
+
routes=server_routes,
|
|
233
|
+
middleware=server_middleware,
|
|
234
|
+
debug=debug,
|
|
235
|
+
)
|
|
229
236
|
|
|
230
237
|
|
|
231
238
|
def create_streamable_http_app(
|
|
@@ -237,7 +244,8 @@ def create_streamable_http_app(
|
|
|
237
244
|
json_response: bool = False,
|
|
238
245
|
stateless_http: bool = False,
|
|
239
246
|
debug: bool = False,
|
|
240
|
-
|
|
247
|
+
routes: list[BaseRoute] | None = None,
|
|
248
|
+
middleware: list[Middleware] | None = None,
|
|
241
249
|
) -> Starlette:
|
|
242
250
|
"""Return an instance of the StreamableHTTP server app.
|
|
243
251
|
|
|
@@ -250,11 +258,15 @@ def create_streamable_http_app(
|
|
|
250
258
|
json_response: Whether to use JSON response format
|
|
251
259
|
stateless_http: Whether to use stateless mode (new transport per request)
|
|
252
260
|
debug: Whether to enable debug mode
|
|
253
|
-
|
|
261
|
+
routes: Optional list of custom routes
|
|
262
|
+
middleware: Optional list of middleware
|
|
254
263
|
|
|
255
264
|
Returns:
|
|
256
265
|
A Starlette application with StreamableHTTP support
|
|
257
266
|
"""
|
|
267
|
+
server_routes: list[BaseRoute] = []
|
|
268
|
+
server_middleware: list[Middleware] = []
|
|
269
|
+
|
|
258
270
|
# Create session manager using the provided event store
|
|
259
271
|
session_manager = StreamableHTTPSessionManager(
|
|
260
272
|
app=server._mcp_server,
|
|
@@ -270,17 +282,17 @@ def create_streamable_http_app(
|
|
|
270
282
|
await session_manager.handle_request(scope, receive, send)
|
|
271
283
|
|
|
272
284
|
# Get auth middleware and routes
|
|
273
|
-
|
|
285
|
+
auth_middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
|
|
274
286
|
auth_server_provider, auth_settings
|
|
275
287
|
)
|
|
276
288
|
|
|
277
|
-
|
|
278
|
-
|
|
289
|
+
server_routes.extend(auth_routes)
|
|
290
|
+
server_middleware.extend(auth_middleware)
|
|
279
291
|
|
|
280
292
|
# Add StreamableHTTP routes with or without auth
|
|
281
293
|
if auth_server_provider:
|
|
282
294
|
# Auth is enabled, wrap endpoint with RequireAuthMiddleware
|
|
283
|
-
|
|
295
|
+
server_routes.append(
|
|
284
296
|
Mount(
|
|
285
297
|
streamable_http_path,
|
|
286
298
|
app=RequireAuthMiddleware(handle_streamable_http, required_scopes),
|
|
@@ -288,7 +300,7 @@ def create_streamable_http_app(
|
|
|
288
300
|
)
|
|
289
301
|
else:
|
|
290
302
|
# No auth required
|
|
291
|
-
|
|
303
|
+
server_routes.append(
|
|
292
304
|
Mount(
|
|
293
305
|
streamable_http_path,
|
|
294
306
|
app=handle_streamable_http,
|
|
@@ -296,8 +308,12 @@ def create_streamable_http_app(
|
|
|
296
308
|
)
|
|
297
309
|
|
|
298
310
|
# Add custom routes with lowest precedence
|
|
299
|
-
if
|
|
300
|
-
|
|
311
|
+
if routes:
|
|
312
|
+
server_routes.extend(routes)
|
|
313
|
+
|
|
314
|
+
# Add middleware
|
|
315
|
+
if middleware:
|
|
316
|
+
server_middleware.extend(middleware)
|
|
301
317
|
|
|
302
318
|
# Create a lifespan manager to start and stop the session manager
|
|
303
319
|
@asynccontextmanager
|
|
@@ -306,4 +322,9 @@ def create_streamable_http_app(
|
|
|
306
322
|
yield
|
|
307
323
|
|
|
308
324
|
# Create and return the app with lifespan
|
|
309
|
-
return create_base_app(
|
|
325
|
+
return create_base_app(
|
|
326
|
+
routes=server_routes,
|
|
327
|
+
middleware=server_middleware,
|
|
328
|
+
debug=debug,
|
|
329
|
+
lifespan=lifespan,
|
|
330
|
+
)
|
fastmcp/server/proxy.py
CHANGED
|
@@ -124,7 +124,7 @@ class ProxyTemplate(ResourceTemplate):
|
|
|
124
124
|
params: dict[str, Any],
|
|
125
125
|
context: Context | None = None,
|
|
126
126
|
) -> ProxyResource:
|
|
127
|
-
#
|
|
127
|
+
# don't use the provided uri, because it may not be the same as the
|
|
128
128
|
# uri_template on the remote server.
|
|
129
129
|
# quote params to ensure they are valid for the uri_template
|
|
130
130
|
parameterized_uri = self.uri_template.format(
|
fastmcp/server/server.py
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import datetime
|
|
6
|
+
import inspect
|
|
7
|
+
import warnings
|
|
6
8
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
7
9
|
from contextlib import (
|
|
8
10
|
AbstractAsyncContextManager,
|
|
@@ -35,9 +37,10 @@ from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
|
35
37
|
from mcp.types import Tool as MCPTool
|
|
36
38
|
from pydantic import AnyUrl
|
|
37
39
|
from starlette.applications import Starlette
|
|
40
|
+
from starlette.middleware import Middleware
|
|
38
41
|
from starlette.requests import Request
|
|
39
42
|
from starlette.responses import Response
|
|
40
|
-
from starlette.routing import Route
|
|
43
|
+
from starlette.routing import BaseRoute, Route
|
|
41
44
|
|
|
42
45
|
import fastmcp.server
|
|
43
46
|
import fastmcp.settings
|
|
@@ -147,7 +150,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
147
150
|
)
|
|
148
151
|
self._auth_server_provider = auth_server_provider
|
|
149
152
|
|
|
150
|
-
self._additional_http_routes: list[
|
|
153
|
+
self._additional_http_routes: list[BaseRoute] = []
|
|
151
154
|
self.dependencies = self.settings.dependencies
|
|
152
155
|
|
|
153
156
|
# Set up MCP protocol handlers
|
|
@@ -169,7 +172,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
169
172
|
|
|
170
173
|
async def run_async(
|
|
171
174
|
self,
|
|
172
|
-
transport: Literal["stdio", "
|
|
175
|
+
transport: Literal["stdio", "streamable-http", "sse"] | None = None,
|
|
173
176
|
**transport_kwargs: Any,
|
|
174
177
|
) -> None:
|
|
175
178
|
"""Run the FastMCP server asynchronously.
|
|
@@ -179,19 +182,21 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
179
182
|
"""
|
|
180
183
|
if transport is None:
|
|
181
184
|
transport = "stdio"
|
|
182
|
-
if transport not in ["stdio", "
|
|
185
|
+
if transport not in ["stdio", "streamable-http", "sse"]:
|
|
183
186
|
raise ValueError(f"Unknown transport: {transport}")
|
|
184
187
|
|
|
185
188
|
if transport == "stdio":
|
|
186
189
|
await self.run_stdio_async(**transport_kwargs)
|
|
190
|
+
elif transport == "streamable-http":
|
|
191
|
+
await self.run_http_async(transport="streamable-http", **transport_kwargs)
|
|
187
192
|
elif transport == "sse":
|
|
188
|
-
await self.
|
|
189
|
-
else:
|
|
190
|
-
|
|
193
|
+
await self.run_http_async(transport="sse", **transport_kwargs)
|
|
194
|
+
else:
|
|
195
|
+
raise ValueError(f"Unknown transport: {transport}")
|
|
191
196
|
|
|
192
197
|
def run(
|
|
193
198
|
self,
|
|
194
|
-
transport: Literal["stdio", "
|
|
199
|
+
transport: Literal["stdio", "streamable-http", "sse"] | None = None,
|
|
195
200
|
**transport_kwargs: Any,
|
|
196
201
|
) -> None:
|
|
197
202
|
"""Run the FastMCP server. Note this is a synchronous function.
|
|
@@ -713,22 +718,31 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
713
718
|
self._mcp_server.create_initialization_options(),
|
|
714
719
|
)
|
|
715
720
|
|
|
716
|
-
async def
|
|
721
|
+
async def run_http_async(
|
|
717
722
|
self,
|
|
723
|
+
transport: Literal["streamable-http", "sse"] = "streamable-http",
|
|
718
724
|
host: str | None = None,
|
|
719
725
|
port: int | None = None,
|
|
720
726
|
log_level: str | None = None,
|
|
721
727
|
path: str | None = None,
|
|
722
|
-
message_path: str | None = None,
|
|
723
728
|
uvicorn_config: dict | None = None,
|
|
724
729
|
) -> None:
|
|
725
|
-
"""Run the server using
|
|
730
|
+
"""Run the server using HTTP transport.
|
|
731
|
+
|
|
732
|
+
Args:
|
|
733
|
+
transport: Transport protocol to use - either "streamable-http" (default) or "sse"
|
|
734
|
+
host: Host address to bind to (defaults to settings.host)
|
|
735
|
+
port: Port to bind to (defaults to settings.port)
|
|
736
|
+
log_level: Log level for the server (defaults to settings.log_level)
|
|
737
|
+
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
|
738
|
+
uvicorn_config: Additional configuration for the Uvicorn server
|
|
739
|
+
"""
|
|
726
740
|
uvicorn_config = uvicorn_config or {}
|
|
727
|
-
# the SSE app hangs even when a signal is sent, so we disable the
|
|
728
|
-
# timeout to make it possible to close immediately. see
|
|
729
|
-
# https://github.com/jlowin/fastmcp/issues/296
|
|
730
741
|
uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
|
|
731
|
-
|
|
742
|
+
# lifespan is required for streamable http
|
|
743
|
+
uvicorn_config["lifespan"] = "on"
|
|
744
|
+
|
|
745
|
+
app = self.http_app(path=path, transport=transport)
|
|
732
746
|
|
|
733
747
|
config = uvicorn.Config(
|
|
734
748
|
app,
|
|
@@ -740,12 +754,58 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
740
754
|
server = uvicorn.Server(config)
|
|
741
755
|
await server.serve()
|
|
742
756
|
|
|
757
|
+
async def run_sse_async(
|
|
758
|
+
self,
|
|
759
|
+
host: str | None = None,
|
|
760
|
+
port: int | None = None,
|
|
761
|
+
log_level: str | None = None,
|
|
762
|
+
path: str | None = None,
|
|
763
|
+
message_path: str | None = None,
|
|
764
|
+
uvicorn_config: dict | None = None,
|
|
765
|
+
) -> None:
|
|
766
|
+
"""Run the server using SSE transport."""
|
|
767
|
+
warnings.warn(
|
|
768
|
+
inspect.cleandoc(
|
|
769
|
+
"""
|
|
770
|
+
The run_sse_async method is deprecated. Use run_http_async for a
|
|
771
|
+
modern (non-SSE) alternative, or create an SSE app with
|
|
772
|
+
`fastmcp.server.http.create_sse_app` and run it directly.
|
|
773
|
+
"""
|
|
774
|
+
),
|
|
775
|
+
DeprecationWarning,
|
|
776
|
+
)
|
|
777
|
+
await self.run_http_async(
|
|
778
|
+
transport="sse",
|
|
779
|
+
host=host,
|
|
780
|
+
port=port,
|
|
781
|
+
log_level=log_level,
|
|
782
|
+
path=path,
|
|
783
|
+
uvicorn_config=uvicorn_config,
|
|
784
|
+
)
|
|
785
|
+
|
|
743
786
|
def sse_app(
|
|
744
787
|
self,
|
|
745
788
|
path: str | None = None,
|
|
746
789
|
message_path: str | None = None,
|
|
790
|
+
middleware: list[Middleware] | None = None,
|
|
747
791
|
) -> Starlette:
|
|
748
|
-
"""
|
|
792
|
+
"""
|
|
793
|
+
Create a Starlette app for the SSE server.
|
|
794
|
+
|
|
795
|
+
Args:
|
|
796
|
+
path: The path to the SSE endpoint
|
|
797
|
+
message_path: The path to the message endpoint
|
|
798
|
+
middleware: A list of middleware to apply to the app
|
|
799
|
+
"""
|
|
800
|
+
warnings.warn(
|
|
801
|
+
inspect.cleandoc(
|
|
802
|
+
"""
|
|
803
|
+
The sse_app method is deprecated. Use http_app as a modern (non-SSE)
|
|
804
|
+
alternative, or call `fastmcp.server.http.create_sse_app` directly.
|
|
805
|
+
"""
|
|
806
|
+
),
|
|
807
|
+
DeprecationWarning,
|
|
808
|
+
)
|
|
749
809
|
return create_sse_app(
|
|
750
810
|
server=self,
|
|
751
811
|
message_path=message_path or self.settings.message_path,
|
|
@@ -753,24 +813,70 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
753
813
|
auth_server_provider=self._auth_server_provider,
|
|
754
814
|
auth_settings=self.settings.auth,
|
|
755
815
|
debug=self.settings.debug,
|
|
756
|
-
|
|
816
|
+
routes=self._additional_http_routes,
|
|
817
|
+
middleware=middleware,
|
|
757
818
|
)
|
|
758
819
|
|
|
759
|
-
def streamable_http_app(
|
|
760
|
-
|
|
761
|
-
|
|
820
|
+
def streamable_http_app(
|
|
821
|
+
self,
|
|
822
|
+
path: str | None = None,
|
|
823
|
+
middleware: list[Middleware] | None = None,
|
|
824
|
+
) -> Starlette:
|
|
825
|
+
"""
|
|
826
|
+
Create a Starlette app for the StreamableHTTP server.
|
|
762
827
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
stateless_http=self.settings.stateless_http,
|
|
771
|
-
debug=self.settings.debug,
|
|
772
|
-
additional_routes=self._additional_http_routes,
|
|
828
|
+
Args:
|
|
829
|
+
path: The path to the StreamableHTTP endpoint
|
|
830
|
+
middleware: A list of middleware to apply to the app
|
|
831
|
+
"""
|
|
832
|
+
warnings.warn(
|
|
833
|
+
"The streamable_http_app method is deprecated. Use http_app() instead.",
|
|
834
|
+
DeprecationWarning,
|
|
773
835
|
)
|
|
836
|
+
return self.http_app(path=path, middleware=middleware)
|
|
837
|
+
|
|
838
|
+
def http_app(
|
|
839
|
+
self,
|
|
840
|
+
path: str | None = None,
|
|
841
|
+
middleware: list[Middleware] | None = None,
|
|
842
|
+
transport: Literal["streamable-http", "sse"] = "streamable-http",
|
|
843
|
+
) -> Starlette:
|
|
844
|
+
"""Create a Starlette app using the specified HTTP transport.
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
path: The path for the HTTP endpoint
|
|
848
|
+
middleware: A list of middleware to apply to the app
|
|
849
|
+
transport: Transport protocol to use - either "streamable-http" (default) or "sse"
|
|
850
|
+
|
|
851
|
+
Returns:
|
|
852
|
+
A Starlette application configured with the specified transport
|
|
853
|
+
"""
|
|
854
|
+
from fastmcp.server.http import create_streamable_http_app
|
|
855
|
+
|
|
856
|
+
if transport == "streamable-http":
|
|
857
|
+
return create_streamable_http_app(
|
|
858
|
+
server=self,
|
|
859
|
+
streamable_http_path=path or self.settings.streamable_http_path,
|
|
860
|
+
event_store=None,
|
|
861
|
+
auth_server_provider=self._auth_server_provider,
|
|
862
|
+
auth_settings=self.settings.auth,
|
|
863
|
+
json_response=self.settings.json_response,
|
|
864
|
+
stateless_http=self.settings.stateless_http,
|
|
865
|
+
debug=self.settings.debug,
|
|
866
|
+
routes=self._additional_http_routes,
|
|
867
|
+
middleware=middleware,
|
|
868
|
+
)
|
|
869
|
+
elif transport == "sse":
|
|
870
|
+
return create_sse_app(
|
|
871
|
+
server=self,
|
|
872
|
+
message_path=path or self.settings.message_path,
|
|
873
|
+
sse_path=path or self.settings.sse_path,
|
|
874
|
+
auth_server_provider=self._auth_server_provider,
|
|
875
|
+
auth_settings=self.settings.auth,
|
|
876
|
+
debug=self.settings.debug,
|
|
877
|
+
routes=self._additional_http_routes,
|
|
878
|
+
middleware=middleware,
|
|
879
|
+
)
|
|
774
880
|
|
|
775
881
|
async def run_streamable_http_async(
|
|
776
882
|
self,
|
|
@@ -780,23 +886,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
780
886
|
path: str | None = None,
|
|
781
887
|
uvicorn_config: dict | None = None,
|
|
782
888
|
) -> None:
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
# lifespan is required for streamable http
|
|
795
|
-
lifespan="on",
|
|
796
|
-
**uvicorn_config,
|
|
889
|
+
warnings.warn(
|
|
890
|
+
"The run_streamable_http_async method is deprecated. Use run_http_async instead.",
|
|
891
|
+
DeprecationWarning,
|
|
892
|
+
)
|
|
893
|
+
await self.run_http_async(
|
|
894
|
+
transport="streamable-http",
|
|
895
|
+
host=host,
|
|
896
|
+
port=port,
|
|
897
|
+
log_level=log_level,
|
|
898
|
+
path=path,
|
|
899
|
+
uvicorn_config=uvicorn_config,
|
|
797
900
|
)
|
|
798
|
-
server = uvicorn.Server(config)
|
|
799
|
-
await server.serve()
|
|
800
901
|
|
|
801
902
|
def mount(
|
|
802
903
|
self,
|
|
@@ -892,7 +993,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
892
993
|
future changes to the imported server will not be reflected in the
|
|
893
994
|
importing server. Server-level configurations and lifespans are not imported.
|
|
894
995
|
|
|
895
|
-
When
|
|
996
|
+
When a server is mounted: - The tools are imported with prefixed names
|
|
896
997
|
using the tool_separator
|
|
897
998
|
Example: If server has a tool named "get_weather", it will be
|
|
898
999
|
available as "weatherget_weather"
|
fastmcp/tools/tool.py
CHANGED
|
@@ -192,7 +192,7 @@ def _convert_to_content(
|
|
|
192
192
|
other_content.append(item)
|
|
193
193
|
if other_content:
|
|
194
194
|
other_content = _convert_to_content(
|
|
195
|
-
other_content, _process_as_single_item=True
|
|
195
|
+
other_content, serializer=serializer, _process_as_single_item=True
|
|
196
196
|
)
|
|
197
197
|
|
|
198
198
|
return other_content + mcp_types
|
fastmcp/utilities/tests.py
CHANGED
|
@@ -55,7 +55,7 @@ def temporary_settings(**kwargs: Any):
|
|
|
55
55
|
def _run_server(mcp_server: FastMCP, transport: Literal["sse"], port: int) -> None:
|
|
56
56
|
# Some Starlette apps are not pickleable, so we need to create them here based on the indicated transport
|
|
57
57
|
if transport == "sse":
|
|
58
|
-
app = mcp_server.
|
|
58
|
+
app = mcp_server.http_app(transport="sse")
|
|
59
59
|
else:
|
|
60
60
|
raise ValueError(f"Invalid transport: {transport}")
|
|
61
61
|
uvicorn_server = uvicorn.Server(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastmcp
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
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
|
|
@@ -19,7 +19,7 @@ Classifier: Typing :: Typed
|
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Requires-Dist: exceptiongroup>=1.2.2
|
|
21
21
|
Requires-Dist: httpx>=0.28.1
|
|
22
|
-
Requires-Dist: mcp
|
|
22
|
+
Requires-Dist: mcp<2.0.0,>=1.8.0
|
|
23
23
|
Requires-Dist: openapi-pydantic>=0.5.1
|
|
24
24
|
Requires-Dist: python-dotenv>=1.1.0
|
|
25
25
|
Requires-Dist: rich>=13.9.4
|
|
@@ -44,7 +44,7 @@ Description-Content-Type: text/markdown
|
|
|
44
44
|
> [!NOTE]
|
|
45
45
|
> #### FastMCP 2.0 & The Official MCP SDK
|
|
46
46
|
>
|
|
47
|
-
> Recognize the `FastMCP` name? You might have
|
|
47
|
+
> Recognize the `FastMCP` name? You might have seen the version that was contributed to the [official MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk), which was based on **FastMCP 1.0**.
|
|
48
48
|
>
|
|
49
49
|
> **Welcome to FastMCP 2.0!** This is the actively developed successor, and it significantly expands on 1.0 by introducing powerful client capabilities, server proxying & composition, OpenAPI/FastAPI integration, and more advanced features.
|
|
50
50
|
>
|
|
@@ -76,7 +76,13 @@ fastmcp run server.py
|
|
|
76
76
|
|
|
77
77
|
### 📚 Documentation
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
FastMCP's complete documentation is available at **[gofastmcp.com](https://gofastmcp.com)**, including detailed guides, API references, and advanced patterns. This readme provides only a high-level overview.
|
|
80
|
+
|
|
81
|
+
Documentation is also available in [llms.txt format](https://llmstxt.org/), which is a simple markdown standard that LLMs can consume easily.
|
|
82
|
+
|
|
83
|
+
There are two ways to access the LLM-friendly documentation:
|
|
84
|
+
- [`llms.txt`](https://gofastmcp.com/llms.txt) is essentially a sitemap, listing all the pages in the documentation.
|
|
85
|
+
- [`llms-full.txt`](https://gofastmcp.com/llms-full.txt) contains the entire documentation. Note this may exceed the context window of your LLM.
|
|
80
86
|
|
|
81
87
|
---
|
|
82
88
|
|
|
@@ -302,50 +308,40 @@ Learn more: [**OpenAPI Integration**](https://gofastmcp.com/patterns/openapi) |
|
|
|
302
308
|
|
|
303
309
|
## Running Your Server
|
|
304
310
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
uv run python server.py
|
|
338
|
-
```
|
|
339
|
-
4. **Claude Desktop Integration (`fastmcp install`)**: The easiest way to make your server persistently available in the Claude Desktop app. It handles creating an isolated environment using `uv`.
|
|
340
|
-
```bash
|
|
341
|
-
fastmcp install server.py --name "My Analysis Tool"
|
|
342
|
-
# Optionally add dependencies and environment variables
|
|
343
|
-
fastmcp install server.py --with requests -v API_KEY=123 -f .env
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
See the [**Server Documentation**](https://gofastmcp.com/servers/fastmcp#running-the-server) for more details on transports and configuration.
|
|
311
|
+
The main way to run a FastMCP server is by calling the `run()` method on your server instance:
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
# server.py
|
|
315
|
+
from fastmcp import FastMCP
|
|
316
|
+
|
|
317
|
+
mcp = FastMCP("Demo 🚀")
|
|
318
|
+
|
|
319
|
+
@mcp.tool()
|
|
320
|
+
def hello(name: str) -> str:
|
|
321
|
+
return f"Hello, {name}!"
|
|
322
|
+
|
|
323
|
+
if __name__ == "__main__":
|
|
324
|
+
mcp.run() # Default: uses STDIO transport
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
FastMCP supports three transport protocols:
|
|
328
|
+
|
|
329
|
+
**STDIO (Default)**: Best for local tools and command-line scripts.
|
|
330
|
+
```python
|
|
331
|
+
mcp.run(transport="stdio") # Default, so transport argument is optional
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Streamable HTTP**: Recommended for web deployments.
|
|
335
|
+
```python
|
|
336
|
+
mcp.run(transport="streamable-http", host="127.0.0.1", port=8000, path="/mcp")
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**SSE**: For compatibility with existing SSE clients.
|
|
340
|
+
```python
|
|
341
|
+
mcp.run(transport="sse", host="127.0.0.1", port=8000)
|
|
342
|
+
```
|
|
348
343
|
|
|
344
|
+
See the [**Running Server Documentation**](https://gofastmcp.com/deployment/running-server) for more details.
|
|
349
345
|
|
|
350
346
|
## Contributing
|
|
351
347
|
|
|
@@ -4,7 +4,7 @@ fastmcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
4
4
|
fastmcp/settings.py,sha256=rDClnYEpYjEl8VsvvVrKp9oaE4YLfNQcMoZ41H_bDL0,2968
|
|
5
5
|
fastmcp/cli/__init__.py,sha256=Ii284TNoG5lxTP40ETMGhHEq3lQZWxu9m9JuU57kUpQ,87
|
|
6
6
|
fastmcp/cli/claude.py,sha256=IAlcZ4qZKBBj09jZUMEx7EANZE_IR3vcu7zOBJmMOuU,4567
|
|
7
|
-
fastmcp/cli/cli.py,sha256=
|
|
7
|
+
fastmcp/cli/cli.py,sha256=Tb-WiIXFZiq4nqlZ6LMXN2iYY30clC4Om_gP89HbJcE,15641
|
|
8
8
|
fastmcp/client/__init__.py,sha256=BXO9NUhntZ5GnUACfaRCzDJ5IzxqFJs8qKG-CRMSco4,490
|
|
9
9
|
fastmcp/client/base.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
fastmcp/client/client.py,sha256=zfSLWSGqiBoADveKehsAL66CdyGGqmzVHR1q46zfdQY,15479
|
|
@@ -21,6 +21,9 @@ fastmcp/contrib/mcp_mixin/README.md,sha256=9DDTJXWkA3yv1fp5V58gofmARPQ2xWDhblYGv
|
|
|
21
21
|
fastmcp/contrib/mcp_mixin/__init__.py,sha256=aw9IQ1ssNjCgws4ZNt8bkdpossAAGVAwwjBpMp9O5ZQ,153
|
|
22
22
|
fastmcp/contrib/mcp_mixin/example.py,sha256=GnunkXmtG5hLLTUsM8aW5ZURU52Z8vI4tNLl-fK7Dg0,1228
|
|
23
23
|
fastmcp/contrib/mcp_mixin/mcp_mixin.py,sha256=cfIRbnSxsVzglTD-auyTE0izVQeHP7Oz18qzYoBZJgg,7899
|
|
24
|
+
fastmcp/low_level/README.md,sha256=IRvElvOOc_RLLsqbUm7e6VOEwrKHPJeox0pV7JVKHWw,106
|
|
25
|
+
fastmcp/low_level/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
fastmcp/low_level/sse_server_transport.py,sha256=pUG3AL4Wjf9LgH9fj1l3emGEjFDFDhmKcDfgiiFJcuQ,4448
|
|
24
27
|
fastmcp/prompts/__init__.py,sha256=An8uMBUh9Hrb7qqcn_5_Hent7IOeSh7EA2IUVsIrtHc,179
|
|
25
28
|
fastmcp/prompts/prompt.py,sha256=psc-YiBRttbjETINaP9P9QV328yk96mDBsZgjOHVyKM,7777
|
|
26
29
|
fastmcp/prompts/prompt_manager.py,sha256=9VcioLE-AoUKe1e9SynNQME9SvWy0q1QAvO1ewIWVmI,3126
|
|
@@ -32,13 +35,12 @@ fastmcp/resources/types.py,sha256=QPDeka_cM1hmvwW4FeFhqy6BEEi4MlwtpvhWUVWh5Fc,64
|
|
|
32
35
|
fastmcp/server/__init__.py,sha256=bMD4aQD4yJqLz7-mudoNsyeV8UgQfRAg3PRwPvwTEds,119
|
|
33
36
|
fastmcp/server/context.py,sha256=ykitQygA7zT5prbFTLCuYlnAzuljf_9ErUT0FYBPv3E,8135
|
|
34
37
|
fastmcp/server/dependencies.py,sha256=1utkxFsV37HZcWBwI69JyngVN2ppGO_PEgxUlUHHy_Q,742
|
|
35
|
-
fastmcp/server/http.py,sha256=
|
|
38
|
+
fastmcp/server/http.py,sha256=utl7vJkMvKUnKIflCptVWk1oqOi7_sJJHqUl22g4JC8,10473
|
|
36
39
|
fastmcp/server/openapi.py,sha256=0nANnwHJ5VZInNyo2f9ErmO0K3igMv6bwyxf3G-BSls,23473
|
|
37
|
-
fastmcp/server/proxy.py,sha256=
|
|
38
|
-
fastmcp/server/server.py,sha256=
|
|
39
|
-
fastmcp/server/streamable_http_manager.py,sha256=noCZSybvbotyiHZbJ7PRIB6peFBGvjIM2Xavs0pLtGQ,8879
|
|
40
|
+
fastmcp/server/proxy.py,sha256=LDTjzc_iQj8AldsfMU37flGRAfJic1w6qsherfyHPAA,9622
|
|
41
|
+
fastmcp/server/server.py,sha256=Srj_aytCxsO7NjSwajW5gj55ilbHpgJlewepnNG9ToA,44420
|
|
40
42
|
fastmcp/tools/__init__.py,sha256=ocw-SFTtN6vQ8fgnlF8iNAOflRmh79xS1xdO0Bc3QPE,96
|
|
41
|
-
fastmcp/tools/tool.py,sha256=
|
|
43
|
+
fastmcp/tools/tool.py,sha256=HGcHjMecqAeN6eI-IfE_2UBcd1KpTV-VOTFLx9tlbpU,7809
|
|
42
44
|
fastmcp/tools/tool_manager.py,sha256=p2nHyLFgz28tbsLpWOurkbWRU2Z34_HcDohjrvwjI0E,3369
|
|
43
45
|
fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
|
|
44
46
|
fastmcp/utilities/cache.py,sha256=aV3oZ-ZhMgLSM9iAotlUlEy5jFvGXrVo0Y5Bj4PBtqY,707
|
|
@@ -46,10 +48,10 @@ fastmcp/utilities/decorators.py,sha256=AjhjsetQZF4YOPV5MTZmIxO21iFp_4fDIS3O2_KNC
|
|
|
46
48
|
fastmcp/utilities/json_schema.py,sha256=mSakhP8bENxhLFMwHJSxJAFllNeByIBDjVohwlpac6w,2026
|
|
47
49
|
fastmcp/utilities/logging.py,sha256=zav8pnFxG_fvGJHUV2XpobmT9WVrmv1mlQBSCz-CPx4,1159
|
|
48
50
|
fastmcp/utilities/openapi.py,sha256=Er3G1MyFwiWVxZXicXtD2j-BvttHEDTi1dgkq1KiBQc,51073
|
|
49
|
-
fastmcp/utilities/tests.py,sha256=
|
|
51
|
+
fastmcp/utilities/tests.py,sha256=9GOxIENGU6vRTVooY5vxb5dM6vltpmgWKSKm8htQ4Yc,3197
|
|
50
52
|
fastmcp/utilities/types.py,sha256=6CcqAQ1QqCO2HGSFlPS6FO5JRWnacjCcO2-EhyEnZV0,4400
|
|
51
|
-
fastmcp-2.3.
|
|
52
|
-
fastmcp-2.3.
|
|
53
|
-
fastmcp-2.3.
|
|
54
|
-
fastmcp-2.3.
|
|
55
|
-
fastmcp-2.3.
|
|
53
|
+
fastmcp-2.3.2.dist-info/METADATA,sha256=XFBGtiKoYu43JZQAfXtaTDL_8qtWIk_7AIJen4XhqmA,15754
|
|
54
|
+
fastmcp-2.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
55
|
+
fastmcp-2.3.2.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
|
|
56
|
+
fastmcp-2.3.2.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
57
|
+
fastmcp-2.3.2.dist-info/RECORD,,
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
"""StreamableHTTP Session Manager for MCP servers."""
|
|
2
|
-
|
|
3
|
-
# follows https://github.com/modelcontextprotocol/python-sdk/blob/ihrpr/shttp/src/mcp/server/streamable_http_manager.py
|
|
4
|
-
# and can be removed once that spec is finalized
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
import contextlib
|
|
9
|
-
import logging
|
|
10
|
-
from collections.abc import AsyncIterator
|
|
11
|
-
from http import HTTPStatus
|
|
12
|
-
from typing import Any
|
|
13
|
-
from uuid import uuid4
|
|
14
|
-
|
|
15
|
-
import anyio
|
|
16
|
-
from anyio.abc import TaskStatus
|
|
17
|
-
from mcp.server.lowlevel.server import Server as MCPServer
|
|
18
|
-
from mcp.server.streamable_http import (
|
|
19
|
-
MCP_SESSION_ID_HEADER,
|
|
20
|
-
EventStore,
|
|
21
|
-
StreamableHTTPServerTransport,
|
|
22
|
-
)
|
|
23
|
-
from starlette.requests import Request
|
|
24
|
-
from starlette.responses import Response
|
|
25
|
-
from starlette.types import Receive, Scope, Send
|
|
26
|
-
|
|
27
|
-
logger = logging.getLogger(__name__)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class StreamableHTTPSessionManager:
|
|
31
|
-
"""
|
|
32
|
-
Manages StreamableHTTP sessions with optional resumability via event store.
|
|
33
|
-
|
|
34
|
-
This class abstracts away the complexity of session management, event storage,
|
|
35
|
-
and request handling for StreamableHTTP transports. It handles:
|
|
36
|
-
|
|
37
|
-
1. Session tracking for clients
|
|
38
|
-
2. Resumability via an optional event store
|
|
39
|
-
3. Connection management and lifecycle
|
|
40
|
-
4. Request handling and transport setup
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
app: The MCP server instance
|
|
44
|
-
event_store: Optional event store for resumability support.
|
|
45
|
-
If provided, enables resumable connections where clients
|
|
46
|
-
can reconnect and receive missed events.
|
|
47
|
-
If None, sessions are still tracked but not resumable.
|
|
48
|
-
json_response: Whether to use JSON responses instead of SSE streams
|
|
49
|
-
stateless: If True, creates a completely fresh transport for each request
|
|
50
|
-
with no session tracking or state persistence between requests.
|
|
51
|
-
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
def __init__(
|
|
55
|
-
self,
|
|
56
|
-
app: MCPServer[Any],
|
|
57
|
-
event_store: EventStore | None = None,
|
|
58
|
-
json_response: bool = False,
|
|
59
|
-
stateless: bool = False,
|
|
60
|
-
):
|
|
61
|
-
self.app = app
|
|
62
|
-
self.event_store = event_store
|
|
63
|
-
self.json_response = json_response
|
|
64
|
-
self.stateless = stateless
|
|
65
|
-
|
|
66
|
-
# Session tracking (only used if not stateless)
|
|
67
|
-
self._session_creation_lock = anyio.Lock()
|
|
68
|
-
self._server_instances: dict[str, StreamableHTTPServerTransport] = {}
|
|
69
|
-
|
|
70
|
-
# The task group will be set during lifespan
|
|
71
|
-
self._task_group = None
|
|
72
|
-
|
|
73
|
-
@contextlib.asynccontextmanager
|
|
74
|
-
async def run(self) -> AsyncIterator[None]:
|
|
75
|
-
"""
|
|
76
|
-
Run the session manager with proper lifecycle management.
|
|
77
|
-
|
|
78
|
-
This creates and manages the task group for all session operations.
|
|
79
|
-
|
|
80
|
-
Use this in the lifespan context manager of your Starlette app:
|
|
81
|
-
|
|
82
|
-
@contextlib.asynccontextmanager
|
|
83
|
-
async def lifespan(app: Starlette) -> AsyncIterator[None]:
|
|
84
|
-
async with session_manager.run():
|
|
85
|
-
yield
|
|
86
|
-
"""
|
|
87
|
-
async with anyio.create_task_group() as tg:
|
|
88
|
-
# Store the task group for later use
|
|
89
|
-
self._task_group = tg
|
|
90
|
-
logger.info("StreamableHTTP session manager started")
|
|
91
|
-
try:
|
|
92
|
-
yield # Let the application run
|
|
93
|
-
finally:
|
|
94
|
-
logger.info("StreamableHTTP session manager shutting down")
|
|
95
|
-
# Cancel task group to stop all spawned tasks
|
|
96
|
-
tg.cancel_scope.cancel()
|
|
97
|
-
self._task_group = None
|
|
98
|
-
# Clear any remaining server instances
|
|
99
|
-
self._server_instances.clear()
|
|
100
|
-
|
|
101
|
-
async def handle_request(
|
|
102
|
-
self,
|
|
103
|
-
scope: Scope,
|
|
104
|
-
receive: Receive,
|
|
105
|
-
send: Send,
|
|
106
|
-
) -> None:
|
|
107
|
-
"""
|
|
108
|
-
Process ASGI request with proper session handling and transport setup.
|
|
109
|
-
|
|
110
|
-
Dispatches to the appropriate handler based on stateless mode.
|
|
111
|
-
|
|
112
|
-
Args:
|
|
113
|
-
scope: ASGI scope
|
|
114
|
-
receive: ASGI receive function
|
|
115
|
-
send: ASGI send function
|
|
116
|
-
"""
|
|
117
|
-
if self._task_group is None:
|
|
118
|
-
raise RuntimeError(
|
|
119
|
-
"Task group is not initialized. Make sure to use the run()."
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
# Dispatch to the appropriate handler
|
|
123
|
-
if self.stateless:
|
|
124
|
-
await self._handle_stateless_request(scope, receive, send)
|
|
125
|
-
else:
|
|
126
|
-
await self._handle_stateful_request(scope, receive, send)
|
|
127
|
-
|
|
128
|
-
async def _handle_stateless_request(
|
|
129
|
-
self,
|
|
130
|
-
scope: Scope,
|
|
131
|
-
receive: Receive,
|
|
132
|
-
send: Send,
|
|
133
|
-
) -> None:
|
|
134
|
-
"""
|
|
135
|
-
Process request in stateless mode - creating a new transport for each request.
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
scope: ASGI scope
|
|
139
|
-
receive: ASGI receive function
|
|
140
|
-
send: ASGI send function
|
|
141
|
-
"""
|
|
142
|
-
logger.debug("Stateless mode: Creating new transport for this request")
|
|
143
|
-
# No session ID needed in stateless mode
|
|
144
|
-
http_transport = StreamableHTTPServerTransport(
|
|
145
|
-
mcp_session_id=None, # No session tracking in stateless mode
|
|
146
|
-
is_json_response_enabled=self.json_response,
|
|
147
|
-
event_store=None, # No event store in stateless mode
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
# Start server in a new task
|
|
151
|
-
async def run_stateless_server(
|
|
152
|
-
*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED
|
|
153
|
-
):
|
|
154
|
-
async with http_transport.connect() as streams:
|
|
155
|
-
read_stream, write_stream = streams
|
|
156
|
-
task_status.started()
|
|
157
|
-
await self.app.run(
|
|
158
|
-
read_stream,
|
|
159
|
-
write_stream,
|
|
160
|
-
self.app.create_initialization_options(),
|
|
161
|
-
stateless=True,
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
# Assert task group is not None for type checking
|
|
165
|
-
assert self._task_group is not None
|
|
166
|
-
# Start the server task
|
|
167
|
-
await self._task_group.start(run_stateless_server)
|
|
168
|
-
|
|
169
|
-
# Handle the HTTP request and return the response
|
|
170
|
-
await http_transport.handle_request(scope, receive, send)
|
|
171
|
-
|
|
172
|
-
async def _handle_stateful_request(
|
|
173
|
-
self,
|
|
174
|
-
scope: Scope,
|
|
175
|
-
receive: Receive,
|
|
176
|
-
send: Send,
|
|
177
|
-
) -> None:
|
|
178
|
-
"""
|
|
179
|
-
Process request in stateful mode - maintaining session state between requests.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
scope: ASGI scope
|
|
183
|
-
receive: ASGI receive function
|
|
184
|
-
send: ASGI send function
|
|
185
|
-
"""
|
|
186
|
-
request = Request(scope, receive)
|
|
187
|
-
request_mcp_session_id = request.headers.get(MCP_SESSION_ID_HEADER)
|
|
188
|
-
|
|
189
|
-
# Existing session case
|
|
190
|
-
if (
|
|
191
|
-
request_mcp_session_id is not None
|
|
192
|
-
and request_mcp_session_id in self._server_instances
|
|
193
|
-
):
|
|
194
|
-
transport = self._server_instances[request_mcp_session_id]
|
|
195
|
-
logger.debug("Session already exists, handling request directly")
|
|
196
|
-
await transport.handle_request(scope, receive, send)
|
|
197
|
-
return
|
|
198
|
-
|
|
199
|
-
if request_mcp_session_id is None:
|
|
200
|
-
# New session case
|
|
201
|
-
logger.debug("Creating new transport")
|
|
202
|
-
async with self._session_creation_lock:
|
|
203
|
-
new_session_id = uuid4().hex
|
|
204
|
-
http_transport = StreamableHTTPServerTransport(
|
|
205
|
-
mcp_session_id=new_session_id,
|
|
206
|
-
is_json_response_enabled=self.json_response,
|
|
207
|
-
event_store=self.event_store, # May be None (no resumability)
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
assert http_transport.mcp_session_id is not None
|
|
211
|
-
self._server_instances[http_transport.mcp_session_id] = http_transport
|
|
212
|
-
logger.info(f"Created new transport with session ID: {new_session_id}")
|
|
213
|
-
|
|
214
|
-
# Define the server runner
|
|
215
|
-
async def run_server(
|
|
216
|
-
*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED
|
|
217
|
-
) -> None:
|
|
218
|
-
async with http_transport.connect() as streams:
|
|
219
|
-
read_stream, write_stream = streams
|
|
220
|
-
task_status.started()
|
|
221
|
-
await self.app.run(
|
|
222
|
-
read_stream,
|
|
223
|
-
write_stream,
|
|
224
|
-
self.app.create_initialization_options(),
|
|
225
|
-
stateless=False, # Stateful mode
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
# Assert task group is not None for type checking
|
|
229
|
-
assert self._task_group is not None
|
|
230
|
-
# Start the server task
|
|
231
|
-
await self._task_group.start(run_server)
|
|
232
|
-
|
|
233
|
-
# Handle the HTTP request and return the response
|
|
234
|
-
await http_transport.handle_request(scope, receive, send)
|
|
235
|
-
else:
|
|
236
|
-
# Invalid session ID
|
|
237
|
-
response = Response(
|
|
238
|
-
"Bad Request: No valid session ID provided",
|
|
239
|
-
status_code=HTTPStatus.BAD_REQUEST,
|
|
240
|
-
)
|
|
241
|
-
await response(scope, receive, send)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|