Spaces:
Running
Running
Merge pull request #917 from jlowin/http
Browse files- CLAUDE.md +2 -1
- README.md +1 -1
- docs/clients/client.mdx +1 -1
- docs/clients/transports.mdx +4 -4
- docs/deployment/running-server.mdx +8 -5
- docs/getting-started/installation.mdx +3 -2
- docs/integrations/anthropic.mdx +2 -2
- docs/integrations/chatgpt.mdx +1 -1
- docs/integrations/claude-code.mdx +1 -1
- docs/integrations/openai.mdx +2 -2
- docs/patterns/cli.mdx +9 -8
- docs/python-sdk/fastmcp-client-transports.mdx +2 -2
- docs/servers/auth/bearer.mdx +1 -1
- docs/servers/proxy.mdx +3 -3
- docs/servers/server.mdx +4 -4
- docs/tutorials/rest-api.mdx +2 -2
- docs/updates.mdx +1 -1
- src/fastmcp/cli/cli.py +1 -1
- src/fastmcp/cli/run.py +1 -3
- src/fastmcp/client/transports.py +2 -2
- src/fastmcp/server/server.py +9 -8
- src/fastmcp/utilities/mcp_config.py +4 -3
- tests/auth/providers/test_bearer.py +3 -3
- tests/auth/test_oauth_client.py +1 -1
- tests/cli/test_cli.py +35 -0
- tests/client/test_openapi.py +2 -2
- tests/client/test_streamable_http.py +25 -1
- tests/deprecated/test_deprecated.py +1 -1
- tests/server/http/test_http_dependencies.py +1 -1
- tests/server/http/test_http_middleware.py +1 -1
CLAUDE.md
CHANGED
|
@@ -32,4 +32,5 @@ async with Client(transport=StreamableHttpTransport(server_url)) as client:
|
|
| 32 |
## Development Workflow
|
| 33 |
|
| 34 |
- You must always run pre-commit if you open a PR, because it is run as part of a required check.
|
| 35 |
-
- When opening PRs, apply labels appropriately for bugs/breaking changes/enhancements/features. Generally, improvements are enhancements (not features) unless told otherwise.
|
|
|
|
|
|
| 32 |
## Development Workflow
|
| 33 |
|
| 34 |
- You must always run pre-commit if you open a PR, because it is run as part of a required check.
|
| 35 |
+
- When opening PRs, apply labels appropriately for bugs/breaking changes/enhancements/features. Generally, improvements are enhancements (not features) unless told otherwise.
|
| 36 |
+
- NEVER modify files in docs/python-sdk/**, as they are auto-generated.
|
README.md
CHANGED
|
@@ -349,7 +349,7 @@ mcp.run(transport="stdio") # Default, so transport argument is optional
|
|
| 349 |
**Streamable HTTP**: Recommended for web deployments.
|
| 350 |
|
| 351 |
```python
|
| 352 |
-
mcp.run(transport="
|
| 353 |
```
|
| 354 |
|
| 355 |
**SSE**: For compatibility with existing SSE clients.
|
|
|
|
| 349 |
**Streamable HTTP**: Recommended for web deployments.
|
| 350 |
|
| 351 |
```python
|
| 352 |
+
mcp.run(transport="http", host="127.0.0.1", port=8000, path="/mcp")
|
| 353 |
```
|
| 354 |
|
| 355 |
**SSE**: For compatibility with existing SSE clients.
|
docs/clients/client.mdx
CHANGED
|
@@ -102,7 +102,7 @@ config = {
|
|
| 102 |
"mcpServers": {
|
| 103 |
"server_name": {
|
| 104 |
# Remote HTTP/SSE server
|
| 105 |
-
"transport": "
|
| 106 |
"url": "https://api.example.com/mcp",
|
| 107 |
"headers": {"Authorization": "Bearer token"},
|
| 108 |
"auth": "oauth" # or bearer token string
|
|
|
|
| 102 |
"mcpServers": {
|
| 103 |
"server_name": {
|
| 104 |
# Remote HTTP/SSE server
|
| 105 |
+
"transport": "http", # or "sse"
|
| 106 |
"url": "https://api.example.com/mcp",
|
| 107 |
"headers": {"Authorization": "Bearer token"},
|
| 108 |
"auth": "oauth" # or bearer token string
|
docs/clients/transports.mdx
CHANGED
|
@@ -41,7 +41,7 @@ Streamable HTTP is the recommended transport for web-based deployments, providin
|
|
| 41 |
|
| 42 |
- **Class:** `fastmcp.client.transports.StreamableHttpTransport`
|
| 43 |
- **Inferred From:** URLs starting with `http://` or `https://` (default for HTTP URLs since v2.3.0) that do not contain `/sse/` in the path
|
| 44 |
-
- **Server Compatibility:** Works with FastMCP servers running in `
|
| 45 |
|
| 46 |
#### Basic Usage
|
| 47 |
|
|
@@ -150,7 +150,7 @@ client = Client(transport)
|
|
| 150 |
- **Use Streamable HTTP when:**
|
| 151 |
- Setting up new deployments (recommended default)
|
| 152 |
- You need bidirectional streaming
|
| 153 |
-
- You're connecting to FastMCP servers running in `
|
| 154 |
|
| 155 |
- **Use SSE when:**
|
| 156 |
- Connecting to legacy FastMCP servers running in `sse` mode
|
|
@@ -397,7 +397,7 @@ config = {
|
|
| 397 |
# Remote HTTP server
|
| 398 |
"weather": {
|
| 399 |
"url": "https://weather-api.example.com/mcp",
|
| 400 |
-
"transport": "
|
| 401 |
},
|
| 402 |
# Local stdio server
|
| 403 |
"assistant": {
|
|
@@ -408,7 +408,7 @@ config = {
|
|
| 408 |
# Another remote server
|
| 409 |
"calendar": {
|
| 410 |
"url": "https://calendar-api.example.com/mcp",
|
| 411 |
-
"transport": "
|
| 412 |
}
|
| 413 |
}
|
| 414 |
}
|
|
|
|
| 41 |
|
| 42 |
- **Class:** `fastmcp.client.transports.StreamableHttpTransport`
|
| 43 |
- **Inferred From:** URLs starting with `http://` or `https://` (default for HTTP URLs since v2.3.0) that do not contain `/sse/` in the path
|
| 44 |
+
- **Server Compatibility:** Works with FastMCP servers running in `http` mode
|
| 45 |
|
| 46 |
#### Basic Usage
|
| 47 |
|
|
|
|
| 150 |
- **Use Streamable HTTP when:**
|
| 151 |
- Setting up new deployments (recommended default)
|
| 152 |
- You need bidirectional streaming
|
| 153 |
+
- You're connecting to FastMCP servers running in `http` mode
|
| 154 |
|
| 155 |
- **Use SSE when:**
|
| 156 |
- Connecting to legacy FastMCP servers running in `sse` mode
|
|
|
|
| 397 |
# Remote HTTP server
|
| 398 |
"weather": {
|
| 399 |
"url": "https://weather-api.example.com/mcp",
|
| 400 |
+
"transport": "http"
|
| 401 |
},
|
| 402 |
# Local stdio server
|
| 403 |
"assistant": {
|
|
|
|
| 408 |
# Another remote server
|
| 409 |
"calendar": {
|
| 410 |
"url": "https://calendar-api.example.com/mcp",
|
| 411 |
+
"transport": "http"
|
| 412 |
}
|
| 413 |
}
|
| 414 |
}
|
docs/deployment/running-server.mdx
CHANGED
|
@@ -105,7 +105,7 @@ When using Stdio transport, you will typically *not* run the server yourself as
|
|
| 105 |
|
| 106 |
Streamable HTTP is a modern, efficient transport for exposing your MCP server via HTTP. It is the recommended transport for web-based deployments.
|
| 107 |
|
| 108 |
-
To run a server using Streamable HTTP, you can use the `run()` method with the `transport` argument set to `"
|
| 109 |
<CodeGroup>
|
| 110 |
```python {6} server.py
|
| 111 |
from fastmcp import FastMCP
|
|
@@ -113,7 +113,7 @@ from fastmcp import FastMCP
|
|
| 113 |
mcp = FastMCP()
|
| 114 |
|
| 115 |
if __name__ == "__main__":
|
| 116 |
-
mcp.run(transport="
|
| 117 |
```
|
| 118 |
```python {5} client.py
|
| 119 |
import asyncio
|
|
@@ -128,6 +128,10 @@ if __name__ == "__main__":
|
|
| 128 |
```
|
| 129 |
</CodeGroup>
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
To customize the host, port, path, or log level, provide appropriate keyword arguments to the `run()` method.
|
| 132 |
|
| 133 |
<CodeGroup>
|
|
@@ -138,7 +142,7 @@ mcp = FastMCP()
|
|
| 138 |
|
| 139 |
if __name__ == "__main__":
|
| 140 |
mcp.run(
|
| 141 |
-
transport="
|
| 142 |
host="127.0.0.1",
|
| 143 |
port=4200,
|
| 144 |
path="/my-custom-path",
|
|
@@ -158,7 +162,6 @@ if __name__ == "__main__":
|
|
| 158 |
```
|
| 159 |
</CodeGroup>
|
| 160 |
|
| 161 |
-
|
| 162 |
### SSE
|
| 163 |
|
| 164 |
<Warning>
|
|
@@ -250,7 +253,7 @@ def hello(name: str) -> str:
|
|
| 250 |
|
| 251 |
async def main():
|
| 252 |
# Use run_async() in async contexts
|
| 253 |
-
await mcp.run_async(transport="
|
| 254 |
|
| 255 |
if __name__ == "__main__":
|
| 256 |
asyncio.run(main())
|
|
|
|
| 105 |
|
| 106 |
Streamable HTTP is a modern, efficient transport for exposing your MCP server via HTTP. It is the recommended transport for web-based deployments.
|
| 107 |
|
| 108 |
+
To run a server using Streamable HTTP, you can use the `run()` method with the `transport` argument set to `"http"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and path (`/mcp/`).
|
| 109 |
<CodeGroup>
|
| 110 |
```python {6} server.py
|
| 111 |
from fastmcp import FastMCP
|
|
|
|
| 113 |
mcp = FastMCP()
|
| 114 |
|
| 115 |
if __name__ == "__main__":
|
| 116 |
+
mcp.run(transport="http")
|
| 117 |
```
|
| 118 |
```python {5} client.py
|
| 119 |
import asyncio
|
|
|
|
| 128 |
```
|
| 129 |
</CodeGroup>
|
| 130 |
|
| 131 |
+
<Tip>
|
| 132 |
+
For backward compatibility, wherever `"http"` is accepted as a transport name, you can also pass `"streamable-http"` as a fully supported alias. This is particularly useful when upgrading from FastMCP 1.x in the official Python SDK and FastMCP \<= 2.9, where `"streamable-http"` was the standard name.
|
| 133 |
+
</Tip>
|
| 134 |
+
|
| 135 |
To customize the host, port, path, or log level, provide appropriate keyword arguments to the `run()` method.
|
| 136 |
|
| 137 |
<CodeGroup>
|
|
|
|
| 142 |
|
| 143 |
if __name__ == "__main__":
|
| 144 |
mcp.run(
|
| 145 |
+
transport="http",
|
| 146 |
host="127.0.0.1",
|
| 147 |
port=4200,
|
| 148 |
path="/my-custom-path",
|
|
|
|
| 162 |
```
|
| 163 |
</CodeGroup>
|
| 164 |
|
|
|
|
| 165 |
### SSE
|
| 166 |
|
| 167 |
<Warning>
|
|
|
|
| 253 |
|
| 254 |
async def main():
|
| 255 |
# Use run_async() in async contexts
|
| 256 |
+
await mcp.run_async(transport="http")
|
| 257 |
|
| 258 |
if __name__ == "__main__":
|
| 259 |
asyncio.run(main())
|
docs/getting-started/installation.mdx
CHANGED
|
@@ -47,7 +47,7 @@ FastMCP root path: ~/Developer/fastmcp
|
|
| 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 {
|
| 51 |
# Before
|
| 52 |
# from mcp.server.fastmcp import FastMCP
|
| 53 |
|
|
@@ -56,8 +56,9 @@ from fastmcp import FastMCP
|
|
| 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 |
## Versioning and Breaking Changes
|
|
|
|
| 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 {5}
|
| 51 |
# Before
|
| 52 |
# from mcp.server.fastmcp import FastMCP
|
| 53 |
|
|
|
|
| 56 |
|
| 57 |
mcp = FastMCP("My MCP Server")
|
| 58 |
```
|
| 59 |
+
|
| 60 |
<Warning>
|
| 61 |
+
Prior to `fastmcp==2.3.0` and `mcp==1.8.0`, the 2.x API always mirrored the official 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.
|
| 62 |
</Warning>
|
| 63 |
|
| 64 |
## Versioning and Breaking Changes
|
docs/integrations/anthropic.mdx
CHANGED
|
@@ -31,7 +31,7 @@ def roll_dice(n_dice: int) -> list[int]:
|
|
| 31 |
return [random.randint(1, 6) for _ in range(n_dice)]
|
| 32 |
|
| 33 |
if __name__ == "__main__":
|
| 34 |
-
mcp.run(transport="
|
| 35 |
```
|
| 36 |
|
| 37 |
## Deploy the Server
|
|
@@ -175,7 +175,7 @@ def roll_dice(n_dice: int) -> list[int]:
|
|
| 175 |
|
| 176 |
if __name__ == "__main__":
|
| 177 |
print(f"\n---\n\n🔑 Dice Roller access token:\n\n{access_token}\n\n---\n")
|
| 178 |
-
mcp.run(transport="
|
| 179 |
```
|
| 180 |
|
| 181 |
### Client Authentication
|
|
|
|
| 31 |
return [random.randint(1, 6) for _ in range(n_dice)]
|
| 32 |
|
| 33 |
if __name__ == "__main__":
|
| 34 |
+
mcp.run(transport="http", port=8000)
|
| 35 |
```
|
| 36 |
|
| 37 |
## Deploy the Server
|
|
|
|
| 175 |
|
| 176 |
if __name__ == "__main__":
|
| 177 |
print(f"\n---\n\n🔑 Dice Roller access token:\n\n{access_token}\n\n---\n")
|
| 178 |
+
mcp.run(transport="http", port=8000)
|
| 179 |
```
|
| 180 |
|
| 181 |
### Client Authentication
|
docs/integrations/chatgpt.mdx
CHANGED
|
@@ -102,7 +102,7 @@ def create_server(
|
|
| 102 |
|
| 103 |
if __name__ == "__main__":
|
| 104 |
mcp = create_server("path/to/records.json")
|
| 105 |
-
mcp.run(transport="
|
| 106 |
```
|
| 107 |
|
| 108 |
### Deploy the Server
|
|
|
|
| 102 |
|
| 103 |
if __name__ == "__main__":
|
| 104 |
mcp = create_server("path/to/records.json")
|
| 105 |
+
mcp.run(transport="http", port=8000)
|
| 106 |
```
|
| 107 |
|
| 108 |
### Deploy the Server
|
docs/integrations/claude-code.mdx
CHANGED
|
@@ -32,7 +32,7 @@ def roll_dice(n_dice: int) -> list[int]:
|
|
| 32 |
return [random.randint(1, 6) for _ in range(n_dice)]
|
| 33 |
|
| 34 |
if __name__ == "__main__":
|
| 35 |
-
mcp.run(transport="
|
| 36 |
```
|
| 37 |
|
| 38 |
## Connect to Claude Code
|
|
|
|
| 32 |
return [random.randint(1, 6) for _ in range(n_dice)]
|
| 33 |
|
| 34 |
if __name__ == "__main__":
|
| 35 |
+
mcp.run(transport="http", port=8000)
|
| 36 |
```
|
| 37 |
|
| 38 |
## Connect to Claude Code
|
docs/integrations/openai.mdx
CHANGED
|
@@ -38,7 +38,7 @@ def roll_dice(n_dice: int) -> list[int]:
|
|
| 38 |
return [random.randint(1, 6) for _ in range(n_dice)]
|
| 39 |
|
| 40 |
if __name__ == "__main__":
|
| 41 |
-
mcp.run(transport="
|
| 42 |
```
|
| 43 |
|
| 44 |
### Deploy the Server
|
|
@@ -172,7 +172,7 @@ def roll_dice(n_dice: int) -> list[int]:
|
|
| 172 |
|
| 173 |
if __name__ == "__main__":
|
| 174 |
print(f"\n---\n\n🔑 Dice Roller access token:\n\n{access_token}\n\n---\n")
|
| 175 |
-
mcp.run(transport="
|
| 176 |
```
|
| 177 |
|
| 178 |
#### Client Authentication
|
|
|
|
| 38 |
return [random.randint(1, 6) for _ in range(n_dice)]
|
| 39 |
|
| 40 |
if __name__ == "__main__":
|
| 41 |
+
mcp.run(transport="http", port=8000)
|
| 42 |
```
|
| 43 |
|
| 44 |
### Deploy the Server
|
|
|
|
| 172 |
|
| 173 |
if __name__ == "__main__":
|
| 174 |
print(f"\n---\n\n🔑 Dice Roller access token:\n\n{access_token}\n\n---\n")
|
| 175 |
+
mcp.run(transport="http", port=8000)
|
| 176 |
```
|
| 177 |
|
| 178 |
#### Client Authentication
|
docs/patterns/cli.mdx
CHANGED
|
@@ -42,11 +42,12 @@ This command runs the server directly in your current Python environment. You ar
|
|
| 42 |
|
| 43 |
| Option | Flag | Description |
|
| 44 |
| ------ | ---- | ----------- |
|
| 45 |
-
| Transport | `--transport`, `-t` | Transport protocol to use (`stdio`, `
|
| 46 |
| Host | `--host` | Host to bind to when using http transport (default: 127.0.0.1) |
|
| 47 |
| Port | `--port`, `-p` | Port to bind to when using http transport (default: 8000) |
|
| 48 |
| Log Level | `--log-level`, `-l` | Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
|
| 49 |
|
|
|
|
| 50 |
#### Server Specification
|
| 51 |
<VersionBadge version="2.3.5" />
|
| 52 |
|
|
@@ -79,14 +80,14 @@ if __name__ == "__main__":
|
|
| 79 |
You can run it with Streamable HTTP transport regardless of what's in the `__main__` block:
|
| 80 |
|
| 81 |
```bash
|
| 82 |
-
fastmcp run server.py --transport
|
| 83 |
```
|
| 84 |
|
| 85 |
**Examples**
|
| 86 |
|
| 87 |
```bash
|
| 88 |
# Run a local server with Streamable HTTP transport on a custom port
|
| 89 |
-
fastmcp run server.py --transport
|
| 90 |
|
| 91 |
# Connect to a remote server and proxy as a stdio server
|
| 92 |
fastmcp run https://example.com/mcp-server
|
|
@@ -112,14 +113,14 @@ The `dev` command is a shortcut for testing a server over STDIO only. When the I
|
|
| 112 |
1. Select "STDIO" from the transport dropdown
|
| 113 |
2. Connect manually
|
| 114 |
|
| 115 |
-
This command does not support HTTP testing. To test a server over HTTP:
|
| 116 |
-
1. Start your server manually with
|
| 117 |
```bash
|
| 118 |
-
fastmcp run server.py --transport
|
| 119 |
```
|
| 120 |
-
or
|
| 121 |
```bash
|
| 122 |
-
python server.py # Assuming your __main__ block sets HTTP transport
|
| 123 |
```
|
| 124 |
2. Open the MCP Inspector separately and connect to your running server
|
| 125 |
</Warning>
|
|
|
|
| 42 |
|
| 43 |
| Option | Flag | Description |
|
| 44 |
| ------ | ---- | ----------- |
|
| 45 |
+
| Transport | `--transport`, `-t` | Transport protocol to use (`stdio`, `http`, or `sse`) |
|
| 46 |
| Host | `--host` | Host to bind to when using http transport (default: 127.0.0.1) |
|
| 47 |
| Port | `--port`, `-p` | Port to bind to when using http transport (default: 8000) |
|
| 48 |
| Log Level | `--log-level`, `-l` | Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
|
| 49 |
|
| 50 |
+
|
| 51 |
#### Server Specification
|
| 52 |
<VersionBadge version="2.3.5" />
|
| 53 |
|
|
|
|
| 80 |
You can run it with Streamable HTTP transport regardless of what's in the `__main__` block:
|
| 81 |
|
| 82 |
```bash
|
| 83 |
+
fastmcp run server.py --transport http --port 8000
|
| 84 |
```
|
| 85 |
|
| 86 |
**Examples**
|
| 87 |
|
| 88 |
```bash
|
| 89 |
# Run a local server with Streamable HTTP transport on a custom port
|
| 90 |
+
fastmcp run server.py --transport http --port 8000
|
| 91 |
|
| 92 |
# Connect to a remote server and proxy as a stdio server
|
| 93 |
fastmcp run https://example.com/mcp-server
|
|
|
|
| 113 |
1. Select "STDIO" from the transport dropdown
|
| 114 |
2. Connect manually
|
| 115 |
|
| 116 |
+
This command does not support HTTP testing. To test a server over Streamable HTTP or SSE:
|
| 117 |
+
1. Start your server manually with the appropriate transport using either the command line:
|
| 118 |
```bash
|
| 119 |
+
fastmcp run server.py --transport http
|
| 120 |
```
|
| 121 |
+
or by setting the transport in your code:
|
| 122 |
```bash
|
| 123 |
+
python server.py # Assuming your __main__ block sets Streamable HTTP transport
|
| 124 |
```
|
| 125 |
2. Open the MCP Inspector separately and connect to your running server
|
| 126 |
</Warning>
|
docs/python-sdk/fastmcp-client-transports.mdx
CHANGED
|
@@ -168,11 +168,11 @@ Transport for connecting to one or more MCP servers defined in an MCPConfig.
|
|
| 168 |
"mcpServers": {
|
| 169 |
"weather": {
|
| 170 |
"url": "https://weather-api.example.com/mcp",
|
| 171 |
-
"transport": "
|
| 172 |
},
|
| 173 |
"calendar": {
|
| 174 |
"url": "https://calendar-api.example.com/mcp",
|
| 175 |
-
"transport": "
|
| 176 |
}
|
| 177 |
}
|
| 178 |
}
|
|
|
|
| 168 |
"mcpServers": {
|
| 169 |
"weather": {
|
| 170 |
"url": "https://weather-api.example.com/mcp",
|
| 171 |
+
"transport": "http"
|
| 172 |
},
|
| 173 |
"calendar": {
|
| 174 |
"url": "https://calendar-api.example.com/mcp",
|
| 175 |
+
"transport": "http"
|
| 176 |
}
|
| 177 |
}
|
| 178 |
}
|
docs/servers/auth/bearer.mdx
CHANGED
|
@@ -19,7 +19,7 @@ The [MCP specification](https://modelcontextprotocol.io/specification/2025-03-26
|
|
| 19 |
|
| 20 |
Bearer Token authentication is a common way to secure HTTP-based APIs. In this model, the client sends a token (usually a JSON Web Token or JWT) in the `Authorization` header with the "Bearer" scheme. The server then validates this token to grant or deny access.
|
| 21 |
|
| 22 |
-
FastMCP supports Bearer Token authentication for its HTTP-based transports (`
|
| 23 |
|
| 24 |
## Authentication Strategy
|
| 25 |
|
|
|
|
| 19 |
|
| 20 |
Bearer Token authentication is a common way to secure HTTP-based APIs. In this model, the client sends a token (usually a JSON Web Token or JWT) in the `Authorization` header with the "Bearer" scheme. The server then validates this token to grant or deny access.
|
| 21 |
|
| 22 |
+
FastMCP supports Bearer Token authentication for its HTTP-based transports (`http` and `sse`), allowing you to protect your server from unauthorized access.
|
| 23 |
|
| 24 |
## Authentication Strategy
|
| 25 |
|
docs/servers/proxy.mdx
CHANGED
|
@@ -118,7 +118,7 @@ config = {
|
|
| 118 |
"mcpServers": {
|
| 119 |
"default": { # For single server configs, 'default' is commonly used
|
| 120 |
"url": "https://example.com/mcp",
|
| 121 |
-
"transport": "
|
| 122 |
}
|
| 123 |
}
|
| 124 |
}
|
|
@@ -145,11 +145,11 @@ config = {
|
|
| 145 |
"mcpServers": {
|
| 146 |
"weather": {
|
| 147 |
"url": "https://weather-api.example.com/mcp",
|
| 148 |
-
"transport": "
|
| 149 |
},
|
| 150 |
"calendar": {
|
| 151 |
"url": "https://calendar-api.example.com/mcp",
|
| 152 |
-
"transport": "
|
| 153 |
}
|
| 154 |
}
|
| 155 |
}
|
|
|
|
| 118 |
"mcpServers": {
|
| 119 |
"default": { # For single server configs, 'default' is commonly used
|
| 120 |
"url": "https://example.com/mcp",
|
| 121 |
+
"transport": "http"
|
| 122 |
}
|
| 123 |
}
|
| 124 |
}
|
|
|
|
| 145 |
"mcpServers": {
|
| 146 |
"weather": {
|
| 147 |
"url": "https://weather-api.example.com/mcp",
|
| 148 |
+
"transport": "http"
|
| 149 |
},
|
| 150 |
"calendar": {
|
| 151 |
"url": "https://calendar-api.example.com/mcp",
|
| 152 |
+
"transport": "http"
|
| 153 |
}
|
| 154 |
}
|
| 155 |
}
|
docs/servers/server.mdx
CHANGED
|
@@ -158,8 +158,8 @@ if __name__ == "__main__":
|
|
| 158 |
# This runs the server, defaulting to STDIO transport
|
| 159 |
mcp.run()
|
| 160 |
|
| 161 |
-
# To use a different transport, e.g., HTTP:
|
| 162 |
-
# mcp.run(transport="
|
| 163 |
```
|
| 164 |
|
| 165 |
FastMCP supports several transport options:
|
|
@@ -260,7 +260,7 @@ Transport settings are provided when running the server and control network beha
|
|
| 260 |
```python
|
| 261 |
# Configure transport when running
|
| 262 |
mcp.run(
|
| 263 |
-
transport="
|
| 264 |
host="0.0.0.0", # Bind to all interfaces
|
| 265 |
port=9000, # Custom port
|
| 266 |
log_level="DEBUG", # Override global log level
|
|
@@ -268,7 +268,7 @@ mcp.run(
|
|
| 268 |
|
| 269 |
# Or for async usage
|
| 270 |
await mcp.run_async(
|
| 271 |
-
transport="
|
| 272 |
host="127.0.0.1",
|
| 273 |
port=8080,
|
| 274 |
)
|
|
|
|
| 158 |
# This runs the server, defaulting to STDIO transport
|
| 159 |
mcp.run()
|
| 160 |
|
| 161 |
+
# To use a different transport, e.g., Streamable HTTP:
|
| 162 |
+
# mcp.run(transport="http", host="127.0.0.1", port=9000)
|
| 163 |
```
|
| 164 |
|
| 165 |
FastMCP supports several transport options:
|
|
|
|
| 260 |
```python
|
| 261 |
# Configure transport when running
|
| 262 |
mcp.run(
|
| 263 |
+
transport="http",
|
| 264 |
host="0.0.0.0", # Bind to all interfaces
|
| 265 |
port=9000, # Custom port
|
| 266 |
log_level="DEBUG", # Override global log level
|
|
|
|
| 268 |
|
| 269 |
# Or for async usage
|
| 270 |
await mcp.run_async(
|
| 271 |
+
transport="http",
|
| 272 |
host="127.0.0.1",
|
| 273 |
port=8080,
|
| 274 |
)
|
docs/tutorials/rest-api.mdx
CHANGED
|
@@ -82,7 +82,7 @@ mcp = FastMCP.from_openapi(
|
|
| 82 |
)
|
| 83 |
|
| 84 |
if __name__ == "__main__":
|
| 85 |
-
mcp.run(transport="
|
| 86 |
```
|
| 87 |
|
| 88 |
And that's it! With just a few lines of code, you've created an MCP server that exposes the entire JSONPlaceholder API as a collection of tools.
|
|
@@ -195,7 +195,7 @@ mcp = FastMCP.from_openapi(
|
|
| 195 |
)
|
| 196 |
|
| 197 |
if __name__ == "__main__":
|
| 198 |
-
mcp.run(transport="
|
| 199 |
```
|
| 200 |
With this configuration:
|
| 201 |
- `GET /users/{id}` becomes a `ResourceTemplate`.
|
|
|
|
| 82 |
)
|
| 83 |
|
| 84 |
if __name__ == "__main__":
|
| 85 |
+
mcp.run(transport="http", port=8000)
|
| 86 |
```
|
| 87 |
|
| 88 |
And that's it! With just a few lines of code, you've created an MCP server that exposes the entire JSONPlaceholder API as a collection of tools.
|
|
|
|
| 195 |
)
|
| 196 |
|
| 197 |
if __name__ == "__main__":
|
| 198 |
+
mcp.run(transport="http", port=8000)
|
| 199 |
```
|
| 200 |
With this configuration:
|
| 201 |
- `GET /users/{id}` becomes a `ResourceTemplate`.
|
docs/updates.mdx
CHANGED
|
@@ -107,7 +107,7 @@ img="https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.M_hv6gEB.png&w=1000&h=5
|
|
| 107 |
cta="Read more"
|
| 108 |
>
|
| 109 |
|
| 110 |
-
FastMCP 2.3 introduces full support for Streamable HTTP, a modern alternative to SSE that simplifies MCP deployments over the web. It’s efficient, reliable, and now the default HTTP transport. Just run your server with transport="
|
| 111 |
|
| 112 |
</Card>
|
| 113 |
</Update>
|
|
|
|
| 107 |
cta="Read more"
|
| 108 |
>
|
| 109 |
|
| 110 |
+
FastMCP 2.3 introduces full support for Streamable HTTP, a modern alternative to SSE that simplifies MCP deployments over the web. It’s efficient, reliable, and now the default HTTP transport. Just run your server with transport="http" and connect clients via a standard URL—FastMCP handles the rest. No special setup required. This release makes deploying MCP servers easier and more portable than ever.
|
| 111 |
|
| 112 |
</Card>
|
| 113 |
</Update>
|
src/fastmcp/cli/cli.py
CHANGED
|
@@ -235,7 +235,7 @@ def run(
|
|
| 235 |
typer.Option(
|
| 236 |
"--transport",
|
| 237 |
"-t",
|
| 238 |
-
help="Transport protocol to use (stdio,
|
| 239 |
),
|
| 240 |
] = None,
|
| 241 |
host: Annotated[
|
|
|
|
| 235 |
typer.Option(
|
| 236 |
"--transport",
|
| 237 |
"-t",
|
| 238 |
+
help="Transport protocol to use (stdio, http, or sse)",
|
| 239 |
),
|
| 240 |
] = None,
|
| 241 |
host: Annotated[
|
src/fastmcp/cli/run.py
CHANGED
|
@@ -4,14 +4,12 @@ import importlib.util
|
|
| 4 |
import re
|
| 5 |
import sys
|
| 6 |
from pathlib import Path
|
| 7 |
-
from typing import Any
|
| 8 |
|
| 9 |
from fastmcp.utilities.logging import get_logger
|
| 10 |
|
| 11 |
logger = get_logger("cli.run")
|
| 12 |
|
| 13 |
-
TransportType = Literal["stdio", "streamable-http", "sse"]
|
| 14 |
-
|
| 15 |
|
| 16 |
def is_url(path: str) -> bool:
|
| 17 |
"""Check if a string is a URL."""
|
|
|
|
| 4 |
import re
|
| 5 |
import sys
|
| 6 |
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
|
| 9 |
from fastmcp.utilities.logging import get_logger
|
| 10 |
|
| 11 |
logger = get_logger("cli.run")
|
| 12 |
|
|
|
|
|
|
|
| 13 |
|
| 14 |
def is_url(path: str) -> bool:
|
| 15 |
"""Check if a string is a URL."""
|
src/fastmcp/client/transports.py
CHANGED
|
@@ -736,11 +736,11 @@ class MCPConfigTransport(ClientTransport):
|
|
| 736 |
"mcpServers": {
|
| 737 |
"weather": {
|
| 738 |
"url": "https://weather-api.example.com/mcp",
|
| 739 |
-
"transport": "
|
| 740 |
},
|
| 741 |
"calendar": {
|
| 742 |
"url": "https://calendar-api.example.com/mcp",
|
| 743 |
-
"transport": "
|
| 744 |
}
|
| 745 |
}
|
| 746 |
}
|
|
|
|
| 736 |
"mcpServers": {
|
| 737 |
"weather": {
|
| 738 |
"url": "https://weather-api.example.com/mcp",
|
| 739 |
+
"transport": "http"
|
| 740 |
},
|
| 741 |
"calendar": {
|
| 742 |
"url": "https://calendar-api.example.com/mcp",
|
| 743 |
+
"transport": "http"
|
| 744 |
}
|
| 745 |
}
|
| 746 |
}
|
src/fastmcp/server/server.py
CHANGED
|
@@ -74,6 +74,7 @@ if TYPE_CHECKING:
|
|
| 74 |
logger = get_logger(__name__)
|
| 75 |
|
| 76 |
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
|
|
| 77 |
|
| 78 |
# Compiled URI parsing regex to split a URI into protocol and path components
|
| 79 |
URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
|
|
@@ -280,7 +281,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 280 |
|
| 281 |
async def run_async(
|
| 282 |
self,
|
| 283 |
-
transport:
|
| 284 |
**transport_kwargs: Any,
|
| 285 |
) -> None:
|
| 286 |
"""Run the FastMCP server asynchronously.
|
|
@@ -290,19 +291,19 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 290 |
"""
|
| 291 |
if transport is None:
|
| 292 |
transport = "stdio"
|
| 293 |
-
if transport not in {"stdio", "
|
| 294 |
raise ValueError(f"Unknown transport: {transport}")
|
| 295 |
|
| 296 |
if transport == "stdio":
|
| 297 |
await self.run_stdio_async(**transport_kwargs)
|
| 298 |
-
elif transport in {"
|
| 299 |
await self.run_http_async(transport=transport, **transport_kwargs)
|
| 300 |
else:
|
| 301 |
raise ValueError(f"Unknown transport: {transport}")
|
| 302 |
|
| 303 |
def run(
|
| 304 |
self,
|
| 305 |
-
transport:
|
| 306 |
**transport_kwargs: Any,
|
| 307 |
) -> None:
|
| 308 |
"""Run the FastMCP server. Note this is a synchronous function.
|
|
@@ -1253,7 +1254,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1253 |
|
| 1254 |
async def run_http_async(
|
| 1255 |
self,
|
| 1256 |
-
transport: Literal["streamable-http", "sse"] = "
|
| 1257 |
host: str | None = None,
|
| 1258 |
port: int | None = None,
|
| 1259 |
log_level: str | None = None,
|
|
@@ -1384,7 +1385,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1384 |
middleware: list[ASGIMiddleware] | None = None,
|
| 1385 |
json_response: bool | None = None,
|
| 1386 |
stateless_http: bool | None = None,
|
| 1387 |
-
transport: Literal["streamable-http", "sse"] = "
|
| 1388 |
) -> StarletteWithLifespan:
|
| 1389 |
"""Create a Starlette app using the specified HTTP transport.
|
| 1390 |
|
|
@@ -1397,7 +1398,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1397 |
A Starlette application configured with the specified transport
|
| 1398 |
"""
|
| 1399 |
|
| 1400 |
-
if transport
|
| 1401 |
return create_streamable_http_app(
|
| 1402 |
server=self,
|
| 1403 |
streamable_http_path=path
|
|
@@ -1444,7 +1445,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1444 |
stacklevel=2,
|
| 1445 |
)
|
| 1446 |
await self.run_http_async(
|
| 1447 |
-
transport="
|
| 1448 |
host=host,
|
| 1449 |
port=port,
|
| 1450 |
log_level=log_level,
|
|
|
|
| 74 |
logger = get_logger(__name__)
|
| 75 |
|
| 76 |
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
| 77 |
+
Transport = Literal["stdio", "http", "sse", "streamable-http"]
|
| 78 |
|
| 79 |
# Compiled URI parsing regex to split a URI into protocol and path components
|
| 80 |
URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
|
|
|
|
| 281 |
|
| 282 |
async def run_async(
|
| 283 |
self,
|
| 284 |
+
transport: Transport | None = None,
|
| 285 |
**transport_kwargs: Any,
|
| 286 |
) -> None:
|
| 287 |
"""Run the FastMCP server asynchronously.
|
|
|
|
| 291 |
"""
|
| 292 |
if transport is None:
|
| 293 |
transport = "stdio"
|
| 294 |
+
if transport not in {"stdio", "http", "sse", "streamable-http"}:
|
| 295 |
raise ValueError(f"Unknown transport: {transport}")
|
| 296 |
|
| 297 |
if transport == "stdio":
|
| 298 |
await self.run_stdio_async(**transport_kwargs)
|
| 299 |
+
elif transport in {"http", "sse", "streamable-http"}:
|
| 300 |
await self.run_http_async(transport=transport, **transport_kwargs)
|
| 301 |
else:
|
| 302 |
raise ValueError(f"Unknown transport: {transport}")
|
| 303 |
|
| 304 |
def run(
|
| 305 |
self,
|
| 306 |
+
transport: Transport | None = None,
|
| 307 |
**transport_kwargs: Any,
|
| 308 |
) -> None:
|
| 309 |
"""Run the FastMCP server. Note this is a synchronous function.
|
|
|
|
| 1254 |
|
| 1255 |
async def run_http_async(
|
| 1256 |
self,
|
| 1257 |
+
transport: Literal["http", "streamable-http", "sse"] = "http",
|
| 1258 |
host: str | None = None,
|
| 1259 |
port: int | None = None,
|
| 1260 |
log_level: str | None = None,
|
|
|
|
| 1385 |
middleware: list[ASGIMiddleware] | None = None,
|
| 1386 |
json_response: bool | None = None,
|
| 1387 |
stateless_http: bool | None = None,
|
| 1388 |
+
transport: Literal["http", "streamable-http", "sse"] = "http",
|
| 1389 |
) -> StarletteWithLifespan:
|
| 1390 |
"""Create a Starlette app using the specified HTTP transport.
|
| 1391 |
|
|
|
|
| 1398 |
A Starlette application configured with the specified transport
|
| 1399 |
"""
|
| 1400 |
|
| 1401 |
+
if transport in ("streamable-http", "http"):
|
| 1402 |
return create_streamable_http_app(
|
| 1403 |
server=self,
|
| 1404 |
streamable_http_path=path
|
|
|
|
| 1445 |
stacklevel=2,
|
| 1446 |
)
|
| 1447 |
await self.run_http_async(
|
| 1448 |
+
transport="http",
|
| 1449 |
host=host,
|
| 1450 |
port=port,
|
| 1451 |
log_level=log_level,
|
src/fastmcp/utilities/mcp_config.py
CHANGED
|
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
|
|
| 19 |
|
| 20 |
def infer_transport_type_from_url(
|
| 21 |
url: str | AnyUrl,
|
| 22 |
-
) -> Literal["
|
| 23 |
"""
|
| 24 |
Infer the appropriate transport type from the given URL.
|
| 25 |
"""
|
|
@@ -34,7 +34,7 @@ def infer_transport_type_from_url(
|
|
| 34 |
if re.search(r"/sse(/|\?|&|$)", path):
|
| 35 |
return "sse"
|
| 36 |
else:
|
| 37 |
-
return "
|
| 38 |
|
| 39 |
|
| 40 |
class StdioMCPServer(FastMCPBaseModel):
|
|
@@ -58,7 +58,7 @@ class StdioMCPServer(FastMCPBaseModel):
|
|
| 58 |
class RemoteMCPServer(FastMCPBaseModel):
|
| 59 |
url: str
|
| 60 |
headers: dict[str, str] = Field(default_factory=dict)
|
| 61 |
-
transport: Literal["streamable-http", "sse"] | None = None
|
| 62 |
auth: Annotated[
|
| 63 |
str | Literal["oauth"] | httpx.Auth | None,
|
| 64 |
Field(
|
|
@@ -79,6 +79,7 @@ class RemoteMCPServer(FastMCPBaseModel):
|
|
| 79 |
if transport == "sse":
|
| 80 |
return SSETransport(self.url, headers=self.headers, auth=self.auth)
|
| 81 |
else:
|
|
|
|
| 82 |
return StreamableHttpTransport(
|
| 83 |
self.url, headers=self.headers, auth=self.auth
|
| 84 |
)
|
|
|
|
| 19 |
|
| 20 |
def infer_transport_type_from_url(
|
| 21 |
url: str | AnyUrl,
|
| 22 |
+
) -> Literal["http", "sse"]:
|
| 23 |
"""
|
| 24 |
Infer the appropriate transport type from the given URL.
|
| 25 |
"""
|
|
|
|
| 34 |
if re.search(r"/sse(/|\?|&|$)", path):
|
| 35 |
return "sse"
|
| 36 |
else:
|
| 37 |
+
return "http"
|
| 38 |
|
| 39 |
|
| 40 |
class StdioMCPServer(FastMCPBaseModel):
|
|
|
|
| 58 |
class RemoteMCPServer(FastMCPBaseModel):
|
| 59 |
url: str
|
| 60 |
headers: dict[str, str] = Field(default_factory=dict)
|
| 61 |
+
transport: Literal["http", "streamable-http", "sse"] | None = None
|
| 62 |
auth: Annotated[
|
| 63 |
str | Literal["oauth"] | httpx.Auth | None,
|
| 64 |
Field(
|
|
|
|
| 79 |
if transport == "sse":
|
| 80 |
return SSETransport(self.url, headers=self.headers, auth=self.auth)
|
| 81 |
else:
|
| 82 |
+
# Both "http" and "streamable-http" map to StreamableHttpTransport
|
| 83 |
return StreamableHttpTransport(
|
| 84 |
self.url, headers=self.headers, auth=self.auth
|
| 85 |
)
|
tests/auth/providers/test_bearer.py
CHANGED
|
@@ -65,7 +65,7 @@ def mcp_server_url(rsa_key_pair: RSAKeyPair) -> Generator[str]:
|
|
| 65 |
with run_server_in_process(
|
| 66 |
run_mcp_server,
|
| 67 |
public_key=rsa_key_pair.public_key,
|
| 68 |
-
run_kwargs=dict(transport="
|
| 69 |
) as url:
|
| 70 |
yield f"{url}/mcp/"
|
| 71 |
|
|
@@ -696,7 +696,7 @@ class TestFastMCPBearerAuth:
|
|
| 696 |
run_mcp_server,
|
| 697 |
public_key=rsa_key_pair.public_key,
|
| 698 |
auth_kwargs=dict(required_scopes=["read", "write"]),
|
| 699 |
-
run_kwargs=dict(transport="
|
| 700 |
) as url:
|
| 701 |
mcp_server_url = f"{url}/mcp/"
|
| 702 |
with pytest.raises(httpx.HTTPStatusError) as exc_info:
|
|
@@ -719,7 +719,7 @@ class TestFastMCPBearerAuth:
|
|
| 719 |
run_mcp_server,
|
| 720 |
public_key=rsa_key_pair.public_key,
|
| 721 |
auth_kwargs=dict(required_scopes=["read", "write"]),
|
| 722 |
-
run_kwargs=dict(transport="
|
| 723 |
) as url:
|
| 724 |
mcp_server_url = f"{url}/mcp/"
|
| 725 |
async with Client(mcp_server_url, auth=BearerAuth(token)) as client:
|
|
|
|
| 65 |
with run_server_in_process(
|
| 66 |
run_mcp_server,
|
| 67 |
public_key=rsa_key_pair.public_key,
|
| 68 |
+
run_kwargs=dict(transport="http"),
|
| 69 |
) as url:
|
| 70 |
yield f"{url}/mcp/"
|
| 71 |
|
|
|
|
| 696 |
run_mcp_server,
|
| 697 |
public_key=rsa_key_pair.public_key,
|
| 698 |
auth_kwargs=dict(required_scopes=["read", "write"]),
|
| 699 |
+
run_kwargs=dict(transport="http"),
|
| 700 |
) as url:
|
| 701 |
mcp_server_url = f"{url}/mcp/"
|
| 702 |
with pytest.raises(httpx.HTTPStatusError) as exc_info:
|
|
|
|
| 719 |
run_mcp_server,
|
| 720 |
public_key=rsa_key_pair.public_key,
|
| 721 |
auth_kwargs=dict(required_scopes=["read", "write"]),
|
| 722 |
+
run_kwargs=dict(transport="http"),
|
| 723 |
) as url:
|
| 724 |
mcp_server_url = f"{url}/mcp/"
|
| 725 |
async with Client(mcp_server_url, auth=BearerAuth(token)) as client:
|
tests/auth/test_oauth_client.py
CHANGED
|
@@ -43,7 +43,7 @@ def run_server(host: str, port: int, **kwargs) -> None:
|
|
| 43 |
|
| 44 |
@pytest.fixture(scope="module")
|
| 45 |
def streamable_http_server() -> Generator[str, None, None]:
|
| 46 |
-
with run_server_in_process(run_server, transport="
|
| 47 |
yield f"{url}/mcp/"
|
| 48 |
|
| 49 |
|
|
|
|
| 43 |
|
| 44 |
@pytest.fixture(scope="module")
|
| 45 |
def streamable_http_server() -> Generator[str, None, None]:
|
| 46 |
+
with run_server_in_process(run_server, transport="http") as url:
|
| 47 |
yield f"{url}/mcp/"
|
| 48 |
|
| 49 |
|
tests/cli/test_cli.py
CHANGED
|
@@ -328,6 +328,41 @@ class TestRunCommand:
|
|
| 328 |
assert result.exit_code == 0
|
| 329 |
mock_server.run.assert_called_once_with(transport="sse")
|
| 330 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
def test_run_command_with_host(self, temp_python_file):
|
| 332 |
"""Test run command with host option."""
|
| 333 |
with (
|
|
|
|
| 328 |
assert result.exit_code == 0
|
| 329 |
mock_server.run.assert_called_once_with(transport="sse")
|
| 330 |
|
| 331 |
+
def test_run_command_with_http_transports(self, temp_python_file):
|
| 332 |
+
"""Test run command with both http and streamable-http transport options."""
|
| 333 |
+
# Test "http" transport
|
| 334 |
+
with (
|
| 335 |
+
patch("fastmcp.cli.run.parse_file_path") as mock_parse,
|
| 336 |
+
patch("fastmcp.cli.run.import_server") as mock_import,
|
| 337 |
+
):
|
| 338 |
+
mock_parse.return_value = (temp_python_file, None)
|
| 339 |
+
mock_server = MagicMock()
|
| 340 |
+
mock_server.name = "test_server"
|
| 341 |
+
mock_import.return_value = mock_server
|
| 342 |
+
|
| 343 |
+
result = runner.invoke(
|
| 344 |
+
cli.app, ["run", str(temp_python_file), "--transport", "http"]
|
| 345 |
+
)
|
| 346 |
+
assert result.exit_code == 0
|
| 347 |
+
mock_server.run.assert_called_once_with(transport="http")
|
| 348 |
+
|
| 349 |
+
# Test "streamable-http" transport (alias for http)
|
| 350 |
+
with (
|
| 351 |
+
patch("fastmcp.cli.run.parse_file_path") as mock_parse,
|
| 352 |
+
patch("fastmcp.cli.run.import_server") as mock_import,
|
| 353 |
+
):
|
| 354 |
+
mock_parse.return_value = (temp_python_file, None)
|
| 355 |
+
mock_server = MagicMock()
|
| 356 |
+
mock_server.name = "test_server"
|
| 357 |
+
mock_import.return_value = mock_server
|
| 358 |
+
|
| 359 |
+
result = runner.invoke(
|
| 360 |
+
cli.app,
|
| 361 |
+
["run", str(temp_python_file), "--transport", "streamable-http"],
|
| 362 |
+
)
|
| 363 |
+
assert result.exit_code == 0
|
| 364 |
+
mock_server.run.assert_called_once_with(transport="streamable-http")
|
| 365 |
+
|
| 366 |
def test_run_command_with_host(self, temp_python_file):
|
| 367 |
"""Test run command with host option."""
|
| 368 |
with (
|
tests/client/test_openapi.py
CHANGED
|
@@ -56,7 +56,7 @@ def run_proxy_server(host: str, port: int, shttp_url: str, **kwargs) -> None:
|
|
| 56 |
class TestClientHeaders:
|
| 57 |
@pytest.fixture(scope="class")
|
| 58 |
def shttp_server(self) -> Generator[str, None, None]:
|
| 59 |
-
with run_server_in_process(run_server, transport="
|
| 60 |
yield f"{url}/mcp/"
|
| 61 |
|
| 62 |
@pytest.fixture(scope="class")
|
|
@@ -69,7 +69,7 @@ class TestClientHeaders:
|
|
| 69 |
with run_server_in_process(
|
| 70 |
run_proxy_server,
|
| 71 |
shttp_url=shttp_server,
|
| 72 |
-
transport="
|
| 73 |
) as url:
|
| 74 |
yield f"{url}/mcp/"
|
| 75 |
|
|
|
|
| 56 |
class TestClientHeaders:
|
| 57 |
@pytest.fixture(scope="class")
|
| 58 |
def shttp_server(self) -> Generator[str, None, None]:
|
| 59 |
+
with run_server_in_process(run_server, transport="http") as url:
|
| 60 |
yield f"{url}/mcp/"
|
| 61 |
|
| 62 |
@pytest.fixture(scope="class")
|
|
|
|
| 69 |
with run_server_in_process(
|
| 70 |
run_proxy_server,
|
| 71 |
shttp_url=shttp_server,
|
| 72 |
+
transport="http",
|
| 73 |
) as url:
|
| 74 |
yield f"{url}/mcp/"
|
| 75 |
|
tests/client/test_streamable_http.py
CHANGED
|
@@ -103,13 +103,24 @@ async def streamable_http_server(
|
|
| 103 |
stateless_http: bool = False,
|
| 104 |
) -> AsyncGenerator[str, None]:
|
| 105 |
with run_server_in_process(
|
| 106 |
-
run_server, stateless_http=stateless_http, transport="
|
| 107 |
) as url:
|
| 108 |
async with Client(transport=StreamableHttpTransport(f"{url}/mcp/")) as client:
|
| 109 |
assert await client.ping()
|
| 110 |
yield f"{url}/mcp/"
|
| 111 |
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
async def test_ping(streamable_http_server: str):
|
| 114 |
"""Test pinging the server."""
|
| 115 |
async with Client(
|
|
@@ -119,6 +130,19 @@ async def test_ping(streamable_http_server: str):
|
|
| 119 |
assert result is True
|
| 120 |
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
async def test_http_headers(streamable_http_server: str):
|
| 123 |
"""Test getting HTTP headers from the server."""
|
| 124 |
async with Client(
|
|
|
|
| 103 |
stateless_http: bool = False,
|
| 104 |
) -> AsyncGenerator[str, None]:
|
| 105 |
with run_server_in_process(
|
| 106 |
+
run_server, stateless_http=stateless_http, transport="http"
|
| 107 |
) as url:
|
| 108 |
async with Client(transport=StreamableHttpTransport(f"{url}/mcp/")) as client:
|
| 109 |
assert await client.ping()
|
| 110 |
yield f"{url}/mcp/"
|
| 111 |
|
| 112 |
|
| 113 |
+
@pytest.fixture()
|
| 114 |
+
async def streamable_http_server_with_streamable_http_alias() -> AsyncGenerator[
|
| 115 |
+
str, None
|
| 116 |
+
]:
|
| 117 |
+
"""Test that the "streamable-http" transport alias works."""
|
| 118 |
+
with run_server_in_process(run_server, transport="streamable-http") as url:
|
| 119 |
+
async with Client(transport=StreamableHttpTransport(f"{url}/mcp/")) as client:
|
| 120 |
+
assert await client.ping()
|
| 121 |
+
yield f"{url}/mcp/"
|
| 122 |
+
|
| 123 |
+
|
| 124 |
async def test_ping(streamable_http_server: str):
|
| 125 |
"""Test pinging the server."""
|
| 126 |
async with Client(
|
|
|
|
| 130 |
assert result is True
|
| 131 |
|
| 132 |
|
| 133 |
+
async def test_ping_with_streamable_http_alias(
|
| 134 |
+
streamable_http_server_with_streamable_http_alias: str,
|
| 135 |
+
):
|
| 136 |
+
"""Test pinging the server."""
|
| 137 |
+
async with Client(
|
| 138 |
+
transport=StreamableHttpTransport(
|
| 139 |
+
streamable_http_server_with_streamable_http_alias
|
| 140 |
+
)
|
| 141 |
+
) as client:
|
| 142 |
+
result = await client.ping()
|
| 143 |
+
assert result is True
|
| 144 |
+
|
| 145 |
+
|
| 146 |
async def test_http_headers(streamable_http_server: str):
|
| 147 |
"""Test getting HTTP headers from the server."""
|
| 148 |
async with Client(
|
tests/deprecated/test_deprecated.py
CHANGED
|
@@ -85,7 +85,7 @@ async def test_run_streamable_http_async_deprecation_warning():
|
|
| 85 |
# Verify the mock was called with the right transport
|
| 86 |
mock_run.assert_called_once()
|
| 87 |
call_kwargs = mock_run.call_args.kwargs
|
| 88 |
-
assert call_kwargs.get("transport") == "
|
| 89 |
|
| 90 |
|
| 91 |
def test_http_app_with_sse_transport():
|
|
|
|
| 85 |
# Verify the mock was called with the right transport
|
| 86 |
mock_run.assert_called_once()
|
| 87 |
call_kwargs = mock_run.call_args.kwargs
|
| 88 |
+
assert call_kwargs.get("transport") == "http"
|
| 89 |
|
| 90 |
|
| 91 |
def test_http_app_with_sse_transport():
|
tests/server/http/test_http_dependencies.py
CHANGED
|
@@ -44,7 +44,7 @@ def run_server(host: str, port: int, **kwargs) -> None:
|
|
| 44 |
|
| 45 |
@pytest.fixture(autouse=True, scope="module")
|
| 46 |
def shttp_server() -> Generator[str, None, None]:
|
| 47 |
-
with run_server_in_process(run_server, transport="
|
| 48 |
yield f"{url}/mcp/"
|
| 49 |
|
| 50 |
|
|
|
|
| 44 |
|
| 45 |
@pytest.fixture(autouse=True, scope="module")
|
| 46 |
def shttp_server() -> Generator[str, None, None]:
|
| 47 |
+
with run_server_in_process(run_server, transport="http") as url:
|
| 48 |
yield f"{url}/mcp/"
|
| 49 |
|
| 50 |
|
tests/server/http/test_http_middleware.py
CHANGED
|
@@ -96,7 +96,7 @@ async def test_streamable_http_app_with_custom_middleware():
|
|
| 96 |
server._additional_http_routes = routes
|
| 97 |
|
| 98 |
# Create the app with custom middleware
|
| 99 |
-
app = server.http_app(transport="
|
| 100 |
|
| 101 |
# Create a test client
|
| 102 |
transport = ASGITransport(app=app)
|
|
|
|
| 96 |
server._additional_http_routes = routes
|
| 97 |
|
| 98 |
# Create the app with custom middleware
|
| 99 |
+
app = server.http_app(transport="http", middleware=custom_middleware)
|
| 100 |
|
| 101 |
# Create a test client
|
| 102 |
transport = ASGITransport(app=app)
|