express-to-mcp
Exposes an existing Express app or router as MCP tools, dispatching tool calls through the Express middleware stack in memory without opening a network port.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@express-to-mcpexpose my Express user routes as MCP tools"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
express-to-mcp
Expose an existing Express router as Model Context Protocol tools — without writing a second server, and without a network hop.
Tool calls are dispatched through your Express middleware stack in memory. No port is bound, no HTTP request leaves the process, and your existing auth, validation, and error-handling middleware all run exactly as they do in production.
LLM ──JSON-RPC──▶ MCP Server ──▶ mock req/res ──▶ your Express stack ──▶ handler
(in-process, no socket)Install
npm install express-to-mcp @modelcontextprotocol/sdk zodRequires Express 5 and Node 20+. express, zod and @modelcontextprotocol/sdk are peer dependencies, so your app's own copies are used.
Related MCP server: FastAPI-MCP
Usage
import express from 'express';
import { z } from 'zod';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { ExpressMcpBridge } from 'express-to-mcp';
const app = express(); // ← your existing app
app.use(express.json());
app.get('/api/users/:id', (req, res) => res.json({ id: req.params.id }));
const bridge = new ExpressMcpBridge({
app,
serverInfo: { name: 'my-api', version: '1.0.0' },
headers: { authorization: `Bearer ${process.env.API_TOKEN}` },
routes: [
{
name: 'get_user',
description: 'Fetch a single user by id.',
method: 'GET',
path: '/api/users/:id',
schema: z.object({ id: z.string().min(1).describe('The user id') }),
annotations: { readOnlyHint: true },
},
],
});
await bridge.connect(new StdioServerTransport());That's a complete MCP server. See examples/stdio.ts for a fuller one.
No transport is bundled — bridge.server is the configured MCP Server, so attach whichever you need (StdioServerTransport, StreamableHTTPServerTransport, …).
How arguments map onto a request
One flat argument object from the LLM becomes a real HTTP request:
Argument | Goes to | Why |
matches a | the URL path | encoded per segment, so a value containing |
anything else, on | the query string | |
anything else, on | a JSON body |
Override per route with argsIn: 'body' | 'query', or per argument with queryParams / bodyParams:
{
name: 'search',
method: 'POST',
path: '/api/search',
queryParams: ['page'], // -> POST /api/search?page=2
schema: z.object({ term: z.string(), page: z.number().optional() }),
}req.params and req.query are never assigned directly — they are derived by Express from the URL we build, which is the only way that works reliably (Express recomputes req.params on every matched layer, and req.query is a get-only accessor).
Auth and per-call context
headers is applied to every request, so existing auth middleware runs unchanged. For per-call identity, use buildRequest — it receives the validated arguments and runs just before dispatch:
new ExpressMcpBridge({
app,
routes,
headers: { 'x-service': 'mcp-bridge' },
buildRequest: (toolName, args) => ({
headers: { authorization: `Bearer ${tokenFor(args.tenantId)}` },
extend: { user: { id: 'svc', scopes: ['read'] } }, // assigned onto `req`
}),
});extend sets properties directly on the mocked req, which is useful when your handlers expect auth middleware to have already populated something like req.user.
Options
Option | Default | |
| — | An Express application, or an |
| — | The tools to expose |
|
| Name and version reported over MCP |
|
| Headers added to every mocked request |
| — | Per-call headers and |
|
| Per-call budget; a hung handler becomes an error result |
| read from |
|
|
| Drop |
Route paths use Express 5 syntax
Paths are parsed with path-to-regexp v8, so Express 4 patterns are rejected at construction with a message telling you the replacement:
Express 4 | Express 5 |
|
|
|
|
| two routes, or validate in the Zod schema |
()[]?+! are reserved; escape them with \.
Error handling
callTool never throws — every failure comes back as an MCP isError result the LLM can act on:
Situation | Result |
arguments fail the Zod schema |
|
handler responds 4xx/5xx |
|
no route matched |
|
error escaped all middleware |
|
handler never responds |
|
Developer mistakes — a malformed path, a duplicate tool name, a non-object argument schema, an argument that can't survive the app's query parser — throw from the constructor instead, so they surface at startup rather than as a confusing tool failure.
Zod 3 and Zod 4
Both work. Zod 4 schemas are converted with its native z.toJSONSchema(); Zod 3 schemas go through zod-to-json-schema. Schemas are compiled once at construction, using the input JSON Schema so that .default() and .transform() fields are advertised as optional.
Limitations
JSON bodies only.
express.urlencoded()andmultipart(multer) are not supported; onlyapplication/jsonis generated.Express 5 only. Express 4's
app._routerand path syntax are not supported.compressionis bypassed, deliberately: noaccept-encodingrequest header is sent, so the middleware negotiatesidentityand we capture readable JSON rather than gzipped bytes.Timeouts cannot cancel a running handler — Node has no such primitive. The dispatch is abandoned and the streams destroyed so
on-finishedcleanup runs, but the handler itself keeps going.
Lower-level API
The mock pipeline is exported on its own, which is handy for testing an app without a server:
import { createExchange, dispatch } from 'express-to-mcp';
const exchange = createExchange({ method: 'POST', url: '/api/items?dry=1', body: { name: 'x' } });
const res = await dispatch(app, exchange);
res.status; // 201
res.headers; // lowercase keys, set-cookie preserved as an array
res.json(); // parsed bodyContributing
npm install
npm run verify # typecheck + tests + build + packaged smoke testThe suite includes regression tests that pin the Node and Express internals this library depends on — response prototype replacement, socket-dependent finish and body parsing, drain forwarding, byte-length content headers. They exist because every one of those fails silently if it regresses; see CONTRIBUTING.md before removing one.
Bug reports and PRs welcome. Development needs Node 22.12+ (a vitest 5 requirement); the published library supports Node 20+, which CI verifies by installing the packed tarball.
License
MIT © Ashish Lohia
This server cannot be deployed
Maintenance
Related MCP Connectors
The OpenRouter for tools. One MCP connection gives any AI agent 254 hosted tools, pay per call.
Host your MCP tool over streamable HTTP in one command.
Give any MCP-compatible AI assistant a builder for live, hosted web tools and workflows.
Find, vet, and run MCP tools through a secure audited gateway with prompt-injection risk scoring
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceExposes Express.js API endpoints as MCP tools, preserving existing schemas and authentication behavior. Supports streaming responses and can be mounted directly to existing Express apps or run as a standalone gateway.3 npm1MIT
- AlicenseNot gradedqualityDmaintenanceExposes FastAPI endpoints as Model Context Protocol (MCP) tools while preserving existing authentication, schemas, and documentation. It enables seamless integration of FastAPI services into MCP ecosystems using a native ASGI transport layer.MIT
- AlicenseNot gradedqualityDmaintenanceA lightweight MCP router for FastAPI that enables adding MCP tools to FastAPI applications with ease.2MIT
- AlicenseNot gradedqualityBmaintenanceExposes FastAPI routes as MCP tools with auto-discovery, Pydantic schema extraction, and built-in observability.Apache 2.0