Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/express-max-body-bytes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@modelcontextprotocol/express": patch
---

Add `maxBodyBytes` option (default: 100kb) to cap JSON request body parsing and return JSON-RPC errors for invalid JSON / oversized payloads.

8 changes: 8 additions & 0 deletions packages/middleware/express/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ import { createMcpExpressApp } from '@modelcontextprotocol/express';
const app = createMcpExpressApp(); // default host is 127.0.0.1; protection enabled
```

`createMcpExpressApp()` also installs `express.json()` so `req.body` is available for MCP transports. The JSON body size limit defaults to 100kb (Express default) and can be configured:

```ts
import { createMcpExpressApp } from '@modelcontextprotocol/express';

const app = createMcpExpressApp({ maxBodyBytes: 1_000_000 });
```

### Streamable HTTP endpoint (Express)

```ts
Expand Down
44 changes: 41 additions & 3 deletions packages/middleware/express/src/express.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
import type { Express } from 'express';
import type { ErrorRequestHandler, Express } from 'express';
import express from 'express';

import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js';

const DEFAULT_MAX_BODY_BYTES = 100 * 1024; // Express default (100kb), made explicit.

// Ensure body parsing failures return JSON-RPC-shaped errors (instead of HTML).
const jsonBodyErrorHandler: ErrorRequestHandler = (error, _req, res, next) => {
if (res.headersSent) return next(error);

const type = typeof (error as { type?: unknown } | null)?.type === 'string' ? String((error as { type: string }).type) : '';
if (type === 'entity.too.large') {
res.status(413).json({
jsonrpc: '2.0',
error: { code: -32_000, message: 'Payload too large' },
id: null
});
return;
}
if (type === 'entity.parse.failed') {
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32_700, message: 'Parse error: Invalid JSON' },
id: null
});
return;
}

next(error);
};

/**
* Options for creating an MCP Express application.
*/
Expand All @@ -22,6 +49,13 @@ export interface CreateMcpExpressAppOptions {
* to restrict which hostnames are allowed.
*/
allowedHosts?: string[];

/**
* Maximum size (in bytes) for JSON request bodies.
*
* Defaults to 100kb (Express default). Increase this if your tool calls need larger payloads.
*/
maxBodyBytes?: number;
}

/**
Expand All @@ -48,10 +82,9 @@ export interface CreateMcpExpressAppOptions {
* ```
*/
export function createMcpExpressApp(options: CreateMcpExpressAppOptions = {}): Express {
const { host = '127.0.0.1', allowedHosts } = options;
const { host = '127.0.0.1', allowedHosts, maxBodyBytes = DEFAULT_MAX_BODY_BYTES } = options;

const app = express();
app.use(express.json());

// If allowedHosts is explicitly provided, use that for validation
if (allowedHosts) {
Expand All @@ -72,5 +105,10 @@ export function createMcpExpressApp(options: CreateMcpExpressAppOptions = {}): E
}
}

// Parse JSON request bodies for MCP endpoints (explicit limit to reduce DoS risk).
app.use(express.json({ limit: maxBodyBytes }));

app.use(jsonBodyErrorHandler);

return app;
}
61 changes: 61 additions & 0 deletions packages/middleware/express/test/express.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ describe('@modelcontextprotocol/express', () => {
});

describe('createMcpExpressApp', () => {
async function withServer(app: ReturnType<typeof createMcpExpressApp>, fn: (baseUrl: string) => Promise<void>) {
const server = await new Promise<import('node:http').Server>(resolve => {
const s = app.listen(0, '127.0.0.1', () => resolve(s));
});
try {
const addr = server.address();
if (!addr || typeof addr === 'string') throw new TypeError('Unexpected server address');
await fn(`http://127.0.0.1:${addr.port}`);
} finally {
await new Promise<void>((resolve, reject) => {
server.close(err => (err ? reject(err) : resolve()));
});
}
}

test('should enable localhost DNS rebinding protection by default', () => {
const app = createMcpExpressApp();

Expand Down Expand Up @@ -178,5 +193,51 @@ describe('@modelcontextprotocol/express', () => {

warn.mockRestore();
});

test('should return JSON-RPC error for invalid JSON', async () => {
const app = createMcpExpressApp({ maxBodyBytes: 1024 });
app.post('/mcp', (_req, res) => {
res.json({ ok: true });
});

await withServer(app, async baseUrl => {
const resp = await fetch(`${baseUrl}/mcp`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{'
});

expect(resp.status).toBe(400);
const data = await resp.json();
expect(data).toEqual({
jsonrpc: '2.0',
error: { code: -32_700, message: 'Parse error: Invalid JSON' },
id: null
});
});
});

test('should return JSON-RPC error for payload too large', async () => {
const app = createMcpExpressApp({ maxBodyBytes: 64 });
app.post('/mcp', (_req, res) => {
res.json({ ok: true });
});

await withServer(app, async baseUrl => {
const resp = await fetch(`${baseUrl}/mcp`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ data: 'x'.repeat(2048) })
});

expect(resp.status).toBe(413);
const data = await resp.json();
expect(data).toEqual({
jsonrpc: '2.0',
error: { code: -32_000, message: 'Payload too large' },
id: null
});
});
});
});
});
3 changes: 2 additions & 1 deletion packages/middleware/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import { McpServer } from '@modelcontextprotocol/server';

const server = new McpServer({ name: 'my-server', version: '1.0.0' });
const app = createMcpExpressApp();
// Default JSON body limit is 100kb (Express default). Increase if your tool calls need larger payloads.
const app = createMcpExpressApp({ maxBodyBytes: 1_000_000 });

app.post('/mcp', async (req, res) => {
const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
Expand Down
Loading