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 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 sse transport (default: 127.0.0.1)",
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 sse transport (default: 8000)",
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 for sse transport (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
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:\n
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.\n
365
- For dependency management, use `mcp install` or `mcp dev` instead.
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, cast
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.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
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
- # 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:
@@ -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[Route | Mount], list[str]]:
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[Route | Mount] = []
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[Route | Mount],
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
- # 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(
@@ -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
- additional_routes: list[Route] | list[Mount] | list[Route | Mount] | None = None,
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
- additional_routes: Optional list of custom routes
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
- middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
180
+ auth_middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
182
181
  auth_server_provider, auth_settings
183
182
  )
184
183
 
185
- # Initialize routes with auth routes
186
- routes: list[Route | Mount] = auth_routes.copy()
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
- routes.append(
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
- routes.append(
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
- routes.append(
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
- routes.append(
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 additional_routes:
225
- routes.extend(cast(list[Route | Mount], additional_routes))
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(routes, middleware, debug)
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
- additional_routes: list[Route] | list[Mount] | list[Route | Mount] | None = None,
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
- additional_routes: Optional list of custom routes
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
- middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
285
+ auth_middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
274
286
  auth_server_provider, auth_settings
275
287
  )
276
288
 
277
- # Initialize routes with auth routes
278
- routes: list[Route | Mount] = auth_routes.copy()
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
- routes.append(
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
- routes.append(
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 additional_routes:
300
- routes.extend(cast(list[Route | Mount], additional_routes))
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(routes, middleware, debug, lifespan)
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
- # 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
@@ -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[Route] = []
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", "sse", "streamable-http"] | None = None,
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", "sse", "streamable-http"]:
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.run_sse_async(**transport_kwargs)
189
- else: # transport == "streamable-http"
190
- await self.run_streamable_http_async(**transport_kwargs)
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", "sse", "streamable-http"] | None = None,
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 run_sse_async(
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 SSE transport."""
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
- app = self.sse_app(path=path, message_path=message_path)
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
- """Return an instance of the SSE server app."""
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
- additional_routes=self._additional_http_routes,
816
+ routes=self._additional_http_routes,
817
+ middleware=middleware,
757
818
  )
758
819
 
759
- def streamable_http_app(self, path: str | None = None) -> Starlette:
760
- """Return an instance of the StreamableHTTP server app."""
761
- from fastmcp.server.http import create_streamable_http_app
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
- return create_streamable_http_app(
764
- server=self,
765
- streamable_http_path=path or self.settings.streamable_http_path,
766
- event_store=None,
767
- auth_server_provider=self._auth_server_provider,
768
- auth_settings=self.settings.auth,
769
- json_response=self.settings.json_response,
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
- """Run the server using StreamableHTTP transport."""
784
- uvicorn_config = uvicorn_config or {}
785
- uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
786
-
787
- app = self.streamable_http_app(path=path)
788
-
789
- config = uvicorn.Config(
790
- app,
791
- host=host or self.settings.host,
792
- port=port or self.settings.port,
793
- log_level=log_level or self.settings.log_level.lower(),
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 an server is mounted: - The tools are imported with prefixed names
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
@@ -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.sse_app()
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.0rc1
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 used the version integrated into the [official MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk), which was based on **FastMCP 1.0**.
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
- 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
 
@@ -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=7s5RsV8D_tPs20EJcrCvyO-i69DmV60OiRGD21oEJSI,15728
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=c_J6y1jkasC3WMCzo3LVXMwJbGrHVwQO2Qtl4IP5RlY,10085
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=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=Srj_aytCxsO7NjSwajW5gj55ilbHpgJlewepnNG9ToA,44420
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
@@ -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=uUV-8CkhCe5zZJkxhgJXnxrjJ3Yq7cCMZN8xWKGuqdY,3181
51
+ fastmcp/utilities/tests.py,sha256=9GOxIENGU6vRTVooY5vxb5dM6vltpmgWKSKm8htQ4Yc,3197
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.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)