Skip to content

Commit 0598f16

Browse files
Theodor N. EngøyTheodor N. Engøy
authored andcommitted
express: add maxBodyBytes + JSON parse error handler
1 parent 65bbcea commit 0598f16

4 files changed

Lines changed: 116 additions & 3 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@modelcontextprotocol/express": patch
3+
---
4+
5+
Add `maxBodyBytes` option (default: 100kb) to cap JSON request body parsing and return JSON-RPC errors for invalid JSON / oversized payloads.
6+

packages/middleware/express/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ import { createMcpExpressApp } from '@modelcontextprotocol/express';
3434
const app = createMcpExpressApp(); // default host is 127.0.0.1; protection enabled
3535
```
3636

37+
`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:
38+
39+
```ts
40+
import { createMcpExpressApp } from '@modelcontextprotocol/express';
41+
42+
const app = createMcpExpressApp({ maxBodyBytes: 1_000_000 });
43+
```
44+
3745
### Streamable HTTP endpoint (Express)
3846

3947
```ts

packages/middleware/express/src/express.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,35 @@
1-
import type { Express } from 'express';
1+
import type { ErrorRequestHandler, Express } from 'express';
22
import express from 'express';
33

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

6+
const DEFAULT_MAX_BODY_BYTES = 100 * 1024; // Express default (100kb), made explicit.
7+
8+
// Ensure body parsing failures return JSON-RPC-shaped errors (instead of HTML).
9+
const jsonBodyErrorHandler: ErrorRequestHandler = (error, _req, res, next) => {
10+
if (res.headersSent) return next(error);
11+
12+
const type = typeof (error as { type?: unknown } | null)?.type === 'string' ? String((error as { type: string }).type) : '';
13+
if (type === 'entity.too.large') {
14+
res.status(413).json({
15+
jsonrpc: '2.0',
16+
error: { code: -32_000, message: 'Payload too large' },
17+
id: null
18+
});
19+
return;
20+
}
21+
if (type === 'entity.parse.failed') {
22+
res.status(400).json({
23+
jsonrpc: '2.0',
24+
error: { code: -32_000, message: 'Invalid JSON' },
25+
id: null
26+
});
27+
return;
28+
}
29+
30+
next(error);
31+
};
32+
633
/**
734
* Options for creating an MCP Express application.
835
*/
@@ -22,6 +49,13 @@ export interface CreateMcpExpressAppOptions {
2249
* to restrict which hostnames are allowed.
2350
*/
2451
allowedHosts?: string[];
52+
53+
/**
54+
* Maximum size (in bytes) for JSON request bodies.
55+
*
56+
* Defaults to 100kb (Express default). Increase this if your tool calls need larger payloads.
57+
*/
58+
maxBodyBytes?: number;
2559
}
2660

2761
/**
@@ -48,10 +82,9 @@ export interface CreateMcpExpressAppOptions {
4882
* ```
4983
*/
5084
export function createMcpExpressApp(options: CreateMcpExpressAppOptions = {}): Express {
51-
const { host = '127.0.0.1', allowedHosts } = options;
85+
const { host = '127.0.0.1', allowedHosts, maxBodyBytes = DEFAULT_MAX_BODY_BYTES } = options;
5286

5387
const app = express();
54-
app.use(express.json());
5588

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

108+
// Parse JSON request bodies for MCP endpoints (explicit limit to reduce DoS risk).
109+
app.use(express.json({ limit: maxBodyBytes }));
110+
111+
app.use(jsonBodyErrorHandler);
112+
75113
return app;
76114
}

packages/middleware/express/test/express.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ describe('@modelcontextprotocol/express', () => {
107107
});
108108

109109
describe('createMcpExpressApp', () => {
110+
async function withServer(app: ReturnType<typeof createMcpExpressApp>, fn: (baseUrl: string) => Promise<void>) {
111+
const server = await new Promise<import('node:http').Server>(resolve => {
112+
const s = app.listen(0, '127.0.0.1', () => resolve(s));
113+
});
114+
try {
115+
const addr = server.address();
116+
if (!addr || typeof addr === 'string') throw new TypeError('Unexpected server address');
117+
await fn(`http://127.0.0.1:${addr.port}`);
118+
} finally {
119+
await new Promise<void>((resolve, reject) => {
120+
server.close(err => (err ? reject(err) : resolve()));
121+
});
122+
}
123+
}
124+
110125
test('should enable localhost DNS rebinding protection by default', () => {
111126
const app = createMcpExpressApp();
112127

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

179194
warn.mockRestore();
180195
});
196+
197+
test('should return JSON-RPC error for invalid JSON', async () => {
198+
const app = createMcpExpressApp({ maxBodyBytes: 1024 });
199+
app.post('/mcp', (_req, res) => {
200+
res.json({ ok: true });
201+
});
202+
203+
await withServer(app, async baseUrl => {
204+
const resp = await fetch(`${baseUrl}/mcp`, {
205+
method: 'POST',
206+
headers: { 'content-type': 'application/json' },
207+
body: '{'
208+
});
209+
210+
expect(resp.status).toBe(400);
211+
const data = await resp.json();
212+
expect(data).toEqual({
213+
jsonrpc: '2.0',
214+
error: { code: -32_000, message: 'Invalid JSON' },
215+
id: null
216+
});
217+
});
218+
});
219+
220+
test('should return JSON-RPC error for payload too large', async () => {
221+
const app = createMcpExpressApp({ maxBodyBytes: 64 });
222+
app.post('/mcp', (_req, res) => {
223+
res.json({ ok: true });
224+
});
225+
226+
await withServer(app, async baseUrl => {
227+
const resp = await fetch(`${baseUrl}/mcp`, {
228+
method: 'POST',
229+
headers: { 'content-type': 'application/json' },
230+
body: JSON.stringify({ data: 'x'.repeat(2048) })
231+
});
232+
233+
expect(resp.status).toBe(413);
234+
const data = await resp.json();
235+
expect(data).toEqual({
236+
jsonrpc: '2.0',
237+
error: { code: -32_000, message: 'Payload too large' },
238+
id: null
239+
});
240+
});
241+
});
181242
});
182243
});

0 commit comments

Comments
 (0)