Jeremiah Lowin commited on
Commit
a52441c
·
1 Parent(s): 8ea5e71

Update docs and tests

Browse files
docs/servers/middleware.mdx CHANGED
@@ -1,7 +1,7 @@
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
 
@@ -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. Unlike traditional HTTP middleware that operates on request/response pairs, MCP middleware is aware of the specific MCP protocol operations and can provide targeted hooks for different types of interactions.
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 MCP Middleware Works
35
 
36
- MCP middleware operates on a pipeline model where 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
41
  4. **Inspect and modify the response** before returning it
42
  5. **Handle errors** that occur during processing
43
 
44
- The middleware system provides specialized hooks for different MCP operations:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <Tip>
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
- ## Creating 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
@@ -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
- ### Middleware Context
100
 
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"
108
- source = context.source # "client" or "server"
109
- message_type = context.type # "request" or "notification"
110
- timestamp = context.timestamp # When the request was received
111
- message = context.message # The actual MCP message
112
- fastmcp_context = context.fastmcp_context # FastMCP Context object (if available)
113
-
114
- # Continue processing
115
- return await call_next(context)
 
 
116
  ```
117
 
118
- ### Middleware Hooks
 
 
 
 
 
 
 
119
 
120
- Each middleware hook receives a `MiddlewareContext` and a `call_next` function. The hooks are organized in a hierarchy:
 
 
 
 
 
 
121
 
122
- 1. **`on_message`**: The broadest hook, called for all MCP messages
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
- 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}")
131
- return await call_next(context)
132
-
133
- async def on_request(self, context: MiddlewareContext, call_next):
134
- """Called only for requests (messages that expect responses)."""
135
- print(f"Request: {context.method}")
136
- return await call_next(context)
137
-
138
- async def on_call_tool(self, context: MiddlewareContext, call_next):
139
- """Called only for tool execution requests."""
140
- tool_name = context.message.name
141
- print(f"Executing tool: {tool_name}")
142
- return await call_next(context)
143
  ```
144
 
145
- ## Middleware Examples
 
 
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/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
-
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
- # Execute with modified context
240
- result = await call_next(modified_context)
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, key=key)
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
- manager.add_resource(resource, key=custom_key)
 
 
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
- manager.add_template(template, key=custom_key)
 
 
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
- manager.add_resource(resource, key=custom_key)
 
 
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
- manager.add_template(template, key=custom_key)
 
 
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
- # Store it under a different name
764
- manager.add_tool(tool, key="proxy_tool")
 
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