Request Limits

Bounded bodies, strict content types, and capped batches.

Every JSON-RPC POST is bounded before its body is read: at most 4 MiB, Content-Type: application/json. Both checks run after origin and auth — cheap header checks first — and before any parsing. A legacy batch is capped at 50 calls, checked as soon as the envelope is parsed and before any call in it runs.

defineMcpHandler({
  name: "my-server",
  version: "1.0.0",

  limits: {
    maxBodySize: 1024 * 1024, // bytes (default: 4 MiB); `false` for unbounded
    contentType: ["application/json"], // default; `false` to accept any type
    maxBatchSize: 50, // default; `false` for unbounded
  },

  tools: [/* ... */],
});

#Responses

RequestResponse
Body larger than maxBodySize413
Batch longer than maxBatchSize413
Missing Content-Type400
Malformed Content-Type422
Content-Type not in the accepted list415

The rejection messages are static — a bad Content-Type is never echoed back to the sender, and the batch rejection does not name the configured cap.

#Why Cap A Batch

A batch is one HTTP request carrying many calls, and every call in it is dispatched concurrently. Left unbounded, a single request that any per-request rate limit counts once fans out into as many tool invocations as fit in the body — and into whatever those tools call in turn. maxBodySize alone is a weak bound on that, since the smallest legal call is a few dozen bytes.

Only the legacy era accepts batches. On 2026-07-28 every message is its own POST, and a top-level array is rejected outright, so maxBatchSize has nothing to do there.

#Never Fully Buffered

An oversized body is rejected up front when Content-Length declares it, and otherwise aborted as it is read, so a lying-small Content-Length on a chunked upload is still caught mid-stream. The body is never accumulated in memory just to measure it.

Only the request body is bounded, so a subscriptions/listen POST — small body, long-lived response stream — is unaffected. GET and DELETE carry no body and skip every check.

#Disabling

defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  limits: false,
  tools: [/* ... */],
});

Or relax one check only:

limits: {
  maxBodySize: false;
} // unbounded body, still application/json only and 50 calls per batch
limits: {
  contentType: false;
} // any content type, still 4 MiB and 50 calls per batch
limits: {
  maxBatchSize: false;
} // unbounded batch, still 4 MiB and application/json only

Warning

limits: false makes the endpoint accept an unbounded body of any type, carrying an unbounded number of calls. If something upstream already enforces limits, fine — otherwise this is the easiest denial-of-service vector an MCP endpoint has, because a single POST can consume memory before any of your code runs.

#Sizing the Limit

4 MiB is generous for JSON-RPC. Tighten it to what your largest legitimate request needs — a tool that accepts a pasted document may need a few hundred KiB; one that takes an id needs a few hundred bytes.

Remember that the limit applies to the whole body: a tools/call embedding base64 content pays roughly 4/3 of the raw byte size. If you need genuinely large inputs, take a URI or an id and fetch it server-side rather than raising the cap.

h3-mcp  MCP servers, built on H3.