Get your API key
MCP goes stateless: what the 2026-07-28 spec changes for PDF servers

MCP goes stateless: what the 2026-07-28 spec changes for PDF servers

The MCP 2026-07-28 revision drops the initialize handshake and session IDs. Here is what changes for a PDF generation server, with verified spec quotes.

10 min read

The Model Context Protocol revision dated 2026-07-28 is now the current spec, and it changes the shape of the protocol rather than adding features to it. The initialize and initialized handshake is gone. The Mcp-Session-Id header is gone. Every request now carries its own protocol version and capabilities, and servers are told not to infer anything from prior requests. For a PDF generation server, where a single tool call can produce a multi-megabyte binary, this pushes one design decision from "nice to have" to "the way the protocol expects you to work": return a handle, not a payload.

What exactly changed in the 2026-07-28 revision

MCP became a stateless request/response protocol. The spec overview lists "Stateless, self-contained requests" and "Per-request capability negotiation" as the two defining properties of the base protocol. The versioning page is blunt about the consequence: "There is no negotiation handshake. Every request carries its protocol version, and the server accepts or rejects each request independently."

The statelessness section spells out what a server may no longer do:

  • Servers MUST NOT rely on prior requests over the same connection to establish context such as capabilities, protocol version, or client identity.
  • Servers SHOULD NOT require that a client reuse the same connection or process to perform related operations.
  • State that spans multiple requests MUST be referenced by an explicit identifier the client passes on each request.

A note in the same section makes the intent explicit: an open connection, including a stdio process, is not a conversation or a session, and clients may interleave unrelated requests on the same transport.

Per-request metadata replaces the handshake. Every client request carries io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in its _meta field, both required, plus optional clientInfo and logLevel. A request missing a required field is malformed and the server must reject it with JSON-RPC error -32602 and HTTP 400.

Capability discovery moved to a dedicated call. Servers MUST implement server/discover, which returns supported protocol versions, capabilities, and identity in a single request. Clients MAY call it first, but they are free to send any request inline and handle an UnsupportedProtocolVersionError (code -32022) if their preferred version is not supported.

Why a stateless protocol matters for a PDF server

Stateless requests remove the single hardest constraint on scaling an MCP server: the need for every request in a conversation to reach the same process. A PDF render is CPU-heavy and slow relative to a database read, so the ability to spread calls across instances behind an ordinary load balancer is worth more here than on a server that only reads rows.

Under the handshake model, a session ID tied a client to a server instance that held negotiated capabilities in memory. Scaling meant sticky routing or shared session storage. Under 2026-07-28, each tools/call is self-describing, so any instance can answer it. For a render workload, that means a burst of 200 invoice renders can fan out across every instance in the pool instead of queueing behind one.

The second effect is on intermediaries. The transport overview says a binding MAY mirror selected body fields into envelope metadata, and Streamable HTTP does exactly that so "intermediaries can route and inspect requests without parsing the body." The announcement post names the headers: Mcp-Method and Mcp-Name. A gateway can rate-limit a render call differently from a template list without deserializing JSON, which is the practical difference between a usable API gateway rule and a custom proxy.

The third effect is caching. The announcement describes ttlMs and cacheScope on list and read responses, so a client can cache a tool catalog across reconnections instead of re-fetching it. A server with 14 tools and 4 resources pays that cost on every cold start today.

The handle pattern, and what PDF4.dev already does

The spec's instruction that cross-request state "MUST be referenced by an explicit identifier the client passes on each request" is the rule a PDF server runs into first, because a rendered PDF is the archetypal piece of state that does not belong in a JSON-RPC response.

PDF4.dev's render_pdf MCP tool accepts a delivery parameter with two values, base64 and url. In URL mode the handler calls saveRender(), then signRenderToken(), and returns this structured result:

if (delivery === "url") {
  const saved = await saveRender(auth.organizationId, pdf);
  const token = signRenderToken(saved.id, saved.expiresAt);
  const url = `${SITE_URL}/api/v1/renders/${saved.id}?token=${token}`;
  return structuredResult({
    success: true as const,
    size_bytes: pdf.length,
    duration_ms: duration,
    format: "url" as const,
    url,
    expires_at: new Date(saved.expiresAt).toISOString(),
  });
}

The token is an HMAC-SHA256 signature over id:expiresAt, and the expiry is embedded in the token itself. The retrieval route, GET /api/v1/renders/[id]?token=..., takes no session cookie and no API key. It verifies the signature with a constant-time comparison, checks the embedded expiry, and streams the bytes. Nothing on the server correlates that fetch with the MCP call that produced it.

Being precise about the claim: this matches the spirit of the handle rule, not its letter. The spec talks about identifiers the client passes back on subsequent MCP requests, as the Tasks extension does. Our signed URL is fetched over plain HTTP, outside the protocol, and is never passed back into another tool call. The useful property it shares with a spec handle is that the reference is fully self-describing and verifiable without any server-side session, which is the property statelessness actually demands. The useful property it lacks is protocol-level lifecycle: no client can ask us to extend, cancel, or re-poll it through MCP.

