BYOKchat Blog

MCP Streamable HTTP Explained

Understand modern MCP Streamable HTTP: stateless requests, required headers, JSON-RPC bodies, subscriptions, routing, retries, authentication, and migration from older session-oriented transports.

· 7 min read

On this page
  1. Start with the modern request shape
  2. The protocol no longer needs a session handshake
  3. Why statelessness matters operationally
  4. Required headers make requests routable
  5. Header/body disagreement is an error
  6. Build headers from the normalized request
  7. Mcp-Param-* can mirror selected parameters
  8. HTTP status and JSON-RPC errors are different layers
  9. Authentication is still ordinary HTTP authorization
  10. A stateless request is easier to retry—but not automatically safe
  11. Distinguish no-response from explicit failure
  12. Do not hide retries below the tool coordinator
  13. Streaming responses are not the same as subscriptions
  14. subscriptions/listen is deliberate opt-in state
  15. Subscription streams are reconnectable, not authoritative state
  16. Graceful close and unexpected close differ
  17. Modern interactive flows use MRTR instead of server-initiated request channels
  18. Long-running work should not hold one HTTP request forever
  19. Request identifiers are not business idempotency keys
  20. Protocol-version handling must be explicit
  21. Migrating from legacy HTTP+SSE
  22. Request cancellation is best effort
  23. Timeouts should be layered
  24. Proxy behavior matters
  25. TLS is part of server identity
  26. Redirects require care
  27. Do not log sensitive headers
  28. Trace operation stages separately
  29. Header mismatch tests are essential
  30. Test load-balanced statelessness
  31. Test unknown outcomes
  32. A clean transport boundary
  33. Where BYOKchat fits
  34. Further reading

Streamable HTTP is the primary network transport shape for modern remote MCP.

The important word in 2026 is no longer “streamable.” It is stateless.

The 2026-07-28 MCP revision removes the modern protocol’s old initialization/session assumptions and makes each request independently routable.

That changes how clients should think about:

  • connection lifecycle;
  • load balancing;
  • retries;
  • server identity;
  • subscriptions;
  • interactive requests;
  • long-running work.

Start with the modern request shape

A modern MCP request is an HTTP POST containing a JSON-RPC message plus protocol-routing headers.

Conceptually:

POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Authorization: Bearer <token>

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": {
      "q": "stateless MCP"
    },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28"
    }
  }
}

The exact metadata depends on the request and implementation, but the architecture is clear:

HTTP request
+ MCP protocol version
+ routable operation metadata
+ JSON-RPC body

The protocol no longer needs a session handshake

Older MCP clients often used this lifecycle:

initialize
-> initialized
-> receive/use Mcp-Session-Id
-> send subsequent requests in that session

For 2026-07-28 modern requests, that protocol-level handshake is removed.

So the modern mental model becomes:

configured endpoint
-> optionally discover capabilities
-> send independent operation
-> receive result

That does not mean the application cannot keep durable state.

It means the transport no longer hides that state in an MCP session.

Why statelessness matters operationally

A request can land on any healthy server instance.

Diagram illustrating the surrounding section

No sticky routing is required by the modern protocol core.

This enables ordinary web infrastructure:

  • round-robin load balancing;
  • autoscaling;
  • serverless request handling;
  • per-request tracing;
  • simpler failover.

The application can still use databases or explicit handles for business state.

Required headers make requests routable

The 2026-07-28 protocol generation requires routing headers on modern Streamable HTTP requests.

Important examples include:

MCP-Protocol-Version
Mcp-Method
Mcp-Name

Mcp-Name applies when the operation has a meaningful name/URI/task identifier that the HTTP binding mirrors.

The goal is that gateways can inspect the operation without parsing the JSON body.

This supports:

  • routing;
  • rate limiting;
  • observability;
  • policy enforcement;
  • diagnostics.

Header/body disagreement is an error

A client must not send:

Mcp-Method: tools/call

while the JSON-RPC body says:

{
  "method": "resources/read"
}

Likewise, if a mirrored name/header disagrees with the body, a compliant modern server can reject the request.

Treat header generation as part of request serialization, not as decorative metadata.

Build headers from the normalized request

A safe implementation order is:

construct normalized MCP request
-> validate operation
-> derive required headers from normalized request
-> serialize body
-> send both together

Do not let UI code or caller-supplied arbitrary header strings define Mcp-Method independently.

One request object should be the source of truth.

Mcp-Param-* can mirror selected parameters

