Jeremiah Lowin commited on
Commit
74e9a36
·
unverified ·
2 Parent(s): d2f6729b4aeccd

Merge pull request #361 from jlowin/streamable-http

Browse files
docs/deployment/authentication.mdx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Authentication
3
+ sidebarTitle: Authentication
4
+ description: Secure your FastMCP server with authentication.
5
+ icon: lock
6
+ ---
7
+ import { VersionBadge } from '/snippets/version-badge.mdx'
8
+
9
+ <VersionBadge version="2.2.7" />
10
+
11
+ This document will cover how to implement authentication for your FastMCP servers.
12
+
13
+ FastMCP leverages the OAuth 2.0 support provided by the underlying Model Context Protocol (MCP) SDK.
14
+
15
+ For now, refer to the [MCP Server Authentication documentation](/servers/fastmcp#authentication) for initial details and the [official MCP SDK documentation](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for more.
docs/deployment/running-server.mdx ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Running Your FastMCP Server
3
+ sidebarTitle: Running the Server
4
+ description: Learn how to run and deploy your FastMCP server using various transport protocols like STDIO, Streamable HTTP, and SSE.
5
+ icon: circle-play
6
+ ---
7
+ import { VersionBadge } from '/snippets/version-badge.mdx'
8
+
9
+
10
+ FastMCP servers can be run in different ways depending on your application's needs, from local command-line tools to persistent web services. This guide covers the primary methods for running your server, focusing on the available transport protocols: STDIO, Streamable HTTP, and SSE.
11
+
12
+ ## The `run()` Method
13
+
14
+ The main way to run a FastMCP server from a Python script is by calling the `run()` method on a `FastMCP` instance.
15
+
16
+ <Tip>
17
+ For maximum compatibility, it's best practice to place the `run()` call within an `if __name__ == "__main__":` block. This ensures the server starts only when the script is executed directly, not when imported as a module.
18
+ </Tip>
19
+
20
+ ```python {9-10} my_server.py
21
+ from fastmcp import FastMCP
22
+
23
+ mcp = FastMCP(name="MyServer")
24
+
25
+ @mcp.tool()
26
+ def hello(name: str) -> str:
27
+ return f"Hello, {name}!"
28
+
29
+ if __name__ == "__main__":
30
+ mcp.run()
31
+ ```
32
+ You can now run this MCP server by executing `python my_server.py`.
33
+
34
+ MCP servers can be run with a variety of different transport options, depending on your application's requirements. The `run()` method can take a `transport` argument and other transport-specific keyword arguments to configure how the server operates.
35
+
36
+ ## Transport Options
37
+
38
+ Below is a comparison of available transport options to help you choose the right one for your needs:
39
+
40
+ | Transport | Use Cases | Recommendation |
41
+ | --------- | --------- | -------------- |
42
+ | **STDIO** | Local tools, command-line scripts, and integrations with clients like Claude Desktop | Best for local tools and when clients manage server processes |
43
+ | **Streamable HTTP** | Web-based deployments, microservices, exposing MCP over a network | Recommended choice for new web-based deployments |
44
+ | **SSE** | Existing web-based deployments that rely on SSE | Suitable for compatibility with SSE clients; prefer Streamable HTTP for new projects |
45
+
46
+ ### STDIO
47
+
48
+ The STDIO transport is the default and most widely compatible option for local MCP server execution. It is ideal for local tools, command-line integrations, and clients like Claude Desktop. However, it has the disadvantage of having to run the MCP code locally, which can introduce security concerns with third-party servers.
49
+
50
+ STDIO is the default transport, so you don't need to specify it when calling `run()`. However, you can specify it explicitly to make your intent clear:
51
+
52
+ ```python {6}
53
+ from fastmcp import FastMCP
54
+
55
+ mcp = FastMCP()
56
+
57
+ if __name__ == "__main__":
58
+ mcp.run(transport="stdio")
59
+ ```
60
+
61
+ When using Stdio transport, you will typically *not* run the server yourself as a separate process. Rather, your *clients* will spin up a new server process for each session. As such, no additional configuration is required.
62
+
63
+ ### Streamable HTTP
64
+
65
+ <VersionBadge version="2.3.0" />
66
+
67
+ Streamable HTTP is a modern, efficient transport for exposing your MCP server via HTTP. It is generally recommended over SSE for new web-based deployments.
68
+
69
+ To run a server using Streamable HTTP, you can use the `run()` method with the `transport` argument set to `"streamable-http"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and path (`/mcp`).
70
+ <CodeGroup>
71
+ ```python {6} server.py
72
+ from fastmcp import FastMCP
73
+
74
+ mcp = FastMCP()
75
+
76
+ if __name__ == "__main__":
77
+ mcp.run(transport="streamable-http")
78
+ ```
79
+ ```python {5} client.py
80
+ import asyncio
81
+ from fastmcp import Client
82
+
83
+ async def example():
84
+ async with Client("http://127.0.0.1:8000/mcp") as client:
85
+ await client.ping()
86
+
87
+ if __name__ == "__main__":
88
+ asyncio.run(example())
89
+ ```
90
+ </CodeGroup>
91
+
92
+ To customize the host, port, path, or log level, provide appropriate keyword arguments to the `run()` method.
93
+
94
+ <CodeGroup>
95
+ ```python {8-11} server.py
96
+ from fastmcp import FastMCP
97
+
98
+ mcp = FastMCP()
99
+
100
+ if __name__ == "__main__":
101
+ mcp.run(
102
+ transport="streamable-http",
103
+ host="127.0.0.1",
104
+ port=4200,
105
+ path="/my-custom-path",
106
+ log_level="debug",
107
+ )
108
+ ```
109
+ ```python {5} client.py
110
+ import asyncio
111
+ from fastmcp import Client
112
+
113
+ async def example():
114
+ async with Client("http://127.0.0.1:4200/my-custom-path") as client:
115
+ await client.ping()
116
+
117
+ if __name__ == "__main__":
118
+ asyncio.run(example())
119
+ ```
120
+ </CodeGroup>
121
+
122
+
123
+ ### SSE
124
+
125
+ Server-Sent Events (SSE) is an HTTP-based protocol for server-to-client streaming. While FastMCP supports SSE, Streamable HTTP is preferred for new projects.
126
+
127
+ To run a server using SSE, you can use the `run()` method with the `transport` argument set to `"sse"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and with default SSE path (`/sse`) and message path (`/messages/`).
128
+
129
+ <CodeGroup>
130
+ ```python {6} server.py
131
+ from fastmcp import FastMCP
132
+
133
+ mcp = FastMCP()
134
+
135
+ if __name__ == "__main__":
136
+ mcp.run(transport="sse")
137
+ ```
138
+ ```python {3,7} client.py
139
+ import asyncio
140
+ from fastmcp import Client
141
+ from fastmcp.client.transports import SSETransport
142
+
143
+ async def example():
144
+ async with Client(
145
+ transport=SSETransport("http://127.0.0.1:8000/sse")
146
+ ) as client:
147
+ await client.ping()
148
+
149
+ if __name__ == "__main__":
150
+ asyncio.run(example())
151
+ ```
152
+ </CodeGroup>
153
+
154
+ <Tip>
155
+ Notice that the client in the above example uses an explicit `SSETransport` to connect to the server. FastMCP will attempt to infer the appropriate transport from the provided configuration, but HTTP URLs are assumed to be Streamable HTTP (as of FastMCP 2.3.0).
156
+ </Tip>
157
+
158
+ To customize the host, port, or log level, provide appropriate keyword arguments to the `run()` method. You can also adjust the SSE path (which clients should connect to) and the message POST endpoint (which clients use to send subsequent messages).
159
+
160
+ <CodeGroup>
161
+ ```python {8-12} server.py
162
+ from fastmcp import FastMCP
163
+
164
+ mcp = FastMCP()
165
+
166
+ if __name__ == "__main__":
167
+ mcp.run(
168
+ transport="sse",
169
+ host="127.0.0.1",
170
+ port=4200,
171
+ log_level="debug",
172
+ path="/my-custom-sse-path",
173
+ message_path="/my-custom-message-path/",
174
+ )
175
+ ```
176
+ ```python {7} client.py
177
+ import asyncio
178
+ from fastmcp import Client
179
+ from fastmcp.client.transports import SSETransport
180
+
181
+ async def example():
182
+ async with Client(
183
+ transport=SSETransport("http://127.0.0.1:4200/my-custom-sse-path")
184
+ ) as client:
185
+ await client.ping()
186
+
187
+ if __name__ == "__main__":
188
+ asyncio.run(example())
189
+ ```
190
+ </CodeGroup>
191
+
192
+ Your client only needs to know the host, port, and "main" path; the message path will be transmitted to it as part of the connection handshake.
docs/docs.json CHANGED
@@ -49,7 +49,16 @@
49
  "servers/tools",
50
  "servers/resources",
51
  "servers/prompts",
52
- "servers/context"
 
 
 
 
 
 
 
 
 
53
  ]
