fastmcp 2.3.0rc1__py3-none-any.whl → 2.3.1__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/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 +20 -17
- fastmcp/server/proxy.py +1 -1
- fastmcp/server/server.py +1 -1
- fastmcp/tools/tool.py +1 -1
- {fastmcp-2.3.0rc1.dist-info → fastmcp-2.3.1.dist-info}/METADATA +42 -46
- {fastmcp-2.3.0rc1.dist-info → fastmcp-2.3.1.dist-info}/RECORD +12 -10
- fastmcp/server/streamable_http_manager.py +0 -241
- {fastmcp-2.3.0rc1.dist-info → fastmcp-2.3.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.3.0rc1.dist-info → fastmcp-2.3.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.3.0rc1.dist-info → fastmcp-2.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
@@ -13,7 +13,7 @@ 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
|
|
@@ -22,8 +22,7 @@ from starlette.responses import Response
|
|
|
22
22
|
from starlette.routing import 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:
|
|
@@ -111,7 +110,7 @@ def setup_auth_middleware_and_routes(
|
|
|
111
110
|
def create_base_app(
|
|
112
111
|
routes: list[Route | Mount],
|
|
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(
|
|
@@ -225,7 +219,11 @@ def create_sse_app(
|
|
|
225
219
|
routes.extend(cast(list[Route | Mount], additional_routes))
|
|
226
220
|
|
|
227
221
|
# Create and return the app
|
|
228
|
-
return create_base_app(
|
|
222
|
+
return create_base_app(
|
|
223
|
+
routes=routes,
|
|
224
|
+
middleware=middleware,
|
|
225
|
+
debug=debug,
|
|
226
|
+
)
|
|
229
227
|
|
|
230
228
|
|
|
231
229
|
def create_streamable_http_app(
|
|
@@ -306,4 +304,9 @@ def create_streamable_http_app(
|
|
|
306
304
|
yield
|
|
307
305
|
|
|
308
306
|
# Create and return the app with lifespan
|
|
309
|
-
return create_base_app(
|
|
307
|
+
return create_base_app(
|
|
308
|
+
routes=routes,
|
|
309
|
+
middleware=middleware,
|
|
310
|
+
debug=debug,
|
|
311
|
+
lifespan=lifespan,
|
|
312
|
+
)
|
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
|
@@ -892,7 +892,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
892
892
|
future changes to the imported server will not be reflected in the
|
|
893
893
|
importing server. Server-level configurations and lifespans are not imported.
|
|
894
894
|
|
|
895
|
-
When
|
|
895
|
+
When a server is mounted: - The tools are imported with prefixed names
|
|
896
896
|
using the tool_separator
|
|
897
897
|
Example: If server has a tool named "get_weather", it will be
|
|
898
898
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastmcp
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.1
|
|
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
|
|
@@ -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
|
|
|
@@ -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=esmeQZJCOxbvYBwF9gTzniymnL93n09hbjgRpmv-9bw,10076
|
|
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=2pwYzjHFlg42Mq8qfjX5VttP1OjVJYNOCbW5H7R0hjk,40826
|
|
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
|
|
@@ -48,8 +50,8 @@ fastmcp/utilities/logging.py,sha256=zav8pnFxG_fvGJHUV2XpobmT9WVrmv1mlQBSCz-CPx4,
|
|
|
48
50
|
fastmcp/utilities/openapi.py,sha256=Er3G1MyFwiWVxZXicXtD2j-BvttHEDTi1dgkq1KiBQc,51073
|
|
49
51
|
fastmcp/utilities/tests.py,sha256=uUV-8CkhCe5zZJkxhgJXnxrjJ3Yq7cCMZN8xWKGuqdY,3181
|
|
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.1.dist-info/METADATA,sha256=EM2ah3-Gf3_vtum6tbRL4627SfRuugJ0xJdbsNa8EJE,15746
|
|
54
|
+
fastmcp-2.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
55
|
+
fastmcp-2.3.1.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
|
|
56
|
+
fastmcp-2.3.1.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
57
|
+
fastmcp-2.3.1.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
|