The modern HTTP binding also supports mirrored request parameters for schemas that designate them with the relevant MCP header annotation/extension.

That enables infrastructure to route or inspect selected operation parameters without reading the body.

The security implication is important:

Any value mirrored into an HTTP header may be exposed to infrastructure logs and intermediaries that treat headers differently from bodies.

Do not casually mark secrets or highly sensitive free-form values for header mirroring.

HTTP status and JSON-RPC errors are different layers

An MCP client should preserve both.

Examples:

TCP/TLS failure
HTTP 401
HTTP 429
HTTP 500
JSON-RPC error
successful JSON-RPC result containing tool-level failure

Those are not interchangeable.

A useful internal error model might include:

interface MCPFailure {
  transport?: TransportError
  httpStatus?: number
  rpcCode?: number
  rpcMessage?: string
  operation?: string
  retryClass: RetryClass
}

This makes retry and UI behavior more precise.

Authentication is still ordinary HTTP authorization

Remote MCP servers can use OAuth bearer tokens or other supported HTTP authentication patterns.

The transport layer should attach credentials through secure headers.

Do not embed tokens in:

query strings
server display names
conversation history
JSON tool arguments

Credential storage and refresh should remain outside the conversation/tool record.

See OAuth for MCP Explained and How to Secure Remote MCP Servers.

A stateless request is easier to retry—but not automatically safe

Transport statelessness removes one source of retry complexity.

It does not make side-effecting operations idempotent.

Suppose:

POST tools/call create_invoice

The server processes it, but the network fails before the client receives the response.

The client sees:

unknown outcome

Retrying blindly may create a second invoice.

The correct rule remains:

Retry only when operation semantics and idempotency guarantees permit it.

See Idempotency for AI Tool Execution.

Distinguish no-response from explicit failure

These cases are different:

server returned validation error

versus:

connection dropped after request bytes were sent

In the second case, the side effect may have happened.

Your retry engine should represent an outcomeUnknown state.

Do not hide retries below the tool coordinator

A generic HTTP library configured for automatic retries can be dangerous.

For example:

network library retries POST on connection reset

without knowing the operation was destructive.

Prefer retry policy at a layer that knows:

  • MCP method;
  • tool identity;
  • side-effect class;
  • idempotency key/support;
  • current approval state.

Streaming responses are not the same as subscriptions

The word “streamable” can create confusion.

There are at least three different streaming concepts:

  1. a response delivered incrementally while one operation is active;
  2. a long-lived subscriptions/listen stream for selected notifications;
  3. model-provider token streaming, which is outside MCP entirely.

Keep them separate in code.

MCP operation response stream != MCP subscription != model token stream

subscriptions/listen is deliberate opt-in state

Modern MCP does not rely on an always-open session notification channel.

A client can explicitly call subscriptions/listen with the notification categories it wants.

Conceptually:

{
  "jsonrpc": "2.0",
  "id": "listen-1",
  "method": "subscriptions/listen",
  "params": {
    "notifications": {
      "toolsListChanged": true,
      "resourcesListChanged": true
    }
  }
}

The server acknowledges the supported subset, then publishes matching notifications on the stream.

Subscription streams are reconnectable, not authoritative state

A subscription can fail because:

  • mobile app suspended;
  • network changed;
  • proxy timeout;
  • server deployment;
  • process crash;
  • authentication expired.

After reconnect, do not assume every notification was observed.

Reconcile authoritative state when needed.

For example:

tools-list-changed notification
-> refresh tools/list

rather than trying to mutate the list from a missed delta stream.

Graceful close and unexpected close differ

A client should know whether the server deliberately completed a subscription or the connection simply disappeared.

The implementation can expose something like:

graceful
remoteDisconnect
cancelledByClient
authExpired

The reconnection policy can then be appropriate.

Modern interactive flows use MRTR instead of server-initiated request channels

The stateless core cannot depend on a server keeping a client-specific request channel alive so it can ask questions later.

For operations that need more input, modern MCP uses Multi Round-Trip Requests.

Conceptually:

client sends tools/call
server returns input_required
client collects user/model input
client retries original call with inputResponses + requestState

This preserves stateless HTTP routing.

See MCP Multi-Round-Trip Requests Explained.

Long-running work should not hold one HTTP request forever

A five-minute deployment or export is better modeled as a task than as one fragile request.

With the Tasks extension:

operation
-> task handle
-> tasks/get
-> tasks/update / tasks/cancel as supported
-> terminal result

This decouples durable work from one connection lifetime.

