What stateless MCP changes for agents that manage links
The 2026-07-28 revision drops sessions and the handshake, adds routing headers and retry-based confirmations. What that means for bulk link jobs, deletes, gateways and OAuth.

As of the 2026-07-28 revision, the Model Context Protocol has no sessions and no handshake. Every request carries its own protocol version and client capabilities, any server instance can answer it, and a server that needs a yes from the user no longer holds a stream open to ask: it returns a result that says "input required" and waits for the client to retry.
For agents that create, edit and delete links, that changes three practical things. Bulk jobs scale behind a plain load balancer, but a dropped connection now means a resend, so creates have to be safe to repeat. A "this will overwrite a live destination" confirmation becomes a second request. And gateways can finally tell delete_link from list_links without parsing JSON. This piece walks through the changes that matter if you build or connect agents to a link API. For what a link should return to an agent, see Links for LLMs; this one is about the protocol.
What changed, in one table
The release post by lead maintainers David Soria Parra and Den Delimarsky says the TypeScript, Python, Go and C# SDKs support the revision, with Rust in beta. Microsoft's C# SDK v2.0, released the same day, runs its HTTP transport statelessly by default. The changes that matter for link tooling:
| Area | 2025-11-25 | 2026-07-28 |
|---|---|---|
| Sessions | Mcp-Session-Id header | Removed; cross-call state is a server-minted handle passed as a tool argument |
| Handshake | initialize, then notifications/initialized | Removed; version and capabilities ride in every request's _meta |
| Discovery | Part of initialize | server/discover: servers must implement it, clients may call it |
| Server asks the user | Server-initiated elicitation/create on an open stream | Multi Round-Trip Requests: resultType: "input_required", client retries |
| Change notifications | HTTP GET stream, resources/subscribe | subscriptions/listen |
| Routing | Parse the body | Required Mcp-Method and Mcp-Name headers |
| List caching | None | Required ttlMs and cacheScope on list results |
| Broken streams | Resumable via Last-Event-ID | Not resumable; re-issue with a new request ID |
The two sources read slightly differently on server/discover. The release post calls it not required; the versioning page is precise: servers must implement it, and clients may call it or simply send any request and handle UnsupportedProtocolVersionError (now code -32022).
Stateless is a scaling feature for bulk link jobs
The release post puts it plainly: "any request can land on any instance behind a plain round-robin load balancer." For a job that creates 500 campaign links, that means no sticky sessions and no shared session store. Each tools/call is a self-describing POST.
POST /mcp HTTP/1.1
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-11-25","capabilities":{},
"clientInfo":{"name":"campaign-bot","version":"1.0.0"}}}
# server replies with Mcp-Session-Id: 7c1e...; client sends
# notifications/initialized, then every call carries that header
POST /mcp HTTP/1.1
Mcp-Session-Id: 7c1e...
MCP-Protocol-Version: 2025-11-25
{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"create_link","arguments":{"url":"https://example.com/fall"}}}POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: create_link
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"create_link",
"arguments":{"url":"https://example.com/fall","slug":"fall-26"},
"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"campaign-bot","version":"1.0.0"},
"io.modelcontextprotocol/clientCapabilities":{}}}}Two consequences are easy to miss.
State becomes explicit. The changelog says servers that need cross-call state use "explicit, server-minted handles passed as ordinary tool arguments." A bulk import that used to live in a session becomes a start_import tool that returns an import_id, and every later call passes it. That is easier to log, retry and hand from one agent to another.
Retries become your problem. The Streamable HTTP transport dropped resumability: "Resumable SSE streams via Last-Event-ID are not supported." A broken response stream loses the in-flight request, and the client must re-issue it with a new ID. Closing the stream also counts as cancellation, but a server that already wrote the link to its database cannot un-write it. So a create_link that timed out may or may not have created the link, and the resend must not mint a second one. Two patterns work: let the caller choose the slug, so a repeat collides with the first attempt instead of creating a random duplicate, or accept an idempotency key as a tool argument and return the existing link on repeat.
The revision also asks servers to return tools/list in a deterministic order and requires ttlMs (a freshness hint in milliseconds) and cacheScope ("public" or "private") on list results. A link server whose tools do not change per tenant can let clients cache the tool list instead of polling it before every batch.
Confirmations become retries
Deleting a link that is printed on 40,000 flyers deserves a confirmation. Under the old revision, the server sent elicitation/create while holding the tools/call stream open, which tied the conversation to one server instance. Multi Round-Trip Requests replace that: the server ends the request with an InputRequiredResult, and the client retries the original call with the answer in params.inputResponses, echoing params.requestState exactly.
{"jsonrpc":"2.0","id":7,
"result":{
"resultType":"input_required",
"inputRequests":{
"confirm_delete":{
"method":"elicitation/create",
"params":{
"mode":"form",
"message":"go.example.com/fall-26 has live traffic. Delete it?",
"requestedSchema":{
"type":"object",
"properties":{"confirm":{"type":"boolean"}},
"required":["confirm"]}}}},
"requestState":"opaque, integrity-protected by the server"}}{"jsonrpc":"2.0","id":8,"method":"tools/call",
"params":{
"name":"delete_link",
"arguments":{"slug":"fall-26"},
"inputResponses":{
"confirm_delete":{"action":"accept","content":{"confirm":true}}},
"requestState":"opaque, integrity-protected by the server",
"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"campaign-bot","version":"1.0.0"},
"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}The retry needs a new JSON-RPC id, and any instance can process it. The elicitationId field and the notifications/elicitation/complete notification from 2025-11-25 are gone; a server that needs to correlate across retries puts its own identifier in requestState.
One more rule shapes the design: servers must not send an elicitation to a client that did not declare the capability, and must not assume the client will retry at all. A link server should therefore keep a fallback for headless agents, such as an explicit confirm: true argument on destructive tools, or refuse the change and say why.
Headers give gateways something to work with
Every POST now carries Mcp-Method, and tools/call, resources/read and prompts/get also carry Mcp-Name. Servers can mirror chosen tool parameters into Mcp-Param-{Name} headers with the x-mcp-header schema annotation. For link platforms that means a stock gateway can rate-limit create_link per key, demand a stronger scope for delete_link, or reject a tool no policy allows, all before reading the body.
The headers are a claim, not proof. The server must compare them with the body and reject a mismatch with 400 and HeaderMismatch (renumbered to -32020). Tigera's Alister Baroi sums it up: "The header is a claim the server will eventually verify; the body remains the truth." Use headers to deny early and route cheaply; for allowing a sensitive operation, keep checking the body. The spec adds that intermediaries enforcing policy on mirrored headers should reject requests whose MCP-Protocol-Version predates header validation.
Auth: check the issuer, register by URL
Three authorization changes affect any client that connects to a link platform over OAuth:
- Issuer validation. Authorization servers should send the
issparameter from RFC 9207, and clients must check it against the recorded issuer before redeeming the code. That closes the mix-up attack, where a client talking to several servers is tricked into sending a code to the wrong one. - Credentials belong to one issuer. Clients must key stored credentials by issuer, must not reuse them with another authorization server, and must re-register when it changes.
- Dynamic Client Registration is deprecated in favour of Client ID Metadata Documents: the
client_idis an HTTPS URL pointing to a JSON document with at leastclient_id,client_nameandredirect_uris. Servers advertise support withclient_id_metadata_document_supported. DCR keeps working for servers that lack it, and clients that still use it must now sendapplication_type.
Token-in-header servers sit outside most of this. Bitly's MCP server, announced in September 2025, takes an API token from account settings in a header, so the OAuth changes do not touch it; the transport changes still do.
Deprecations have a clock now. The new lifecycle policy guarantees at least twelve months before removal, and the deprecated list includes Roots, Sampling, Logging, the old HTTP+SSE transport and DCR.
Where rii.link fits
rii.link runs a remote MCP server at https://mcp.rii.link. Per its docs, any client that speaks Streamable HTTP uses that URL, interactive agents sign in with OAuth 2.1 (PKCE S256, one-hour access tokens), and automation sends a workspace API key as a Bearer header, where "a read-only key yields a read-only agent." The tools are create_link, list_links, update_link, delete_link, list_domains, render_qr and link_stats.
The docs do not yet state which protocol revision the server speaks, or whether create_link deduplicates retries, so we will not claim either here. A 2026-07-28 client can ask the server itself with server/discover. Until retry behaviour is documented, pass your own slug on creates in bulk jobs, so a resend after a dropped stream cannot quietly produce a second, randomly named link.
Migration checklist
If you run an MCP server for links:
- Implement
server/discoverand answer unknown versions withUnsupportedProtocolVersionError, listing what you support. - Move session state into explicit handles returned by tools.
- Make every create safe to repeat: caller-chosen slugs or an idempotency argument.
- Replace server-initiated elicitation with
InputRequiredResult, protectrequestStatewith an HMAC or AEAD, and keep a fallback for clients without elicitation. - Validate
Mcp-Method,Mcp-Nameand anyMcp-Param-*headers against the body. - Return
ttlMsandcacheScopeon list results, and list tools in a stable order. - Update error codes:
-32020,-32021,-32022, and-32602for a missing resource.
If you build an agent or client:
- Send
MCP-Protocol-Version,Mcp-Method,Mcp-Nameand the_metafields on every request. - On a broken stream, re-issue with a new ID, and only for tools you know are safe to repeat.
- Handle
resultType: "input_required"by collecting answers and retrying with a new ID. - Validate
iss, key credentials by issuer, and prefer Client ID Metadata Documents over DCR. - Keep a legacy fallback for servers that still need
initialize: on a400without a modern JSON-RPC error body, fall back.
Written by
Florian
Florian builds rii.link, the routing layer behind short links: redirects, dynamic responses and links that answer AI agents. He writes The Redirect about what happens between a click and its destination.


