Middleware lets you intercept MCP operations to add logging, authentication, rate limiting, or any cross-cutting logic — without touching individual tool handlers.
Quick Start
import { MCPServer , text } from "mcp-use/server" ;
const server = new MCPServer ({ name: "my-server" , version: "1.0.0" });
// Log every tool call
server . use ( "mcp:tools/call" , async ( ctx , next ) => {
console . log ( `→ ${ ctx . params . name } ` );
const result = await next ();
console . log ( `← ${ ctx . params . name } ` );
return result ;
});
server . tool (
{ name: "greet" , description: "Say hello" , schema: z . object ({ name: z . string () }) },
async ({ name }) => text ( `Hello, ${ name } !` )
);
await server . listen ();
How It Works
MCP middleware uses an onion model — each middleware wraps the next, with the handler at the center.
Request
→ Middleware A (before)
→ Middleware B (before)
→ Handler
← Middleware B (after)
← Middleware A (after)
Response
Middleware is registered with server.use('mcp:pattern', handler) where the mcp: prefix distinguishes MCP middleware from HTTP middleware. Multiple middleware functions are composed in registration order (first registered = outermost).
Each middleware can:
Inspect or modify the request before calling next()
Inspect or modify the response after next() returns
Short-circuit by returning early without calling next()
Reject by throwing an error
Patterns
The pattern string (after mcp:) determines which operations trigger the middleware:
Pattern Matches mcp:tools/callTool executions only mcp:tools/listRequests to list available tools mcp:tools/*All tool operations mcp:resources/readResource reads mcp:resources/listRequests to list resources mcp:resources/*All resource operations mcp:prompts/getPrompt fetches mcp:prompts/listRequests to list prompts mcp:prompts/*All prompt operations mcp:*Every MCP operation
Context
Every middleware receives a MiddlewareContext:
Field Type Description methodstringMCP method name (e.g. "tools/call") paramsRecord<string, unknown>Request params — mutable, mutations flow downstream session{ sessionId: string } | undefinedSession ID (HTTP transports) authAuthInfo | undefinedOAuth info when authentication is configured stateMap<string, unknown>Shared state for passing data across middleware
ctx.params is mutable. Middleware can modify it before calling next() and the handler will receive the modified values.
ctx.auth and OAuth
When OAuth is configured (via server.use(oauthAuth0Provider(...)) etc.), ctx.auth is automatically populated from the verified JWT:
server . use ( "mcp:tools/call" , async ( ctx , next ) => {
if ( ctx . auth ) {
console . log ( `User: ${ ctx . auth . user . email } ` );
console . log ( `Scopes: ${ ctx . auth . scopes . join ( ", " ) } ` );
}
return next ();
});
Examples
Logging
Scope Guard
Rate Limiting
Tool Filtering
Log all MCP operations with timing: server . use ( "mcp:*" , async ( ctx , next ) => {
const start = Date . now ();
const result = await next ();
console . log ( ` ${ ctx . method } — ${ Date . now () - start } ms` );
return result ;
});
Require an OAuth scope before any tool call: server . use ( "mcp:tools/call" , async ( ctx , next ) => {
const toolName = ( ctx . params as any ). name ;
const required = `tools:call: ${ toolName } ` ;
if ( ! ctx . auth ?. scopes . includes ( required ) &&
! ctx . auth ?. scopes . includes ( "tools:*" )) {
throw new Error ( `Insufficient scope. Required: ${ required } ` );
}
return next ();
});
Limit tool calls per session per minute: const rateLimits = new Map < string , number []>();
server . use ( "mcp:tools/call" , async ( ctx , next ) => {
const key = ctx . session ?. sessionId ?? "anonymous" ;
const now = Date . now ();
const calls = ( rateLimits . get ( key ) ?? [])
. filter ( t => t > now - 60_000 );
if ( calls . length >= 30 ) {
throw new Error ( "Rate limit exceeded (30 calls/min)" );
}
calls . push ( now );
rateLimits . set ( key , calls );
return next ();
});
Hide internal tools from the list: server . use ( "mcp:tools/list" , async ( ctx , next ) => {
const result = ( await next ()) as any ;
const tools = Array . isArray ( result ) ? result : result ?. tools ?? [];
const visible = tools . filter (( t : any ) => ! t . name . startsWith ( "_" ));
return Array . isArray ( result ) ? visible : { ... result , tools: visible };
});
Middleware Order
Middleware executes in registration order. Earlier middleware wraps later ones.
server . use ( "mcp:*" , loggingMiddleware ); // 1. Outermost — sees everything
server . use ( "mcp:tools/call" , authMiddleware ); // 2. Rejects unauthorized early
server . use ( "mcp:tools/call" , rateLimitMiddleware ); // 3. Limits request rate
Recommended order : Logging → Authentication → Rate limiting → Validation. This ensures logging sees all requests (including rejected ones) and auth rejects early before expensive operations.
HTTP vs MCP Middleware
server.use() handles both HTTP and MCP middleware. The mcp: prefix distinguishes them:
// MCP middleware — intercepts MCP operations
server . use ( "mcp:tools/call" , async ( ctx , next ) => { ... });
// HTTP middleware — intercepts HTTP requests (existing Hono behavior)
server . use ( "/api/*" , someHttpMiddleware );
server . use ( someHonoMiddleware );
HTTP middleware runs at the HTTP layer (before the MCP protocol). MCP middleware runs at the MCP layer (after the protocol parses the request). Use HTTP middleware for things like CORS; use MCP middleware for things like scope-checking tool access.
Best Practices
Single responsibility — each middleware does one thing
Fail fast — reject invalid requests before expensive operations
Always call next() — unless intentionally short-circuiting
Re-raise exceptions — if you catch errors to log them, always re-throw
server . use ( "mcp:tools/call" , async ( ctx , next ) => {
try {
return await next ();
} catch ( err ) {
console . error ( `Tool failed: ${ err } ` );
throw err ; // always re-raise
}
});
Full Example
middleware example Complete server with logging, scope guard, rate limiter, and tool filter middleware.