Skip to main content
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:
PatternMatches
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:
FieldTypeDescription
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

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;
});

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.