See MCP Tasks Explained.

Request identifiers are not business idempotency keys

JSON-RPC id identifies a request/response exchange.

Do not assume reusing the same JSON-RPC ID guarantees side-effect deduplication.

If the application/server needs idempotency, use a mechanism explicitly designed for that operation.

Protocol-version handling must be explicit

A server may support:

modern 2026-07-28 behavior
older initialize/session behavior
both through compatibility mode

The client should not mix lifecycle rules accidentally.

For example:

modern request + legacy Mcp-Session-Id assumptions

is a sign that version handling is leaking.

Keep a protocol profile per connection/request path.

Migrating from legacy HTTP+SSE

Legacy HTTP+SSE transport is deprecated in the current protocol generation.

A migration path should avoid one giant compatibility abstraction.

Separate transports behind an interface:

interface MCPTransport {
  send(request: MCPRequest): AsyncResult<MCPResponse>
  listen?(filter: SubscriptionFilter): AsyncSequence<MCPNotification>
}

Then implement:

LegacySSETransport
ModernStreamableHTTPTransport

with version-specific behavior inside each.

Do not sprinkle if legacy across conversation logic.

Request cancellation is best effort

If the user cancels while an HTTP request is in flight, the client can abort its connection/request.

That does not prove the server stopped processing.

For side-effecting operations, cancellation should mean:

client no longer waiting

not necessarily:

operation definitely did not happen

If the server exposes a task or cancellation operation, reconcile authoritative state.

Timeouts should be layered

Useful timeout categories include:

connect timeout
TLS/auth timeout
first-response timeout
idle-stream timeout
overall operation timeout
task polling deadline
subscription reconnect backoff

One global 30-second timeout is rarely enough.

See Timeout Design for AI Applications when that article is published.

Proxy behavior matters

Remote MCP may sit behind:

  • CDN;
  • reverse proxy;
  • API gateway;
  • corporate proxy;
  • service mesh.

Streaming/subscription behavior can be affected by:

  • buffering;
  • idle timeouts;
  • maximum request duration;
  • header stripping;
  • auth rewriting.

When debugging, show the resolved endpoint and protocol stage, not only “MCP failed.”

TLS is part of server identity

For remote servers, validate HTTPS normally.

Do not disable certificate validation just because the server is an MCP endpoint.

A custom endpoint should not get weaker TLS policy than any other API connection.

Redirects require care

Automatic redirects can change origin.

Before forwarding authorization credentials to a redirected destination, apply normal secure HTTP rules.

Do not blindly preserve bearer tokens across arbitrary cross-origin redirects.

Do not log sensitive headers

Diagnostics are useful, but redact:

Authorization
Cookie
private custom headers
token-like query parameters

It is usually safe to log protocol headers such as:

MCP-Protocol-Version
Mcp-Method
Mcp-Name

provided names themselves are not sensitive in your application context.

Trace operation stages separately

A useful trace can include:

resolve connection
obtain/refresh token
serialize MCP request
connect
server response start
parse JSON-RPC
normalize result

For subscription:

listen opened
acknowledged filter
notification received
stream closed
reconnect scheduled

This is much easier to debug than one duration value.

Header mismatch tests are essential

Test that your serializer rejects or catches:

Mcp-Method != JSON body method
Mcp-Name != body name
missing protocol version
missing required name header
malformed mirrored parameter header

These are protocol correctness tests, not just networking tests.

Test load-balanced statelessness

A powerful integration test alternates requests between server instances.

request 1 -> instance A
request 2 -> instance B
request 3 -> instance C

If the modern client/server unexpectedly depends on hidden session state, this test exposes it quickly.

Test unknown outcomes

Simulate:

server commits side effect
connection closes before response

Verify the client does not blindly repeat the operation.

This is one of the most important reliability tests for any HTTP-based tool system.

A clean transport boundary

Diagram illustrating the surrounding section

The tool coordinator should not care how TLS sockets, redirects, or JSON framing are implemented.

The HTTP layer should not decide whether a tool call is safe to retry.

Where BYOKchat fits

A provider-neutral client can treat MCP Streamable HTTP as one transport implementation beneath its MCP tool layer.

The application can keep:

  • server configuration durable;
  • credentials in secure storage;
  • tool rounds in local conversation state;
  • permission decisions in app policy;
  • HTTP requests transient;
  • subscription streams reconnectable;
  • Tasks durable when needed.

That separation matches the modern stateless MCP architecture and avoids tying user workflows to one network connection.

Further reading

Keep reading