There is also a storage caveat worth stating. saveRender writes to data/renders/ on local disk with a 24-hour TTL and a 2000-entry soft cap. The protocol surface is stateless; the storage behind the handle is single-instance today. Moving to multiple instances means moving those bytes to object storage first. The signing and verification logic does not change, because the token needs no lookup.

Concerndelivery: "base64"delivery: "url"
Where the PDF bytes goInline in the tool resultSigned URL, fetched out of band
Agent context costFull base64 payloadOne URL plus metadata
Server state between callsNoneRender file plus JSON sidecar, 24h TTL
Auth on retrievalCovered by the tool callHMAC token with embedded expiry
Fits the 2026-07-28 handle guidanceNo, payload inlinePartially, self-describing reference outside the protocol

For any PDF over roughly 1 MB, base64 inflates the payload by about a third and spends the agent's context window on bytes it cannot read. URL delivery was added for that reason, before this revision existed. The revision makes it the default-shaped answer rather than an optimization.

Deprecations and the twelve-month window

Four things are deprecated in this revision and none of them stop working today. The versioning page defines the policy: a deprecated feature "remains part of the specification, but is scheduled for removal," documents a migration path, and stays in the spec "for at least twelve months, or at least ninety days under the policy's expedited-removal exception," before it becomes eligible for removal.

The deprecated list, per the announcement:

DeprecatedReplacementNotes
RootsNone named in the announcementRemains functional during the window
SamplingMulti round-trip requests for mid-call inputRemains functional during the window
LoggingNone named in the announcementlogLevel still travels per request in _meta
Legacy HTTP+SSE transportStreamable HTTPFallback path for dual-era clients
Dynamic Client RegistrationClient ID Metadata DocumentsDCR deprecated, still functional for compatibility

Server-initiated requests that used to need an open stream are replaced by multi round-trip requests. A result now carries a resultType field; "complete" means the final content is present, and "input_required" means the server needs more from the client, which retries the original call with the answers attached. Clients must treat an absent resultType as "complete" for backward compatibility with earlier revisions.

Authorization tightened in three ways. RFC 9207 issuer validation is required. An application_type parameter was added to Dynamic Client Registration. Client credentials are bound to their issuing authorization server. PDF4.dev's MCP server authenticates with a bearer API key rather than an OAuth flow, so none of this is load-bearing for us today, but any MCP server fronting an OAuth authorization server needs to read the authorization page directly rather than trusting a summary.

Backward compatibility: what breaks and what does not

Nothing breaks on its own, because the spec defines a dual-era mode. A server may implement both behaviors on the same endpoint: a request carrying modern per-request _meta is served statelessly under 2026-07-28, while an initialize request selects legacy semantics scoped to the process or session.

The compatibility matrix in the spec gives the outcome for each pairing. Two rows are the ones that hurt:

  • A modern client against a legacy-only server fails. On stdio the spec recommends sending server/discover first so the failure is deterministic and the client can surface an actionable error.
  • A legacy client against a modern-only server fails, and legacy clients have no fall-forward mechanism. The spec asks modern-only servers to name their supported versions in any error returned to an initialize request, because that message may be the only diagnostic the user ever sees.

On the SDK side, the announcement states that the TypeScript, Python, Go, and C# Tier 1 SDKs support the revision immediately, with the Rust SDK in beta.

Where PDF4.dev stands, honestly

Our MCP server does not speak 2026-07-28 yet. It runs on mcp-handler 1.0.x over Streamable HTTP with SSE disabled, which means the legacy handshake and session model. Migration depends on the handler library shipping modern-era support, not on our tool code, since every tool is already registered through server.registerTool with an outputSchema and returns structuredContent.

What already lines up:

  • Auth per request. The bearer token is read from the request headers and carried to handlers through AsyncLocalStorage. There is no negotiated auth state held between calls, so nothing has to change when the session disappears.
  • Handle-shaped output. delivery: "url" returns a signed reference with a self-contained expiry, verified without a database lookup.
  • Structured errors. authError(), permissionError(), notFoundError(), invalidRequestError(), and apiError() all return the same envelope with isError: true, so a client parsing a failure does not depend on conversation context.

What does not line up yet:

  • No server/discover implementation, because the handler library owns the protocol layer.
  • Render bytes live on a single instance's disk, so horizontal scaling needs an object-storage swap behind saveRender before statelessness buys anything real.
  • No Tasks extension support. A render that outgrows a request timeout would be the natural candidate, and io.modelcontextprotocol/tasks with its poll-based operations and tasks/update method is where that belongs. We have not built it.

What to do this week if you run an MCP server

Read the spec pages rather than summaries, then check three things in your own code. First, grep for anything your handlers read that was set during initialize, because that is the code the new model deletes. Second, find every tool that returns a payload larger than a few kilobytes and give it a handle mode, because the protocol now says cross-request state belongs behind an explicit identifier. Third, decide whether you will run dual-era or modern-only, and if modern-only, make sure your error response to an initialize request names the versions you support.

The twelve-month deprecation window means there is no emergency. The stateless core is not a deprecation, though: it is the protocol now, and a server built around a session object will drift further from it with every revision.

Sources:

Start generating PDFs

Build PDF templates with a visual editor. Render them via API from any language in ~300ms.