Support the stateless MCP protocol revision 2026-07-28 (server/discover, per-request _meta)
Why
Follow-up from #611372 (closed) and !253324 (merged).
MCP spec revision 2026-07-28 replaces the stateful protocol with a stateless one:
- Drops the
initializehandshake,Mcp-Session-Id, the GET stream, and SSE resumability. - Every request carries
_metawith a protocol version and client capabilities. - Servers expose a new
server/discovermethod. - Results carry a
resultType.
Our server is already stateless, so the gap is small. !253324 (merged) made us stop claiming 2026-07-28, so an initialize asking for it gets 2025-11-25 back. That is correct, but it leaves us speaking only the legacy protocol. This issue adds the stateless revision alongside the legacy one, on the same endpoints.
Extensions are negotiated the same way, through clientCapabilities.extensions in _meta and capabilities.extensions in server/discover. So the tasks, elicitation, skills and apps work is blocked on this issue.
Where we are today
What we have
- A stateless server in practice: we never mint or read
Mcp-Session-Id, handle each POST independently, return 405 on GET, and never push to clients. initializeas a pure function that echoes back a version and capabilities, inlib/api/mcp/handlers/initialize_request.rband its EE twinee/lib/api/orbit/mcp_handlers/initialize_request.rb.- Two endpoints with duplicated JSON-RPC handling,
lib/api/mcp/base.rbandee/lib/api/orbit/mcp.rb. Reviewers asked for a shared helper on !253324 (merged) and !253329 (merged). - A start on that helper. !253329 (merged) adds
lib/api/helpers/mcp/json_rpc.rb, which holds onlyjsonrpc_request_idtoday. - A user-specific
tools/list, since it depends on the current user and theX-Gitlab-Enabled-Mcp-Server-Toolsheader, so itscacheScopemust be"private". - A docs update in flight. !253836 (merged) adds the supported version table and the deprecation policy to
doc/user/model_context_protocol/mcp_server.md.
What we do not have
server/discover. A request for it returns HTTP 404 with-32601. The spec makes this method mandatory.- Any reading of
_metafrom requests. - Validation of the
MCP-Protocol-Version,Mcp-Method, andMcp-Nameheaders. resultType,io.modelcontextprotocol/serverInfo,ttlMs, orcacheScopein results. Note the full_metakey name, it is not a bareserverInfo.-32020,-32021, and-32022, the three error codes this revision defines.
What the earlier version of this issue got wrong
- It said we have
-32001for version mismatch and that it "stays correct for legacy requests". Neither part holds. The entry sits inlib/api/mcp/base.rband nothing references it, inlib/,ee/lib/,spec/, oree/spec/. An unsupported version raisesArgumentError, which the endpoint turns into-32602. The spec also now treats-32000to-32019as legacy and tells new implementations not to use it. So-32001is dead code to delete, not behaviour to keep. - It listed only
-32020and-32022. The revision defines three codes.-32021MissingRequiredClientCapabilitywas missing. - It wrote
_meta.serverInfo. The real key isio.modelcontextprotocol/serverInfo. - It said header validation returns
-32020"on a mismatch". A missing required header is also a-32020. It also left out the=?base64?VALUE?=sentinel onMcp-Name, which the server must decode before it compares the value to the body. That comparison is the whole point of the header rules, so skipping the decode would let a caller put one name in the header and another in the body. - It listed the shared helper and the docs update as new work. !253329 (merged) and !253836 (merged) already start both.
Proposed approach
Phase 1: dual-era server (this issue)
- Extend
lib/api/helpers/mcp/json_rpc.rbto hold the JSON-RPC version constant, the shared error table,method_not_found!, andformat_jsonrpc_response. Both endpoints include it. Drop the dead-32001. Keep-32603in the EE endpoint, which is the only one that uses it. Keep-32700out, since Workhorse answers 400 before we see a parse error. No behaviour changes. - Read
params[:_meta]. A request is modern whenio.modelcontextprotocol/protocolVersionis present, otherwise it stays legacy and nothing changes for it. For a modern request, requireio.modelcontextprotocol/protocolVersionandio.modelcontextprotocol/clientCapabilities, and answer-32602with HTTP 400 when either is missing. Answer-32022with HTTP 400 and adataholdingsupportedandrequestedfor a version we do not speak. Add-32021withdata.requiredCapabilitiesfor a capability the client did not declare, even though no tool needs one today. Logio.modelcontextprotocol/clientInfowhen it arrives. - Validate the headers on modern requests.
MCP-Protocol-Versionmust equal the_metaversion,Mcp-Methodmust equal the bodymethod, andMcp-Namemust equalparams.nameontools/call. A missing required header and a mismatched one both answer-32020with HTTP 400. Decode the=?base64?VALUE?=sentinel onMcp-Namefirst. Compare header names case insensitively and values case sensitively. IgnoreMcp-Session-IdandLast-Event-ID. - Add
server/discover, returningresultType,supportedVersions,capabilities,_meta["io.modelcontextprotocol/serverInfo"],ttlMs, andcacheScope. - Add
resultType: "complete"andio.modelcontextprotocol/serverInfoto every modern result. AddttlMsandcacheScope: "private"totools/list.ttlMsmust be>= 0. Legacy results gain none of these fields. - Document the stateless revision on top of !253836 (merged): the
_metakeys, the headers, the new error codes,server/discover, the caching hints, and the fact that a legacy client keeps working unchanged on the same endpoint.
Every item covers both endpoints and needs specs, plus a request spec showing a legacy client and a modern client hitting the same endpoint.
Phase 2: later
MRTR (Now tracked in #585072.input_required) for tools that need user confirmation.subscriptions/listen(not needed whilelistChangedisfalse).- Deterministic tool ordering for prompt caching.
Open questions
- Which protocol version do the Duo Agent Platform executor and Duo Chat clients send today, and will they move to
langchain.mcpor the FastMCP client? This decides how urgent phase 1 is. - How does a dual-era client react to our current 404
-32601forserver/discover? The spec lists-32601as a recognized modern error, so a client could read us as a modern server with no discovery support. We should test this with the LangChain client before and after the change. - The transport revision also says a server must validate the
Originheader and answer 403 when it is present and invalid. We should check whether Workhorse or Rails already covers this for API endpoints before we add anything. - This revision says a request
idmust not be null and a notification must not carry one. Our Grape params markidoptional on both endpoints. Worth deciding whether to tighten that for modern requests. No urgency signal yet:Answered in the Why section: the tasks, elicitation, skills and apps extensions are blocked on this issue.@jessieaytested on !253324 (merged) that Cursor 3.15 and 3.19 still ask for2025-11-25. No popular client needs this today. This is about being ready, not fixing a break.
References
- Changelog
- SEP-2575: stateless protocol,
_meta,server/discover - SEP-2567: sessions removed
- SEP-2243:
Mcp-MethodandMcp-Nameheaders - SEP-2549:
ttlMsandcacheScope - SEP-2322:
resultType - Error codes:
-32020,-32021,-32022 - Versioning and backward compatibility
- Streamable HTTP
server/discover- Base protocol and
_meta - Caching
- LangChain: MCP in LangChain, stateless protocol, elicitation, and more