Recursive AI

Streamable HTTP server: limits and defaults

All onemodelcontext docs

StreamableHttpEndpoint serves both protocol eras on one path over the small IHttpExchange abstraction. Every limit below is a property of StreamableHttpOptions.

OptionDefaultEffect
ValidateHostonThe Host header must be a loopback name or listed in AllowedHosts.
AllowedOrigins, AllowLoopbackOriginsnone, onA request with an Origin header outside the list is refused with 403 (DNS-rebinding protection). A request without Origin is accepted. * disables the check and is unsafe.
MaxRequestBodyBytes8 MiB (ModelContext.ServeHttpAsync: 1 MiB)A larger declared body gets 413 without being read; a chunked body is cut off at the limit. A body with a declared length is held in a buffer of exactly that size.
MaxInFlightBodyBytes64 MiBRequest bodies held in memory at once, over all requests of the endpoint. A body is charged its declared length (chunked: its size so far) until its request ends; a request that would exceed the budget gets 503 with Retry-After: 1.
MaxStreamLifetime1 hourA legacy GET event stream is ended with a retry field after about this long (checked every KeepAliveInterval); a live client resumes with Last-Event-ID, a reader that never reads is dropped. Zero keeps streams until the client leaves.
BodyReadTimeout30 sA body that trickles in more slowly gets 408.
SseWriteTimeout30 sA client that stops reading an event stream is dropped.
KeepAliveInterval15 sComment lines keep idle streams open.
MaxSessions, SessionIdleTimeout1000, 30 minLegacy sessions only; more sessions get 503, idle ones are removed.
MaxReplayEvents, MaxReplayBytes256, 4 MiBEvents kept per session for Last-Event-ID resumption.
MaxStreamsPerSession16Concurrent GET streams of one legacy session.

Status codes

  • Envelope errors are 400: parse errors, invalid requests, a missing or mismatched MCP-Protocol-Version, Mcp-Method, Mcp-Name or Mcp-Param-* header (-32020), an unsupported version (-32022), a missing capability (-32021), and a modern request whose _meta lacks the protocol version or the client capabilities (2026-07-28 Streamable HTTP, "Protocol Version Header"; SEP-2575).
  • A POST without Content-Type: application/json (parameters such as charset are fine) is 415. Browsers can then only send it after a CORS preflight, which the Origin check answers.
  • A POST that finds the in-flight body budget used up is 503.
  • A method the modern endpoint does not implement is 404 with -32601, including initialize sent with the 2026-07-28 version header.
  • Everything a handler says (unknown tool, bad arguments, resource not found) is a 200 with the error in the body.
  • A legacy request other than initialize without Mcp-Session-Id is 400; with an unknown or ended session it is 404.

Era handling

  • Modern requests carry no session; a stray Mcp-Session-Id is ignored and never echoed. Closing the response stream cancels the request.
  • Legacy requests are pinned to the session. A dropped connection does not cancel a running handler, so the client can resume the stream with Last-Event-ID; an unknown id is 400.
  • server/discover without _meta is answered -32602 with status 400: the discovery page defines its request as the standard _meta and nothing else, and every 2026-07-28 request must carry the version and client capabilities there.

The built-in HTTP listener: limits and defaults

HttpServer is a small HTTP/1.1 server on Socket (with SslStream when a certificate is set) for local tools and command-line programs. ModelContext.ServeHttpAsync starts one on the loopback address. For a server on the internet, host the endpoint in a web framework. The listener checks nothing about Origin or Host; StreamableHttpEndpoint does, as above. Every limit below is a property of HttpServerOptions.

OptionDefaultEffect
Address, Portloopback, 0Any other address is an explicit choice and warns unless Certificate or TrustedProxies is set; TrustedProxies on an any-address bind warns too, because direct clients reach the port as well. The warning is a DiagnosticKind.Warning event when a diagnostics subscriber exists, otherwise one line on standard error. Port 0 picks a free port (LocalEndPoint).
Certificate, SslProtocolsnone, OS defaultWith a certificate every connection is TLS and only http/1.1 is offered through ALPN.
TrustedProxiesnoneOnly these peers' X-Forwarded-For, -Proto and -Host are believed (the client address is the first address from the right that is not itself a trusted proxy; the last proto and host value). For every other peer all X-Forwarded-* and Forwarded headers are removed before the handler sees the request.
MaxConnections256Further connections queue in the operating system's accept backlog.
MaxConnectionsPerAddress64Connections one client IP address may hold at once (IPv4-mapped IPv6 folded in). A surplus connection is closed at once. Behind a reverse proxy all clients share the proxy's address: raise it there.
MaxRequestLineBytes8 KiB414.
MaxHeaderBytes, MaxHeaderCount32 KiB, 100431. The head is read into one pooled buffer of this size.
MaxRequestBodyBytes8 MiBA larger Content-Length gets 413 before anything is read; a chunked body is cut off at the limit with 413.
FirstByteTimeout10 sA new connection that sends no byte is closed silently (the shorter of this and IdleTimeout).
IdleTimeout60 sA keep-alive connection that sends no byte between requests is closed silently.
HeaderReadTimeout10 sFrom the first byte of a request to the end of its head (and for the TLS handshake). Slower gets 408. A sender that trickles bytes still hits it.
RequestTimeout30 sFrom the end of the head to the end of the body. Slower gets 408.
WriteTimeout30 sOne write to the peer. A client that stops reading is dropped.
ShutdownGracePeriod5 sHow long DisposeAsync waits for handlers after canceling them.

What is refused

The parser is strict (RFC 9112), because a disagreement with a proxy about where a request ends is what request smuggling uses. Each of these gets 400 (unless stated), is answered with Connection: close, and nothing after it on the connection is parsed:

  • a bare CR or LF anywhere in the head; obsolete line folding; whitespace before a header colon; control characters in values;
  • Content-Length together with Transfer-Encoding; a repeated, signed, spaced or list-valued Content-Length;
  • a transfer coding other than a lone chunked (501); Transfer-Encoding on HTTP/1.0;
  • a missing or repeated Host on HTTP/1.1; request targets other than origin-form; lower-case method names; versions other than 1.0 and 1.1 (505); bad percent escapes and encoded NULs in the path;
  • malformed chunk sizes (non-hex, signed, spaced, empty, more than 15 digits), a chunk not followed by CRLF, bare LF in chunk framing, obsolete folding in a trailer; Expect other than 100-continue (417).

Chunk extensions are read and ignored (RFC 9112 section 7.1.1) and trailers are read and dropped, both bounded.

Connection behavior

  • Keep-alive on HTTP/1.1; HTTP/1.0 connections close after the response. Pipelined requests are answered in order, one at a time.
  • A handler that does not read the whole request body leaves the connection open only if the rest is at most 64 KiB; otherwise it closes.
  • IHttpExchange.Aborted is canceled when the peer closes the connection, including closing only its sending side, or the server stops. While a handler runs with a pipelined request already waiting, a hang-up is not watched for.
  • Every accepted socket has TCP keep-alive probes on (first after 60 s, then every 15 s, 4 misses), so a peer that vanished is evicted; the TLS close notification is bounded to 2 s.
  • Error responses the listener writes itself carry X-Content-Type-Options: nosniff.
  • After an error response the server finishes its side, then reads and discards up to 64 KiB for up to 2 s so the peer sees the response instead of a connection reset.
  • A response that is never flushed is sent with Content-Length; a flushed one (every event stream) is chunked. A handler may set Content-Length itself, and then writing more or fewer bytes closes the connection. Transfer-Encoding cannot be set, and header names and values with control characters are refused when set.