Skip to content

SEP-2567: Sessionless MCP via Explicit State Handles#2567

Merged
pja-ant merged 10 commits into
mainfrom
sep/sessionless-mcp
May 7, 2026
Merged

SEP-2567: Sessionless MCP via Explicit State Handles#2567
pja-ant merged 10 commits into
mainfrom
sep/sessionless-mcp

Conversation

@pja-ant

@pja-ant pja-ant commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

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:

  • Application state is better served by explicit identifiers
  • The mere possibility of session-scoped tools/list forces O(subagents × servers) re-fetches on everyone, even when ~zero servers actually use it
  • Session cardinality of exactly-one prevents mixed shared/isolated state (e.g., subagents sharing a cart but isolating browser state)
  • Explicit handles are strictly more expressive and make list endpoints cacheable at (deployment, auth) granularity

What changes:

  • No session/create, session/destroy, or Mcp-Session-Id header
  • tools/list / resources/list / prompts/list MUST NOT depend on per-connection or prior-tool-call state
  • Stateful workflows use create_*() -> handle + threaded parameters (guidance, not a protocol construct)

Imported from modelcontextprotocol/transports-wg#25.

@pja-ant pja-ant requested review from a team as code owners April 14, 2026 12:43
@pja-ant pja-ant changed the title SEP-XXXX: Sessionless MCP via Explicit State Handles SEP-2567: Sessionless MCP via Explicit State Handles Apr 14, 2026
@pja-ant pja-ant added SEP draft SEP proposal with a sponsor. labels Apr 14, 2026
@kurtisvg kurtisvg self-assigned this Apr 14, 2026
@kurtisvg kurtisvg added the transport Related to MCP transports label Apr 14, 2026
@dsp-ant dsp-ant added the roadmap/transport Roadmap: Transport Evolution & Scalability (incl. Server Cards) label Apr 15, 2026
@kurtisvg

Copy link
Copy Markdown
Contributor

tools/list / resources/list / prompts/list MUST NOT depend on per-connection or prior-tool-call state

Should this be "prior request call state"? I don't think we want other requests to effect the outcome as well right?

Comment thread docs/seps/2567-sessionless-mcp.mdx Outdated
Comment thread docs/seps/2567-sessionless-mcp.mdx Outdated
Comment thread docs/seps/2567-sessionless-mcp.mdx Outdated
Comment thread docs/seps/2567-sessionless-mcp.mdx Outdated

**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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@CaitieM20

Copy link
Copy Markdown
Contributor

This looks good to me. Supportive of this!

Comment thread docs/seps/2567-sessionless-mcp.mdx
@CaitieM20 CaitieM20 added this to the 2026-06-30-RC milestone Apr 17, 2026
@pja-ant

pja-ant commented Apr 20, 2026

Copy link
Copy Markdown
Contributor Author

This was reviewed by Core Maintainers: 6 Accepted, 2 Accept with Changes.

@pja-ant pja-ant added accepted SEP accepted by core maintainers, but still requires final wording and reference implementation. and removed draft SEP proposal with a sponsor. labels Apr 20, 2026
@mcp-commander mcp-commander Bot removed the accepted SEP accepted by core maintainers, but still requires final wording and reference implementation. label Apr 20, 2026
@mcp-commander

mcp-commander Bot commented Apr 20, 2026

Copy link
Copy Markdown

New commits were pushed — removed the accepted label. Re-approve with /lgtm.

@briankrug

briankrug commented Apr 27, 2026

Copy link
Copy Markdown

tools/list / resources/list / prompts/list MUST NOT depend on per-connection or prior-tool-call state

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

@dawid-nowak

dawid-nowak commented Apr 28, 2026

Copy link
Copy Markdown

Continuing from #2575 (comment)

After:

create_counter() // returns counter_id
increase_counter(counter_id)
get_counter(counter_id)

  1. It looks like the server logic needs to tweaked to suit MCP Protocol. And MCP server needs to introduce their own sessions handling if necessary.
  2. Is there a mechanism to pass information about the principal of a user who made these calls. For example from the security/audit perspective if create_counter is created by user A and that counter id is leaked then any other user can call increase_counter and thus hijack the session.

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.

@pja-ant

pja-ant commented Apr 28, 2026

Copy link
Copy Markdown
Contributor Author

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?

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.

@vanya-lebedev

Copy link
Copy Markdown

The problem we've been seeing is that clients simply do not implement the session handling consistently, which has created a lot of confusion for server authors and adds non-trivial operational overhead for client implementers.

@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?

@pja-ant

pja-ant commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

@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 x-mcp-remembered it doesn't really address the issues outlined in the SEP that we saw with mcp-session-id, namely that no clients would agree on what to mint a new one: per tool call / per conversation / per app start / per user - and it raises a bunch of unanswerable questions around what happens to them when you introduce session forking, sub-agents etc. -- there is no single answer there that works and x-mcp-remembered doesn't avoid that.

@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.

@baharclerode

Copy link
Copy Markdown

that will not be supported by the next MCP spec version natively.

it is a reasonable use case though

In light of these two things, would there be any chance to mark Mcp-Session-Id as deprecated for the upcoming version of the spec and only remove it in the following version when a replacement capability that covers this use-case is provided? (Even if the rest of the changes regarding stateless tool/prompt/resource listing and explicit state handles still move forward - those sound great!)

