@@ -3,6 +3,39 @@ import { Hono } from 'hono';
33
44import { hostHeaderValidation , localhostHostValidation } from './middleware/hostHeaderValidation.js' ;
55
6+ const DEFAULT_MAX_BODY_BYTES = 1_000_000 ; // 1MB
7+
8+ async function readRequestTextWithLimit ( req : Request , maxBytes : number ) : Promise < string > {
9+ const body = req . body ;
10+ if ( ! body ) return '' ;
11+
12+ const reader = body . getReader ( ) ;
13+ const chunks : Uint8Array [ ] = [ ] ;
14+ let total = 0 ;
15+
16+ while ( true ) {
17+ const { value, done } = await reader . read ( ) ;
18+ if ( done ) break ;
19+ if ( ! value ) continue ;
20+
21+ total += value . byteLength ;
22+ if ( total > maxBytes ) {
23+ void reader . cancel ( ) . catch ( ( ) => { } ) ;
24+ throw new Error ( 'payload_too_large' ) ;
25+ }
26+ chunks . push ( value ) ;
27+ }
28+
29+ const out = new Uint8Array ( total ) ;
30+ let offset = 0 ;
31+ for ( const c of chunks ) {
32+ out . set ( c , offset ) ;
33+ offset += c . byteLength ;
34+ }
35+
36+ return new TextDecoder ( ) . decode ( out ) ;
37+ }
38+
639/**
740 * Options for creating an MCP Hono application.
841 */
@@ -22,6 +55,14 @@ export interface CreateMcpHonoAppOptions {
2255 * to restrict which hostnames are allowed.
2356 */
2457 allowedHosts ?: string [ ] ;
58+
59+ /**
60+ * Maximum JSON request body size in bytes.
61+ * Used by the built-in JSON parsing middleware for basic DoS resistance.
62+ *
63+ * @default 1_000_000 (1 MB)
64+ */
65+ maxBodyBytes ?: number ;
2566}
2667
2768/**
@@ -39,7 +80,7 @@ export interface CreateMcpHonoAppOptions {
3980 * @returns A configured Hono application
4081 */
4182export function createMcpHonoApp ( options : CreateMcpHonoAppOptions = { } ) : Hono {
42- const { host = '127.0.0.1' , allowedHosts } = options ;
83+ const { host = '127.0.0.1' , allowedHosts, maxBodyBytes = DEFAULT_MAX_BODY_BYTES } = options ;
4384
4485 const app = new Hono ( ) ;
4586
@@ -55,9 +96,26 @@ export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono {
5596 return await next ( ) ;
5697 }
5798
99+ // Fast-path: reject known oversized payloads without reading.
100+ const clRaw = c . req . header ( 'content-length' ) ?? '' ;
101+ const cl = Number ( clRaw ) ;
102+ if ( Number . isFinite ( cl ) && cl > maxBodyBytes ) {
103+ return c . text ( 'Payload too large' , 413 ) ;
104+ }
105+
106+ // Parse from a clone so we don't consume the original request stream.
107+ let text : string ;
108+ try {
109+ text = await readRequestTextWithLimit ( c . req . raw . clone ( ) , maxBodyBytes ) ;
110+ } catch ( error ) {
111+ if ( error instanceof Error && error . message === 'payload_too_large' ) {
112+ return c . text ( 'Payload too large' , 413 ) ;
113+ }
114+ return c . text ( 'Invalid JSON' , 400 ) ;
115+ }
116+
58117 try {
59- // Parse from a clone so we don't consume the original request stream.
60- const parsed = await c . req . raw . clone ( ) . json ( ) ;
118+ const parsed = JSON . parse ( text ) ;
61119 c . set ( 'parsedBody' , parsed ) ;
62120 } catch {
63121 // Mirror express.json() behavior loosely: reject invalid JSON.
0 commit comments