Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
a52441c
1
Parent(s): 8ea5e71
Update docs and tests
Browse files- docs/servers/middleware.mdx +254 -348
- src/fastmcp/server/server.py +1 -2
- tests/resources/test_resource_manager.py +12 -4
- tests/tools/test_tool_manager.py +3 -2
docs/servers/middleware.mdx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
---
|
| 2 |
title: MCP Middleware
|
| 3 |
sidebarTitle: Middleware
|
| 4 |
-
description: Add
|
| 5 |
icon: layers
|
| 6 |
---
|
| 7 |
|
|
@@ -21,7 +21,7 @@ MCP middleware is a brand new concept and may be subject to breaking changes in
|
|
| 21 |
|
| 22 |
## What is MCP Middleware?
|
| 23 |
|
| 24 |
-
MCP middleware lets you intercept and modify MCP requests and responses as they flow through your server.
|
| 25 |
|
| 26 |
Common use cases for MCP middleware include:
|
| 27 |
- **Authentication and Authorization**: Verify client permissions before executing operations
|
|
@@ -31,17 +31,60 @@ Common use cases for MCP middleware include:
|
|
| 31 |
- **Caching**: Store frequently requested data to improve performance
|
| 32 |
- **Error Handling**: Provide consistent error responses across your server
|
| 33 |
|
| 34 |
-
## How
|
| 35 |
|
| 36 |
-
|
| 37 |
|
| 38 |
1. **Inspect the incoming request** and its context
|
| 39 |
2. **Modify the request** before passing it to the next middleware or handler
|
| 40 |
-
3. **Execute the next middleware/handler** in the chain
|
| 41 |
4. **Inspect and modify the response** before returning it
|
| 42 |
5. **Handle errors** that occur during processing
|
| 43 |
|
| 44 |
-
The middleware
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
- `on_message`: Called for all MCP messages (requests and notifications)
|
| 47 |
- `on_request`: Called specifically for MCP requests (that expect responses)
|
|
@@ -54,15 +97,152 @@ The middleware system provides specialized hooks for different MCP operations:
|
|
| 54 |
- `on_list_resource_templates`: Called when listing resource templates
|
| 55 |
- `on_list_prompts`: Called when listing available prompts
|
| 56 |
|
| 57 |
-
|
| 58 |
-
The middleware hook system is designed to be extensible. As FastMCP evolves and new MCP operations are added, additional hooks will be introduced to provide fine-grained control over new functionality.
|
| 59 |
-
</Tip>
|
| 60 |
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
```python
|
| 68 |
from fastmcp import FastMCP
|
|
@@ -75,77 +255,85 @@ class LoggingMiddleware(Middleware):
|
|
| 75 |
"""Called for all MCP messages."""
|
| 76 |
print(f"Processing {context.method} from {context.source}")
|
| 77 |
|
| 78 |
-
# Call the next middleware/handler in the chain
|
| 79 |
result = await call_next(context)
|
| 80 |
|
| 81 |
print(f"Completed {context.method}")
|
| 82 |
return result
|
| 83 |
-
|
| 84 |
-
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
| 85 |
-
"""Called specifically for tool calls."""
|
| 86 |
-
tool_name = context.message.name
|
| 87 |
-
print(f"Calling tool: {tool_name}")
|
| 88 |
-
|
| 89 |
-
result = await call_next(context)
|
| 90 |
-
|
| 91 |
-
print(f"Tool {tool_name} completed")
|
| 92 |
-
return result
|
| 93 |
|
| 94 |
# Add middleware to your server
|
| 95 |
mcp = FastMCP("MyServer")
|
| 96 |
mcp.add_middleware(LoggingMiddleware())
|
| 97 |
```
|
| 98 |
|
| 99 |
-
|
| 100 |
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
```python
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
| 116 |
```
|
| 117 |
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
|
| 123 |
-
2. **`on_request`** / **`on_notification`**: Called based on message type
|
| 124 |
-
3. **Operation-specific hooks**: Called for specific MCP operations
|
| 125 |
|
| 126 |
```python
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
print(f"Executing tool: {tool_name}")
|
| 142 |
-
return await call_next(context)
|
| 143 |
```
|
| 144 |
|
| 145 |
-
|
|
|
|
|
|
|
| 146 |
|
| 147 |
### Authentication Middleware
|
| 148 |
|
|
|
|
|
|
|
| 149 |
```python
|
| 150 |
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
| 151 |
from fastmcp.exceptions import ToolError
|
|
@@ -155,12 +343,8 @@ class AuthenticationMiddleware(Middleware):
|
|
| 155 |
self.required_token = required_token
|
| 156 |
|
| 157 |
async def on_request(self, context: MiddlewareContext, call_next):
|
| 158 |
-
"""Verify authentication for all requests."""
|
| 159 |
-
|
| 160 |
-
# Check if this is an HTTP request with headers
|
| 161 |
if hasattr(context, 'fastmcp_context') and context.fastmcp_context:
|
| 162 |
try:
|
| 163 |
-
# Access HTTP request if available
|
| 164 |
request = context.fastmcp_context.get_http_request()
|
| 165 |
auth_header = request.headers.get("Authorization")
|
| 166 |
|
|
@@ -172,7 +356,6 @@ class AuthenticationMiddleware(Middleware):
|
|
| 172 |
raise ToolError("Invalid authentication token")
|
| 173 |
|
| 174 |
except Exception:
|
| 175 |
-
# If HTTP request is not available, continue without auth
|
| 176 |
pass
|
| 177 |
|
| 178 |
return await call_next(context)
|
|
@@ -184,6 +367,8 @@ mcp.add_middleware(AuthenticationMiddleware("secret-token-123"))
|
|
| 184 |
|
| 185 |
### Performance Monitoring Middleware
|
| 186 |
|
|
|
|
|
|
|
| 187 |
```python
|
| 188 |
import time
|
| 189 |
import logging
|
|
@@ -193,7 +378,6 @@ class PerformanceMiddleware(Middleware):
|
|
| 193 |
self.logger = logging.getLogger("performance")
|
| 194 |
|
| 195 |
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
| 196 |
-
"""Monitor tool execution performance."""
|
| 197 |
tool_name = context.message.name
|
| 198 |
start_time = time.time()
|
| 199 |
|
|
@@ -215,300 +399,22 @@ class PerformanceMiddleware(Middleware):
|
|
| 215 |
raise
|
| 216 |
```
|
| 217 |
|
| 218 |
-
### Request
|
|
|
|
|
|
|
| 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 |
-
|
| 225 |
-
# Access and modify tool arguments
|
| 226 |
if hasattr(context.message, 'arguments'):
|
| 227 |
args = context.message.arguments or {}
|
| 228 |
-
|
| 229 |
-
# Example: Add a timestamp to all tool calls
|
| 230 |
args['_middleware_timestamp'] = context.timestamp.isoformat()
|
| 231 |
|
| 232 |
-
# Create a modified context
|
| 233 |
modified_context = context.copy(
|
| 234 |
message=context.message.model_copy(update={'arguments': args})
|
| 235 |
)
|
| 236 |
else:
|
| 237 |
modified_context = context
|
| 238 |
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
# Transform the result if needed
|
| 243 |
-
if hasattr(result, 'content'):
|
| 244 |
-
# Example: Add metadata to tool results
|
| 245 |
-
if result.content and len(result.content) > 0:
|
| 246 |
-
original_content = result.content[0].text
|
| 247 |
-
enhanced_content = f"[Processed at {context.timestamp}]\n{original_content}"
|
| 248 |
-
result.content[0].text = enhanced_content
|
| 249 |
-
|
| 250 |
-
return result
|
| 251 |
-
```
|
| 252 |
-
|
| 253 |
-
### Rate Limiting Middleware
|
| 254 |
-
|
| 255 |
-
```python
|
| 256 |
-
import asyncio
|
| 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)
|
| 264 |
-
self.requests = defaultdict(list) # client_id -> [timestamps]
|
| 265 |
-
|
| 266 |
-
async def on_request(self, context: MiddlewareContext, call_next):
|
| 267 |
-
"""Implement rate limiting per client."""
|
| 268 |
-
|
| 269 |
-
# Get client identifier (you may need to implement this based on your auth)
|
| 270 |
-
client_id = getattr(context.fastmcp_context, 'client_id', 'anonymous')
|
| 271 |
-
|
| 272 |
-
now = datetime.now()
|
| 273 |
-
|
| 274 |
-
# Clean old requests
|
| 275 |
-
self.requests[client_id] = [
|
| 276 |
-
timestamp for timestamp in self.requests[client_id]
|
| 277 |
-
if now - timestamp < self.window
|
| 278 |
-
]
|
| 279 |
-
|
| 280 |
-
# Check rate limit
|
| 281 |
-
if len(self.requests[client_id]) >= self.max_requests:
|
| 282 |
-
raise ToolError(
|
| 283 |
-
f"Rate limit exceeded: {self.max_requests} requests per "
|
| 284 |
-
f"{self.window.total_seconds()/60:.0f} minutes"
|
| 285 |
-
)
|
| 286 |
-
|
| 287 |
-
# Record this request
|
| 288 |
-
self.requests[client_id].append(now)
|
| 289 |
-
|
| 290 |
-
return await call_next(context)
|
| 291 |
-
```
|
| 292 |
-
|
| 293 |
-
## Adding Middleware to Your Server
|
| 294 |
-
|
| 295 |
-
### Single Middleware
|
| 296 |
-
|
| 297 |
-
```python
|
| 298 |
-
from fastmcp import FastMCP
|
| 299 |
-
|
| 300 |
-
mcp = FastMCP("MyServer")
|
| 301 |
-
|
| 302 |
-
# Add a single middleware instance
|
| 303 |
-
logging_middleware = LoggingMiddleware()
|
| 304 |
-
mcp.add_middleware(logging_middleware)
|
| 305 |
-
```
|
| 306 |
-
|
| 307 |
-
### Multiple Middleware
|
| 308 |
-
|
| 309 |
-
Middleware is executed in the order it's added to the server:
|
| 310 |
-
|
| 311 |
-
```python
|
| 312 |
-
mcp = FastMCP("MyServer")
|
| 313 |
-
|
| 314 |
-
# Add multiple middleware - they execute in order
|
| 315 |
-
mcp.add_middleware(AuthenticationMiddleware("secret-token"))
|
| 316 |
-
mcp.add_middleware(PerformanceMiddleware())
|
| 317 |
-
mcp.add_middleware(LoggingMiddleware())
|
| 318 |
-
|
| 319 |
-
# Request flow:
|
| 320 |
-
# 1. AuthenticationMiddleware.on_request()
|
| 321 |
-
# 2. PerformanceMiddleware.on_request()
|
| 322 |
-
# 3. LoggingMiddleware.on_request()
|
| 323 |
-
# 4. Actual tool/resource handler
|
| 324 |
-
# 5. LoggingMiddleware response processing
|
| 325 |
-
# 6. PerformanceMiddleware response processing
|
| 326 |
-
# 7. AuthenticationMiddleware response processing
|
| 327 |
-
```
|
| 328 |
-
|
| 329 |
-
## Advanced Patterns
|
| 330 |
-
|
| 331 |
-
### Conditional Middleware
|
| 332 |
-
|
| 333 |
-
```python
|
| 334 |
-
class ConditionalMiddleware(Middleware):
|
| 335 |
-
def __init__(self, condition_func):
|
| 336 |
-
self.should_process = condition_func
|
| 337 |
-
|
| 338 |
-
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
| 339 |
-
"""Only process certain tools."""
|
| 340 |
-
|
| 341 |
-
if not self.should_process(context.message.name):
|
| 342 |
-
# Skip processing for this tool
|
| 343 |
-
return await call_next(context)
|
| 344 |
-
|
| 345 |
-
# Apply middleware logic
|
| 346 |
-
print(f"Processing tool: {context.message.name}")
|
| 347 |
-
return await call_next(context)
|
| 348 |
-
|
| 349 |
-
# Usage
|
| 350 |
-
def only_expensive_tools(tool_name: str) -> bool:
|
| 351 |
-
return tool_name in ["complex_analysis", "heavy_computation"]
|
| 352 |
-
|
| 353 |
-
mcp.add_middleware(ConditionalMiddleware(only_expensive_tools))
|
| 354 |
-
```
|
| 355 |
-
|
| 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()
|
| 363 |
-
|
| 364 |
-
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
| 365 |
-
"""Track usage statistics."""
|
| 366 |
-
self.call_count += 1
|
| 367 |
-
self.tools_used.add(context.message.name)
|
| 368 |
-
|
| 369 |
-
print(f"Total calls: {self.call_count}, Unique tools: {len(self.tools_used)}")
|
| 370 |
-
|
| 371 |
-
return await call_next(context)
|
| 372 |
-
|
| 373 |
-
def get_stats(self):
|
| 374 |
-
return {
|
| 375 |
-
"total_calls": self.call_count,
|
| 376 |
-
"unique_tools": len(self.tools_used),
|
| 377 |
-
"tools_used": list(self.tools_used)
|
| 378 |
-
}
|
| 379 |
-
```
|
| 380 |
-
|
| 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:
|
| 388 |
-
return await call_next(context)
|
| 389 |
-
except ToolError:
|
| 390 |
-
# Re-raise ToolErrors as-is
|
| 391 |
-
raise
|
| 392 |
-
except Exception as e:
|
| 393 |
-
# Log the error and convert to a user-friendly message
|
| 394 |
-
logging.error(f"Tool {context.message.name} failed: {e}")
|
| 395 |
-
raise ToolError(f"Tool execution failed: {str(e)}")
|
| 396 |
-
```
|
| 397 |
-
|
| 398 |
-
## Best Practices
|
| 399 |
-
|
| 400 |
-
### Performance Considerations
|
| 401 |
-
|
| 402 |
-
1. **Keep middleware lightweight**: Avoid heavy computations in middleware
|
| 403 |
-
2. **Use async operations**: Don't block the event loop with synchronous operations
|
| 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 |
-
|
| 411 |
-
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
| 412 |
-
"""Example of efficient middleware with caching."""
|
| 413 |
-
|
| 414 |
-
# Check cache first
|
| 415 |
-
cache_key = f"{context.message.name}:{hash(str(context.message.arguments))}"
|
| 416 |
-
|
| 417 |
-
if cache_key in self._cache:
|
| 418 |
-
print("Returning cached result")
|
| 419 |
-
return self._cache[cache_key]
|
| 420 |
-
|
| 421 |
-
# Execute and cache result
|
| 422 |
-
result = await call_next(context)
|
| 423 |
-
self._cache[cache_key] = result
|
| 424 |
-
|
| 425 |
-
return result
|
| 426 |
-
```
|
| 427 |
-
|
| 428 |
-
### Error Handling
|
| 429 |
-
|
| 430 |
-
1. **Always call `call_next`**: Unless you're intentionally stopping the chain
|
| 431 |
-
2. **Handle exceptions appropriately**: Don't let middleware errors break the entire request
|
| 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:
|
| 439 |
-
# Middleware logic here
|
| 440 |
-
return await call_next(context)
|
| 441 |
-
except ToolError:
|
| 442 |
-
# Client-facing errors should be re-raised
|
| 443 |
-
raise
|
| 444 |
-
except Exception as e:
|
| 445 |
-
# Log internal errors but don't expose details
|
| 446 |
-
logging.error(f"Middleware error: {e}")
|
| 447 |
-
# Optionally continue without middleware processing
|
| 448 |
-
return await call_next(context)
|
| 449 |
-
```
|
| 450 |
-
|
| 451 |
-
### Testing Middleware
|
| 452 |
-
|
| 453 |
-
```python
|
| 454 |
-
import pytest
|
| 455 |
-
from fastmcp import FastMCP, Client
|
| 456 |
-
|
| 457 |
-
@pytest.mark.asyncio
|
| 458 |
-
async def test_logging_middleware():
|
| 459 |
-
"""Test middleware functionality."""
|
| 460 |
-
|
| 461 |
-
# Create server with middleware
|
| 462 |
-
mcp = FastMCP("TestServer")
|
| 463 |
-
logging_middleware = LoggingMiddleware()
|
| 464 |
-
mcp.add_middleware(logging_middleware)
|
| 465 |
-
|
| 466 |
-
@mcp.tool
|
| 467 |
-
def test_tool(x: int) -> int:
|
| 468 |
-
return x * 2
|
| 469 |
-
|
| 470 |
-
# Test with client
|
| 471 |
-
async with Client(mcp) as client:
|
| 472 |
-
result = await client.call_tool("test_tool", {"x": 5})
|
| 473 |
-
assert result == 10
|
| 474 |
-
|
| 475 |
-
# Verify middleware was called
|
| 476 |
-
# (You'll need to add tracking to your middleware for testing)
|
| 477 |
-
```
|
| 478 |
-
|
| 479 |
-
## Server Composition and Middleware
|
| 480 |
-
|
| 481 |
-
When using [Server Composition](/servers/composition) with `mount` or `import_server`, middleware behavior follows these rules:
|
| 482 |
-
|
| 483 |
-
1. **Parent server middleware** runs for all requests, including those routed to mounted servers
|
| 484 |
-
2. **Mounted server middleware** only runs for requests handled by that specific server
|
| 485 |
-
3. **Middleware order** is preserved within each server
|
| 486 |
-
|
| 487 |
-
```python
|
| 488 |
-
# Parent server with middleware
|
| 489 |
-
parent = FastMCP("Parent")
|
| 490 |
-
parent.add_middleware(AuthenticationMiddleware("token"))
|
| 491 |
-
|
| 492 |
-
# Child server with its own middleware
|
| 493 |
-
child = FastMCP("Child")
|
| 494 |
-
child.add_middleware(LoggingMiddleware())
|
| 495 |
-
|
| 496 |
-
@child.tool
|
| 497 |
-
def child_tool() -> str:
|
| 498 |
-
return "from child"
|
| 499 |
-
|
| 500 |
-
# Mount the child server
|
| 501 |
-
parent.mount(child, prefix="child")
|
| 502 |
-
|
| 503 |
-
# Request to "child_tool" will:
|
| 504 |
-
# 1. Run parent's AuthenticationMiddleware
|
| 505 |
-
# 2. Route to child server
|
| 506 |
-
# 3. Run child's LoggingMiddleware
|
| 507 |
-
# 4. Execute child_tool
|
| 508 |
-
```
|
| 509 |
-
|
| 510 |
-
This allows you to create layered middleware architectures where parent servers handle cross-cutting concerns like authentication, while child servers focus on domain-specific middleware.
|
| 511 |
-
|
| 512 |
-
<Tip>
|
| 513 |
-
When debugging middleware issues in composed servers, remember that both parent and child middleware may be executing. Use detailed logging to trace the middleware execution path.
|
| 514 |
-
</Tip>
|
|
|
|
| 1 |
---
|
| 2 |
title: MCP Middleware
|
| 3 |
sidebarTitle: Middleware
|
| 4 |
+
description: Add cross-cutting functionality to your MCP server with middleware that can inspect, modify, and respond to all MCP requests and responses.
|
| 5 |
icon: layers
|
| 6 |
---
|
| 7 |
|
|
|
|
| 21 |
|
| 22 |
## What is MCP Middleware?
|
| 23 |
|
| 24 |
+
MCP middleware lets you intercept and modify MCP requests and responses as they flow through your server. Think of it as a pipeline where each piece of middleware can inspect what's happening, make changes, and then pass control to the next middleware in the chain.
|
| 25 |
|
| 26 |
Common use cases for MCP middleware include:
|
| 27 |
- **Authentication and Authorization**: Verify client permissions before executing operations
|
|
|
|
| 31 |
- **Caching**: Store frequently requested data to improve performance
|
| 32 |
- **Error Handling**: Provide consistent error responses across your server
|
| 33 |
|
| 34 |
+
## How Middleware Works
|
| 35 |
|
| 36 |
+
FastMCP middleware operates on a pipeline model. When a request comes in, it flows through your middleware in the order they were added to the server. Each middleware can:
|
| 37 |
|
| 38 |
1. **Inspect the incoming request** and its context
|
| 39 |
2. **Modify the request** before passing it to the next middleware or handler
|
| 40 |
+
3. **Execute the next middleware/handler** in the chain by calling `call_next()`
|
| 41 |
4. **Inspect and modify the response** before returning it
|
| 42 |
5. **Handle errors** that occur during processing
|
| 43 |
|
| 44 |
+
The key insight is that middleware forms a chain where each piece decides whether to continue processing or stop the chain entirely.
|
| 45 |
+
|
| 46 |
+
If you're familiar with ASGI middleware, the basic structure of FastMCP middleware will feel familiar. At its core, middleware is a callable class that receives a context object containing information about the current JSON-RPC message and a handler function to continue the middleware chain.
|
| 47 |
+
|
| 48 |
+
It's important to understand that MCP operates on the [JSON-RPC specification](https://spec.modelcontextprotocol.io/specification/basic/transports/). While FastMCP presents requests and responses in a familiar way, these are fundamentally JSON-RPC messages, not HTTP request/response pairs like you might be used to in web applications. FastMCP middleware works with all [transport types](/clients/transports), including local stdio transport and HTTP transports, though not all middleware implementations are compatible across all transports (e.g., middleware that inspects HTTP headers won't work with stdio transport).
|
| 49 |
+
|
| 50 |
+
The most fundamental way to implement middleware is by overriding the `__call__` method on the `Middleware` base class:
|
| 51 |
+
|
| 52 |
+
```python
|
| 53 |
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
| 54 |
+
|
| 55 |
+
class RawMiddleware(Middleware):
|
| 56 |
+
async def __call__(self, context: MiddlewareContext, call_next):
|
| 57 |
+
# This method receives ALL messages regardless of type
|
| 58 |
+
print(f"Raw middleware processing: {context.method}")
|
| 59 |
+
result = await call_next(context)
|
| 60 |
+
print(f"Raw middleware completed: {context.method}")
|
| 61 |
+
return result
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
This gives you complete control over every message that flows through your server, but requires you to handle all message types manually.
|
| 65 |
+
|
| 66 |
+
## Middleware Hooks
|
| 67 |
+
|
| 68 |
+
To make it easier for users to target specific types of messages, FastMCP middleware provides a variety of specialized hooks. Instead of implementing the raw `__call__` method, you can override specific hook methods that are called only for certain types of operations, allowing you to target exactly the level of specificity you need for your middleware logic.
|
| 69 |
+
|
| 70 |
+
### Hook Hierarchy and Execution Order
|
| 71 |
+
|
| 72 |
+
FastMCP provides multiple hooks that are called with varying levels of specificity. Understanding this hierarchy is crucial for effective middleware design.
|
| 73 |
+
|
| 74 |
+
When a request comes in, **multiple hooks may be called for the same request**, going from general to specific:
|
| 75 |
+
|
| 76 |
+
1. **`on_message`** - Called for ALL MCP messages (both requests and notifications)
|
| 77 |
+
2. **`on_request` or `on_notification`** - Called based on the message type
|
| 78 |
+
3. **Operation-specific hooks** - Called for specific MCP operations like `on_call_tool`
|
| 79 |
+
|
| 80 |
+
For example, when a client calls a tool, your middleware will receive **three separate hook calls**:
|
| 81 |
+
1. First: `on_message` (because it's any MCP message)
|
| 82 |
+
2. Second: `on_request` (because tool calls expect responses)
|
| 83 |
+
3. Third: `on_call_tool` (because it's specifically a tool execution)
|
| 84 |
+
|
| 85 |
+
This hierarchy allows you to target your middleware logic with the right level of specificity. Use `on_message` for broad concerns like logging, `on_request` for authentication, and `on_call_tool` for tool-specific logic like performance monitoring.
|
| 86 |
+
|
| 87 |
+
### Available Hooks
|
| 88 |
|
| 89 |
- `on_message`: Called for all MCP messages (requests and notifications)
|
| 90 |
- `on_request`: Called specifically for MCP requests (that expect responses)
|
|
|
|
| 97 |
- `on_list_resource_templates`: Called when listing resource templates
|
| 98 |
- `on_list_prompts`: Called when listing available prompts
|
| 99 |
|
| 100 |
+
## Component Access in Middleware
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
Understanding how to access component information (tools, resources, prompts) in middleware is crucial for building powerful middleware functionality. The access patterns differ significantly between listing operations and execution operations.
|
| 103 |
+
|
| 104 |
+
### Listing Operations vs Execution Operations
|
| 105 |
+
|
| 106 |
+
FastMCP middleware handles two types of operations differently:
|
| 107 |
+
|
| 108 |
+
**Listing Operations** (`on_list_tools`, `on_list_resources`, `on_list_prompts`, etc.):
|
| 109 |
+
- Middleware receives **FastMCP component objects** with full metadata
|
| 110 |
+
- These objects include FastMCP-specific properties like `tags` that aren't part of the MCP specification
|
| 111 |
+
- The result contains complete component information before it's converted to MCP format
|
| 112 |
+
- Tags and other metadata are stripped when finally returned to the MCP client
|
| 113 |
+
|
| 114 |
+
**Execution Operations** (`on_call_tool`, `on_read_resource`, `on_get_prompt`):
|
| 115 |
+
- Middleware runs **before** the component is executed
|
| 116 |
+
- The middleware result is either the execution result or an error if the component wasn't found
|
| 117 |
+
- Component metadata isn't directly available in the hook parameters
|
| 118 |
+
|
| 119 |
+
### Accessing Component Metadata During Execution
|
| 120 |
+
|
| 121 |
+
If you need to check component properties (like tags) during execution operations, use the FastMCP server instance available through the context:
|
| 122 |
+
|
| 123 |
+
```python
|
| 124 |
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
| 125 |
+
from fastmcp.exceptions import ToolError
|
| 126 |
+
|
| 127 |
+
class TagBasedMiddleware(Middleware):
|
| 128 |
+
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
| 129 |
+
# Access the tool object to check its metadata
|
| 130 |
+
if context.fastmcp_context:
|
| 131 |
+
try:
|
| 132 |
+
tool = await context.fastmcp_context.fastmcp.get_tool(context.message.name)
|
| 133 |
+
|
| 134 |
+
# Check if this tool has a "private" tag
|
| 135 |
+
if "private" in tool.tags:
|
| 136 |
+
raise ToolError("Access denied: private tool")
|
| 137 |
+
|
| 138 |
+
# Check if tool is enabled
|
| 139 |
+
if not tool.enabled:
|
| 140 |
+
raise ToolError("Tool is currently disabled")
|
| 141 |
+
|
| 142 |
+
except Exception:
|
| 143 |
+
# Tool not found or other error - let execution continue
|
| 144 |
+
# and handle the error naturally
|
| 145 |
+
pass
|
| 146 |
+
|
| 147 |
+
return await call_next(context)
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
The same pattern works for resources and prompts:
|
| 151 |
+
|
| 152 |
+
```python
|
| 153 |
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
| 154 |
+
from fastmcp.exceptions import ResourceError, PromptError
|
| 155 |
+
|
| 156 |
+
class ComponentAccessMiddleware(Middleware):
|
| 157 |
+
async def on_read_resource(self, context: MiddlewareContext, call_next):
|
| 158 |
+
if context.fastmcp_context:
|
| 159 |
+
try:
|
| 160 |
+
resource = await context.fastmcp_context.fastmcp.get_resource(context.message.uri)
|
| 161 |
+
if "restricted" in resource.tags:
|
| 162 |
+
raise ResourceError("Access denied: restricted resource")
|
| 163 |
+
except Exception:
|
| 164 |
+
pass
|
| 165 |
+
return await call_next(context)
|
| 166 |
+
|
| 167 |
+
async def on_get_prompt(self, context: MiddlewareContext, call_next):
|
| 168 |
+
if context.fastmcp_context:
|
| 169 |
+
try:
|
| 170 |
+
prompt = await context.fastmcp_context.fastmcp.get_prompt(context.message.name)
|
| 171 |
+
if not prompt.enabled:
|
| 172 |
+
raise PromptError("Prompt is currently disabled")
|
| 173 |
+
except Exception:
|
| 174 |
+
pass
|
| 175 |
+
return await call_next(context)
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
### Working with Listing Results
|
| 179 |
+
|
| 180 |
+
For listing operations, you can inspect and modify the FastMCP components directly:
|
| 181 |
+
|
| 182 |
+
```python
|
| 183 |
+
from fastmcp.server.middleware import Middleware, MiddlewareContext, ListToolsResult
|
| 184 |
+
|
| 185 |
+
class ListingFilterMiddleware(Middleware):
|
| 186 |
+
async def on_list_tools(self, context: MiddlewareContext, call_next):
|
| 187 |
+
result = await call_next(context)
|
| 188 |
+
|
| 189 |
+
# Filter out tools with "private" tag
|
| 190 |
+
filtered_tools = {
|
| 191 |
+
name: tool for name, tool in result.tools.items()
|
| 192 |
+
if "private" not in tool.tags
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
# Return modified result
|
| 196 |
+
return ListToolsResult(tools=filtered_tools)
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
This filtering happens before the components are converted to MCP format and returned to the client, so the tags (which are FastMCP-specific) are naturally stripped in the final response.
|
| 200 |
+
|
| 201 |
+
### Anatomy of a Hook
|
| 202 |
+
|
| 203 |
+
Every middleware hook follows the same pattern. Let's examine the `on_message` hook to understand the structure:
|
| 204 |
|
| 205 |
+
```python
|
| 206 |
+
async def on_message(self, context: MiddlewareContext, call_next):
|
| 207 |
+
# 1. Pre-processing: Inspect and optionally modify the request
|
| 208 |
+
print(f"Processing {context.method}")
|
| 209 |
+
|
| 210 |
+
# 2. Chain continuation: Call the next middleware/handler
|
| 211 |
+
result = await call_next(context)
|
| 212 |
+
|
| 213 |
+
# 3. Post-processing: Inspect and optionally modify the response
|
| 214 |
+
print(f"Completed {context.method}")
|
| 215 |
+
|
| 216 |
+
# 4. Return the result (potentially modified)
|
| 217 |
+
return result
|
| 218 |
+
```
|
| 219 |
+
|
| 220 |
+
### Hook Parameters
|
| 221 |
+
|
| 222 |
+
Every hook receives two parameters:
|
| 223 |
+
|
| 224 |
+
1. **`context: MiddlewareContext`** - Contains information about the current request:
|
| 225 |
+
- `context.method` - The MCP method name (e.g., "tools/call")
|
| 226 |
+
- `context.source` - Where the request came from ("client" or "server")
|
| 227 |
+
- `context.type` - Message type ("request" or "notification")
|
| 228 |
+
- `context.message` - The MCP message data
|
| 229 |
+
- `context.timestamp` - When the request was received
|
| 230 |
+
- `context.fastmcp_context` - FastMCP Context object (if available)
|
| 231 |
+
|
| 232 |
+
2. **`call_next`** - A function that continues the middleware chain. You **must** call this to proceed, unless you want to stop processing entirely.
|
| 233 |
|
| 234 |
+
### Control Flow
|
| 235 |
+
|
| 236 |
+
You have complete control over the request flow:
|
| 237 |
+
- **Continue processing**: Call `await call_next(context)` to proceed
|
| 238 |
+
- **Modify the request**: Change the context before calling `call_next`
|
| 239 |
+
- **Modify the response**: Change the result after calling `call_next`
|
| 240 |
+
- **Stop the chain**: Don't call `call_next` (rarely needed)
|
| 241 |
+
- **Handle errors**: Wrap `call_next` in try/catch blocks
|
| 242 |
+
|
| 243 |
+
## Creating Middleware
|
| 244 |
+
|
| 245 |
+
FastMCP middleware is implemented by subclassing the `Middleware` base class and overriding the hooks you need. You only need to implement the hooks that are relevant to your use case.
|
| 246 |
|
| 247 |
```python
|
| 248 |
from fastmcp import FastMCP
|
|
|
|
| 255 |
"""Called for all MCP messages."""
|
| 256 |
print(f"Processing {context.method} from {context.source}")
|
| 257 |
|
|
|
|
| 258 |
result = await call_next(context)
|
| 259 |
|
| 260 |
print(f"Completed {context.method}")
|
| 261 |
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
# Add middleware to your server
|
| 264 |
mcp = FastMCP("MyServer")
|
| 265 |
mcp.add_middleware(LoggingMiddleware())
|
| 266 |
```
|
| 267 |
|
| 268 |
+
This creates a basic logging middleware that will print information about every request that flows through your server.
|
| 269 |
|
| 270 |
+
## Adding Middleware to Your Server
|
| 271 |
+
|
| 272 |
+
### Single Middleware
|
| 273 |
+
|
| 274 |
+
Adding middleware to your server is straightforward:
|
| 275 |
|
| 276 |
```python
|
| 277 |
+
mcp = FastMCP("MyServer")
|
| 278 |
+
mcp.add_middleware(LoggingMiddleware())
|
| 279 |
+
```
|
| 280 |
+
|
| 281 |
+
### Multiple Middleware
|
| 282 |
+
|
| 283 |
+
Middleware executes in the order it's added to the server. The first middleware added runs first on the way in, and last on the way out:
|
| 284 |
+
|
| 285 |
+
```python
|
| 286 |
+
mcp = FastMCP("MyServer")
|
| 287 |
+
|
| 288 |
+
mcp.add_middleware(AuthenticationMiddleware("secret-token"))
|
| 289 |
+
mcp.add_middleware(PerformanceMiddleware())
|
| 290 |
+
mcp.add_middleware(LoggingMiddleware())
|
| 291 |
```
|
| 292 |
|
| 293 |
+
This creates the following execution flow:
|
| 294 |
+
1. AuthenticationMiddleware (pre-processing)
|
| 295 |
+
2. PerformanceMiddleware (pre-processing)
|
| 296 |
+
3. LoggingMiddleware (pre-processing)
|
| 297 |
+
4. Actual tool/resource handler
|
| 298 |
+
5. LoggingMiddleware (post-processing)
|
| 299 |
+
6. PerformanceMiddleware (post-processing)
|
| 300 |
+
7. AuthenticationMiddleware (post-processing)
|
| 301 |
|
| 302 |
+
## Server Composition and Middleware
|
| 303 |
+
|
| 304 |
+
When using [Server Composition](/servers/composition) with `mount` or `import_server`, middleware behavior follows these rules:
|
| 305 |
+
|
| 306 |
+
1. **Parent server middleware** runs for all requests, including those routed to mounted servers
|
| 307 |
+
2. **Mounted server middleware** only runs for requests handled by that specific server
|
| 308 |
+
3. **Middleware order** is preserved within each server
|
| 309 |
|
| 310 |
+
This allows you to create layered middleware architectures where parent servers handle cross-cutting concerns like authentication, while child servers focus on domain-specific middleware.
|
|
|
|
|
|
|
| 311 |
|
| 312 |
```python
|
| 313 |
+
# Parent server with middleware
|
| 314 |
+
parent = FastMCP("Parent")
|
| 315 |
+
parent.add_middleware(AuthenticationMiddleware("token"))
|
| 316 |
+
|
| 317 |
+
# Child server with its own middleware
|
| 318 |
+
child = FastMCP("Child")
|
| 319 |
+
child.add_middleware(LoggingMiddleware())
|
| 320 |
+
|
| 321 |
+
@child.tool
|
| 322 |
+
def child_tool() -> str:
|
| 323 |
+
return "from child"
|
| 324 |
+
|
| 325 |
+
# Mount the child server
|
| 326 |
+
parent.mount(child, prefix="child")
|
|
|
|
|
|
|
| 327 |
```
|
| 328 |
|
| 329 |
+
When a client calls "child_tool", the request will flow through the parent's authentication middleware first, then route to the child server where it will go through the child's logging middleware.
|
| 330 |
+
|
| 331 |
+
## Examples
|
| 332 |
|
| 333 |
### Authentication Middleware
|
| 334 |
|
| 335 |
+
This middleware checks for a valid authorization token on all requests:
|
| 336 |
+
|
| 337 |
```python
|
| 338 |
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
| 339 |
from fastmcp.exceptions import ToolError
|
|
|
|
| 343 |
self.required_token = required_token
|
| 344 |
|
| 345 |
async def on_request(self, context: MiddlewareContext, call_next):
|
|
|
|
|
|
|
|
|
|
| 346 |
if hasattr(context, 'fastmcp_context') and context.fastmcp_context:
|
| 347 |
try:
|
|
|
|
| 348 |
request = context.fastmcp_context.get_http_request()
|
| 349 |
auth_header = request.headers.get("Authorization")
|
| 350 |
|
|
|
|
| 356 |
raise ToolError("Invalid authentication token")
|
| 357 |
|
| 358 |
except Exception:
|
|
|
|
| 359 |
pass
|
| 360 |
|
| 361 |
return await call_next(context)
|
|
|
|
| 367 |
|
| 368 |
### Performance Monitoring Middleware
|
| 369 |
|
| 370 |
+
This middleware tracks how long tools take to execute:
|
| 371 |
+
|
| 372 |
```python
|
| 373 |
import time
|
| 374 |
import logging
|
|
|
|
| 378 |
self.logger = logging.getLogger("performance")
|
| 379 |
|
| 380 |
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
|
|
|
| 381 |
tool_name = context.message.name
|
| 382 |
start_time = time.time()
|
| 383 |
|
|
|
|
| 399 |
raise
|
| 400 |
```
|
| 401 |
|
| 402 |
+
### Request Transformation Middleware
|
| 403 |
+
|
| 404 |
+
This middleware adds metadata to tool calls:
|
| 405 |
|
| 406 |
```python
|
| 407 |
class TransformationMiddleware(Middleware):
|
| 408 |
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
|
|
|
|
|
|
|
|
|
| 409 |
if hasattr(context.message, 'arguments'):
|
| 410 |
args = context.message.arguments or {}
|
|
|
|
|
|
|
| 411 |
args['_middleware_timestamp'] = context.timestamp.isoformat()
|
| 412 |
|
|
|
|
| 413 |
modified_context = context.copy(
|
| 414 |
message=context.message.model_copy(update={'arguments': args})
|
| 415 |
)
|
| 416 |
else:
|
| 417 |
modified_context = context
|
| 418 |
|
| 419 |
+
return await call_next(modified_context)
|
| 420 |
+
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/fastmcp/server/server.py
CHANGED
|
@@ -42,7 +42,6 @@ from starlette.routing import BaseRoute, Route
|
|
| 42 |
|
| 43 |
import fastmcp
|
| 44 |
import fastmcp.server
|
| 45 |
-
import fastmcp.server.middleware
|
| 46 |
from fastmcp.exceptions import DisabledError, NotFoundError
|
| 47 |
from fastmcp.prompts import Prompt, PromptManager
|
| 48 |
from fastmcp.prompts.prompt import FunctionPrompt
|
|
@@ -917,7 +916,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,
|
|
|
|
| 42 |
|
| 43 |
import fastmcp
|
| 44 |
import fastmcp.server
|
|
|
|
| 45 |
from fastmcp.exceptions import DisabledError, NotFoundError
|
| 46 |
from fastmcp.prompts import Prompt, PromptManager
|
| 47 |
from fastmcp.prompts.prompt import FunctionPrompt
|
|
|
|
| 916 |
Args:
|
| 917 |
template: A ResourceTemplate instance to add
|
| 918 |
"""
|
| 919 |
+
self._resource_manager.add_template(template)
|
| 920 |
|
| 921 |
def add_resource_fn(
|
| 922 |
self,
|
tests/resources/test_resource_manager.py
CHANGED
|
@@ -464,7 +464,9 @@ class TestCustomResourceKeys:
|
|
| 464 |
fn=get_data,
|
| 465 |
)
|
| 466 |
|
| 467 |
-
|
|
|
|
|
|
|
| 468 |
|
| 469 |
# Resource should be accessible via custom key
|
| 470 |
assert custom_key in manager._resources
|
|
@@ -489,7 +491,9 @@ class TestCustomResourceKeys:
|
|
| 489 |
name="test_template",
|
| 490 |
)
|
| 491 |
|
| 492 |
-
|
|
|
|
|
|
|
| 493 |
|
| 494 |
# Template should be accessible via custom key
|
| 495 |
assert custom_key in manager._templates
|
|
@@ -514,7 +518,9 @@ class TestCustomResourceKeys:
|
|
| 514 |
fn=get_data,
|
| 515 |
)
|
| 516 |
|
| 517 |
-
|
|
|
|
|
|
|
| 518 |
|
| 519 |
# Should be retrievable by the custom key
|
| 520 |
retrieved = await manager.get_resource(custom_key)
|
|
@@ -541,7 +547,9 @@ class TestCustomResourceKeys:
|
|
| 541 |
name="custom_greeter",
|
| 542 |
)
|
| 543 |
|
| 544 |
-
|
|
|
|
|
|
|
| 545 |
|
| 546 |
# Using a URI that matches the custom key pattern
|
| 547 |
resource = await manager.get_resource("custom://greet/world")
|
|
|
|
| 464 |
fn=get_data,
|
| 465 |
)
|
| 466 |
|
| 467 |
+
# Use with_key to create a new resource with the custom key
|
| 468 |
+
resource_with_custom_key = resource.with_key(custom_key)
|
| 469 |
+
manager.add_resource(resource_with_custom_key)
|
| 470 |
|
| 471 |
# Resource should be accessible via custom key
|
| 472 |
assert custom_key in manager._resources
|
|
|
|
| 491 |
name="test_template",
|
| 492 |
)
|
| 493 |
|
| 494 |
+
# Use with_key to create a new template with the custom key
|
| 495 |
+
template_with_custom_key = template.with_key(custom_key)
|
| 496 |
+
manager.add_template(template_with_custom_key)
|
| 497 |
|
| 498 |
# Template should be accessible via custom key
|
| 499 |
assert custom_key in manager._templates
|
|
|
|
| 518 |
fn=get_data,
|
| 519 |
)
|
| 520 |
|
| 521 |
+
# Use with_key to create a new resource with the custom key
|
| 522 |
+
resource_with_custom_key = resource.with_key(custom_key)
|
| 523 |
+
manager.add_resource(resource_with_custom_key)
|
| 524 |
|
| 525 |
# Should be retrievable by the custom key
|
| 526 |
retrieved = await manager.get_resource(custom_key)
|
|
|
|
| 547 |
name="custom_greeter",
|
| 548 |
)
|
| 549 |
|
| 550 |
+
# Use with_key to create a new template with the custom key
|
| 551 |
+
template_with_custom_key = template.with_key(custom_key)
|
| 552 |
+
manager.add_template(template_with_custom_key)
|
| 553 |
|
| 554 |
# Using a URI that matches the custom key pattern
|
| 555 |
resource = await manager.get_resource("custom://greet/world")
|
tests/tools/test_tool_manager.py
CHANGED
|
@@ -760,8 +760,9 @@ class TestCustomToolNames:
|
|
| 760 |
# Create a tool with a specific name
|
| 761 |
tool = Tool.from_function(fn, name="my_tool")
|
| 762 |
manager = ToolManager()
|
| 763 |
-
#
|
| 764 |
-
|
|
|
|
| 765 |
# The tool is accessible under the key
|
| 766 |
stored = await manager.get_tool("proxy_tool")
|
| 767 |
assert stored is not None
|
|
|
|
| 760 |
# Create a tool with a specific name
|
| 761 |
tool = Tool.from_function(fn, name="my_tool")
|
| 762 |
manager = ToolManager()
|
| 763 |
+
# Use with_key to create a new tool with the custom key
|
| 764 |
+
tool_with_custom_key = tool.with_key("proxy_tool")
|
| 765 |
+
manager.add_tool(tool_with_custom_key)
|
| 766 |
# The tool is accessible under the key
|
| 767 |
stored = await manager.get_tool("proxy_tool")
|
| 768 |
assert stored is not None
|