54
  },
55
  {
@@ -62,8 +71,6 @@
62
  {
63
  "group": "Patterns",
64
  "pages": [
65
- "patterns/proxy",
66
- "patterns/composition",
67
  "patterns/decorating-methods",
68
  "patterns/http-requests",
69
  "patterns/openapi",
 
49
  "servers/tools",
50
  "servers/resources",
51
  "servers/prompts",
52
+ "servers/context",
53
+ "patterns/proxy",
54
+ "patterns/composition"
55
+ ]
56
+ },
57
+ {
58
+ "group": "Deployment",
59
+ "pages": [
60
+ "deployment/running-server",
61
+ "deployment/authentication"
62
  ]
63
  },
64
  {
 
71
  {
72
  "group": "Patterns",
73
  "pages": [
 
 
74
  "patterns/decorating-methods",
75
  "patterns/http-requests",
76
  "patterns/openapi",
docs/getting-started/quickstart.mdx CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
  title: Quickstart
3
- icon: rocket
4
  ---
5
 
6
  Welcome! This guide will help you quickly set up FastMCP and run your first MCP server.
 
1
  ---
2
  title: Quickstart
3
+ icon: rocket-launch
4
  ---
5
 
6
  Welcome! This guide will help you quickly set up FastMCP and run your first MCP server.
docs/patterns/proxy.mdx CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
- title: Proxying Servers
3
- sidebarTitle: Proxying
4
  description: Use FastMCP to act as an intermediary or change transport for other MCP servers.
5
  icon: arrows-retweet
6
  ---
 
1
  ---
2
+ title: Proxy Servers
3
+ sidebarTitle: Proxy Servers
4
  description: Use FastMCP to act as an intermediary or change transport for other MCP servers.
5
  icon: arrows-retweet
6
  ---
docs/servers/fastmcp.mdx CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
  title: The FastMCP Server
3
- sidebarTitle: FastMCP Server
4
  description: Learn about the core FastMCP server class and how to run it.
5
  icon: server
6
  ---
@@ -97,11 +97,7 @@ See [Prompts](/servers/prompts) for detailed documentation.
97
 
98
  ## Running the Server
99
 
100
- FastMCP servers need a transport mechanism to communicate with clients. In the MCP protocol, servers typically run as separate processes that clients connect to.
101
-
102
- ### The `__main__` Block Pattern
103
-
104
- The standard way to make your server executable is to include a `run()` call inside an `if __name__ == "__main__":` block:
105
 
106
  ```python
107
  # my_server.py
@@ -115,118 +111,17 @@ def greet(name: str) -> str:
115
  return f"Hello, {name}!"
116
 
117
  if __name__ == "__main__":
118
- # This code only runs when the file is executed directly
119
-
120
- # Basic run with default settings (stdio transport)
121
  mcp.run()
122
 
123
- # Or with specific transport and parameters
124
- # mcp.run(transport="sse", host="127.0.0.1", port=9000)
125
- ```
126
-
127
- This pattern is important because:
128
-
129
- 1. **Client Compatibility**: Standard MCP clients (like Claude Desktop) expect to execute your server file directly with `python my_server.py`
130
- 2. **Process Isolation**: Each server runs in its own process, allowing clients to manage multiple servers independently
131
- 3. **Import Safety**: The main block prevents the server from running when the file is imported by other code
132
-
133
- While this pattern is technically optional when using FastMCP's CLI, it's considered a best practice for maximum compatibility with all MCP clients.
134
-
135
- ### Transport Options
136
-
137
- FastMCP supports two transport mechanisms:
138
-
139
- #### STDIO Transport (Default)
140
-
141
- The standard input/output (STDIO) transport is the default and most widely compatible option:
142
-
143
- ```python
144
- # Run with stdio (default)
145
- mcp.run() # or explicitly: mcp.run(transport="stdio")
146
- ```
147
-
148
- With STDIO:
149
- - The client starts a new server process for each session
150
- - Communication happens through standard input/output streams
151
- - The server process terminates when the client disconnects
152
- - This is ideal for integrations with tools like Claude Desktop, where each conversation gets its own server instance
153
-
154
- #### SSE Transport (Server-Sent Events)
155
-
156
- For long-running servers that serve multiple clients, FastMCP supports SSE:
157
-
158
- ```python
159
- # Run with SSE on default host/port (0.0.0.0:8000)
160
- mcp.run(transport="sse")
161
- ```
162
-
163
- With SSE:
164
- - The server runs as a persistent web server
165
- - Multiple clients can connect simultaneously
166
- - The server stays running until explicitly terminated
167
- - This is ideal for remote access to services
168
-
169
- You can configure transport parameters directly when running the server:
170
-
171
- ```python
172
- # Configure with specific parameters
173
- mcp.run(
174
- transport="sse",
175
- host="127.0.0.1", # Override default host
176
- port=8888, # Override default port
177
- log_level="debug" # Set logging level
178
- )
179
-
180
- # You can also run asynchronously with the same parameters
181
- import asyncio
182
- asyncio.run(
183
- mcp.run_sse_async(
184
- host="127.0.0.1",
185
- port=8888,
186
- log_level="debug"
187
- )
188
- )
189
  ```
190
 
191
- Transport parameters passed to `run()` or `run_sse_async()` override any settings defined when creating the FastMCP instance. The most common parameters for SSE transport are:
192
-
193
- - `host`: Host to bind to (default: "0.0.0.0")
194
- - `port`: Port to bind to (default: 8000)
195
- - `log_level`: Logging level (default: "INFO")
196
-
197
- #### Advanced Transport Configuration
198
-
199
- Under the hood, FastMCP's `run()` method accepts arbitrary keyword arguments (`**transport_kwargs`) that are passed to the transport-specific run methods:
200
-
201
- ```python
202
- # For SSE transport, kwargs are passed to run_sse_async()
203
- mcp.run(transport="sse", **transport_kwargs)
204
-
205
- # For stdio transport, kwargs are passed to run_stdio_async()
206
- mcp.run(transport="stdio", **transport_kwargs)
207
- ```
208
-
209
- This means that any future transport-specific options will be automatically available through the same interface without requiring changes to your code.
210
-
211
- ### Using the FastMCP CLI
212
-
213
- The FastMCP CLI provides a convenient way to run servers:
214
 
215
- ```bash
216
- # Run a server (defaults to stdio transport)
217
- fastmcp run my_server.py:mcp
218
 
219
- # Explicitly specify a transport
220
- fastmcp run my_server.py:mcp --transport sse
221
-
222
- # Configure SSE transport with host and port
223
- fastmcp run my_server.py:mcp --transport sse --host 127.0.0.1 --port 8888
224
-
225
- # With log level
226
- fastmcp run my_server.py:mcp --transport sse --log-level DEBUG
227
- ```
228
-
229
- The CLI can dynamically find and run FastMCP server objects in your files, but including the `if __name__ == "__main__":` block ensures compatibility with all clients.
230
 
231
  ## Composing Servers
232
 
@@ -289,7 +184,7 @@ print(mcp.settings.on_duplicate_tools) # Output: "error"
289
 
290
  ### Key Configuration Options
291
 
292
- - **`host`**: Host address for SSE transport (default: "0.0.0.0")
293
  - **`port`**: Port number for SSE transport (default: 8000)
294
  - **`log_level`**: Logging level (default: "INFO")
295
  - **`on_duplicate_tools`**: How to handle duplicate tool registrations
@@ -336,36 +231,24 @@ If the serializer function raises an exception, the tool will fall back to the d
336
 
337
  <VersionBadge version="2.2.7" />
338
 
339
- FastMCP inherits support for OAuth 2.0 authentication from the MCP protocol, allowing servers to protect their tools and resources behind authentication.
340
-
341
- ### OAuth 2.0 Support
342
-
343
- The `mcp.server.auth` module implements an OAuth 2.0 server interface that servers can use by providing an implementation of the `OAuthServerProvider` protocol.
344
 
345
  ```python
346
  from fastmcp import FastMCP
347
- from mcp.server.auth.settings import (
348
- RevocationOptions,
349
- ClientRegistrationOptions,
350
- AuthSettings,
351
- )
352
-
353
-
354
- # Create a server with authentication
355
- mcp = FastMCP(
356
- name="SecureApp",
357
- auth_provider=MyOAuthServerProvider(),
358
- auth=AuthSettings(
359
- issuer_url="https://myapp.com",
360
- revocation_options=RevocationOptions(
361
- enabled=True,
362
- ),
363
- client_registration_options=ClientRegistrationOptions(
364
- enabled=True,
365
- valid_scopes=["myscope", "myotherscope"],
366
- default_scopes=["myscope"],
367
- ),
368
- required_scopes=["myscope"],
369
- ),
370
- )
371
- ```
 
1
  ---
2
  title: The FastMCP Server
3
+ sidebarTitle: FastMCP Servers
4
  description: Learn about the core FastMCP server class and how to run it.
5
  icon: server
6
  ---
 
97
 
98
  ## Running the Server
99
 
100
+ FastMCP servers need a transport mechanism to communicate with clients. You typically start your server by calling the `mcp.run()` method on your `FastMCP` instance, often within an `if __name__ == "__main__":` block in your main server script. This pattern ensures compatibility with various MCP clients.
 
 
 
 
101
 
102
  ```python
103
  # my_server.py
 
111
  return f"Hello, {name}!"
112
 
113
  if __name__ == "__main__":
114
+ # This runs the server, defaulting to STDIO transport
 
 
115
  mcp.run()
116
 
117
+ # To use a different transport, e.g., Streamable HTTP:
118
+ # mcp.run(transport="streamable-http", host="127.0.0.1", port=9000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  ```
120
 
121
+ FastMCP supports several transport options like STDIO (default, for local tools), Streamable HTTP (recommended for web services), and SSE (legacy web transport). The server can also be run using the FastMCP CLI.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
+ For detailed information on each transport, how to configure them (host, port, paths), and when to use which, please refer to the [**Running Your FastMCP Server**](/deployment/running-server) guide.
 
 
124
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  ## Composing Servers
127
 
 
184
 
185
  ### Key Configuration Options
186
 
187
+ - **`host`**: Host address for SSE transport (default: "127.0.0.1")
188
  - **`port`**: Port number for SSE transport (default: 8000)
189
  - **`log_level`**: Logging level (default: "INFO")
190
  - **`on_duplicate_tools`**: How to handle duplicate tool registrations
 
231
 
232
  <VersionBadge version="2.2.7" />
233
 
234
+ FastMCP supports OAuth 2.0 authentication, allowing servers to protect their tools and resources. This is configured by providing an `auth_server_provider` and `auth` settings during `FastMCP` initialization.
 
 
 
 
235
 
236
  ```python
237
  from fastmcp import FastMCP
238
+ from mcp.server.auth.settings import AuthSettings #, ... other auth imports
239
+ # from your_auth_implementation import MyOAuthServerProvider # Placeholder
240
+
241
+ # Create a server with authentication (conceptual example)
242
+ # mcp = FastMCP(
243
+ # name="SecureApp",
244
+ # auth_server_provider=MyOAuthServerProvider(),
245
+ # auth=AuthSettings(
246
+ # issuer_url="https://myapp.com",
247
+ # # ... other OAuth settings ...
248
+ # required_scopes=["myscope"],
249
+ # ),
250
+ # )
251
+ ```
252
+ Due to the low-level nature of the current MCP SDK's auth provider interface, detailed implementation is beyond a quick example. Refer to the [MCP SDK documentation](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for specifics on implementing an `OAuthAuthorizationServerProvider`. FastMCP integrates with this by passing the provider and settings to the underlying MCP server.
253
+
254
+ A dedicated [Authentication guide](/deployment/authentication) will cover this in more detail once higher-level abstractions are available in FastMCP.
 
 
 
 
 
 
 
 
src/fastmcp/cli/cli.py CHANGED
@@ -334,7 +334,7 @@ def run(
334
  str | None,
335
  typer.Option(
336
  "--host",
337
- help="Host to bind to when using sse transport (default: 0.0.0.0)",
338
  ),
339
  ] = None,
340
  port: Annotated[
 
334
  str | None,
335
  typer.Option(
336
  "--host",
337
+ help="Host to bind to when using sse transport (default: 127.0.0.1)",
338
  ),
339
  ] = None,
340
  port: Annotated[
src/fastmcp/client/transports.py CHANGED
@@ -1,9 +1,11 @@
1
  import abc
2
  import contextlib
3
  import datetime
 
4
  import os
5
  import shutil
6
  import sys
 
7
  from collections.abc import AsyncIterator
8
  from pathlib import Path
9
  from typing import Any, TypedDict
@@ -450,6 +452,8 @@ def infer_transport(
450
  This function attempts to infer the correct transport type from the provided
451
  argument, handling various input types and converting them to the appropriate
452
  ClientTransport subclass.
 
 
453
  """
454
  # the transport is already a ClientTransport
455
  if isinstance(transport, ClientTransport):
@@ -470,7 +474,19 @@ def infer_transport(
470
 
471
  # the transport is an http(s) URL
472
  elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"):
473
- return SSETransport(url=transport)
 
 
 
 
 
 
 
 
 
 
 
 
474
 
475
  # the transport is a websocket URL
476
  elif isinstance(transport, AnyUrl | str) and str(transport).startswith("ws"):
 
1
  import abc
2
  import contextlib
3
  import datetime
4
+ import inspect
5
  import os
6
  import shutil
7
  import sys
8
+ import warnings
9
  from collections.abc import AsyncIterator
10
  from pathlib import Path
11
  from typing import Any, TypedDict
 
452
  This function attempts to infer the correct transport type from the provided
453
  argument, handling various input types and converting them to the appropriate
454
  ClientTransport subclass.
455
+
456
+ For HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`.
457
  """
458
  # the transport is already a ClientTransport
459
  if isinstance(transport, ClientTransport):
 
474
 
475
  # the transport is an http(s) URL
476
  elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"):
477
+ if str(transport).rstrip("/").endswith("/sse"):
478
+ warnings.warn(
479
+ inspect.cleandoc(
480
+ """
481
+ As of FastMCP 2.3.0, HTTP URLs are inferred to use Streamable HTTP.
482
+ The provided URL ends in `/sse`, so you may encounter unexpected behavior.
483
+ If you intended to use SSE, please use the `SSETransport` class directly.
484
+ """
485
+ ),
486
+ category=UserWarning,
487
+ stacklevel=2,
488
+ )
489
+ return StreamableHttpTransport(url=transport)
490
 
491
  # the transport is a websocket URL
492
  elif isinstance(transport, AnyUrl | str) and str(transport).startswith("ws"):
src/fastmcp/server/http.py CHANGED
@@ -1,9 +1,9 @@
1
  from __future__ import annotations
2
 
3
- from collections.abc import Generator
4
- from contextlib import contextmanager
5
  from contextvars import ContextVar
6
- from typing import TYPE_CHECKING
7
 
8
  from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
9
  from mcp.server.auth.middleware.bearer_auth import (
@@ -22,10 +22,12 @@ from starlette.responses import Response
22
  from starlette.routing import Mount, Route
23
  from starlette.types import Receive, Scope, Send
24
 
 
 
25
  from fastmcp.utilities.logging import get_logger
26
 
27
  if TYPE_CHECKING:
28
- from fastmcp import FastMCP
29
 
30
  logger = get_logger(__name__)
31
 
@@ -53,10 +55,92 @@ class RequestContextMiddleware:
53
  self.app = app
54
 
55
  async def __call__(self, scope, receive, send):
56
- with set_http_request(Request(scope)):
 
 
 
57
  await self.app(scope, receive, send)
58
 
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  def create_sse_app(
61
  server: FastMCP,
62
  message_path: str,
@@ -93,42 +177,17 @@ def create_sse_app(
93
  )
94
  return Response()
95
 
96
- # Configure routes and middleware
97
- routes: list[Route | Mount] = []
98
- middleware: list[Middleware] = []
99
-
100
- # Handle authentication configuration
101
- if auth_server_provider:
102
- # Ensure auth settings are provided when auth provider is present
103
- if not auth_settings:
104
- raise ValueError(
105
- "auth_settings must be provided when auth_server_provider is specified"
106
- )
107
-
108
- # Configure auth middleware
109
- middleware = [
110
- Middleware(
111
- AuthenticationMiddleware,
112
- backend=BearerAuthBackend(provider=auth_server_provider),
113
- ),
114
- Middleware(AuthContextMiddleware),
115
- ]
116
 
117
- # Get required scopes for authentication
118
- required_scopes = auth_settings.required_scopes or []
119
-
120
- # Add auth routes
121
- routes.extend(
122
- create_auth_routes(
123
- provider=auth_server_provider,
124
- issuer_url=auth_settings.issuer_url,
125
- service_documentation_url=auth_settings.service_documentation_url,
126
- client_registration_options=auth_settings.client_registration_options,
127
- revocation_options=auth_settings.revocation_options,
128
- )
129
- )
130
 
131
- # Add authenticated routes
 
 
132
  routes.append(
133
  Route(
134
  sse_path,
@@ -143,7 +202,7 @@ def create_sse_app(
143
  )
144
  )
145
  else:
146
- # No authentication required
147
  async def sse_endpoint(request: Request) -> Response:
148
  return await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
149
 
@@ -163,10 +222,88 @@ def create_sse_app(
163
 
164
  # Add custom routes with lowest precedence
165
  if additional_routes:
166
- routes.extend(additional_routes)
167
 
168
- # Add RequestContextMiddleware as the outermost middleware
169
- middleware.append(Middleware(RequestContextMiddleware))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- # Create and return the Starlette app with middleware
172
- return Starlette(debug=debug, routes=routes, middleware=middleware)
 
1
  from __future__ import annotations
2
 
3
+ from collections.abc import AsyncGenerator, Callable, Generator
4
+ from contextlib import asynccontextmanager, contextmanager
5
  from contextvars import ContextVar
6
+ from typing import TYPE_CHECKING, cast
7
 
8
  from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
9
  from mcp.server.auth.middleware.bearer_auth import (
 
22
  from starlette.routing import Mount, Route
23
  from starlette.types import Receive, Scope, Send
24
 
25
+ # This import is vendored until it is finalized in the upstream SDK
26
+ from fastmcp.server.streamable_http_manager import StreamableHTTPSessionManager
27
  from fastmcp.utilities.logging import get_logger
28
 
29
  if TYPE_CHECKING:
30
+ from fastmcp.server.server import FastMCP
31
 
32
  logger = get_logger(__name__)
33
 
 
55
  self.app = app
56
 
57
  async def __call__(self, scope, receive, send):
58
+ if scope["type"] == "http":
59
+ with set_http_request(Request(scope)):
60
+ await self.app(scope, receive, send)
61
+ else:
62
  await self.app(scope, receive, send)
63
 
64
 
65
+ def setup_auth_middleware_and_routes(
66
+ auth_server_provider: OAuthAuthorizationServerProvider | None,
67
+ auth_settings: AuthSettings | None,
68
+ ) -> tuple[list[Middleware], list[Route | Mount], list[str]]:
69
+ """Set up authentication middleware and routes if auth is enabled.
70
+
71
+ Args:
72
+ auth_server_provider: The OAuth authorization server provider
73
+ auth_settings: The auth settings
74
+
75
+ Returns:
76
+ Tuple of (middleware, auth_routes, required_scopes)
77
+ """
78
+ middleware: list[Middleware] = []
79
+ auth_routes: list[Route | Mount] = []
80
+ required_scopes: list[str] = []
81
+
82
+ if auth_server_provider:
83
+ if not auth_settings:
84
+ raise ValueError(
85
+ "auth_settings must be provided when auth_server_provider is specified"
86
+ )
87
+
88
+ middleware = [
89
+ Middleware(
90
+ AuthenticationMiddleware,
91
+ backend=BearerAuthBackend(provider=auth_server_provider),
92
+ ),
93
+ Middleware(AuthContextMiddleware),
94
+ ]
95
+
96
+ required_scopes = auth_settings.required_scopes or []
97
+
98
+ auth_routes.extend(
99
+ create_auth_routes(
100
+ provider=auth_server_provider,
101
+ issuer_url=auth_settings.issuer_url,
102
+ service_documentation_url=auth_settings.service_documentation_url,
103
+ client_registration_options=auth_settings.client_registration_options,
104
+ revocation_options=auth_settings.revocation_options,
105
+ )
106
+ )
107
+
108
+ return middleware, auth_routes, required_scopes
109
+
110
+
111
+ def create_base_app(
112
+ routes: list[Route | Mount],
113
+ middleware: list[Middleware],
114
+ debug: bool,
115
+ lifespan: Callable | None = None,
116
+ ) -> Starlette:
117
+ """Create a base Starlette app with common middleware and routes.
118
+
119
+ Args:
120
+ routes: List of routes to include in the app
121
+ middleware: List of middleware to include in the app
122
+ debug: Whether to enable debug mode
123
+ lifespan: Optional lifespan manager for the app
124
+
125
+ Returns:
126
+ A Starlette application
127
+ """
128
+ # Always add RequestContextMiddleware as the outermost middleware
129
+ middleware.append(Middleware(RequestContextMiddleware))
130
+
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)
142
+
143
+
144
  def create_sse_app(
145
  server: FastMCP,
146
  message_path: str,
 
177
  )
178
  return Response()
179
 
180
+ # Get auth middleware and routes
181
+ middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
182
+ auth_server_provider, auth_settings
183
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
+ # Initialize routes with auth routes
186
+ routes: list[Route | Mount] = auth_routes.copy()
 
 
 
 
 
 
 
 
 
 
 
187
 
188
+ # Add SSE routes with or without auth
189
+ if auth_server_provider:
190
+ # Auth is enabled, wrap endpoints with RequireAuthMiddleware
191
  routes.append(
192
  Route(
193
  sse_path,
 
202
  )
203
  )
204
  else:
205
+ # No auth required
206
  async def sse_endpoint(request: Request) -> Response:
207
  return await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
208
 
 
222
 
223
  # Add custom routes with lowest precedence
224
  if additional_routes:
225
+ routes.extend(cast(list[Route | Mount], additional_routes))
226
 
227
+ # Create and return the app
228
+ return create_base_app(routes, middleware, debug)
229
+
230
+
231
+ def create_streamable_http_app(
232
+ server: FastMCP,
233
+ streamable_http_path: str,
234
+ event_store: None = None,
235
+ auth_server_provider: OAuthAuthorizationServerProvider | None = None,
236
+ auth_settings: AuthSettings | None = None,
237
+ json_response: bool = False,
238
+ stateless_http: bool = False,
239
+ debug: bool = False,
240
+ additional_routes: list[Route] | list[Mount] | list[Route | Mount] | None = None,
241
+ ) -> Starlette:
242
+ """Return an instance of the StreamableHTTP server app.
243
+
244
+ Args:
245
+ server: The FastMCP server instance
246
+ streamable_http_path: Path for StreamableHTTP connections
247
+ event_store: Optional event store for session management
248
+ auth_server_provider: Optional auth provider
249
+ auth_settings: Optional auth settings
250
+ json_response: Whether to use JSON response format
251
+ stateless_http: Whether to use stateless mode (new transport per request)
252
+ debug: Whether to enable debug mode
253
+ additional_routes: Optional list of custom routes
254
+
255
+ Returns:
256
+ A Starlette application with StreamableHTTP support
257
+ """
258
+ # Create session manager using the provided event store
259
+ session_manager = StreamableHTTPSessionManager(
260
+ app=server._mcp_server,
261
+ event_store=event_store,
262
+ json_response=json_response,
263
+ stateless=stateless_http,
264
+ )
265
+
266
+ # Create the ASGI handler
267
+ async def handle_streamable_http(
268
+ scope: Scope, receive: Receive, send: Send
269
+ ) -> None:
270
+ await session_manager.handle_request(scope, receive, send)
271
+
272
+ # Get auth middleware and routes
273
+ middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
274
+ auth_server_provider, auth_settings
275
+ )
276
+
277
+ # Initialize routes with auth routes
278
+ routes: list[Route | Mount] = auth_routes.copy()
279
+
280
+ # Add StreamableHTTP routes with or without auth
281
+ if auth_server_provider:
282
+ # Auth is enabled, wrap endpoint with RequireAuthMiddleware
283
+ routes.append(
284
+ Mount(
285
+ streamable_http_path,
286
+ app=RequireAuthMiddleware(handle_streamable_http, required_scopes),
287
+ )
288
+ )
289
+ else:
290
+ # No auth required
291
+ routes.append(
292
+ Mount(
293
+ streamable_http_path,
294
+ app=handle_streamable_http,
295
+ )
296
+ )
297
+
298
+ # Add custom routes with lowest precedence
299
+ if additional_routes:
300
+ routes.extend(cast(list[Route | Mount], additional_routes))
301
+
302
+ # Create a lifespan manager to start and stop the session manager
303
+ @asynccontextmanager
304
+ async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
305
+ async with session_manager.run():
306
+ yield
307
 
308
+ # Create and return the app with lifespan
309
+ return create_base_app(routes, middleware, debug, lifespan)
src/fastmcp/server/server.py CHANGED
@@ -146,6 +146,7 @@ class FastMCP(Generic[LifespanResultT]):
146
  "is specified"
147
  )
148
  self._auth_server_provider = auth_server_provider
 
149
  self._additional_http_routes: list[Route] = []
150
  self.dependencies = self.settings.dependencies
151
 
@@ -167,30 +168,36 @@ class FastMCP(Generic[LifespanResultT]):
167
  return self._mcp_server.instructions
168
 
169
  async def run_async(
170
- self, transport: Literal["stdio", "sse"] | None = None, **transport_kwargs: Any
 
 
171
  ) -> None:
172
  """Run the FastMCP server asynchronously.
173
 
174
  Args:
175
- transport: Transport protocol to use ("stdio" or "sse")
176
  """
177
  if transport is None:
178
  transport = "stdio"
179
- if transport not in ["stdio", "sse"]:
180
  raise ValueError(f"Unknown transport: {transport}")
181
 
182
  if transport == "stdio":
183
  await self.run_stdio_async(**transport_kwargs)
184
- else: # transport == "sse"
185
  await self.run_sse_async(**transport_kwargs)
 
 
186
 
187
  def run(
188
- self, transport: Literal["stdio", "sse"] | None = None, **transport_kwargs: Any
 
 
189
  ) -> None:
190
  """Run the FastMCP server. Note this is a synchronous function.
191
 
192
  Args:
193
- transport: Transport protocol to use ("stdio" or "sse")
194
  """
195
  logger.info(f'Starting server "{self.name}"...')
196
 
@@ -711,6 +718,8 @@ class FastMCP(Generic[LifespanResultT]):
711
  host: str | None = None,
712
  port: int | None = None,
713
  log_level: str | None = None,
 
 
714
  uvicorn_config: dict | None = None,
715
  ) -> None:
716
  """Run the server using SSE transport."""
@@ -719,7 +728,7 @@ class FastMCP(Generic[LifespanResultT]):
719
  # timeout to make it possible to close immediately. see
720
  # https://github.com/jlowin/fastmcp/issues/296
721
  uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
722
- app = self.sse_app()
723
 
724
  config = uvicorn.Config(
725
  app,
@@ -731,18 +740,64 @@ class FastMCP(Generic[LifespanResultT]):
731
  server = uvicorn.Server(config)
732
  await server.serve()
733
 
734
- def sse_app(self) -> Starlette:
 
 
 
 
735
  """Return an instance of the SSE server app."""
736
  return create_sse_app(
737
  server=self,
738
- message_path=self.settings.message_path,
739
- sse_path=self.settings.sse_path,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
740
  auth_server_provider=self._auth_server_provider,
741
  auth_settings=self.settings.auth,
 
 
742
  debug=self.settings.debug,
743
  additional_routes=self._additional_http_routes,
744
  )
745
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
746
  def mount(
747
  self,
748
  prefix: str,
 
146
  "is specified"
147
  )
148
  self._auth_server_provider = auth_server_provider
149
+
150
  self._additional_http_routes: list[Route] = []
151
  self.dependencies = self.settings.dependencies
152
 
 
168
  return self._mcp_server.instructions
169
 
170
  async def run_async(
171
+ self,
172
+ transport: Literal["stdio", "sse", "streamable-http"] | None = None,
173
+ **transport_kwargs: Any,
174
  ) -> None:
175
  """Run the FastMCP server asynchronously.
176
 
177
  Args:
178
+ transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
179
  """
180
  if transport is None:
181
  transport = "stdio"
182
+ if transport not in ["stdio", "sse", "streamable-http"]:
183
  raise ValueError(f"Unknown transport: {transport}")
184
 
185
  if transport == "stdio":
186
  await self.run_stdio_async(**transport_kwargs)
187
+ 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)
191
 
192
  def run(
193
+ self,
194
+ transport: Literal["stdio", "sse", "streamable-http"] | None = None,
195
+ **transport_kwargs: Any,
196
  ) -> None:
197
  """Run the FastMCP server. Note this is a synchronous function.
198
 
199
  Args:
200
+ transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
201
  """
202
  logger.info(f'Starting server "{self.name}"...')
203
 
 
718
  host: str | None = None,
719
  port: int | None = None,
720
  log_level: str | None = None,
721
+ path: str | None = None,
722
+ message_path: str | None = None,
723
  uvicorn_config: dict | None = None,
724
  ) -> None:
725
  """Run the server using SSE transport."""
 
728
  # timeout to make it possible to close immediately. see
729
  # https://github.com/jlowin/fastmcp/issues/296
730
  uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
731
+ app = self.sse_app(path=path, message_path=message_path)
732
 
733
  config = uvicorn.Config(
734
  app,
 
740
  server = uvicorn.Server(config)
741
  await server.serve()
742
 
743
+ def sse_app(
744
+ self,
745
+ path: str | None = None,
746
+ message_path: str | None = None,
747
+ ) -> Starlette:
748
  """Return an instance of the SSE server app."""
749
  return create_sse_app(
750
  server=self,
751
+ message_path=message_path or self.settings.message_path,
752
+ sse_path=path or self.settings.sse_path,
753
+ auth_server_provider=self._auth_server_provider,
754
+ auth_settings=self.settings.auth,
755
+ debug=self.settings.debug,
756
+ additional_routes=self._additional_http_routes,
757
+ )
758
+
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
762
+
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,
773
  )
774
 
775
+ async def run_streamable_http_async(
776
+ self,
777
+ host: str | None = None,
778
+ port: int | None = None,
779
+ log_level: str | None = None,
780
+ path: str | None = None,
781
+ uvicorn_config: dict | None = None,
782
+ ) -> 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,
797
+ )
798
+ server = uvicorn.Server(config)
799
+ await server.serve()
800
+
801
  def mount(
802
  self,
803
  prefix: str,
src/fastmcp/server/streamable_http_manager.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)
src/fastmcp/settings.py CHANGED
@@ -61,6 +61,7 @@ class ServerSettings(BaseSettings):
61
  port: int = 8000
62
  sse_path: str = "/sse"
63
  message_path: str = "/messages/"
 
64
  debug: bool = False
65
 
66
  # resource settings
@@ -82,6 +83,12 @@ class ServerSettings(BaseSettings):
82
 
83
  auth: AuthSettings | None = None
84
 
 
 
 
 
 
 
85
 
86
  class ClientSettings(BaseSettings):
87
  """FastMCP client settings."""
 
61
  port: int = 8000
62
  sse_path: str = "/sse"
63
  message_path: str = "/messages/"
64
+ streamable_http_path: str = "/mcp"
65
  debug: bool = False
66
 
67
  # resource settings
 
83
 
84
  auth: AuthSettings | None = None
85
 
86
+ # StreamableHTTP settings
87
+ json_response: bool = False
88
+ stateless_http: bool = (
89
+ False # If True, uses true stateless mode (new transport per request)
90
+ )
91
+
92
 
93
  class ClientSettings(BaseSettings):
94
  """FastMCP client settings."""
test.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastmcp import FastMCP
2
+
3
+ mcp = FastMCP()
4
+
5
+ if __name__ == "__main__":
6
+ mcp.run(
7
+ transport="streamable-http",
8
+ host="127.0.0.1",
9
+ port=4200,
10
+ path="/my-custom-path/",
11
+ log_level="debug",
12
+ )
tests/client/test_streamable_http.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import sys
3
+ from collections.abc import Generator
4
+
5
+ import pytest
6
+ import uvicorn
7
+ from mcp.types import TextResourceContents
8
+
9
+ from fastmcp.client import Client
10
+ from fastmcp.client.transports import StreamableHttpTransport
11
+ from fastmcp.server.dependencies import get_http_request
12
+ from fastmcp.server.server import FastMCP
13
+ from fastmcp.utilities.tests import run_server_in_process
14
+
15
+
16
+ def fastmcp_server():
17
+ """Fixture that creates a FastMCP server with tools, resources, and prompts."""
18
+ server = FastMCP("TestServer")
19
+
20
+ # Add a tool
21
+ @server.tool()
22
+ def greet(name: str) -> str:
23
+ """Greet someone by name."""
24
+ return f"Hello, {name}!"
25
+
26
+ # Add a second tool
27
+ @server.tool()
28
+ def add(a: int, b: int) -> int:
29
+ """Add two numbers together."""
30
+ return a + b
31
+
32
+ # Add a resource
33
+ @server.resource(uri="data://users")
34
+ async def get_users():
35
+ return ["Alice", "Bob", "Charlie"]
36
+
37
+ # Add a resource template
38
+ @server.resource(uri="data://user/{user_id}")
39
+ async def get_user(user_id: str):
40
+ return {"id": user_id, "name": f"User {user_id}", "active": True}
41
+
42
+ @server.resource(uri="request://headers")
43
+ async def get_headers() -> dict[str, str]:
44
+ request = get_http_request()
45
+
46
+ return dict(request.headers)
47
+
48
+ # Add a prompt
49
+ @server.prompt()
50
+ def welcome(name: str) -> str:
51
+ """Example greeting prompt."""
52
+ return f"Welcome to FastMCP, {name}!"
53
+
54
+ return server
55
+
56
+
57
+ def run_server(host: str, port: int) -> None:
58
+ try:
59
+ app = fastmcp_server().streamable_http_app()
60
+ server = uvicorn.Server(
61
+ config=uvicorn.Config(
62
+ app=app,
63
+ host=host,
64
+ port=port,
65
+ log_level="error",
66
+ lifespan="on",
67
+ )
68
+ )
69
+ server.run()
70
+ except Exception as e:
71
+ print(f"Server error: {e}")
72
+ sys.exit(1)
73
+ sys.exit(0)
74
+
75
+
76
+ @pytest.fixture(scope="module")
77
+ def streamable_http_server() -> Generator[str, None, None]:
78
+ with run_server_in_process(run_server) as url:
79
+ yield f"{url}/mcp"
80
+
81
+
82
+ async def test_ping(streamable_http_server: str):
83
+ """Test pinging the server."""
84
+ async with Client(
85
+ transport=StreamableHttpTransport(streamable_http_server)
86
+ ) as client:
87
+ result = await client.ping()
88
+ assert result is True
89
+
90
+
91
+ async def test_http_headers(streamable_http_server: str):
92
+ """Test getting HTTP headers from the server."""
93
+ async with Client(
94
+ transport=StreamableHttpTransport(
95
+ streamable_http_server, headers={"X-DEMO-HEADER": "ABC"}
96
+ )
97
+ ) as client:
98
+ raw_result = await client.read_resource("request://headers")
99
+ assert isinstance(raw_result[0], TextResourceContents)
100
+ json_result = json.loads(raw_result[0].text)
101
+ assert "x-demo-header" in json_result
102
+ assert json_result["x-demo-header"] == "ABC"
tests/server/test_http_dependencies.py CHANGED
@@ -7,7 +7,7 @@ import uvicorn
7
  from mcp.types import TextContent, TextResourceContents
8
 
9
  from fastmcp.client import Client
10
- from fastmcp.client.transports import SSETransport
11
  from fastmcp.server.dependencies import get_http_request
12
  from fastmcp.server.server import FastMCP
13
  from fastmcp.utilities.tests import run_server_in_process
@@ -43,9 +43,15 @@ def fastmcp_server():
43
 
44
  def run_server(host: str, port: int) -> None:
45
  try:
46
- app = fastmcp_server().sse_app()
47
  server = uvicorn.Server(
48
- config=uvicorn.Config(app=app, host=host, port=port, log_level="error")
 
 
 
 
 
 
49
  )
50
  server.run()
51
  except Exception as e:
@@ -57,13 +63,13 @@ def run_server(host: str, port: int) -> None:
57
  @pytest.fixture(autouse=True, scope="module")
58
  def sse_server() -> Generator[str, None, None]:
59
  with run_server_in_process(run_server) as url:
60
- yield f"{url}/sse"
61
 
62
 
63
  async def test_http_headers_resource(sse_server: str):
64
  """Test getting HTTP headers from the server."""
65
  async with Client(
66
- transport=SSETransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
67
  ) as client:
68
  raw_result = await client.read_resource("request://headers")
69
  assert isinstance(raw_result[0], TextResourceContents)
@@ -75,7 +81,7 @@ async def test_http_headers_resource(sse_server: str):
75
  async def test_http_headers_tool(sse_server: str):
76
  """Test getting HTTP headers from the server."""
77
  async with Client(
78
- transport=SSETransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
79
  ) as client:
80
  result = await client.call_tool("get_headers_tool")
81
  assert isinstance(result[0], TextContent)
@@ -87,7 +93,7 @@ async def test_http_headers_tool(sse_server: str):
87
  async def test_http_headers_prompt(sse_server: str):
88
  """Test getting HTTP headers from the server."""
89
  async with Client(
90
- transport=SSETransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
91
  ) as client:
92
  result = await client.get_prompt("get_headers_prompt")
93
  assert isinstance(result.messages[0].content, TextContent)
 
7
  from mcp.types import TextContent, TextResourceContents
8
 
9
  from fastmcp.client import Client
10
+ from fastmcp.client.transports import StreamableHttpTransport
11
  from fastmcp.server.dependencies import get_http_request
12
  from fastmcp.server.server import FastMCP
13
  from fastmcp.utilities.tests import run_server_in_process
 
43
 
44
  def run_server(host: str, port: int) -> None:
45
  try:
46
+ app = fastmcp_server().streamable_http_app()
47
  server = uvicorn.Server(
48
+ config=uvicorn.Config(
49
+ app=app,
50
+ host=host,
51
+ port=port,
52
+ log_level="error",
53
+ lifespan="on",
54
+ )
55
  )
56
  server.run()
57
  except Exception as e:
 
63
  @pytest.fixture(autouse=True, scope="module")
64
  def sse_server() -> Generator[str, None, None]:
65
  with run_server_in_process(run_server) as url:
66
+ yield f"{url}/mcp"
67
 
68
 
69
  async def test_http_headers_resource(sse_server: str):
70
  """Test getting HTTP headers from the server."""
71
  async with Client(
72
+ transport=StreamableHttpTransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
73
  ) as client:
74
  raw_result = await client.read_resource("request://headers")
75
  assert isinstance(raw_result[0], TextResourceContents)
 
81
  async def test_http_headers_tool(sse_server: str):
82
  """Test getting HTTP headers from the server."""
83
  async with Client(
84
+ transport=StreamableHttpTransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
85
  ) as client:
86
  result = await client.call_tool("get_headers_tool")
87
  assert isinstance(result[0], TextContent)
 
93
  async def test_http_headers_prompt(sse_server: str):
94
  """Test getting HTTP headers from the server."""
95
  async with Client(
96
+ transport=StreamableHttpTransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
97
  ) as client:
98
  result = await client.get_prompt("get_headers_prompt")
99
  assert isinstance(result.messages[0].content, TextContent)
tests/server/test_lifespan.py CHANGED
@@ -5,7 +5,6 @@ from contextlib import asynccontextmanager
5
 
6
  import anyio
7
  import pytest
8
- from mcp.server.fastmcp import Context, FastMCP
9
  from mcp.server.lowlevel.server import NotificationOptions, Server
10
  from mcp.server.models import InitializationOptions
11
  from mcp.shared.message import SessionMessage
@@ -19,6 +18,8 @@ from mcp.types import (
19
  )
20
  from pydantic import TypeAdapter
21
 
 
 
22
 
23
  @pytest.mark.anyio
24
  async def test_lowlevel_server_lifespan():
 
5
 
6
  import anyio
7
  import pytest
 
8
  from mcp.server.lowlevel.server import NotificationOptions, Server
9
  from mcp.server.models import InitializationOptions
10
  from mcp.shared.message import SessionMessage
 
18
  )
19
  from pydantic import TypeAdapter
20
 
21
+ from fastmcp import Context, FastMCP
22
+
23
 
24
  @pytest.mark.anyio
25
  async def test_lowlevel_server_lifespan():