@pja-ant

pja-ant commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

In light of these two things, would there be any chance to mark Mcp-Session-Id as deprecated for the upcoming version of the spec and only remove it in the following version when a replacement capability that covers this use-case is provided? (Even if the rest of the changes regarding stateless tool/prompt/resource listing and explicit state handles still move forward - those sound great!)

There was a lot of discussion around this, and unfortunately it's not really possible for a few reasons:

  1. Due to the stateless changes (SEP-2575) we're removing the initialize handshake, so we're losing the place where Mcp-Session-Ids were minted in the first place.
  2. We did discuss re-introducing sessions in another form, but voted against this for a number of reasons, primarily that sessions were so poorly implemented by existing applications, we were not confident that any new session implementation would be implemented any better, which would exacerbate the incompatibility issues we're seeing now and just lead to another year or two of pain. Additionally, the mere existence of sessions complicates almost every aspect of the spec since you need to specify when things are and aren't scoped to sessions.
  3. If people really want to depend on sessions they can still use them by pinning their server to the 2025-11-25 spec version, which will be supported for at least a year from the next spec release.

@bittola

bittola commented Jun 1, 2026

Copy link
Copy Markdown

@pja-ant fair point on x-mcp-remembered - you're right that the "when to mint" question is unanswerable for application state (sub-agents, forking, etc.).

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 Mcp-Session-Id) the gateway wraps responses as <sessionId>;<affinityId> and the client echoes it back transparently - the LLM never sees the affinity ID, and that's correct because it's a gateway-routing concern, not application state.

With explicit handles + x-mcp-header, the only way to get the affinity ID back to the gateway is to bundle it into the session ID the model threads - putting infrastructure metadata into LLM context. Functionally works, but it's the "hidden state, wrong layer" problem inverted.

The pattern I'd suggest is a generalization of requestState from SEP-2322 (MRTR): the spec already says server returns an opaque value, client echoes it back without surfacing to the model, any server instance can pick up the retry. Same shape; just generalize the scope from "one logical operation" to "the conversation" for transport-only metadata.

Concretely: a sibling to x-mcp-header for the response->request direction. Server emits a header annotated as transport-only; conforming clients MUST echo it on subsequent requests in the same conversation without surfacing to the model. No protocol-managed lifecycle - value lives with the conversation, dies when the host resets it (same as requestState's implicit scoping by request continuation).

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.

@bittola

bittola commented Jun 2, 2026

Copy link
Copy Markdown

The spec change removing the initialize handshake creates a critical issue worth surfacing.

The handshake was where servers minted per-conversation identifiers like Mcp-Session-Id that the client then carried on subsequent requests. With the handshake gone and no replacement mechanism, a stateful backend wanting deterministic first-call routing has no per-conversation discriminator the spec guarantees on every request. This is the bootstrap path every conversation goes through, not an edge case.

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.

@bittola

bittola commented Jun 4, 2026

Copy link
Copy Markdown

@pja-ant any response to the last comments?

@pja-ant

pja-ant commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

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 workflow_id parameter that gets mirrored to a header via the new protocol affordances to do that, which can then be used for routing.

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:

  1. Existing LLM applications rarely scoped an Mcp-Session-Id to a conversation, including the most popular clients. Mcp-Session-Id has existed for well over a year now and it is showing no signs of having a converged definition. People just have to work around it.
  2. Despite everyone's best efforts (this has been discussed for well over 6 months now), the Transports WG has yet to arrive at the definition of a "session" or "conversation" or "logically related operations" that everyone agrees on.

Just to give some examples of where the idea of this breaks down:

  • Does a session end if a chat is cleared?
  • What happens if a session forks?
  • What happens if the application restarts?
  • What happens if the chat uses sub-agents, or talks to other agents?
  • What happens if my app is just one long-running "conversation" (e.g. LLM chat bot that lives in single Discord channel)?

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.

@bittola

bittola commented Jun 4, 2026

Copy link
Copy Markdown

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 Mcp-Session-Id: cardinality (who mints, when to replace, behavior across sub-agents and forking), reliance on the LLM to thread the value correctly, and the bootstrap on top - the first call has no handle to thread, so parallel first calls go to different instances and each mints its own, you get split state. Deployment-layer routing on identity doesn't fix this at our scale either - a single user can have hundreds of agents in parallel, each in its own workflow; routing them all to one instance would create hot-spots and defeat horizontal scaling.

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.

@Patdolitse

Patdolitse commented Jun 6, 2026

Copy link
Copy Markdown

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 Mcp-Session-Id for any of it. Every piece of knowledge gets its own ID when created, updates go through update_knowledge(id, ...), session context is saved/restored via explicit save_agent_context() / get_resume_brief() calls. Our tools/list is the same regardless of who's calling or what happened before.

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 — search_knowledge(query) always works even if you lost the original ID. Might be worth mentioning "servers SHOULD provide discovery mechanisms for state handles" as guidance.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

final SEP finalized. roadmap/transport Roadmap: Transport Evolution & Scalability (incl. Server Cards) SEP transport Related to MCP transports

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.