SEP-2567: Sessionless MCP via Explicit State Handles#2567
Conversation
Should this be "prior request call state"? I don't think we want other requests to effect the outcome as well right? |
|
|
||
| **Clients.** Clients become simpler: they no longer track or resend session identifiers, or need to determine whether a given server is stateful. List-endpoint caching becomes safe. | ||
|
|
||
| Rollout is a clean break: sessions are removed in the next spec version, with no deprecation window. Servers that currently rely on session-scoped state stay on the current protocol version until they have migrated to explicit handles. Protocol version negotiation already handles mixed-version deployments — a client that supports both versions speaks the old protocol to an unmigrated server and the new one to everyone else. This avoids shipping a version where clients support both modes simultaneously, which would prevent the caching benefit (a client cannot cache list endpoints if any connected server might be session-scoped). |
There was a problem hiding this comment.
Rollout is a clean break: sessions are removed in the next spec version, with no deprecation window. Servers that currently rely on session-scoped state stay on the current protocol version until they have migrated to explicit handles.
Somewhere in this SEP we should provide an example of how SDKs should handle this. I think we are essentially saying that the session functionality become a no-op.
|
This looks good to me. Supportive of this! |
|
This was reviewed by Core Maintainers: 6 Accepted, 2 Accept with Changes. |
|
New commits were pushed — removed the |
d95feda to
dada908
Compare
This means that MCP Servers cannot filter tools, resources, or prompts based on authorization. Seems like a step backwards. Why list an entity the client / user can't use? Actually, I guess you could do an auth to entity permission look-up on every call. It just means greater burden on the server |
|
Continuing from #2575 (comment)
Even if all transport calls are authenticated/authorized, there is a danger that the user with lower permissions could potentially hijack the session of a super user by using their handle id. |
You can still filter based on auth. I should clarify that sentence as I can see why it looks like it implies the opposite. "Per-connection state" means relying on information sent in previous messages on the connection. The auth headers are on every request, so it is stateless. |
@pja-ant , mcp-session-id was documented in the Lifecycle section of the protocol. The Custom Headers from Tool Parameters are documented much later on in the transport section. So the chances of client authors forgetting to implement custom headers are higher, I would think. As @bittola mentioned, perhaps the Custom Headers from Tool Responses feature could be considered if mcp-session-id is too specific? |
|
@bittola Looking forward to your real-world eval results. Just a note on what I did: it wasn't a simple eval but rather a stress-test eval specifically designed to try and make this fail, i.e. the model was juggling several concurrent sessions, several hundred tool calls, distractors interspersed that presented fake IDs to try and confuse the model. It should be much more complex than any real world usage. That being said, it was still synthetic at the end of the day and we should trust real-world data more. @vanya-lebedev for the headers, this time round we now have the Conformance testing framework so that we can verify that Tier 1 SDKs will have support for this from day 1. Note: even if we were to introduce something like @baharclerode thanks for the clarification and you are right about the difference in security posture, I was wrong there. Regarding your use case of anonymous resource ownership without information leaking to the model, that does appear to be a use case that will not be supported by the next MCP spec version natively. I do think it is a reasonable use case though and probably deserves some proper integration into the auth layer to support anonymous users in a future spec release. |
In light of these two things, would there be any chance to mark |
There was a lot of discussion around this, and unfortunately it's not really possible for a few reasons:
|
|
@pja-ant fair point on But I think there's a narrower case the current draft leaves uncovered: transport metadata that the gateway needs but the LLM has no business seeing. Concrete example we're hitting: our gateway routes by an affinity ID identifying which backend instance holds a workflow's process-local state. Today (with With explicit handles + The pattern I'd suggest is a generalization of Concretely: a sibling to Crucially this is additional to explicit handles, not a replacement. Application state still flows through tool args where the model can reason about it. This is just for the genuinely infra-only category (routing affinity, trace context propagation, etc.) where the model has nothing useful to do with the value and putting it in context costs tokens for no benefit. Scoping it tightly to transport metadata (not general "remembered headers") sidesteps the sub-agent / forking ambiguity you flagged - there's no application-level question about which conversation owns the value, because the value's scope is whatever scope the host's conversation reset clears. |
|
The spec change removing the The handshake was where servers minted per-conversation identifiers like Concrete failure: two tool calls fired in parallel at the start of a conversation (which some hosts do) each random-route to different backend instances. Each mints its own state. The conversation ends up with split-brain state across instances that can't see each other. The mandated per-request fields are all "client identity" shaped, not per-conversation. The change leaves a critical flow for stateful backends without a spec-level mechanism - either a replacement is needed, or the handshake removal needs to be reconsidered. |
|
@pja-ant any response to the last comments? |
|
Hey, apologies, got busy and forgot to come back to this. From what you are describing, it sounds like you have some form of workflow, for which all the tool calls need to hit the same sticky server, is that right? Currently these workflows are tied to conversations(*) and so the use case puts the affinity ID into the session ID so that you have a routing handle. Functionally the recommendation is that for your workflow tools to add a It seems the main objection to this is that you'd like the LLM to not see the workflow/affinity ID. What's the constraint there that's driving the requirement? Regarding the "per conversation" framing and suggestion, I do want to emphasize that:
Just to give some examples of where the idea of this breaks down:
As Luca linked to above, there are some other discussions in this area around similar ideas, e.g. #2822 - the Transports WG will be discussing these. All are welcome to join the discussion if that would be useful. It's possible we'll have something else in this area in the near future, but unlikely in time for the new spec. Perhaps something will come in as an extension. |
|
You're right that "workflow" is a more accurate framing than "conversation" for our case - the affinity is per-workflow, not per the broader chat. On LLM visibility: honestly it's a preference, not a hard requirement. The model CAN see the workflow_id and our system works. The concerns are token cost (small), increased leak surface (routing metadata in LLM context is loggable and screenshot-able where transport metadata isn't), and layer cleanliness. None are showstoppers. The harder issue is that workflow_id inherits the same problems SEP-2567 originally raised against What actually works is a client-generated per-workflow identifier present on the very first request, which is what PR 2822 proposes. Will engage there. Agreed it likely doesn't make 2026-07-28 - just flagging the impact: this affects the F&O MCP server, which is part of the Dynamics 365 platform deployed across tens of thousands of enterprise organizations, and we don't currently have a way to prevent the parallel-first-call failure mode without protocol-level support. |
|
Just want to share some real-world experience here — we built piia-engram, an open-source MCP memory server (personal knowledge store across tools). It's inherently stateful — user identity, knowledge graph, session history, the whole thing. But we never needed Basically we ended up doing exactly what this SEP describes — explicit state handles — without even thinking about it, because it's just... the natural way to build a knowledge store. The state lives in the data layer, not the transport. One thing I can confirm from @pja-ant's point about handle compaction: yeah, it happens. When Claude Code compacts a long conversation, sometimes the knowledge IDs get dropped. We deal with it by making handles re-discoverable — Re: @vanya-lebedev's Dynamics case — I get that some apps are deeply session-coupled and can't change overnight. But I think the distinction is: apps that are stateful in their data (like us) vs apps that are stateful in their transport (like form wizards). This SEP handles the first case really well. The second case might need a different solution, but probably shouldn't hold up the spec. |
Summary
Draft SEP proposing the removal of protocol-level sessions from MCP, replacing implicit session-scoped state with explicit, server-minted state handles. Builds on SEP-1442 (stateless-by-default) but argues the opt-in stateful path should not exist at all.
Core claims:
tools/listforcesO(subagents × servers)re-fetches on everyone, even when ~zero servers actually use it(deployment, auth)granularityWhat changes:
session/create,session/destroy, orMcp-Session-Idheadertools/list/resources/list/prompts/listMUST NOT depend on per-connection or prior-tool-call statecreate_*() -> handle+ threaded parameters (guidance, not a protocol construct)Imported from modelcontextprotocol/transports-wg#25.