Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
da4845b
1
Parent(s): 9663571
Deprecate transport-specific methods
Browse files- README.md +1 -1
- docs/deployment/asgi.mdx +22 -14
- docs/deployment/running-server.mdx +38 -6
- docs/getting-started/installation.mdx +4 -3
- docs/getting-started/welcome.mdx +1 -1
- docs/servers/fastmcp.mdx +7 -2
- src/fastmcp/server/server.py +95 -41
- src/fastmcp/utilities/tests.py +1 -1
- tests/client/test_sse.py +2 -2
- tests/client/test_streamable_http.py +2 -2
- tests/server/test_http_dependencies.py +1 -1
- tests/server/test_http_middleware.py +4 -4
- tests/test_deprecated.py +82 -0
README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
| 15 |
> [!NOTE]
|
| 16 |
> #### FastMCP 2.0 & The Official MCP SDK
|
| 17 |
>
|
| 18 |
-
> Recognize the `FastMCP` name? You might have
|
| 19 |
>
|
| 20 |
> **Welcome to FastMCP 2.0!** This is the actively developed successor, and it significantly expands on 1.0 by introducing powerful client capabilities, server proxying & composition, OpenAPI/FastAPI integration, and more advanced features.
|
| 21 |
>
|
|
|
|
| 15 |
> [!NOTE]
|
| 16 |
> #### FastMCP 2.0 & The Official MCP SDK
|
| 17 |
>
|
| 18 |
+
> Recognize the `FastMCP` name? You might have seen the version that was contributed to the [official MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk), which was based on **FastMCP 1.0**.
|
| 19 |
>
|
| 20 |
> **Welcome to FastMCP 2.0!** This is the actively developed successor, and it significantly expands on 1.0 by introducing powerful client capabilities, server proxying & composition, OpenAPI/FastAPI integration, and more advanced features.
|
| 21 |
>
|
docs/deployment/asgi.mdx
CHANGED
|
@@ -19,7 +19,7 @@ Please note that all FastMCP servers have a `run()` method that can be used to s
|
|
| 19 |
|
| 20 |
FastMCP servers can be created as [Starlette](https://www.starlette.io/) ASGI apps for straightforward hosting or integration into existing applications.
|
| 21 |
|
| 22 |
-
The first step is to obtain a Starlette application instance from your FastMCP server using
|
| 23 |
|
| 24 |
```python
|
| 25 |
from fastmcp import FastMCP
|
|
@@ -30,18 +30,23 @@ mcp = FastMCP("MyServer")
|
|
| 30 |
def hello(name: str) -> str:
|
| 31 |
return f"Hello, {name}!"
|
| 32 |
|
| 33 |
-
# Get a Starlette app instance for
|
| 34 |
-
http_app = mcp.
|
| 35 |
-
|
|
|
|
|
|
|
| 36 |
```
|
| 37 |
|
| 38 |
-
Both
|
| 39 |
|
| 40 |
-
The MCP server's endpoint is mounted at the root path `/mcp` for Streamable HTTP transport, and `/sse` for SSE transport, though you can change these paths by passing a `path` argument to the `
|
| 41 |
|
| 42 |
```python
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
| 45 |
```
|
| 46 |
|
| 47 |
### Running the Server
|
|
@@ -49,9 +54,12 @@ sse_app = mcp.sse_app(path="/custom-sse-path")
|
|
| 49 |
To run the FastMCP server, you can use the `uvicorn` ASGI server:
|
| 50 |
|
| 51 |
```python
|
|
|
|
| 52 |
import uvicorn
|
| 53 |
|
| 54 |
-
|
|
|
|
|
|
|
| 55 |
|
| 56 |
if __name__ == "__main__":
|
| 57 |
uvicorn.run(http_app, host="0.0.0.0", port=8000)
|
|
@@ -83,7 +91,7 @@ custom_middleware = [
|
|
| 83 |
]
|
| 84 |
|
| 85 |
# Create ASGI app with custom middleware
|
| 86 |
-
http_app = mcp.
|
| 87 |
```
|
| 88 |
|
| 89 |
|
|
@@ -91,7 +99,7 @@ http_app = mcp.streamable_http_app(middleware=custom_middleware)
|
|
| 91 |
|
| 92 |
<VersionBadge version="2.3.1" />
|
| 93 |
|
| 94 |
-
You can mount your FastMCP server in another Starlette application
|
| 95 |
|
| 96 |
```python
|
| 97 |
from fastmcp import FastMCP
|
|
@@ -102,7 +110,7 @@ from starlette.routing import Mount
|
|
| 102 |
mcp = FastMCP("MyServer")
|
| 103 |
|
| 104 |
# Create the ASGI app
|
| 105 |
-
mcp_app = mcp.
|
| 106 |
|
| 107 |
# Create a Starlette app and mount the MCP server
|
| 108 |
app = Starlette(
|
|
@@ -134,7 +142,7 @@ from starlette.routing import Mount
|
|
| 134 |
mcp = FastMCP("MyServer")
|
| 135 |
|
| 136 |
# Create the ASGI app
|
| 137 |
-
mcp_app = mcp.
|
| 138 |
|
| 139 |
# Create nested application structure
|
| 140 |
inner_app = Starlette(routes=[Mount("/inner", app=mcp_app)])
|
|
@@ -164,7 +172,7 @@ from starlette.routing import Mount
|
|
| 164 |
mcp = FastMCP("MyServer")
|
| 165 |
|
| 166 |
# Create the ASGI app
|
| 167 |
-
mcp_app = mcp.
|
| 168 |
|
| 169 |
# Create a FastAPI app and mount the MCP server
|
| 170 |
app = FastAPI(lifespan=mcp_app.router.lifespan_context)
|
|
|
|
| 19 |
|
| 20 |
FastMCP servers can be created as [Starlette](https://www.starlette.io/) ASGI apps for straightforward hosting or integration into existing applications.
|
| 21 |
|
| 22 |
+
The first step is to obtain a Starlette application instance from your FastMCP server using the `http_app()` method:
|
| 23 |
|
| 24 |
```python
|
| 25 |
from fastmcp import FastMCP
|
|
|
|
| 30 |
def hello(name: str) -> str:
|
| 31 |
return f"Hello, {name}!"
|
| 32 |
|
| 33 |
+
# Get a Starlette app instance for Streamable HTTP transport (recommended)
|
| 34 |
+
http_app = mcp.http_app()
|
| 35 |
+
|
| 36 |
+
# For legacy SSE transport (deprecated)
|
| 37 |
+
sse_app = mcp.http_app(transport="sse")
|
| 38 |
```
|
| 39 |
|
| 40 |
+
Both approaches return a Starlette application that can be integrated with other ASGI-compatible web frameworks.
|
| 41 |
|
| 42 |
+
The MCP server's endpoint is mounted at the root path `/mcp` for Streamable HTTP transport, and `/sse` for SSE transport, though you can change these paths by passing a `path` argument to the `http_app()` method:
|
| 43 |
|
| 44 |
```python
|
| 45 |
+
# For Streamable HTTP transport
|
| 46 |
+
http_app = mcp.http_app(path="/custom-mcp-path")
|
| 47 |
+
|
| 48 |
+
# For SSE transport (deprecated)
|
| 49 |
+
sse_app = mcp.http_app(path="/custom-sse-path", transport="sse")
|
| 50 |
```
|
| 51 |
|
| 52 |
### Running the Server
|
|
|
|
| 54 |
To run the FastMCP server, you can use the `uvicorn` ASGI server:
|
| 55 |
|
| 56 |
```python
|
| 57 |
+
from fastmcp import FastMCP
|
| 58 |
import uvicorn
|
| 59 |
|
| 60 |
+
mcp = FastMCP("MyServer")
|
| 61 |
+
|
| 62 |
+
http_app = mcp.http_app()
|
| 63 |
|
| 64 |
if __name__ == "__main__":
|
| 65 |
uvicorn.run(http_app, host="0.0.0.0", port=8000)
|
|
|
|
| 91 |
]
|
| 92 |
|
| 93 |
# Create ASGI app with custom middleware
|
| 94 |
+
http_app = mcp.http_app(middleware=custom_middleware)
|
| 95 |
```
|
| 96 |
|
| 97 |
|
|
|
|
| 99 |
|
| 100 |
<VersionBadge version="2.3.1" />
|
| 101 |
|
| 102 |
+
You can mount your FastMCP server in another Starlette application:
|
| 103 |
|
| 104 |
```python
|
| 105 |
from fastmcp import FastMCP
|
|
|
|
| 110 |
mcp = FastMCP("MyServer")
|
| 111 |
|
| 112 |
# Create the ASGI app
|
| 113 |
+
mcp_app = mcp.http_app(path='/mcp')
|
| 114 |
|
| 115 |
# Create a Starlette app and mount the MCP server
|
| 116 |
app = Starlette(
|
|
|
|
| 142 |
mcp = FastMCP("MyServer")
|
| 143 |
|
| 144 |
# Create the ASGI app
|
| 145 |
+
mcp_app = mcp.http_app(path='/mcp')
|
| 146 |
|
| 147 |
# Create nested application structure
|
| 148 |
inner_app = Starlette(routes=[Mount("/inner", app=mcp_app)])
|
|
|
|
| 172 |
mcp = FastMCP("MyServer")
|
| 173 |
|
| 174 |
# Create the ASGI app
|
| 175 |
+
mcp_app = mcp.http_app(path='/mcp')
|
| 176 |
|
| 177 |
# Create a FastAPI app and mount the MCP server
|
| 178 |
app = FastAPI(lifespan=mcp_app.router.lifespan_context)
|
docs/deployment/running-server.mdx
CHANGED
|
@@ -40,8 +40,8 @@ Below is a comparison of available transport options to help you choose the righ
|
|
| 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
|
| 44 |
-
| **SSE** | Existing web-based deployments that rely on SSE |
|
| 45 |
|
| 46 |
### STDIO
|
| 47 |
|
|
@@ -64,7 +64,7 @@ When using Stdio transport, you will typically *not* run the server yourself as
|
|
| 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
|
| 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>
|
|
@@ -122,7 +122,12 @@ if __name__ == "__main__":
|
|
| 122 |
|
| 123 |
### SSE
|
| 124 |
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|
|
@@ -170,7 +175,6 @@ if __name__ == "__main__":
|
|
| 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
|
|
@@ -189,9 +193,37 @@ if __name__ == "__main__":
|
|
| 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.
|
| 193 |
|
| 194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
## Custom Routes
|
| 197 |
|
|
|
|
| 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 web-based deployments |
|
| 44 |
+
| **SSE** | Existing web-based deployments that rely on SSE | Deprecated - prefer Streamable HTTP for new projects |
|
| 45 |
|
| 46 |
### STDIO
|
| 47 |
|
|
|
|
| 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 the recommended transport for 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>
|
|
|
|
| 122 |
|
| 123 |
### SSE
|
| 124 |
|
| 125 |
+
<Warning>
|
| 126 |
+
The SSE transport is deprecated and may be removed in a future version.
|
| 127 |
+
New applications should use Streamable HTTP transport instead.
|
| 128 |
+
</Warning>
|
| 129 |
+
|
| 130 |
+
Server-Sent Events (SSE) is an HTTP-based protocol for server-to-client streaming. While FastMCP still supports SSE, it is deprecated and Streamable HTTP is preferred for new projects.
|
| 131 |
|
| 132 |
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/`).
|
| 133 |
|
|
|
|
| 175 |
port=4200,
|
| 176 |
log_level="debug",
|
| 177 |
path="/my-custom-sse-path",
|
|
|
|
| 178 |
)
|
| 179 |
```
|
| 180 |
```python {7} client.py
|
|
|
|
| 193 |
```
|
| 194 |
</CodeGroup>
|
| 195 |
|
|
|
|
| 196 |
|
| 197 |
|
| 198 |
+
## Async Usage
|
| 199 |
+
|
| 200 |
+
FastMCP provides both synchronous and asynchronous APIs for running your server. The `run()` method seen in previous examples is a synchronous method that internally uses `anyio.run()` to run the asynchronous server. For applications that are already running in an async context, FastMCP provides the `run_async()` method.
|
| 201 |
+
|
| 202 |
+
```python {10-12}
|
| 203 |
+
from fastmcp import FastMCP
|
| 204 |
+
import asyncio
|
| 205 |
+
|
| 206 |
+
mcp = FastMCP(name="MyServer")
|
| 207 |
+
|
| 208 |
+
@mcp.tool()
|
| 209 |
+
def hello(name: str) -> str:
|
| 210 |
+
return f"Hello, {name}!"
|
| 211 |
+
|
| 212 |
+
async def main():
|
| 213 |
+
# Use run_async() in async contexts
|
| 214 |
+
await mcp.run_async(transport="streamable-http")
|
| 215 |
+
|
| 216 |
+
if __name__ == "__main__":
|
| 217 |
+
asyncio.run(main())
|
| 218 |
+
```
|
| 219 |
+
|
| 220 |
+
<Warning>
|
| 221 |
+
The `run()` method cannot be called from inside an async function because it already creates its own async event loop internally. If you attempt to call `run()` from inside an async function, you'll get an error about the event loop already running.
|
| 222 |
+
|
| 223 |
+
Always use `run_async()` inside async functions and `run()` in synchronous contexts.
|
| 224 |
+
</Warning>
|
| 225 |
+
|
| 226 |
+
Both `run()` and `run_async()` accept the same transport arguments, so all the examples above apply to both methods.
|
| 227 |
|
| 228 |
## Custom Routes
|
| 229 |
|
docs/getting-started/installation.mdx
CHANGED
|
@@ -44,7 +44,7 @@ FastMCP root path: ~/Developer/fastmcp
|
|
| 44 |
```
|
| 45 |
## Upgrading from the Official MCP SDK
|
| 46 |
|
| 47 |
-
Upgrading from the official MCP SDK's FastMCP 1.0 to FastMCP 2.0 is
|
| 48 |
|
| 49 |
|
| 50 |
```python {1-5}
|
|
@@ -56,8 +56,9 @@ from fastmcp import FastMCP
|
|
| 56 |
|
| 57 |
mcp = FastMCP("My MCP Server")
|
| 58 |
```
|
| 59 |
-
|
| 60 |
-
|
|
|
|
| 61 |
|
| 62 |
## Installing for Development
|
| 63 |
|
|
|
|
| 44 |
```
|
| 45 |
## Upgrading from the Official MCP SDK
|
| 46 |
|
| 47 |
+
Upgrading from the official MCP SDK's FastMCP 1.0 to FastMCP 2.0 is generally straightforward. The core server API is highly compatible, and in many cases, changing your import statement from `from mcp.server.fastmcp import FastMCP` to `from fastmcp import FastMCP` will be sufficient.
|
| 48 |
|
| 49 |
|
| 50 |
```python {1-5}
|
|
|
|
| 56 |
|
| 57 |
mcp = FastMCP("My MCP Server")
|
| 58 |
```
|
| 59 |
+
<Warning>
|
| 60 |
+
Prior to `fastmcp==2.3.0` and `mcp==1.8.0`, the 2.x API always mirrored the 1.0 API. However, as the projects diverge, this can not be guaranteed. You may see deprecation warnings if you attempt to use 1.0 APIs in FastMCP 2.x. Please refer to this documentation for details on new capabilities.
|
| 61 |
+
</Warning>
|
| 62 |
|
| 63 |
## Installing for Development
|
| 64 |
|
docs/getting-started/welcome.mdx
CHANGED
|
@@ -27,7 +27,7 @@ if __name__ == "__main__":
|
|
| 27 |
## FastMCP 2.0 and the Official MCP SDK
|
| 28 |
|
| 29 |
<Tip>
|
| 30 |
-
Recognize the `FastMCP` name? You might have
|
| 31 |
|
| 32 |
|
| 33 |
**Welcome to FastMCP 2.0!** This is the [actively developed successor](https://github.com/jlowin/fastmcp), and it significantly expands on 1.0 by introducing powerful client capabilities, server proxying & composition, OpenAPI/FastAPI integration, and more advanced features.
|
|
|
|
| 27 |
## FastMCP 2.0 and the Official MCP SDK
|
| 28 |
|
| 29 |
<Tip>
|
| 30 |
+
Recognize the `FastMCP` name? You might have seen the version that was contributed to the [official MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk), which was based on **FastMCP 1.0**.
|
| 31 |
|
| 32 |
|
| 33 |
**Welcome to FastMCP 2.0!** This is the [actively developed successor](https://github.com/jlowin/fastmcp), and it significantly expands on 1.0 by introducing powerful client capabilities, server proxying & composition, OpenAPI/FastAPI integration, and more advanced features.
|
docs/servers/fastmcp.mdx
CHANGED
|
@@ -114,11 +114,16 @@ if __name__ == "__main__":
|
|
| 114 |
# This runs the server, defaulting to STDIO transport
|
| 115 |
mcp.run()
|
| 116 |
|
| 117 |
-
# To use a different transport, e.g.,
|
| 118 |
# mcp.run(transport="streamable-http", host="127.0.0.1", port=9000)
|
| 119 |
```
|
| 120 |
|
| 121 |
-
FastMCP supports several transport options
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|
|
|
|
| 114 |
# This runs the server, defaulting to STDIO transport
|
| 115 |
mcp.run()
|
| 116 |
|
| 117 |
+
# To use a different transport, e.g., HTTP:
|
| 118 |
# mcp.run(transport="streamable-http", host="127.0.0.1", port=9000)
|
| 119 |
```
|
| 120 |
|
| 121 |
+
FastMCP supports several transport options:
|
| 122 |
+
- STDIO (default, for local tools)
|
| 123 |
+
- Streamable HTTP (recommended for web services)
|
| 124 |
+
- SSE (legacy web transport, deprecated)
|
| 125 |
+
|
| 126 |
+
The server can also be run using the FastMCP CLI.
|
| 127 |
|
| 128 |
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.
|
| 129 |
|
src/fastmcp/server/server.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import datetime
|
|
|
|
| 6 |
from collections.abc import AsyncIterator, Awaitable, Callable
|
| 7 |
from contextlib import (
|
| 8 |
AbstractAsyncContextManager,
|
|
@@ -170,7 +171,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 170 |
|
| 171 |
async def run_async(
|
| 172 |
self,
|
| 173 |
-
transport: Literal["stdio", "
|
| 174 |
**transport_kwargs: Any,
|
| 175 |
) -> None:
|
| 176 |
"""Run the FastMCP server asynchronously.
|
|
@@ -180,19 +181,21 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 180 |
"""
|
| 181 |
if transport is None:
|
| 182 |
transport = "stdio"
|
| 183 |
-
if transport not in ["stdio", "
|
| 184 |
raise ValueError(f"Unknown transport: {transport}")
|
| 185 |
|
| 186 |
if transport == "stdio":
|
| 187 |
await self.run_stdio_async(**transport_kwargs)
|
|
|
|
|
|
|
| 188 |
elif transport == "sse":
|
| 189 |
-
await self.
|
| 190 |
-
else:
|
| 191 |
-
|
| 192 |
|
| 193 |
def run(
|
| 194 |
self,
|
| 195 |
-
transport: Literal["stdio", "
|
| 196 |
**transport_kwargs: Any,
|
| 197 |
) -> None:
|
| 198 |
"""Run the FastMCP server. Note this is a synchronous function.
|
|
@@ -714,22 +717,22 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 714 |
self._mcp_server.create_initialization_options(),
|
| 715 |
)
|
| 716 |
|
| 717 |
-
async def
|
| 718 |
self,
|
|
|
|
| 719 |
host: str | None = None,
|
| 720 |
port: int | None = None,
|
| 721 |
log_level: str | None = None,
|
| 722 |
path: str | None = None,
|
| 723 |
-
message_path: str | None = None,
|
| 724 |
uvicorn_config: dict | None = None,
|
| 725 |
) -> None:
|
| 726 |
-
"""Run the server using
|
| 727 |
uvicorn_config = uvicorn_config or {}
|
| 728 |
-
# the SSE app hangs even when a signal is sent, so we disable the
|
| 729 |
-
# timeout to make it possible to close immediately. see
|
| 730 |
-
# https://github.com/jlowin/fastmcp/issues/296
|
| 731 |
uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
|
| 732 |
-
|
|
|
|
|
|
|
|
|
|
| 733 |
|
| 734 |
config = uvicorn.Config(
|
| 735 |
app,
|
|
@@ -741,6 +744,33 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 741 |
server = uvicorn.Server(config)
|
| 742 |
await server.serve()
|
| 743 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
def sse_app(
|
| 745 |
self,
|
| 746 |
path: str | None = None,
|
|
@@ -755,6 +785,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 755 |
message_path: The path to the message endpoint
|
| 756 |
middleware: A list of middleware to apply to the app
|
| 757 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
return create_sse_app(
|
| 759 |
server=self,
|
| 760 |
message_path=message_path or self.settings.message_path,
|
|
@@ -778,20 +813,44 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 778 |
path: The path to the StreamableHTTP endpoint
|
| 779 |
middleware: A list of middleware to apply to the app
|
| 780 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
from fastmcp.server.http import create_streamable_http_app
|
| 782 |
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 795 |
|
| 796 |
async def run_streamable_http_async(
|
| 797 |
self,
|
|
@@ -801,23 +860,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 801 |
path: str | None = None,
|
| 802 |
uvicorn_config: dict | None = None,
|
| 803 |
) -> None:
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
# lifespan is required for streamable http
|
| 816 |
-
lifespan="on",
|
| 817 |
-
**uvicorn_config,
|
| 818 |
)
|
| 819 |
-
server = uvicorn.Server(config)
|
| 820 |
-
await server.serve()
|
| 821 |
|
| 822 |
def mount(
|
| 823 |
self,
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import datetime
|
| 6 |
+
import warnings
|
| 7 |
from collections.abc import AsyncIterator, Awaitable, Callable
|
| 8 |
from contextlib import (
|
| 9 |
AbstractAsyncContextManager,
|
|
|
|
| 171 |
|
| 172 |
async def run_async(
|
| 173 |
self,
|
| 174 |
+
transport: Literal["stdio", "streamable-http", "sse"] | None = None,
|
| 175 |
**transport_kwargs: Any,
|
| 176 |
) -> None:
|
| 177 |
"""Run the FastMCP server asynchronously.
|
|
|
|
| 181 |
"""
|
| 182 |
if transport is None:
|
| 183 |
transport = "stdio"
|
| 184 |
+
if transport not in ["stdio", "streamable-http", "sse"]:
|
| 185 |
raise ValueError(f"Unknown transport: {transport}")
|
| 186 |
|
| 187 |
if transport == "stdio":
|
| 188 |
await self.run_stdio_async(**transport_kwargs)
|
| 189 |
+
elif transport == "streamable-http":
|
| 190 |
+
await self.run_http_async(transport="streamable-http", **transport_kwargs)
|
| 191 |
elif transport == "sse":
|
| 192 |
+
await self.run_http_async(transport="sse", **transport_kwargs)
|
| 193 |
+
else:
|
| 194 |
+
raise ValueError(f"Unknown transport: {transport}")
|
| 195 |
|
| 196 |
def run(
|
| 197 |
self,
|
| 198 |
+
transport: Literal["stdio", "streamable-http", "sse"] | None = None,
|
| 199 |
**transport_kwargs: Any,
|
| 200 |
) -> None:
|
| 201 |
"""Run the FastMCP server. Note this is a synchronous function.
|
|
|
|
| 717 |
self._mcp_server.create_initialization_options(),
|
| 718 |
)
|
| 719 |
|
| 720 |
+
async def run_http_async(
|
| 721 |
self,
|
| 722 |
+
transport: Literal["streamable-http", "sse"] = "streamable-http",
|
| 723 |
host: str | None = None,
|
| 724 |
port: int | None = None,
|
| 725 |
log_level: str | None = None,
|
| 726 |
path: str | None = None,
|
|
|
|
| 727 |
uvicorn_config: dict | None = None,
|
| 728 |
) -> None:
|
| 729 |
+
"""Run the server using Streamable HTTP transport."""
|
| 730 |
uvicorn_config = uvicorn_config or {}
|
|
|
|
|
|
|
|
|
|
| 731 |
uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
|
| 732 |
+
# lifespan is required for streamable http
|
| 733 |
+
uvicorn_config["lifespan"] = "on"
|
| 734 |
+
|
| 735 |
+
app = self.http_app(path=path, transport=transport)
|
| 736 |
|
| 737 |
config = uvicorn.Config(
|
| 738 |
app,
|
|
|
|
| 744 |
server = uvicorn.Server(config)
|
| 745 |
await server.serve()
|
| 746 |
|
| 747 |
+
async def run_sse_async(
|
| 748 |
+
self,
|
| 749 |
+
host: str | None = None,
|
| 750 |
+
port: int | None = None,
|
| 751 |
+
log_level: str | None = None,
|
| 752 |
+
path: str | None = None,
|
| 753 |
+
message_path: str | None = None,
|
| 754 |
+
uvicorn_config: dict | None = None,
|
| 755 |
+
) -> None:
|
| 756 |
+
"""Run the server using SSE transport."""
|
| 757 |
+
warnings.warn(
|
| 758 |
+
"""
|
| 759 |
+
The run_sse_async method is deprecated. Use run_http_async for a
|
| 760 |
+
modern (non-SSE) alternative, or create an SSE app with
|
| 761 |
+
`fastmcp.server.http.create_sse_app` and run it directly.
|
| 762 |
+
""",
|
| 763 |
+
DeprecationWarning,
|
| 764 |
+
)
|
| 765 |
+
await self.run_http_async(
|
| 766 |
+
transport="sse",
|
| 767 |
+
host=host,
|
| 768 |
+
port=port,
|
| 769 |
+
log_level=log_level,
|
| 770 |
+
path=path,
|
| 771 |
+
uvicorn_config=uvicorn_config,
|
| 772 |
+
)
|
| 773 |
+
|
| 774 |
def sse_app(
|
| 775 |
self,
|
| 776 |
path: str | None = None,
|
|
|
|
| 785 |
message_path: The path to the message endpoint
|
| 786 |
middleware: A list of middleware to apply to the app
|
| 787 |
"""
|
| 788 |
+
warnings.warn(
|
| 789 |
+
"""The sse_app method is deprecated. Use http_app as a modern (non-SSE)
|
| 790 |
+
alternative, or call `fastmcp.server.http.create_sse_app` directly.""",
|
| 791 |
+
DeprecationWarning,
|
| 792 |
+
)
|
| 793 |
return create_sse_app(
|
| 794 |
server=self,
|
| 795 |
message_path=message_path or self.settings.message_path,
|
|
|
|
| 813 |
path: The path to the StreamableHTTP endpoint
|
| 814 |
middleware: A list of middleware to apply to the app
|
| 815 |
"""
|
| 816 |
+
warnings.warn(
|
| 817 |
+
"The streamable_http_app method is deprecated. Use http_app() instead.",
|
| 818 |
+
DeprecationWarning,
|
| 819 |
+
)
|
| 820 |
+
return self.http_app(path=path, middleware=middleware)
|
| 821 |
+
|
| 822 |
+
def http_app(
|
| 823 |
+
self,
|
| 824 |
+
path: str | None = None,
|
| 825 |
+
middleware: list[Middleware] | None = None,
|
| 826 |
+
transport: Literal["streamable-http", "sse"] = "streamable-http",
|
| 827 |
+
) -> Starlette:
|
| 828 |
from fastmcp.server.http import create_streamable_http_app
|
| 829 |
|
| 830 |
+
if transport == "streamable-http":
|
| 831 |
+
return create_streamable_http_app(
|
| 832 |
+
server=self,
|
| 833 |
+
streamable_http_path=path or self.settings.streamable_http_path,
|
| 834 |
+
event_store=None,
|
| 835 |
+
auth_server_provider=self._auth_server_provider,
|
| 836 |
+
auth_settings=self.settings.auth,
|
| 837 |
+
json_response=self.settings.json_response,
|
| 838 |
+
stateless_http=self.settings.stateless_http,
|
| 839 |
+
debug=self.settings.debug,
|
| 840 |
+
routes=self._additional_http_routes,
|
| 841 |
+
middleware=middleware,
|
| 842 |
+
)
|
| 843 |
+
elif transport == "sse":
|
| 844 |
+
return create_sse_app(
|
| 845 |
+
server=self,
|
| 846 |
+
message_path=path or self.settings.message_path,
|
| 847 |
+
sse_path=path or self.settings.sse_path,
|
| 848 |
+
auth_server_provider=self._auth_server_provider,
|
| 849 |
+
auth_settings=self.settings.auth,
|
| 850 |
+
debug=self.settings.debug,
|
| 851 |
+
routes=self._additional_http_routes,
|
| 852 |
+
middleware=middleware,
|
| 853 |
+
)
|
| 854 |
|
| 855 |
async def run_streamable_http_async(
|
| 856 |
self,
|
|
|
|
| 860 |
path: str | None = None,
|
| 861 |
uvicorn_config: dict | None = None,
|
| 862 |
) -> None:
|
| 863 |
+
warnings.warn(
|
| 864 |
+
"The run_streamable_http_async method is deprecated. Use run_http_async instead.",
|
| 865 |
+
DeprecationWarning,
|
| 866 |
+
)
|
| 867 |
+
await self.run_http_async(
|
| 868 |
+
transport="streamable-http",
|
| 869 |
+
host=host,
|
| 870 |
+
port=port,
|
| 871 |
+
log_level=log_level,
|
| 872 |
+
path=path,
|
| 873 |
+
uvicorn_config=uvicorn_config,
|
|
|
|
|
|
|
|
|
|
| 874 |
)
|
|
|
|
|
|
|
| 875 |
|
| 876 |
def mount(
|
| 877 |
self,
|
src/fastmcp/utilities/tests.py
CHANGED
|
@@ -55,7 +55,7 @@ def temporary_settings(**kwargs: Any):
|
|
| 55 |
def _run_server(mcp_server: FastMCP, transport: Literal["sse"], port: int) -> None:
|
| 56 |
# Some Starlette apps are not pickleable, so we need to create them here based on the indicated transport
|
| 57 |
if transport == "sse":
|
| 58 |
-
app = mcp_server.
|
| 59 |
else:
|
| 60 |
raise ValueError(f"Invalid transport: {transport}")
|
| 61 |
uvicorn_server = uvicorn.Server(
|
|
|
|
| 55 |
def _run_server(mcp_server: FastMCP, transport: Literal["sse"], port: int) -> None:
|
| 56 |
# Some Starlette apps are not pickleable, so we need to create them here based on the indicated transport
|
| 57 |
if transport == "sse":
|
| 58 |
+
app = mcp_server.http_app(transport="sse")
|
| 59 |
else:
|
| 60 |
raise ValueError(f"Invalid transport: {transport}")
|
| 61 |
uvicorn_server = uvicorn.Server(
|
tests/client/test_sse.py
CHANGED
|
@@ -58,7 +58,7 @@ def fastmcp_server():
|
|
| 58 |
|
| 59 |
def run_server(host: str, port: int) -> None:
|
| 60 |
try:
|
| 61 |
-
app = fastmcp_server().
|
| 62 |
server = uvicorn.Server(
|
| 63 |
config=uvicorn.Config(app=app, host=host, port=port, log_level="error")
|
| 64 |
)
|
|
@@ -96,7 +96,7 @@ async def test_http_headers(sse_server: str):
|
|
| 96 |
|
| 97 |
def run_nested_server(host: str, port: int) -> None:
|
| 98 |
try:
|
| 99 |
-
app = fastmcp_server().
|
| 100 |
mount = Starlette(routes=[Mount("/nest-inner", app=app)])
|
| 101 |
mount2 = Starlette(routes=[Mount("/nest-outer", app=mount)])
|
| 102 |
server = uvicorn.Server(
|
|
|
|
| 58 |
|
| 59 |
def run_server(host: str, port: int) -> None:
|
| 60 |
try:
|
| 61 |
+
app = fastmcp_server().http_app(transport="sse")
|
| 62 |
server = uvicorn.Server(
|
| 63 |
config=uvicorn.Config(app=app, host=host, port=port, log_level="error")
|
| 64 |
)
|
|
|
|
| 96 |
|
| 97 |
def run_nested_server(host: str, port: int) -> None:
|
| 98 |
try:
|
| 99 |
+
app = fastmcp_server().http_app(transport="sse")
|
| 100 |
mount = Starlette(routes=[Mount("/nest-inner", app=app)])
|
| 101 |
mount2 = Starlette(routes=[Mount("/nest-outer", app=mount)])
|
| 102 |
server = uvicorn.Server(
|
tests/client/test_streamable_http.py
CHANGED
|
@@ -58,7 +58,7 @@ def fastmcp_server():
|
|
| 58 |
|
| 59 |
def run_server(host: str, port: int) -> None:
|
| 60 |
try:
|
| 61 |
-
app = fastmcp_server().
|
| 62 |
server = uvicorn.Server(
|
| 63 |
config=uvicorn.Config(
|
| 64 |
app=app,
|
|
@@ -106,7 +106,7 @@ async def test_http_headers(streamable_http_server: str):
|
|
| 106 |
|
| 107 |
def run_nested_server(host: str, port: int) -> None:
|
| 108 |
try:
|
| 109 |
-
mcp_app = fastmcp_server().
|
| 110 |
|
| 111 |
mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)])
|
| 112 |
mount2 = Starlette(
|
|
|
|
| 58 |
|
| 59 |
def run_server(host: str, port: int) -> None:
|
| 60 |
try:
|
| 61 |
+
app = fastmcp_server().http_app()
|
| 62 |
server = uvicorn.Server(
|
| 63 |
config=uvicorn.Config(
|
| 64 |
app=app,
|
|
|
|
| 106 |
|
| 107 |
def run_nested_server(host: str, port: int) -> None:
|
| 108 |
try:
|
| 109 |
+
mcp_app = fastmcp_server().http_app()
|
| 110 |
|
| 111 |
mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)])
|
| 112 |
mount2 = Starlette(
|
tests/server/test_http_dependencies.py
CHANGED
|
@@ -43,7 +43,7 @@ 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 |
app=app,
|
|
|
|
| 43 |
|
| 44 |
def run_server(host: str, port: int) -> None:
|
| 45 |
try:
|
| 46 |
+
app = fastmcp_server().http_app()
|
| 47 |
server = uvicorn.Server(
|
| 48 |
config=uvicorn.Config(
|
| 49 |
app=app,
|
tests/server/test_http_middleware.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Tests for
|
| 2 |
|
| 3 |
from collections.abc import Callable
|
| 4 |
from typing import Any
|
|
@@ -68,7 +68,7 @@ async def test_sse_app_with_custom_middleware():
|
|
| 68 |
server._additional_http_routes = routes
|
| 69 |
|
| 70 |
# Create the app with custom middleware
|
| 71 |
-
app = server.
|
| 72 |
|
| 73 |
# Create a test client
|
| 74 |
transport = ASGITransport(app=app)
|
|
@@ -99,7 +99,7 @@ async def test_streamable_http_app_with_custom_middleware():
|
|
| 99 |
server._additional_http_routes = routes
|
| 100 |
|
| 101 |
# Create the app with custom middleware
|
| 102 |
-
app = server.
|
| 103 |
|
| 104 |
# Create a test client
|
| 105 |
transport = ASGITransport(app=app)
|
|
@@ -204,7 +204,7 @@ async def test_multiple_middleware_ordering():
|
|
| 204 |
server._additional_http_routes = routes
|
| 205 |
|
| 206 |
# Create the app with custom middleware
|
| 207 |
-
app = server.
|
| 208 |
|
| 209 |
# Create a test client
|
| 210 |
transport = ASGITransport(app=app)
|
|
|
|
| 1 |
+
"""Tests for middleware in HTTP apps."""
|
| 2 |
|
| 3 |
from collections.abc import Callable
|
| 4 |
from typing import Any
|
|
|
|
| 68 |
server._additional_http_routes = routes
|
| 69 |
|
| 70 |
# Create the app with custom middleware
|
| 71 |
+
app = server.http_app(transport="sse", middleware=custom_middleware)
|
| 72 |
|
| 73 |
# Create a test client
|
| 74 |
transport = ASGITransport(app=app)
|
|
|
|
| 99 |
server._additional_http_routes = routes
|
| 100 |
|
| 101 |
# Create the app with custom middleware
|
| 102 |
+
app = server.http_app(transport="streamable-http", middleware=custom_middleware)
|
| 103 |
|
| 104 |
# Create a test client
|
| 105 |
transport = ASGITransport(app=app)
|
|
|
|
| 204 |
server._additional_http_routes = routes
|
| 205 |
|
| 206 |
# Create the app with custom middleware
|
| 207 |
+
app = server.http_app(transport="sse", middleware=custom_middleware)
|
| 208 |
|
| 209 |
# Create a test client
|
| 210 |
transport = ASGITransport(app=app)
|
tests/test_deprecated.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for deprecated functionality."""
|
| 2 |
+
|
| 3 |
+
import warnings
|
| 4 |
+
from unittest.mock import AsyncMock, patch
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
from starlette.applications import Starlette
|
| 8 |
+
|
| 9 |
+
from fastmcp import FastMCP
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def test_sse_app_deprecation_warning():
|
| 13 |
+
"""Test that sse_app raises a deprecation warning."""
|
| 14 |
+
server = FastMCP("TestServer")
|
| 15 |
+
|
| 16 |
+
with pytest.warns(DeprecationWarning, match="The sse_app method is deprecated"):
|
| 17 |
+
app = server.sse_app()
|
| 18 |
+
assert isinstance(app, Starlette)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_streamable_http_app_deprecation_warning():
|
| 22 |
+
"""Test that streamable_http_app raises a deprecation warning."""
|
| 23 |
+
server = FastMCP("TestServer")
|
| 24 |
+
|
| 25 |
+
with pytest.warns(
|
| 26 |
+
DeprecationWarning, match="The streamable_http_app method is deprecated"
|
| 27 |
+
):
|
| 28 |
+
app = server.streamable_http_app()
|
| 29 |
+
assert isinstance(app, Starlette)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@pytest.mark.asyncio
|
| 33 |
+
async def test_run_sse_async_deprecation_warning():
|
| 34 |
+
"""Test that run_sse_async raises a deprecation warning."""
|
| 35 |
+
server = FastMCP("TestServer")
|
| 36 |
+
|
| 37 |
+
# Use patch to avoid actually running the server
|
| 38 |
+
with patch.object(server, "run_http_async", new_callable=AsyncMock) as mock_run:
|
| 39 |
+
with pytest.warns(
|
| 40 |
+
DeprecationWarning, match="The run_sse_async method is deprecated"
|
| 41 |
+
):
|
| 42 |
+
await server.run_sse_async()
|
| 43 |
+
|
| 44 |
+
# Verify the mock was called with the right transport
|
| 45 |
+
mock_run.assert_called_once()
|
| 46 |
+
call_kwargs = mock_run.call_args.kwargs
|
| 47 |
+
assert call_kwargs.get("transport") == "sse"
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@pytest.mark.asyncio
|
| 51 |
+
async def test_run_streamable_http_async_deprecation_warning():
|
| 52 |
+
"""Test that run_streamable_http_async raises a deprecation warning."""
|
| 53 |
+
server = FastMCP("TestServer")
|
| 54 |
+
|
| 55 |
+
# Use patch to avoid actually running the server
|
| 56 |
+
with patch.object(server, "run_http_async", new_callable=AsyncMock) as mock_run:
|
| 57 |
+
with pytest.warns(
|
| 58 |
+
DeprecationWarning,
|
| 59 |
+
match="The run_streamable_http_async method is deprecated",
|
| 60 |
+
):
|
| 61 |
+
await server.run_streamable_http_async()
|
| 62 |
+
|
| 63 |
+
# Verify the mock was called with the right transport
|
| 64 |
+
mock_run.assert_called_once()
|
| 65 |
+
call_kwargs = mock_run.call_args.kwargs
|
| 66 |
+
assert call_kwargs.get("transport") == "streamable-http"
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def test_http_app_with_sse_transport():
|
| 70 |
+
"""Test that http_app with SSE transport works (no warning)."""
|
| 71 |
+
server = FastMCP("TestServer")
|
| 72 |
+
|
| 73 |
+
# This should not raise a warning since we're using the new API
|
| 74 |
+
with warnings.catch_warnings(record=True) as recorded_warnings:
|
| 75 |
+
app = server.http_app(transport="sse")
|
| 76 |
+
assert isinstance(app, Starlette)
|
| 77 |
+
|
| 78 |
+
# Verify no deprecation warnings were raised for using transport parameter
|
| 79 |
+
deprecation_warnings = [
|
| 80 |
+
w for w in recorded_warnings if issubclass(w.category, DeprecationWarning)
|
| 81 |
+
]
|
| 82 |
+
assert len(deprecation_warnings) == 0
|