Middleware & Interceptors

mcponce features a full-featured, composable middleware pipeline based on the classic Onion pattern (similar to Koa, Express, and Hono). Middleware handlers wrap every tool execution, giving you complete control before and after the handler runs.

Mermaid Diagram
View diagram source
graph LR
  Client[Client / LLM] --> M1[Auth Middleware]
  M1 --> M2[Audit Logger]
  M2 --> Handler[Tool Handler]
  Handler --> M2
  M2 --> M1
  M1 --> Client

The Onion Pipeline #

A middleware is a function that receives a ToolMiddlewareContext and a next function. Calling await next() invokes the downstream middleware or the target tool handler:

TYPESCRIPT
import { createMcpServer } from 'mcponce';

const app = createMcpServer({ name: 'my-server' });

// Global timing middleware
app.use(async (ctx, next) => {
  const start = performance.now();
  console.log(`[START] Executing tool: ${ctx.tool}`);

  try {
    const result = await next();
    const duration = (performance.now() - start).toFixed(2);
    console.log(`[DONE] ${ctx.tool} took ${duration}ms`);
    return result;
  } catch (error) {
    console.error(`[FAIL] ${ctx.tool} threw:`, error);
    throw error;
  }
});

Filtering Middleware #

You can attach middleware globally, filter by tool names, apply regular expressions, or define tool-specific middleware:

// Run only on specific tools
app.use('delete_user', async (ctx, next) => {
if (!ctx.context.isAdmin) {
throw new Error('Forbidden: Only administrators can delete users.');
}
return next();
});

// Run across a list of tool names app.use(['create_order', 'cancel_order'], async (ctx, next) => { console.log(Audit: Transaction tool ${ctx.tool} initiated); return next(); });


Middleware Context (ctx) #

Each middleware receives a rich context object with the following properties:

PropertyTypeDescription
ctx.toolstringThe name of the tool currently being executed.
ctx.argsRecord<string, any>The validated and coerced arguments passed to the tool.
ctx.contextTContextShared server context, accessible across all tools.
ctx.extraToolExtraExecution helpers (signal, callTool, reportProgress, clearCache).
ctx.callStackstring[]Ordered call stack of tools invoked in this chain (for inter-tool tracking).
ctx.optionsCallOptions?Programmatic invocation options passed to callTool.

Common Patterns #

1. Argument Mutation & Normalization #

Middleware can sanitize, trim, or enrich arguments before they reach the tool handler:

TYPESCRIPT
app.use(async (ctx, next) => {
  // Normalize all string arguments by trimming whitespace
  for (const [key, value] of Object.entries(ctx.args)) {
    if (typeof value === 'string') {
      ctx.args[key] = value.trim();
    }
  }
  return next();
});

2. Context Injection #

Inject authentication states, tenant IDs, or database clients directly into ctx.context:

TYPESCRIPT
app.use(async (ctx, next) => {
  const token = ctx.args._authToken;
  if (token) {
    ctx.context.user = await verifyJwt(token);
    delete ctx.args._authToken; // Do not leak token to the handler
  }
  return next();
});

3. Response Transformation #

Modify or sanitize the handler output before returning it to the client:

TYPESCRIPT
app.use(async (ctx, next) => {
  const result = await next();

  // If output contains sensitive fields, mask them
  if (result && typeof result === 'object' && 'ssn' in result) {
    result.ssn = '***-**-****';
  }

  return result;
});
Short-Circuiting

If a validation check fails in your middleware, you can return a custom response or throw an error without calling next(). Downstream handlers and the tool itself will never be called.

Updated