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.

@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

This comment was marked as spam.

koic added a commit to modelcontextprotocol/ruby-sdk that referenced this pull request Jun 17, 2026
## Motivation and Context

SEP-2567 (modelcontextprotocol/modelcontextprotocol#2567, merged for the 2026-07-28 spec release)
makes MCP sessionless: every Streamable HTTP POST must be fully self-contained,
with no protocol-level session state shared between requests.

`StreamableHTTPTransport` already implements the SEP's transport surface via
`stateless: true` (no `Mcp-Session-Id` issued or required, GET returns 405,
DELETE is a no-op, server-to-client requests raise), but its dispatch had a state leak:
stateless POSTs were handled with `session: nil`, so `Server#init` wrote `@client` and
`@client_capabilities` onto the shared `Server` instance.
Concurrent stateless requests could therefore observe another client's identity,
and the data persisted across requests, which is exactly what SEP-2567 forbids.
The TypeScript SDK's stateless prototypes (the closed typescript-sdk#2058/#2131/#2251 stack)
solve this with per-request dispatch; this change applies the equivalent fix within the existing architecture:

- Stateless `handle_initialization`, `handle_regular_request`, and `dispatch_notification` now
  run handlers against an ephemeral per-request `ServerSession` (with `session_id: nil`),
  so client info, logging level, and initialized state live only for the duration of that POST.
  Repeated `initialize` requests are naturally permitted because each POST gets a fresh,
  never-initialized session.
- `send_notification` in stateless mode now returns `false` (non-delivery) instead of raising.
  With ephemeral sessions in place, a tool calling `server_context.report_progress` or `notify_log_message`
  would otherwise route every call into the exception reporter; non-delivery matches how
  these helpers already degrade when no session exists. `send_request` (server-to-client requests) still raises,
  as those are genuinely unsupported without a stream.

Resolves #388.

## How Has This Been Tested?

New tests in `test/mcp/server/transports/streamable_http_transport_test.rb`:

- a stateless `initialize` POST leaves `Server#client_capabilities` and the server's `@client` untouched
  (the leak regression)
- repeated `initialize` POSTs both succeed with 200 and no `Mcp-Session-Id` header
- a tool calling `server_context.report_progress` under stateless mode returns its result normally and
  the exception reporter is never invoked
- the existing "stateless mode does not support server-sent events" test is updated to assert
  the new `false` return instead of the removed raise

All other existing stateless-mode tests pass unchanged.

## Breaking Changes

`StreamableHTTPTransport#send_notification` in stateless mode now returns `false` instead of raising `RuntimeError`.
The raise message was not a documented contract, broadcasting in stateless mode was always a non-deliverable operation,
and the boolean return matches the method's documented semantics in every other non-delivery case.
Default (session-oriented) mode is unchanged.
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.