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/hono-body-size-limit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modelcontextprotocol/hono': patch
---

Add `maxBodyBytes` option to `createMcpHonoApp()` and enforce a default JSON request body size limit (413 on oversized payloads).

8 changes: 8 additions & 0 deletions packages/middleware/hono/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ const app = createMcpHonoApp();
app.all('/mcp', c => transport.handleRequest(c.req.raw, { parsedBody: c.get('parsedBody') }));
```

### Options

`createMcpHonoApp({ ... })` supports:

- `host` (default: `127.0.0.1`): used for localhost DNS rebinding protection behavior
- `allowedHosts` (optional): explicit allowed hostnames
- `maxBodyBytes` (default: `1_000_000`): maximum JSON request body size parsed by the built-in middleware

### Host header validation (DNS rebinding protection)

```ts
Expand Down
64 changes: 61 additions & 3 deletions packages/middleware/hono/src/hono.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,39 @@ import { Hono } from 'hono';

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

const DEFAULT_MAX_BODY_BYTES = 1_000_000; // 1MB

async function readRequestTextWithLimit(req: Request, maxBytes: number): Promise<string> {
const body = req.body;
if (!body) return '';

const reader = body.getReader();
const chunks: Uint8Array[] = [];
let total = 0;

while (true) {
const { value, done } = await reader.read();
if (done) break;
if (!value) continue;

total += value.byteLength;
if (total > maxBytes) {
void reader.cancel().catch(() => {});
throw new Error('payload_too_large');
}
chunks.push(value);
}

const out = new Uint8Array(total);
let offset = 0;
for (const c of chunks) {
out.set(c, offset);
offset += c.byteLength;
}

return new TextDecoder().decode(out);
}

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

/**
* Maximum JSON request body size in bytes.
* Used by the built-in JSON parsing middleware for basic DoS resistance.
*
* @default 1_000_000 (1 MB)
*/
maxBodyBytes?: number;
}

/**
Expand All @@ -39,7 +80,7 @@ export interface CreateMcpHonoAppOptions {
* @returns A configured Hono application
*/
export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono {
const { host = '127.0.0.1', allowedHosts } = options;
const { host = '127.0.0.1', allowedHosts, maxBodyBytes = DEFAULT_MAX_BODY_BYTES } = options;

const app = new Hono();

Expand All @@ -55,9 +96,26 @@ export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono {
return await next();
}

// Fast-path: reject known oversized payloads without reading.
const clRaw = c.req.header('content-length') ?? '';
const cl = Number(clRaw);
if (Number.isFinite(cl) && cl > maxBodyBytes) {
return c.text('Payload too large', 413);
}

// Parse from a clone so we don't consume the original request stream.
let text: string;
try {
text = await readRequestTextWithLimit(c.req.raw.clone(), maxBodyBytes);
} catch (error) {
if (error instanceof Error && error.message === 'payload_too_large') {
return c.text('Payload too large', 413);
}
return c.text('Invalid JSON', 400);
}

try {
// Parse from a clone so we don't consume the original request stream.
const parsed = await c.req.raw.clone().json();
const parsed = JSON.parse(text);
c.set('parsedBody', parsed);
} catch {
// Mirror express.json() behavior loosely: reject invalid JSON.
Expand Down
13 changes: 13 additions & 0 deletions packages/middleware/hono/test/hono.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ describe('@modelcontextprotocol/hono', () => {
expect(await res.json()).toEqual({ a: 1 });
});

test('createMcpHonoApp returns 413 on oversized JSON bodies', async () => {
const app = createMcpHonoApp({ maxBodyBytes: 10 });
app.post('/echo', (c: Context) => c.text('ok'));

const res = await app.request('http://localhost/echo', {
method: 'POST',
headers: { Host: 'localhost:3000', 'content-type': 'application/json' },
body: JSON.stringify({ a: '0123456789' })
});
expect(res.status).toBe(413);
expect(await res.text()).toBe('Payload too large');
});

test('createMcpHonoApp returns 400 on invalid JSON', async () => {
const app = createMcpHonoApp();
app.post('/echo', (c: Context) => c.text('ok'));
Expand Down
Loading