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 aListToolsHandlerand returns a newListToolsHandler.CallToolMiddleware– A higher‑order function that receives aCallToolHandlerand returns a newCallToolHandler.
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
ListToolsHandlerorCallToolHandlerto intercept requests and responses. - The
composeutility infunctional-middleware.tschains middleware right-to-left, creating a single wrapped handler. - Developers implement
ListToolsMiddlewarefor tool discovery operations andCallToolMiddlewarefor tool execution operations. - Custom middleware can inspect, transform, or block requests by modifying the
requestobject before calling the wrappedhandlerand altering theresponsebefore returning. - Register new middleware by importing it into
apps/backend/src/routers/public-metamcp/openapi/handlers.tsand adding it to thecomposechain.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →