Files
mcp-use__mcp-use/docs/python/client/middleware.mdx
Pietro Zullo 8348c59a53 docs: reorganize, fix accuracy, and update for agent readability (#1502)
* docs: reorganize navigation and clean up content

- Remove 'Misc' group with misplaced blog posts
- Remove 'Libraries' redirect group from Home nav
- Simplify 'Dev Tools' group (flatten Inspector, CLI, Tunneling)
- Remove individual changelog versions from Python nav (keep summary)
- Remove changelog from TypeScript 'Get Started' group
- Remove tunneling duplicate from Python Development group
- Rewrite mcp101 as clean MCP concepts page (was pitch deck)
- Delete typescript/blog posts (not technical docs)
- Add redirects for deleted blog post URLs

* docs: fix content accuracy and remove sloppy language

- Fix broken python/server/mcp-apps link in home page
- Remove informal pitch-speak from python/server/index.mdx
- Fix grammar error in typescript/server/index.mdx
- Update typescript/client/logging.mdx: remove fake 'coming soon' API code
- Fix non-existent model name gpt-5.1 in typescript quickstart
- Fix mcp_use naming in development contributing guide

* docs: fix incorrect TypeScript server API calls in inspector docs

- Replace server.addTool() with server.tool() (correct mcp-use API)
- Add missing zod import to debugging-chatgpt-apps example

* docs: fix fake model names (gpt-5.1 → gpt-4o) in agent docs

* docs: update model names, fix APIs, and add agent visibility hints

- Replace all deprecated model names with latest: gpt-4.1, gpt-4.1-mini,
  claude-sonnet-4-6, claude-opus-4-7, gemini-2.5-flash across all docs
- Fix TypeScript server.tool() API in installation.mdx (old positional string form)
- Fix TypeScript agent.run() in quickstart.mdx (deprecated string form → object)
- Remove broken manufact.com/docs/llms.txt hidden hints from 5 pages
- Add <Visibility for="agents"> block to mcp101.mdx with install commands,
  key classes, and API signatures for agent-oriented navigation
- Add LangChain provider install note to Python quickstart (mcp-use agent
  requires a separate langchain-* provider package)
- Update model name cards in Python llm-integration.mdx

* docs: clean up remaining stale model names and improve accuracy

- Update supported model lists in Python and TypeScript llm-integration pages
- Fix cost optimization table to use current model IDs
- Update TypeScript agent overview supported providers list

* docs: add LangChain provider install to TypeScript quickstart agent section

The MCPAgent requires a separate @langchain/* provider package; this was
not mentioned before the code example that imports from @langchain/openai.

* docs: update to latest flagship model IDs (May 2026)

- OpenAI: gpt-4.1 → gpt-5.5, gpt-4.1-mini → gpt-5.5-mini
- Google: gemini-2.5-flash → gemini-3.1-flash, gemini-2.5-pro → gemini-3.1-pro
- Groq: llama-3.1-* → llama-4-scout-17b-16e-instruct
- Anthropic (claude-opus-4-7, claude-sonnet-4-6) already current, no change

* docs: replace all em dashes with hyphens

* docs: restore llms.txt hints using Visibility component

* docs: simplify TypeScript logging page to unsupported notice only

* docs: rewrite mcp101 to explain MCP protocol with mcp-use examples and doc links

* docs: remove goal.md

* docs: revert nav changes, keep only Misc group removal and blog redirects

* docs: reorder mcp101 to Server/App -> Client/Inspector -> Agent, restore title

* docs: add code examples for Resources and Prompts in mcp101

* docs: remove em dashes from mcp101

* docs: add MCPClient code examples to mcp101

* docs: add React client example to mcp101

* docs: add agent code examples to mcp101

* docs: expand MCP Apps section in mcp101 with example and links

* docs: restore Contacts, Socials, and Founders sections to mcp101

* docs: add context-setting intro paragraph to mcp101 for new readers

* docs: add MCP stores section to mcp101 intro with Claude, ChatGPT, Cursor

* docs: fix Claude store link

* docs: fix ChatGPT store link

* docs: fix Cursor store link

* docs: replace store Cards with linked image grid

* docs: add captions to store images

* docs: add Notifications to client primitives table in mcp101

* docs: add lead-in paragraph before resource cards at bottom of mcp101

* docs: move resource cards to end of page after Contacts/Socials/Founders

* docs: demote Contacts/Socials/Founders to bold text to remove from TOC

* docs: move Contacts/Socials/Founders to very end of mcp101

* docs: add Resources and Contacts section headers, link official MCP docs

* chore(docs): update site metadata, footer links, and rename mcp_use to mcp-use

* update docs metadata + images

* docs: enhance MCP section by highlighting the reach of MCP stores with 1 billion weekly active users

* api ref
2026-05-14 18:49:06 +02:00

332 lines
12 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Middleware System"
description: "Flexible middleware system for intercepting and processing MCP requests and responses with typed hooks"
icon: "layers"
---
The mcp-use middleware system provides a powerful, flexible way to intercept and process all MCP (Model Context Protocol) requests and responses. It uses a class-based hook pattern (with an Express-like `next` flow) so you can log, collect metrics, filter/transform requests, cache, rate limit, and more.
## Middleware Process
mcp-use runs middleware in a chain. When a request like `initialize`, `tools/call`, `resources/read`, etc. is made, it flows through your middlewares (in order). In each middleware you can:
1. Analyze the incoming request through the context
2. Modify the request (and its metadata) before execution
3. Execute the next handler via `await call_next(context)`
4. Inspect/transform the result or handle errors
Transport support:
- Fully supported: `stdio`, `http` (streamable HTTP and SSE), `sandbox`
- Not yet supported: `websocket`
## Overview
You implement middleware by subclassing `Middleware` and overriding one or more hooks. Each hook receives:
- `context: MiddlewareContext[T]` - typed request context
- `call_next: NextFunctionT[T, R]` - call to the next middleware or the actual MCP handler
Hooks are typed to the specific MCP method so you get strong IDE assistance when overriding specific operations like `on_call_tool`.
## Quick Start
```python
from mcp.types import CallToolRequestParams, CallToolResult
from mcp_use import MCPClient
from mcp_use.middleware import Middleware, MiddlewareContext, NextFunctionT
class CustomMiddleware(Middleware):
async def on_call_tool(
self, context: MiddlewareContext[CallToolRequestParams], call_next: NextFunctionT
) -> CallToolResult:
print(f"Calling tool {context.params.name}")
return await call_next(context)
config = {
"mcpServers": {
"playwright": {"command": "npx", "args": ["@playwright/mcp@latest"], "env": {"DISPLAY": ":1"}}
}
}
# MCPClient automatically prepends a default logging middleware.
# You can add your own middlewares after it.
client = MCPClient(config=config, middleware=[CustomMiddleware()])
```
## Core Types
### `MiddlewareContext[T]`
```python
from dataclasses import dataclass, field
from typing import Any, Generic, TypeVar
T = TypeVar("T")
@dataclass
class MiddlewareContext(Generic[T]):
id: str # Unique request ID
method: str # JSON-RPC method name (e.g., "tools/call")
params: T # Typed parameters for the method (may be None for list APIs)
connection_id: str # Connector identifier (e.g., "stdio:...", "http:...")
timestamp: float # Request start time
metadata: dict[str, Any] = field(default_factory=dict)
```
### `NextFunctionT[T, R]`
```python
from typing import Protocol
class NextFunctionT(Protocol[T, R]):
async def __call__(self, context: MiddlewareContext[T]) -> R: ...
```
### `Middleware` base class
You override hooks on the base class. If you only need a single entry point for all requests, override `on_request`.
```python
from typing import Any
from mcp_use.middleware import Middleware, MiddlewareContext, NextFunctionT
class MyMiddleware(Middleware):
async def on_request(self, context: MiddlewareContext[Any], call_next: NextFunctionT) -> Any:
# before
result = await call_next(context)
# after
return result
```
## Hook Reference
Available hooks (override any subset). Types below come from `mcp.types`.
```python
from typing import Any
from mcp.types import (
InitializeRequestParams, InitializeResult,
CallToolRequestParams, CallToolResult,
ListToolsRequest, ListToolsResult,
ListResourcesRequest, ListResourcesResult,
ReadResourceRequestParams, ReadResourceResult,
ListPromptsRequest, ListPromptsResult,
GetPromptRequestParams, GetPromptResult,
)
from mcp_use.middleware import Middleware, MiddlewareContext, NextFunctionT
class Middleware:
async def on_request(self, context: MiddlewareContext[Any], call_next: NextFunctionT) -> Any: ...
async def on_initialize(
self, context: MiddlewareContext[InitializeRequestParams], call_next: NextFunctionT
) -> InitializeResult: ...
async def on_call_tool(
self, context: MiddlewareContext[CallToolRequestParams], call_next: NextFunctionT
) -> CallToolResult: ...
async def on_list_tools(
self, context: MiddlewareContext[ListToolsRequest], call_next: NextFunctionT
) -> ListToolsResult: ...
async def on_list_resources(
self, context: MiddlewareContext[ListResourcesRequest], call_next: NextFunctionT
) -> ListResourcesResult: ...
async def on_read_resource(
self, context: MiddlewareContext[ReadResourceRequestParams], call_next: NextFunctionT
) -> ReadResourceResult: ...
async def on_list_prompts(
self, context: MiddlewareContext[ListPromptsRequest], call_next: NextFunctionT
) -> ListPromptsResult: ...
async def on_get_prompt(
self, context: MiddlewareContext[GetPromptRequestParams], call_next: NextFunctionT
) -> GetPromptResult: ...
```
## Writing Middleware
### Timing Example
```python
import time
from typing import Any
from mcp_use.middleware import Middleware, MiddlewareContext, NextFunctionT
class TimingMiddleware(Middleware):
async def on_request(self, context: MiddlewareContext[Any], call_next: NextFunctionT) -> Any:
start = time.time()
try:
result = await call_next(context)
return result
finally:
duration = time.time() - start
print(f"{context.method} took {duration:.3f}s")
```
## Built-in Middleware
### Default logging
- A default logging middleware is automatically prepended by `MCPClient`.
- It logs each request/response at debug level with timing.
- You don’t need to add it manually; just pass your custom middlewares.
```python
from mcp_use import MCPClient
from mcp_use.middleware import Middleware
client = MCPClient(config=config, middleware=[MyCustomMiddleware()])
# Order = [default_logging_middleware, MyCustomMiddleware()]
```
### Metrics
Instantiate and pass to the client. Each middleware exposes a getter on the instance.
```python
from mcp_use.middleware import MetricsMiddleware, PerformanceMetricsMiddleware, CombinedAnalyticsMiddleware
metrics_mw = MetricsMiddleware()
perf_mw = PerformanceMetricsMiddleware()
analytics_mw = CombinedAnalyticsMiddleware()
client = MCPClient(config=config, middleware=[metrics_mw, perf_mw, analytics_mw])
# Later, retrieve data
print(metrics_mw.get_metrics())
print(perf_mw.get_performance_metrics())
print(analytics_mw.get_combined_analytics())
```
## Middleware Chain Execution
Middleware executes in the order provided (outermost first):
```python
middleware = [
mw1, # Executes first (outermost)
mw2, # Executes second
mw3, # Executes third (innermost)
]
```
Flow:
1. `mw1` starts → calls `await call_next(context)`
2. `mw2` starts → calls `await call_next(context)`
3. `mw3` starts → calls `await call_next(context)`
4. Actual MCP call executes
5. `mw3` resumes with result
6. `mw2` resumes with result
7. `mw1` resumes with result
## Error Handling
Always re-raise unless you’re intentionally transforming errors.
```python
from typing import Any
from mcp_use.middleware import Middleware, MiddlewareContext, NextFunctionT
class ErrorAware(Middleware):
async def on_request(self, context: MiddlewareContext[Any], call_next: NextFunctionT) -> Any:
try:
return await call_next(context)
except Exception as e:
context.metadata["error"] = str(e)
raise
```
## Best Practices
1. Re-raise exceptions unless you have a clear alternative behavior.
2. Use type hints on hooks for better IDE support.
3. Keep each middleware focused on a single concern.
4. Use `context.metadata` for cross-middleware communication.
```python
class Auth(Middleware):
async def on_request(self, context, call_next):
context.metadata["user"] = "alice"
return await call_next(context)
class Audit(Middleware):
async def on_request(self, context, call_next):
user = context.metadata.get("user", "unknown")
print(f"{user} -> {context.method}")
return await call_next(context)
```
**Note on Modifying `context.params` and Headers**
Middleware receives a typed `context.params` object and may modify it before the request is executed. The runtime guarantees and recommended patterns are:
- **Preferred - Mutate fields on `context.params`:**
- Example: `context.params.arguments["user_id"] = "alice"`
- These mutations are observed by the final MCP client call and are the most compatible approach.
- **Replacement - Reassigning `context.params`:**
- Example: `context.params = NewParams(...)`
- This pattern is supported: the middleware system reads `context.params` at call-time so replacements are respected. However, prefer mutation for clarity and to avoid surprises for readers of middleware code.
- **Per-request HTTP headers:**
- Adding `context.metadata["headers"]` is useful for carrying header-like information through middleware, but it will only be applied to the actual HTTP transport if the connector/transport code explicitly reads and merges those values into the request headers.
- There is no global automatic mechanism that takes `context.metadata["headers"]` and injects them into every transport unless the connector implements that behavior.
Example - add a trace id that downstream middleware or server can observe (does not automatically modify HTTP headers):
```python
class AddTraceMiddleware(Middleware):
async def on_call_tool(self, context: MiddlewareContext[CallToolRequestParams], call_next: NextFunctionT):
# Mutate params (preferred)
context.params.arguments.setdefault("meta", {})["trace_id"] = "trace-123"
# Or store headers-like info in metadata for connector-level handling
context.metadata.setdefault("headers", {})["X-Trace-Id"] = "trace-123"
return await call_next(context)
```
If you need middleware to inject actual HTTP headers per request, there are two safe approaches:
1. **Connector support (recommended):** update the connector/transport to read `context.metadata["headers"]` and merge them into the outgoing HTTPX request headers for that call. This is robust and concurrency-safe when implemented correctly.
2. **Mutate request params the server understands:** include header-like fields inside `context.params` (for example inside a tool's `arguments`) and let the server interpret them.
If you'd like, we can add a small connector example showing how to merge `context.metadata["headers"]` into the HTTP request - say if you want automatic per-request header injection. Let me know and I will add that example as a follow-up.
## Integration with MCP Clients
### MCPClient
```python
from mcp_use import MCPClient
from mcp_use.middleware import MetricsMiddleware
metrics_mw = MetricsMiddleware()
client = MCPClient(
config={
"mcpServers": {
"my_server": {"command": "my-mcp-server", "args": ["--port", "8080"]}
}
},
middleware=[metrics_mw], # default logging is automatically prepended
)
# You can also add middleware during execution with:
client.add_middleware(CustomMiddleware())
```
### Per-session stacks
```python
prod_client = MCPClient(config=prod_config, middleware=[SecurityMw(), MetricsMiddleware()])
dev_client = MCPClient(config=dev_config) # default logging only
```
<Note>
See the examples directory for a complete working example:
- [`examples/example_middleware.py`](https://github.com/mcp-use/mcp-use/blob/main/examples/example_middleware.py)
</Note>