Skip to main content
Glama
ashishlohia70

express-to-mcp

express-to-mcp

npm CI node license

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 zod

Requires 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 :param in path

the URL path

encoded per segment, so a value containing / stays in one segment

anything else, on GET/HEAD/DELETE/OPTIONS

the query string

anything else, on POST/PUT/PATCH

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

app

An Express application, or an express.Router() (mounted on a throwaway app for you)

routes

The tools to expose

serverInfo

express-to-mcp

Name and version reported over MCP

headers

{}

Headers added to every mocked request

buildRequest

Per-call headers and req properties

timeoutMs

30000

Per-call budget; a hung handler becomes an error result

queryStyle

read from app.get('query parser')

'simple' or 'extended'

stripPoweredBy

true

Drop x-powered-by from reported headers

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

/:file.:ext?

/:file{.:ext}

/*

/*splat

/:id(\d+)

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

isError, naming the offending fields; the handler is never invoked

handler responds 4xx/5xx

isError with the status line and response body

no route matched

isError 404 noting the path didn't match

error escaped all middleware

isError with the real message (not a finalhandler HTML page)

handler never responds

isError after timeoutMs

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() and multipart (multer) are not supported; only application/json is generated.

  • Express 5 only. Express 4's app._router and path syntax are not supported.

  • compression is bypassed, deliberately: no accept-encoding request header is sent, so the middleware negotiates identity and 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-finished cleanup 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 body

Contributing

npm install
npm run verify   # typecheck + tests + build + packaged smoke test

The 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

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Exposes 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.
    10
    1
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Exposes 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
  • A
    license
    Not graded
    quality
    C
    maintenance
    A lightweight MCP router for FastAPI that enables adding MCP tools to FastAPI applications with ease.
    2
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ashishlohia70/express-to-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server