Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
8ea5e71
1
Parent(s): 09cabfa
Minor updates
Browse files- docs/docs.json +1 -0
- docs/servers/fastmcp.mdx +1 -1
- docs/servers/middleware.mdx +22 -18
- src/fastmcp/server/middleware.py +2 -2
- src/fastmcp/server/server.py +9 -9
- tests/server/middleware/test_middleware.py +2 -2
docs/docs.json
CHANGED
|
@@ -78,6 +78,7 @@
|
|
| 78 |
"servers/auth/bearer"
|
| 79 |
]
|
| 80 |
},
|
|
|
|
| 81 |
"servers/openapi",
|
| 82 |
"servers/proxy",
|
| 83 |
"servers/composition",
|
|
|
|
| 78 |
"servers/auth/bearer"
|
| 79 |
]
|
| 80 |
},
|
| 81 |
+
"servers/middleware",
|
| 82 |
"servers/openapi",
|
| 83 |
"servers/proxy",
|
| 84 |
"servers/composition",
|
docs/servers/fastmcp.mdx
CHANGED
|
@@ -193,7 +193,7 @@ def hello():
|
|
| 193 |
return "hi"
|
| 194 |
|
| 195 |
# Mount directly
|
| 196 |
-
main.mount(
|
| 197 |
```
|
| 198 |
|
| 199 |
## Proxying Servers
|
|
|
|
| 193 |
return "hi"
|
| 194 |
|
| 195 |
# Mount directly
|
| 196 |
+
main.mount(sub, prefix="sub")
|
| 197 |
```
|
| 198 |
|
| 199 |
## Proxying Servers
|
docs/servers/middleware.mdx
CHANGED
|
@@ -1,18 +1,22 @@
|
|
| 1 |
---
|
| 2 |
title: MCP Middleware
|
| 3 |
sidebarTitle: Middleware
|
| 4 |
-
description: Add
|
| 5 |
icon: layers
|
| 6 |
---
|
| 7 |
|
| 8 |
import { VersionBadge } from "/snippets/version-badge.mdx"
|
| 9 |
|
| 10 |
-
<VersionBadge version="2.
|
| 11 |
|
| 12 |
MCP middleware is a powerful concept that allows you to add cross-cutting functionality to your FastMCP server. Unlike traditional web middleware, MCP middleware is designed specifically for the Model Context Protocol, providing hooks for different types of MCP operations like tool calls, resource reads, and prompt requests.
|
| 13 |
|
| 14 |
-
<
|
| 15 |
MCP middleware is a FastMCP-specific concept and is not part of the official MCP protocol specification. This middleware system is designed to work with FastMCP servers and may not be compatible with other MCP implementations.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
</Warning>
|
| 17 |
|
| 18 |
## What is MCP Middleware?
|
|
@@ -58,13 +62,13 @@ The middleware hook system is designed to be extensible. As FastMCP evolves and
|
|
| 58 |
|
| 59 |
### Basic Middleware Structure
|
| 60 |
|
| 61 |
-
MCP middleware is implemented by subclassing the `
|
| 62 |
|
| 63 |
```python
|
| 64 |
from fastmcp import FastMCP
|
| 65 |
-
from fastmcp.server.middleware import
|
| 66 |
|
| 67 |
-
class LoggingMiddleware(
|
| 68 |
"""Middleware that logs all MCP operations."""
|
| 69 |
|
| 70 |
async def on_message(self, context: MiddlewareContext, call_next):
|
|
@@ -97,7 +101,7 @@ mcp.add_middleware(LoggingMiddleware())
|
|
| 97 |
The `MiddlewareContext` object provides access to information about the current request:
|
| 98 |
|
| 99 |
```python
|
| 100 |
-
class InspectionMiddleware(
|
| 101 |
async def on_request(self, context: MiddlewareContext, call_next):
|
| 102 |
# Access request information
|
| 103 |
method = context.method # e.g., "tools/call"
|
|
@@ -120,7 +124,7 @@ Each middleware hook receives a `MiddlewareContext` and a `call_next` function.
|
|
| 120 |
3. **Operation-specific hooks**: Called for specific MCP operations
|
| 121 |
|
| 122 |
```python
|
| 123 |
-
class ComprehensiveMiddleware(
|
| 124 |
async def on_message(self, context: MiddlewareContext, call_next):
|
| 125 |
"""Called for ALL messages (requests and notifications)."""
|
| 126 |
print(f"Message: {context.method}")
|
|
@@ -143,10 +147,10 @@ class ComprehensiveMiddleware(MCPMiddleware):
|
|
| 143 |
### Authentication Middleware
|
| 144 |
|
| 145 |
```python
|
| 146 |
-
from fastmcp.server.middleware import
|
| 147 |
from fastmcp.exceptions import ToolError
|
| 148 |
|
| 149 |
-
class AuthenticationMiddleware(
|
| 150 |
def __init__(self, required_token: str):
|
| 151 |
self.required_token = required_token
|
| 152 |
|
|
@@ -184,7 +188,7 @@ mcp.add_middleware(AuthenticationMiddleware("secret-token-123"))
|
|
| 184 |
import time
|
| 185 |
import logging
|
| 186 |
|
| 187 |
-
class PerformanceMiddleware(
|
| 188 |
def __init__(self):
|
| 189 |
self.logger = logging.getLogger("performance")
|
| 190 |
|
|
@@ -214,7 +218,7 @@ class PerformanceMiddleware(MCPMiddleware):
|
|
| 214 |
### Request/Response Transformation Middleware
|
| 215 |
|
| 216 |
```python
|
| 217 |
-
class TransformationMiddleware(
|
| 218 |
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
| 219 |
"""Transform tool arguments and results."""
|
| 220 |
|
|
@@ -253,7 +257,7 @@ import asyncio
|
|
| 253 |
from collections import defaultdict
|
| 254 |
from datetime import datetime, timedelta
|
| 255 |
|
| 256 |
-
class RateLimitMiddleware(
|
| 257 |
def __init__(self, max_requests: int = 100, window_minutes: int = 1):
|
| 258 |
self.max_requests = max_requests
|
| 259 |
self.window = timedelta(minutes=window_minutes)
|
|
@@ -327,7 +331,7 @@ mcp.add_middleware(LoggingMiddleware())
|
|
| 327 |
### Conditional Middleware
|
| 328 |
|
| 329 |
```python
|
| 330 |
-
class ConditionalMiddleware(
|
| 331 |
def __init__(self, condition_func):
|
| 332 |
self.should_process = condition_func
|
| 333 |
|
|
@@ -352,7 +356,7 @@ mcp.add_middleware(ConditionalMiddleware(only_expensive_tools))
|
|
| 352 |
### Middleware with State
|
| 353 |
|
| 354 |
```python
|
| 355 |
-
class StatefulMiddleware(
|
| 356 |
def __init__(self):
|
| 357 |
self.call_count = 0
|
| 358 |
self.tools_used = set()
|
|
@@ -377,7 +381,7 @@ class StatefulMiddleware(MCPMiddleware):
|
|
| 377 |
### Error Handling Middleware
|
| 378 |
|
| 379 |
```python
|
| 380 |
-
class ErrorHandlingMiddleware(
|
| 381 |
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
| 382 |
"""Provide consistent error handling."""
|
| 383 |
try:
|
|
@@ -400,7 +404,7 @@ class ErrorHandlingMiddleware(MCPMiddleware):
|
|
| 400 |
3. **Cache when possible**: Store frequently accessed data to avoid repeated lookups
|
| 401 |
|
| 402 |
```python
|
| 403 |
-
class EfficientMiddleware(
|
| 404 |
def __init__(self):
|
| 405 |
self._cache = {}
|
| 406 |
|
|
@@ -428,7 +432,7 @@ class EfficientMiddleware(MCPMiddleware):
|
|
| 428 |
3. **Use `ToolError` for client-facing errors**: Keep internal errors internal
|
| 429 |
|
| 430 |
```python
|
| 431 |
-
class RobustMiddleware(
|
| 432 |
async def on_request(self, context: MiddlewareContext, call_next):
|
| 433 |
"""Robust error handling example."""
|
| 434 |
try:
|
|
|
|
| 1 |
---
|
| 2 |
title: MCP Middleware
|
| 3 |
sidebarTitle: Middleware
|
| 4 |
+
description: Add custom functionality to your MCP server with middleware that can inspect, modify, and respond to all MCP requests and responses.
|
| 5 |
icon: layers
|
| 6 |
---
|
| 7 |
|
| 8 |
import { VersionBadge } from "/snippets/version-badge.mdx"
|
| 9 |
|
| 10 |
+
<VersionBadge version="2.9.0" />
|
| 11 |
|
| 12 |
MCP middleware is a powerful concept that allows you to add cross-cutting functionality to your FastMCP server. Unlike traditional web middleware, MCP middleware is designed specifically for the Model Context Protocol, providing hooks for different types of MCP operations like tool calls, resource reads, and prompt requests.
|
| 13 |
|
| 14 |
+
<Tip>
|
| 15 |
MCP middleware is a FastMCP-specific concept and is not part of the official MCP protocol specification. This middleware system is designed to work with FastMCP servers and may not be compatible with other MCP implementations.
|
| 16 |
+
</Tip>
|
| 17 |
+
|
| 18 |
+
<Warning>
|
| 19 |
+
MCP middleware is a brand new concept and may be subject to breaking changes in future versions.
|
| 20 |
</Warning>
|
| 21 |
|
| 22 |
## What is MCP Middleware?
|
|
|
|
| 62 |
|
| 63 |
### Basic Middleware Structure
|
| 64 |
|
| 65 |
+
MCP middleware is implemented by subclassing the `Middleware` base class and overriding the hooks you need:
|
| 66 |
|
| 67 |
```python
|
| 68 |
from fastmcp import FastMCP
|
| 69 |
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
| 70 |
|
| 71 |
+
class LoggingMiddleware(Middleware):
|
| 72 |
"""Middleware that logs all MCP operations."""
|
| 73 |
|
| 74 |
async def on_message(self, context: MiddlewareContext, call_next):
|
|
|
|
| 101 |
The `MiddlewareContext` object provides access to information about the current request:
|
| 102 |
|
| 103 |
```python
|
| 104 |
+
class InspectionMiddleware(Middleware):
|
| 105 |
async def on_request(self, context: MiddlewareContext, call_next):
|
| 106 |
# Access request information
|
| 107 |
method = context.method # e.g., "tools/call"
|
|
|
|
| 124 |
3. **Operation-specific hooks**: Called for specific MCP operations
|
| 125 |
|
| 126 |
```python
|
| 127 |
+
class ComprehensiveMiddleware(Middleware):
|
| 128 |
async def on_message(self, context: MiddlewareContext, call_next):
|
| 129 |
"""Called for ALL messages (requests and notifications)."""
|
| 130 |
print(f"Message: {context.method}")
|
|
|
|
| 147 |
### Authentication Middleware
|
| 148 |
|
| 149 |
```python
|
| 150 |
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
| 151 |
from fastmcp.exceptions import ToolError
|
| 152 |
|
| 153 |
+
class AuthenticationMiddleware(Middleware):
|
| 154 |
def __init__(self, required_token: str):
|
| 155 |
self.required_token = required_token
|
| 156 |
|
|
|
|
| 188 |
import time
|
| 189 |
import logging
|
| 190 |
|
| 191 |
+
class PerformanceMiddleware(Middleware):
|
| 192 |
def __init__(self):
|
| 193 |
self.logger = logging.getLogger("performance")
|
| 194 |
|
|
|
|
| 218 |
### Request/Response Transformation Middleware
|
| 219 |
|
| 220 |
```python
|
| 221 |
+
class TransformationMiddleware(Middleware):
|
| 222 |
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
| 223 |
"""Transform tool arguments and results."""
|
| 224 |
|
|
|
|
| 257 |
from collections import defaultdict
|
| 258 |
from datetime import datetime, timedelta
|
| 259 |
|
| 260 |
+
class RateLimitMiddleware(Middleware):
|
| 261 |
def __init__(self, max_requests: int = 100, window_minutes: int = 1):
|
| 262 |
self.max_requests = max_requests
|
| 263 |
self.window = timedelta(minutes=window_minutes)
|
|
|
|
| 331 |
### Conditional Middleware
|
| 332 |
|
| 333 |
```python
|
| 334 |
+
class ConditionalMiddleware(Middleware):
|
| 335 |
def __init__(self, condition_func):
|
| 336 |
self.should_process = condition_func
|
| 337 |
|
|
|
|
| 356 |
### Middleware with State
|
| 357 |
|
| 358 |
```python
|
| 359 |
+
class StatefulMiddleware(Middleware):
|
| 360 |
def __init__(self):
|
| 361 |
self.call_count = 0
|
| 362 |
self.tools_used = set()
|
|
|
|
| 381 |
### Error Handling Middleware
|
| 382 |
|
| 383 |
```python
|
| 384 |
+
class ErrorHandlingMiddleware(Middleware):
|
| 385 |
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
| 386 |
"""Provide consistent error handling."""
|
| 387 |
try:
|
|
|
|
| 404 |
3. **Cache when possible**: Store frequently accessed data to avoid repeated lookups
|
| 405 |
|
| 406 |
```python
|
| 407 |
+
class EfficientMiddleware(Middleware):
|
| 408 |
def __init__(self):
|
| 409 |
self._cache = {}
|
| 410 |
|
|
|
|
| 432 |
3. **Use `ToolError` for client-facing errors**: Keep internal errors internal
|
| 433 |
|
| 434 |
```python
|
| 435 |
+
class RobustMiddleware(Middleware):
|
| 436 |
async def on_request(self, context: MiddlewareContext, call_next):
|
| 437 |
"""Robust error handling example."""
|
| 438 |
try:
|
src/fastmcp/server/middleware.py
CHANGED
|
@@ -104,7 +104,7 @@ class MiddlewareContext(Generic[T]):
|
|
| 104 |
|
| 105 |
|
| 106 |
def make_middleware_wrapper(
|
| 107 |
-
middleware:
|
| 108 |
) -> CallNext[T, R]:
|
| 109 |
"""Create a wrapper that applies a single middleware to a context. The
|
| 110 |
closure bakes in the middleware and call_next function, so it can be
|
|
@@ -116,7 +116,7 @@ def make_middleware_wrapper(
|
|
| 116 |
return wrapper
|
| 117 |
|
| 118 |
|
| 119 |
-
class
|
| 120 |
"""Base class for FastMCP middleware with dispatching hooks."""
|
| 121 |
|
| 122 |
async def __call__(
|
|
|
|
| 104 |
|
| 105 |
|
| 106 |
def make_middleware_wrapper(
|
| 107 |
+
middleware: Middleware, call_next: CallNext[T, R]
|
| 108 |
) -> CallNext[T, R]:
|
| 109 |
"""Create a wrapper that applies a single middleware to a context. The
|
| 110 |
closure bakes in the middleware and call_next function, so it can be
|
|
|
|
| 116 |
return wrapper
|
| 117 |
|
| 118 |
|
| 119 |
+
class Middleware:
|
| 120 |
"""Base class for FastMCP middleware with dispatching hooks."""
|
| 121 |
|
| 122 |
async def __call__(
|
src/fastmcp/server/server.py
CHANGED
|
@@ -35,7 +35,7 @@ from mcp.types import Resource as MCPResource
|
|
| 35 |
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
| 36 |
from mcp.types import Tool as MCPTool
|
| 37 |
from pydantic import AnyUrl
|
| 38 |
-
from starlette.middleware import Middleware
|
| 39 |
from starlette.requests import Request
|
| 40 |
from starlette.responses import Response
|
| 41 |
from starlette.routing import BaseRoute, Route
|
|
@@ -55,7 +55,7 @@ from fastmcp.server.http import (
|
|
| 55 |
create_sse_app,
|
| 56 |
create_streamable_http_app,
|
| 57 |
)
|
| 58 |
-
from fastmcp.server.middleware import
|
| 59 |
from fastmcp.settings import Settings
|
| 60 |
from fastmcp.tools import ToolManager
|
| 61 |
from fastmcp.tools.tool import FunctionTool, Tool
|
|
@@ -118,7 +118,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 118 |
*,
|
| 119 |
version: str | None = None,
|
| 120 |
auth: OAuthProvider | None = None,
|
| 121 |
-
middleware: list[
|
| 122 |
lifespan: (
|
| 123 |
Callable[
|
| 124 |
[FastMCP[LifespanResultT]],
|
|
@@ -335,7 +335,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 335 |
chain = partial(mw, call_next=chain)
|
| 336 |
return await chain(context)
|
| 337 |
|
| 338 |
-
def add_middleware(self, middleware:
|
| 339 |
self.middleware.append(middleware)
|
| 340 |
|
| 341 |
async def get_tools(self) -> dict[str, Tool]:
|
|
@@ -917,7 +917,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 917 |
Args:
|
| 918 |
template: A ResourceTemplate instance to add
|
| 919 |
"""
|
| 920 |
-
self._resource_manager.add_template(template)
|
| 921 |
|
| 922 |
def add_resource_fn(
|
| 923 |
self,
|
|
@@ -1260,7 +1260,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1260 |
log_level: str | None = None,
|
| 1261 |
path: str | None = None,
|
| 1262 |
uvicorn_config: dict[str, Any] | None = None,
|
| 1263 |
-
middleware: list[
|
| 1264 |
) -> None:
|
| 1265 |
"""Run the server using HTTP transport.
|
| 1266 |
|
|
@@ -1331,7 +1331,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1331 |
self,
|
| 1332 |
path: str | None = None,
|
| 1333 |
message_path: str | None = None,
|
| 1334 |
-
middleware: list[
|
| 1335 |
) -> StarletteWithLifespan:
|
| 1336 |
"""
|
| 1337 |
Create a Starlette app for the SSE server.
|
|
@@ -1361,7 +1361,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1361 |
def streamable_http_app(
|
| 1362 |
self,
|
| 1363 |
path: str | None = None,
|
| 1364 |
-
middleware: list[
|
| 1365 |
) -> StarletteWithLifespan:
|
| 1366 |
"""
|
| 1367 |
Create a Starlette app for the StreamableHTTP server.
|
|
@@ -1382,7 +1382,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1382 |
def http_app(
|
| 1383 |
self,
|
| 1384 |
path: str | None = None,
|
| 1385 |
-
middleware: list[
|
| 1386 |
json_response: bool | None = None,
|
| 1387 |
stateless_http: bool | None = None,
|
| 1388 |
transport: Literal["streamable-http", "sse"] = "streamable-http",
|
|
|
|
| 35 |
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
| 36 |
from mcp.types import Tool as MCPTool
|
| 37 |
from pydantic import AnyUrl
|
| 38 |
+
from starlette.middleware import Middleware as ASGIMiddleware
|
| 39 |
from starlette.requests import Request
|
| 40 |
from starlette.responses import Response
|
| 41 |
from starlette.routing import BaseRoute, Route
|
|
|
|
| 55 |
create_sse_app,
|
| 56 |
create_streamable_http_app,
|
| 57 |
)
|
| 58 |
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
| 59 |
from fastmcp.settings import Settings
|
| 60 |
from fastmcp.tools import ToolManager
|
| 61 |
from fastmcp.tools.tool import FunctionTool, Tool
|
|
|
|
| 118 |
*,
|
| 119 |
version: str | None = None,
|
| 120 |
auth: OAuthProvider | None = None,
|
| 121 |
+
middleware: list[Middleware] | None = None,
|
| 122 |
lifespan: (
|
| 123 |
Callable[
|
| 124 |
[FastMCP[LifespanResultT]],
|
|
|
|
| 335 |
chain = partial(mw, call_next=chain)
|
| 336 |
return await chain(context)
|
| 337 |
|
| 338 |
+
def add_middleware(self, middleware: Middleware) -> None:
|
| 339 |
self.middleware.append(middleware)
|
| 340 |
|
| 341 |
async def get_tools(self) -> dict[str, Tool]:
|
|
|
|
| 917 |
Args:
|
| 918 |
template: A ResourceTemplate instance to add
|
| 919 |
"""
|
| 920 |
+
self._resource_manager.add_template(template, key=key)
|
| 921 |
|
| 922 |
def add_resource_fn(
|
| 923 |
self,
|
|
|
|
| 1260 |
log_level: str | None = None,
|
| 1261 |
path: str | None = None,
|
| 1262 |
uvicorn_config: dict[str, Any] | None = None,
|
| 1263 |
+
middleware: list[ASGIMiddleware] | None = None,
|
| 1264 |
) -> None:
|
| 1265 |
"""Run the server using HTTP transport.
|
| 1266 |
|
|
|
|
| 1331 |
self,
|
| 1332 |
path: str | None = None,
|
| 1333 |
message_path: str | None = None,
|
| 1334 |
+
middleware: list[ASGIMiddleware] | None = None,
|
| 1335 |
) -> StarletteWithLifespan:
|
| 1336 |
"""
|
| 1337 |
Create a Starlette app for the SSE server.
|
|
|
|
| 1361 |
def streamable_http_app(
|
| 1362 |
self,
|
| 1363 |
path: str | None = None,
|
| 1364 |
+
middleware: list[ASGIMiddleware] | None = None,
|
| 1365 |
) -> StarletteWithLifespan:
|
| 1366 |
"""
|
| 1367 |
Create a Starlette app for the StreamableHTTP server.
|
|
|
|
| 1382 |
def http_app(
|
| 1383 |
self,
|
| 1384 |
path: str | None = None,
|
| 1385 |
+
middleware: list[ASGIMiddleware] | None = None,
|
| 1386 |
json_response: bool | None = None,
|
| 1387 |
stateless_http: bool | None = None,
|
| 1388 |
transport: Literal["streamable-http", "sse"] = "streamable-http",
|
tests/server/middleware/test_middleware.py
CHANGED
|
@@ -7,7 +7,7 @@ import pytest
|
|
| 7 |
|
| 8 |
from fastmcp import Client, FastMCP
|
| 9 |
from fastmcp.server.context import Context
|
| 10 |
-
from fastmcp.server.middleware import
|
| 11 |
|
| 12 |
|
| 13 |
@dataclass
|
|
@@ -18,7 +18,7 @@ class Recording:
|
|
| 18 |
result: mcp.types.ServerResult | None
|
| 19 |
|
| 20 |
|
| 21 |
-
class RecordingMiddleware(
|
| 22 |
"""A middleware that automatically records all method calls."""
|
| 23 |
|
| 24 |
def __init__(self, name: str | None = None):
|
|
|
|
| 7 |
|
| 8 |
from fastmcp import Client, FastMCP
|
| 9 |
from fastmcp.server.context import Context
|
| 10 |
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
| 11 |
|
| 12 |
|
| 13 |
@dataclass
|
|
|
|
| 18 |
result: mcp.types.ServerResult | None
|
| 19 |
|
| 20 |
|
| 21 |
+
class RecordingMiddleware(Middleware):
|
| 22 |
"""A middleware that automatically records all method calls."""
|
| 23 |
|
| 24 |
def __init__(self, name: str | None = None):
|