How to Create Custom Middleware in MetaMCP to Intercept and Transform MCP Requests

Developers create custom middleware in MetaMCP by implementing higher-order functions that wrap ListToolsHandler or CallToolHandler, then composing them into the request pipeline using the compose utility from functional-middleware.ts.

The MetaMCP repository (metatool-ai/metamcp) provides a lightweight, functional middleware architecture that enables developers to intercept, inspect, and modify Model Context Protocol (MCP) requests and responses without altering core business logic. This system leverages higher-order functions to create composable transformation layers for both tool discovery and tool execution operations.

Understanding the MetaMCP Middleware Architecture

MetaMCP processes List‑Tools and Call‑Tool operations through a functional pipeline built on two core abstractions:

  • ListToolsMiddleware – A higher‑order function that receives a ListToolsHandler and returns a new ListToolsHandler.
  • CallToolMiddleware – A higher‑order function that receives a CallToolHandler and returns a new CallToolHandler.

The compose helper, defined in apps/backend/src/lib/metamcp/metamcp-middleware/functional-middleware.ts, reduces an array of middleware functions right‑to‑left, stitching them into a single wrapped handler. When a request arrives, MetaMCP creates the original handlers (createOriginalListToolsHandler, createOriginalCallToolHandler) and then applies the middleware stack in apps/backend/src/routers/public-metamcp/openapi/handlers.ts.

Creating a Custom ListTools Middleware

To intercept tool discovery requests, implement the ListToolsMiddleware type and inject your logic into the handler chain.

Implementing Request/Response Logging

The following example demonstrates a logging middleware that records incoming requests and outgoing responses for the List‑Tools operation:

// apps/backend/src/lib/metamcp/metamcp-middleware/logging.middleware.ts
import {
  ListToolsHandler,
  ListToolsMiddleware,
} from "./functional-middleware";

export const createLoggingMiddleware = (): ListToolsMiddleware => {
  return (handler: ListToolsHandler) => {
    return async (request, context) => {
      console.log(
        `[MetaMCP] ListTools request → namespace=${context.namespaceUuid}`,
        request,
      );

      const response = await handler(request, context);

      console.log(
        `[MetaMCP] ListTools response ← tools=${response.tools?.length ?? 0}`,
        response,
      );

      return response;
    };
  };
};

Registering Your Middleware in the Pipeline

Add your custom middleware to the compose chain in apps/backend/src/routers/public-metamcp/openapi/handlers.ts:

import { createLoggingMiddleware } from "@/lib/metamcp/metamcp-middleware/logging.middleware";

const listToolsWithMiddleware = compose(
  createToolOverridesListToolsMiddleware({ cacheEnabled: true }),
  createFilterListToolsMiddleware({ cacheEnabled: true }),
  createLoggingMiddleware(),               // ← custom middleware
)(originalListToolsHandler);

Creating a Custom CallTool Middleware

For tool execution operations, implement CallToolMiddleware to transform arguments, validate inputs, or modify responses before they reach the client.

Transforming Tool Arguments and Responses

This example injects default arguments and annotates the response to indicate transformation occurred:

// apps/backend/src/lib/metamcp/metamcp-middleware/arg-transform.middleware.ts
import {
  CallToolHandler,
  CallToolMiddleware,
} from "./functional-middleware";

export const createArgTransformMiddleware = (): CallToolMiddleware => {
  return (handler: CallToolHandler) => {
    return async (request, context) => {
      // Example: inject a default `language` argument if missing
      if (!request.params.arguments?.language) {
        request.params.arguments = {
          ...request.params.arguments,
          language: "en",
        };
      }

      const response = await handler(request, context);
      
      // Example: add a custom flag to the result annotations
      response.annotations = {
        ...response.annotations,
        transformed: true,
      };
      return response;
    };
  };
};

Register this middleware in the Call‑Tool pipeline:

import { createArgTransformMiddleware } from "@/lib/metamcp/metamcp-middleware/arg-transform.middleware";

const callToolWithMiddleware = compose(
  createFilterCallToolMiddleware({
    cacheEnabled: true,
    customErrorMessage: (tool, reason) =>
      `Access denied to tool "${tool}": ${reason}`,
  }),
  createToolOverridesCallToolMiddleware({ cacheEnabled: true }),
  createArgTransformMiddleware(),          // ← custom middleware
)(originalCallToolHandler);

Key Files for Middleware Development

Understanding the repository structure helps you locate the correct extension points when you create custom middleware in MetaMCP:

File Role
apps/backend/src/lib/metamcp/metamcp-middleware/functional-middleware.ts Defines MetaMCPHandlerContext, middleware type signatures, createFunctionalMiddleware, and the compose helper used to chain middleware.
apps/backend/src/lib/metamcp/metamcp-middleware/filter-tools.functional.ts Reference implementation of List‑Tools and Call‑Tool middleware that filters inactive tools based on database state.
apps/backend/src/lib/metamcp/metamcp-middleware/tool-overrides.functional.ts Example middleware that rewrites tool names and descriptions based on namespace-specific overrides stored in the cache.
apps/backend/src/routers/public-metamcp/openapi/handlers.ts The integration point where middleware is composed with original handlers; modify this file to register your custom middleware in the request pipeline.

Summary

  • MetaMCP middleware consists of higher-order functions that wrap ListToolsHandler or CallToolHandler to intercept requests and responses.
  • The compose utility in functional-middleware.ts chains middleware right-to-left, creating a single wrapped handler.
  • Developers implement ListToolsMiddleware for tool discovery operations and CallToolMiddleware for tool execution operations.
  • Custom middleware can inspect, transform, or block requests by modifying the request object before calling the wrapped handler and altering the response before returning.
  • Register new middleware by importing it into apps/backend/src/routers/public-metamcp/openapi/handlers.ts and adding it to the compose chain.

Frequently Asked Questions

What is the difference between ListToolsMiddleware and CallToolMiddleware?

ListToolsMiddleware intercepts operations that enumerate available tools, allowing you to filter, rename, or annotate the tool list before it reaches the client. CallToolMiddleware intercepts actual tool execution requests, enabling you to validate arguments, inject defaults, transform inputs, or modify the execution response. Both follow the same higher-order function pattern but operate on different handler types defined in functional-middleware.ts.

How do I order multiple custom middleware in the compose chain?

The compose function applies middleware right-to-left, meaning the rightmost middleware executes first on the incoming request and last on the outgoing response. Place logging or validation middleware toward the right (outer) side of the chain, and transformation or filtering logic toward the left (inner) side to ensure they operate on already-validated or logged data.

Can I modify the request context in custom middleware?

Yes, the context object passed to every handler contains the namespaceUuid and other metadata, and you can extend or modify it within your middleware. However, avoid mutating core properties that downstream middleware or the original handler depend on unless you intend to override framework behavior. Always return the modified context alongside the request when calling the wrapped handler.

Where should I store my custom middleware files?

Store custom middleware in apps/backend/src/lib/metamcp/metamcp-middleware/ alongside existing implementations like filter-tools.functional.ts and tool-overrides.functional.ts. This location keeps your code co-located with the core middleware utilities, ensuring easy imports into apps/backend/src/routers/public-metamcp/openapi/handlers.ts where the composition occurs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →