Express

Purpose and Scope

Use Express when you want an MCP Streamable HTTP endpoint inside a familiar Node web application. In this SDK, Express is not a separate protocol implementation; it is a thin mounting layer around the same server factory used by the generic HTTP guide. The central idea is to build an McpServer inside a factory, wrap that factory with createMcpHandler, adapt the resulting web-standard handler to Node request and response objects with toNodeHandler, and mount it on an Express route. The Express helper supplies safe defaults so the route can focus on MCP behavior instead of framework plumbing.

Sources: docs/serving/express.md, docs/serving/http.md

Install the Express stack with the server package, the Express middleware package, the Node adapter, and Express itself. The documented command is intentionally explicit because each package has a different job: @modelcontextprotocol/server owns MCP server construction and Streamable HTTP handling, @modelcontextprotocol/node adapts web-standard handlers to Node IncomingMessage and ServerResponse, and @modelcontextprotocol/express provides Express-specific application defaults and authentication middleware. That split keeps framework integration small while preserving the same MCP handler semantics across Express, Fastify, Hono, plain Node, and web-standard runtimes.

npm install @modelcontextprotocol/server @modelcontextprotocol/express @modelcontextprotocol/node express

Sources: docs/serving/express.md, docs/serving/fastify.md, docs/serving/hono.md, docs/serving/http.md

Relevant Source Files

  • docs/serving/express.md - Primary how-to for installing the Express packages, mounting /mcp, protecting Host and Origin headers, forwarding auth, and verifying with curl.
  • docs/serving/authorization.md - Shows how requireBearerAuth is mounted in front of the Express MCP route and how token verification becomes handler authInfo.
  • docs/serving/fastify.md - Provides the sibling Node-framework pattern, useful for understanding that Express differs mainly in request, response, and body handling details.
  • docs/serving/hono.md - Shows the web-standard runtime variant and clarifies that Hono can call handler.fetch directly instead of using the Node adapter.
  • docs/serving/http.md - Defines the shared createMcpHandler factory model, per-request server instances, request context, and runtime mounting model used by Express.
  • docs/serving/legacy-clients.md - Documents the legacy posture options on createMcpHandler, which still apply when the handler is mounted through Express.

Core Flow

Start by writing a server factory. A factory is a function that returns a fresh McpServer; the HTTP guide states that the factory runs once per HTTP request, and the Express recipe uses that same model. Register tools, resources, and prompts inside the factory rather than on a shared server instance. That matters because the handler itself is stateless between HTTP requests, while shared infrastructure such as connection pools or caches should live at module scope and be closed over by the factory. If a request has authentication context, the factory can build the server for that caller.

Sources: docs/serving/http.md, docs/serving/express.md

import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { toNodeHandler } from '@modelcontextprotocol/node';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
 
const handler = createMcpHandler(() => {
    const server = new McpServer({ name: 'notes', version: '1.0.0' });
    server.registerTool('add-note', { description: 'Append a note', inputSchema: z.object({ text: z.string() }) }, async ({ text }) => ({
        content: [{ type: 'text', text: `Saved: ${text}` }]
    }));
    return server;
});
 
const app = createMcpExpressApp();
const node = toNodeHandler(handler);
app.all('/mcp', (req, res) => void node(req, res, req.body));

createMcpExpressApp returns an ordinary Express app with useful MCP defaults already installed. The documented route uses app.all('/mcp', ...) so the single endpoint can receive every MCP HTTP request method that the handler supports. The call to toNodeHandler(handler) should happen once, outside the route callback, because it adapts the web-standard MCP handler to Express's (req, res) shape. Inside the route, pass req.body as the third argument because Express JSON parsing has already consumed the request stream; this prevents the Node adapter from attempting to read the body again.

Sources: docs/serving/express.md, docs/serving/http.md

API Components

ComponentPackageRole in an Express deployment
createMcpHandler(factory, options?)@modelcontextprotocol/serverCreates the Streamable HTTP handler from a per-request McpServer factory.
McpServer@modelcontextprotocol/serverRegisters tools, resources, prompts, and server capabilities for each request instance.
toNodeHandler(handler)@modelcontextprotocol/nodeAdapts the web-standard handler to Node req and res objects used by Express.
createMcpExpressApp(options?)@modelcontextprotocol/expressCreates an Express app with JSON parsing and DNS rebinding protection defaults.
requireBearerAuth(options)@modelcontextprotocol/expressVerifies bearer tokens before /mcp and attaches auth for the MCP request context.
allowedHosts and allowedOrigins@modelcontextprotocol/express app optionsConfigure port-agnostic Host and Origin allowlists when binding beyond localhost.

The most important runtime boundary is between the Express route and the MCP handler. Express owns parsing middleware, route matching, and the Node request lifecycle. The MCP handler owns protocol negotiation, server instantiation, JSON-RPC processing, response streaming, and notification support. The Node adapter is the bridge between those worlds. Hono demonstrates the contrast: because Hono exposes a web-standard Request, it can call handler.fetch directly. Fastify uses the same Node adapter pattern as Express, but hands over request.raw, reply.raw, and request.body.

Sources: docs/serving/express.md, docs/serving/fastify.md, docs/serving/hono.md

Security and Authorization

Do not mount the bare MCP handler on Express without considering Host, Origin, and bearer-token checks. The HTTP guide explains that the handler trusts the caller and does not validate Host headers, Origin headers, or tokens by itself. The Express helper exists partly to put those framework-level defenses in front of the handler. By default, createMcpExpressApp protects localhost deployments against DNS rebinding by validating Host and Origin values for local binds such as 127.0.0.1, localhost, and ::1; requests carrying non-localhost values are rejected before the MCP handler runs.

Sources: docs/serving/express.md, docs/serving/http.md

When binding to all interfaces, the default localhost assumption no longer applies, so name the hosts and origins your deployment actually serves. allowedHosts and allowedOrigins are hostnames and are port-agnostic. Requests without an Origin header pass the origin check, which keeps non-browser MCP clients working while still protecting browser-originated traffic. This distinction is useful during development: a local command-line client can call the endpoint without browser CORS-like context, while a malicious webpage cannot use DNS rebinding to treat a loopback MCP server as same-origin.

const publicApp = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['api.example.com'] });

Sources: docs/serving/express.md

For protected servers, mount requireBearerAuth before the /mcp route. In the authorization guide, the MCP server acts as an OAuth resource server: it verifies access tokens issued elsewhere and never issues them itself. Your verifier returns AuthInfo, and the middleware converts missing, malformed, expired, or under-scoped tokens into the documented OAuth-style HTTP responses. After verification, auth is attached to the Express request and forwarded through toNodeHandler, so server handlers read it as ctx.http.authInfo or receive it through the factory request context.

import { requireBearerAuth } from '@modelcontextprotocol/express';
 
const auth = requireBearerAuth({ verifier });
publicApp.all('/mcp', auth, (req, res) => void node(req, res, req.body));

Sources: docs/serving/authorization.md, docs/serving/express.md

Run and Verify

Once the route is mounted, the Express app is still only an application object; nothing listens until you start the server. The documented minimal step is app.listen(3000), then run the TypeScript file with a runner such as npx tsx server.ts. Verification uses a direct JSON-RPC tools/list POST to /mcp with Content-Type: application/json and an Accept header that permits both JSON and text/event-stream. The expected response is a single SSE message event containing the result, including the registered add-note tool and its JSON Schema input shape.

app.listen(3000);
curl -s -X POST http://127.0.0.1:3000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Sources: docs/serving/express.md

Treat this verification as more than a smoke test. If the request returns 403 before reaching your route logic, inspect Host and Origin configuration first. If authentication is enabled and the response is 401 invalid_token or 403 insufficient_scope, inspect the verifier result, expiresAt, and required scopes from the authorization guide. If Express reports body-related issues, confirm that you pass req.body to the Node adapter because createMcpExpressApp already installed express.json(). These checks map directly to the boundaries among Express middleware, auth middleware, and the MCP handler.

Sources: docs/serving/express.md, docs/serving/authorization.md

Legacy Posture and Next Steps

Express inherits the same protocol-version and legacy-client behavior as the underlying createMcpHandler. The legacy-clients guide defines two HTTP postures: the default legacy: 'stateless', which serves legacy requests from a fresh factory instance without sessions, and legacy: 'reject', which makes the endpoint modern-only for legacy initialize requests. If you need to keep an existing sessionful 2025 deployment running, route in front of a strict handler with isLegacyRequest and send modern traffic to strict.fetch. That decision belongs in the handler configuration and routing layer, not in Express business logic.

Sources: docs/serving/legacy-clients.md, docs/serving/http.md

Next, read the Streamable HTTP guide to understand the per-request factory and scaling model in depth, then read Authorization before exposing the route outside localhost. If your deployment is not Express, compare Fastify for Node framework integration and Hono for web-standard runtimes. For production, decide three things explicitly: which hostnames and origins are allowed, whether bearer auth is required and which scopes are enforced, and whether legacy clients are served statelessly or rejected. Those choices make the Express route predictable, secure, and aligned with the SDK's shared HTTP serving model.

Sources: docs/serving/express.md, docs/serving/http.md, docs/serving/authorization.md, docs/serving/fastify.md, docs/serving/hono.md, docs/serving/legacy-clients.md