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.
@@ -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.sse import SseServerTransport
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
- # This import is vendored until it is finalized in the upstream SDK
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
- # Create the app
132
- app_kwargs = {
133
- "debug": debug,
134
- "routes": routes,
135
- "middleware": middleware,
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(routes, middleware, debug)
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(routes, middleware, debug, lifespan)
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
- # dont use the provided uri, because it may not be the same as the
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 an server is mounted: - The tools are imported with prefixed names
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.0rc1
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
- This readme provides only a high-level overview. For detailed guides, API references, and advanced patterns, please refer to the complete FastMCP documentation at **[gofastmcp.com](https://gofastmcp.com)**.
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
- You can run your FastMCP server in several ways:
306
-
307
- 1. **Development (`fastmcp dev`)**: Recommended for building and testing. Provides an interactive testing environment with the MCP Inspector.
308
- ```bash
309
- fastmcp dev server.py
310
- # Optionally add temporary dependencies
311
- fastmcp dev server.py --with pandas numpy
312
- ```
313
-
314
- 2. **FastMCP CLI**: Run your server with the FastMCP CLI. This can autodetect and load your server object and run it with any transport configuration you want.
315
- ```bash
316
- fastmcp run path/to/server.py:server_object
317
-
318
- # Run as SSE on port 4200
319
- fastmcp run path/to/server.py:server_object --transport sse --port 4200
320
- ```
321
- FastMCP will auto-detect the server object if it's named `mcp`, `app`, or `server`. In these cases, you can omit the `:server_object` part unless you need to select a specific object.
322
-
323
- 3. **Direct Execution**: For maximum compatibility with the MCP ecosystem, you can run your server directly as part of a Python script. You will typically do this within an `if __name__ == "__main__":` block in your script:
324
- ```python
325
- # Add this to server.py
326
- if __name__ == "__main__":
327
- # Default: runs stdio transport
328
- mcp.run()
329
-
330
- # Example: Run with SSE transport on a specific port
331
- mcp.run(transport="sse", host="127.0.0.1", port=9000)
332
- ```
333
- Run your script:
334
- ```bash
335
- python server.py
336
- # or using uv to manage the environment
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=c_J6y1jkasC3WMCzo3LVXMwJbGrHVwQO2Qtl4IP5RlY,10085
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=qcBD2wWMcXA4dhqppStVH4UhsyWm0cpPUuItLpO6H6A,9621
38
- fastmcp/server/server.py,sha256=usocXySZvmrp6GOJzQQrh1lUiAyVMkRnJhk4NBhp0FQ,40827
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=lr9F90-A36Z6DT4LScJBW88uFq8xrSv00ivFxiERJe8,7786
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.0rc1.dist-info/METADATA,sha256=5hLCFQ8nJWFwllIUbC2D0V9YlOaA7lnNuJwoXTQrw7s,16385
52
- fastmcp-2.3.0rc1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
- fastmcp-2.3.0rc1.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
54
- fastmcp-2.3.0rc1.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
55
- fastmcp-2.3.0rc1.dist-info/RECORD,,
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)