Spaces:
Running
Running
Merge pull request #361 from jlowin/streamable-http
Browse files- docs/deployment/authentication.mdx +15 -0
- docs/deployment/running-server.mdx +192 -0
- docs/docs.json +10 -3
- docs/getting-started/quickstart.mdx +1 -1
- docs/patterns/proxy.mdx +2 -2
- docs/servers/fastmcp.mdx +26 -143
- src/fastmcp/cli/cli.py +1 -1
- src/fastmcp/client/transports.py +17 -1
- src/fastmcp/server/http.py +182 -45
- src/fastmcp/server/server.py +65 -10
- src/fastmcp/server/streamable_http_manager.py +241 -0
- src/fastmcp/settings.py +7 -0
- test.py +12 -0
- tests/client/test_streamable_http.py +102 -0
- tests/server/test_http_dependencies.py +13 -7
- tests/server/test_lifespan.py +2 -1
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:
|
| 3 |
-
sidebarTitle:
|
| 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
|
| 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.
|
| 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
|
| 119 |
-
|
| 120 |
-
# Basic run with default settings (stdio transport)
|
| 121 |
mcp.run()
|
| 122 |
|
| 123 |
-
#
|
| 124 |
-
# mcp.run(transport="
|
| 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 |
-
|
| 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 |
-
|
| 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: "
|
| 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
|
| 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 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
#
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 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:
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 97 |
-
|
| 98 |
-
|
| 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 |
-
|
| 118 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 132 |
routes.append(
|
| 133 |
Route(
|
| 134 |
sse_path,
|
|
@@ -143,7 +202,7 @@ def create_sse_app(
|
|
| 143 |
)
|
| 144 |
)
|
| 145 |
else:
|
| 146 |
-
# No
|
| 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 |
-
#
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
-
# Create and return the
|
| 172 |
-
return
|
|
|
|
| 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,
|
|
|
|
|
|
|
| 171 |
) -> None:
|
| 172 |
"""Run the FastMCP server asynchronously.
|
| 173 |
|
| 174 |
Args:
|
| 175 |
-
transport: Transport protocol to use ("stdio" or "
|
| 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 |
-
|
| 185 |
await self.run_sse_async(**transport_kwargs)
|
|
|
|
|
|
|
| 186 |
|
| 187 |
def run(
|
| 188 |
-
self,
|
|
|
|
|
|
|
| 189 |
) -> None:
|
| 190 |
"""Run the FastMCP server. Note this is a synchronous function.
|
| 191 |
|
| 192 |
Args:
|
| 193 |
-
transport: Transport protocol to use ("stdio" or "
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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().
|
| 47 |
server = uvicorn.Server(
|
| 48 |
-
config=uvicorn.Config(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}/
|
| 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=
|
| 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=
|
| 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=
|
| 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():
|