Skip to content

SEP-XXXX: Sessionless MCP via Explicit State Handles#25

Open
pja-ant wants to merge 3 commits intomainfrom
pja/sessionless-mcp-sep
Open

SEP-XXXX: Sessionless MCP via Explicit State Handles#25
pja-ant wants to merge 3 commits intomainfrom
pja/sessionless-mcp-sep

Conversation

@pja-ant
Copy link
Collaborator

@pja-ant pja-ant commented Mar 12, 2026

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:

  • Every legitimate use of session scoping — application state, mutable tool lists, resource subscriptions — 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)

Includes two possible follow-on sketches: first-class state handles (host UX, compaction survival) and tools-returning-tools (replacement for list mutation).

Test plan

  • WG review of motivation and specification sections
  • Gather feedback on the clean-break rollout vs. deprecation window
  • Reference implementation: migrate a session-scoped Playwright server to explicit create_browser() / destroy_browser() handles
  • Resolve open question on handle durability metadata (machine-readable TTL vs. documented policy)

pja-ant added 3 commits March 12, 2026 14:00
Add draft proposal to remove the protocol-level session concept from MCP
entirely, replacing implicit session-scoped state with explicit,
server-minted state handles that the model threads through subsequent
calls.

Key arguments:
- Session lifetime is undefined and server authors cannot design around it
- List endpoints cannot be cached across sessions, forcing O(subagents ×
  servers) re-fetches even when tool sets never change
- Session cardinality is fixed at exactly one, preventing mixed
  shared/isolated state in subagent fan-out patterns
- Explicit handles are strictly more expressive: anything expressible
  with a session is expressible with a single ID created up front

Builds on SEP-1442 (stateless-by-default) but goes further by asking
whether the opt-in stateful path needs to exist at all.

## Open Questions

- **Do models actually handle explicit IDs as reliably as implicit session state?** The Rationale section argues that models are already good at carrying opaque identifiers through a conversation, but that's an argument, not data. We should validate empirically — comparative evals of session-scoped vs. handle-threaded workflows, looking specifically at hallucinated IDs, dropped IDs across long conversations, and recovery behavior when an ID is lost — before treating this as a safe assumption.
Copy link
Member

@evalstate evalstate Mar 12, 2026

Choose a reason for hiding this comment

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

This is my primary concern here: even a 99.999% accuracy means 10 failures per million calls. If we agree sessions are relatively low-stakes, and that errors are recoverable this is fine.

If your tool is proxying to a remote environment, and session ABC123 is the production environment and ABC223 is the test environment, you can see how this could be unexpectedly calamitous.

<8B class models are where this really breaks down - although tool calling at this size tends to be a bit hit-or-miss with >4-5 tools anyway.

Edit: the real test here is on a context window at turn 300 at 180k with all kinds of mixed tool successes, failures etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A few thoughts on this. I do agree this is a concern, trying to find some data around this. However:

  1. This is already the world we are in. If I ask Notion to create a page and update it, it isn't using sessions. It has to remember and pass around opaque page IDs. The vast majority of MCP servers (in my experience) already do not use sessions and are doing this.
  2. There are many places the model can get IDs wrong that could be calamitous. I could ask Notion to delete page ID ABC123 and it deletes ABC223, or merge PR 12 and it merges 13, or it uses the wrong variable name in some code.
  3. For smaller/less capable models, I generally just expect they are going to struggle with multi-turn tasks anyway.

Copy link
Member

@evalstate evalstate Mar 13, 2026

Choose a reason for hiding this comment

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

@pja-ant - It's the possibility rather than the probability that we need to confront. At any scale you will see misgenerated - or more likely - model confusion on tool calls.

For most people, for most of the time this approach is simple and workable.

Transparently I support this proposal - but this risk needs to be clear to systems designers - and experience so far has shown that many MCP Integrators have higher expectations of LLMs than reality.

For positioning would this be a "documented common pattern published alongside the protocol" or "MCPs formal session spec"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For positioning would this be a "documented common pattern published alongside the protocol" or "MCPs formal session spec"?

Sorry, what is "this" referring to here?

I do think we'd probably want some guidance in the spec of how to deal with stateful interactions and some best practices there.

Copy link
Member

Choose a reason for hiding this comment

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

"this" being the proposal.

It's already the case that models hallucinate, tool calls may be invalid due to bad generations etc.

If we are formalising state handles as a primitive we have to be clear that we cannot guarantee the integrity of those handles.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Gotcha. I don't think we'd formalise the concept of "state handles", not yet at least. I think in future (if this goes ahead), it would be good to explore a more formal concept of state handles with more semantics, but I think that's not later spec thing.

For this SEP, what I'd propose is that the spec just says that there are no sessions and that servers are responsible for just managing their state through tool calls in whatever way they want, but with some guidance around best practices for common scenarios.


This SEP closes off one avenue for dynamic tool exposure: a server can no longer have `enable_admin_tools()` mutate what `tools/list` returns, because `tools/list` is now deployment-scoped. The workaround within this SEP's bounds is to expose everything at list time and enforce at call time, which works but means the full tool surface is always in the model's context.

A cleaner replacement would be to let a tool call return additional tool definitions as part of its result — `enable_admin_tools()` doesn't mutate a list, it hands back the admin tools directly. The client scopes those however it likes (per-conversation, per-subagent-tree), and the server still validates authorization at call time. This would also compose nicely with the handle pattern: `create_browser()` could return both a `browser_id` *and* the tools that operate on browsers, so a model that never creates a browser never has those tools in context at all.
Copy link
Member

Choose a reason for hiding this comment

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

This is similar to the hf dynamic_space tool -- is the proposal that the additional definitions would be intercepted by the Client for presentation to the LLM, or that the LLM synthesize calls dynamically?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I was imagining that the client would intercept and introduce those tools to the model however it prefers. To be clear, I'm not explicitly proposing this, was just a sketch of one way that we could potentially solve this if we think dynamic tools are really important. I haven't given it a huge amount of thought and there are likely better ways (that also do not require sessions).

Copy link
Member

Choose a reason for hiding this comment

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

I think it's a good idea (and taking it to the extreme this would probably be a better way of managing tool lists altogether).


#### Subagent fan-out pays per-session cost

An orchestrator agent spawning subagents is a common and growing pattern. If subagents get their own sessions — and there are isolation reasons they might — each spawn pays a per-server cost. [SEP-1442] removes the `initialize` handshake from that cost, which helps. But a `session/create` message is being proposed as the explicit replacement, and the forced `tools/list` re-fetch described above still applies regardless.
Copy link
Member

Choose a reason for hiding this comment

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

There's some design assumptions here; but in this type of case I think this is where light "client-state" sessions make sense (with the opaque token).

Choose a reason for hiding this comment

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

It seems to be this PR is definitely a kind of client-state though it's merely a handle to server maintained data. The fact that the handle is maintained by the client but the payload is in the server's DB/store feels like a design smell. What if 10 of these handles are related? Maintaining consistency would be a nightmare. I think a server developer would opt for one handle that has a full context which is just another name for a session.


### Summary of changes

1. **Remove the session concept from the protocol.** There is no `session/create`, no `session/destroy`, and no `Mcp-Session-Id` header. The protocol is sessionless at every layer. (This supersedes the opt-in stateful path that [SEP-1442] retained.)

Choose a reason for hiding this comment

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

Why remove it? It's already optional. Sessions/session management are very standard in HTTP requests/responses. Server may want to keep state associated with a series of requests/responses to models. One doesn't replace the other. I guess a single "handle" could suffice as a session ID but it's not as convenient as the current design which is very standard for HTTP.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There's a section towards the end that covers why not just make it optional.


[SEP-1442] already does the heavy lifting of making MCP work behind load balancers and without sticky routing. The argument for going further is that the existence of the opt-in shapes the ecosystem even when it's rarely taken:

- **The mere existence of the opt-in forces the `O(subagents × servers)` cost on everyone.** A client cannot cache `tools/list` across session boundaries unless it knows the server doesn't opt into session-scoped mutation — and it can't know that in advance. So the client re-fetches, every session, every server, even though approximately zero servers actually opt in. This is the big-O inefficiency from the Motivation section restated: it's not caused by sessions being *used*, it's caused by sessions being *possible*. Defaulting them off doesn't help; only removing them does.

Choose a reason for hiding this comment

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

the client re-fetches, every session, every server

Why does the client do this? For a given session a client doesn't need to refetch unless they get a list change notification.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If tools are a function of session then the client cannot assume that sessions fetched as part of a previous session are valid for the new session, so you need to refetch. List change would have to be scoped to a session, so a list change from a different session wouldn't help.

An alternative is that we could say that tools cannot be scoped to a session, but sessions still exist. That would avoid this situation.

Another alternative would be to have language that says that tools are scoped to sessions, but the initial tools list of a session is always the same, so you can cache and re-use them. I feel this is just making things complicated though and we might as well just remove the idea altogether and make the protocol simpler and achieve the same benefit.

Copy link

@Randgalt Randgalt Mar 13, 2026

Choose a reason for hiding this comment

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

I agree tools/resources/etc lists should be improved. I believe there are SEPs that address this.


Sessions at least gave a lifecycle signal — session ends, state is freed. Without that, the model might forget to call `destroy_basket()`, and state leaks.

The counter is that sessions do not actually deliver this in practice. Chat conversations persist indefinitely; sessions do not cleanly end. Stateless HTTP servers behind load balancers never see a connection-close. Servers are already TTL-ing or leaking today — and notably, the current proposals for explicit session management are *themselves* adding TTLs, which is a concession that session-end alone does not solve the problem.

Choose a reason for hiding this comment

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

This asserts that everything stored in a session is of interest to the model. Servers may store internal context in a session, again this is typical of HTTP request/response.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Could you give an example here of something that works with sessions but wouldn't work with this proposal? Agree that servers can have session state that is not visible to the client. I think what I'm missing is why a create_session -> session_id tool would not solve that equally well.

Choose a reason for hiding this comment

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

Too many to list. A few examples: the server might keep some stats about client/session usage for monitoring purposes: qps, timing, etc.; internal context based on the current session; etc. These things shouldn't not be stored in a client held session handle.


#### "Models have to carry the IDs forward"

With implicit session state, the model never tracks an identifier — the server does. With explicit IDs, the model is responsible for threading `basket_abc123` through every relevant call. Two failure modes: hallucinating a slightly-wrong ID, or the ID falling out of context when the conversation is compacted.

Choose a reason for hiding this comment

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

This is a nice feature but it's not what session state is. A server does not always want to push session state on to client. There is a proposal for cookie-like storage for MCP. It has the same problem. As an MCP server developer, I don't necessarily want the model to manage these atoms of data. I might want it for some things, but not everything. Again, I'm asserting that Sessions address a different concern than this SEP.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you help explain what concern you would like sessions to address?

Choose a reason for hiding this comment

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

An HTTP session is context for a server. It is not necessarily shared state between a client and a server. In this proposal, an HTTP session could be emulated but it seems contrived. It is currently optional in the spec so why remove it? It has value for server developers (i.e. me 😄) and is low cost for clients/models. This SEP is narrowly focused on values that represent some kind of client state (a shopping cart, etc.). Sessions allow a server to manage internal state: security, monitoring, tracking, etc. It's good that MCP has this and would be bad to lose it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the fact that it is optional undermines the use cases you've described. If clients just ignore the session it's not useful.

Copy link
Contributor

@markdroth markdroth left a comment

Choose a reason for hiding this comment

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

I really like this direction! Just one comment here.

Comment on lines +102 to +117
Where a server would previously have relied on implicit session-scoped state:

```
add_item("shoes")
add_item("socks")
checkout()
```

It instead exposes an explicit creation tool that returns a handle, and threads that handle through subsequent calls:

```
basket = create_basket() # returns { "basket_id": "bsk_a1b2c3" }
add_item(basket, "shoes")
add_item(basket, "socks")
checkout(basket)
```
Copy link
Contributor

Choose a reason for hiding this comment

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

This example implies that the basket state is actually stored on the server side, not the client side. It seems like we would also want to support cases where the state is stored on the client side. (I assume that many shopping cart impls on the web actually store the data on the client side by using HTTP cookies.)

To do this, we'd need the add_item() tool call to be able to manipulate the state held on the client somehow. One simple way to do that would be to just have add_item() return the modified basket handle. But that approach requires all actions on the basket to be serialized rather than being done in parallel, which could be a problem for the case of an orchestrator spawning a bunch of sub-agents.

We should give some thought to whether there's a better way to do this. The "first-class state handles" approach you suggest below might be a good avenue: we could define such a state handle with semantics allowing mutations with appropriate synchronization, and we could define some way in which the state handle could be shared across sub-agents, which could allow multiple sub-agents to modify the cart at the same time.

Regardless of where we land on this, I think we should at least document how we think cases with client-side state should be handled.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I'd love to use first-class state handles for things like this, but thinking it would be a later addition rather than trying to get it in for next spec. I'd also like to just better understand how common this use case really is. I'd be skeptical that a shopping cart would use this for example since you really want server state for that kind of thing (e.g. the item goes out of stock, need to remove it from the cart).


This proposal removes the protocol-level session concept from MCP entirely, replacing implicit session-scoped state with explicit, server-minted state handles that the model carries and threads through subsequent calls. Where [SEP-1442] makes sessions *optional* by defaulting to stateless operation, this proposal goes one step further and asks whether the opt-in needs to exist at all. The claim is that every legitimate use of session scoping today — application state, mutable tool lists, and resource subscriptions — is better served by explicit identifiers, and that the session abstraction itself introduces rigidity (fixed cardinality, undefined lifetime, uncacheable list endpoints) without corresponding benefit.

Under this proposal, a server that currently scopes a shopping cart to the session instead exposes `create_basket() -> basket_id` and threads that id through `add_item(basket_id, ...)`. The model decides what is shared and what is isolated; list endpoints become cacheable across what used to be session boundaries; and subagent fan-out no longer pays a per-server setup cost: no initialize, no `session/create`, and re-use cached `tools/list`.

Choose a reason for hiding this comment

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

The model decides

Does "model" here mean the LLM? I hope not ... I think that would be very strange. I hope it really means "server" and if so probably should say that explicitly.

Or maybe "model" really means "MCP Host", in that the host decides when to create a basket and when it uses a particular basked_id on subsequent calls?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It does mean the LLM. What are you finding strange with this?

Choose a reason for hiding this comment

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

Well, to start, "The model decides what is shared and what is isolated" seems to violate a core MCP principle:

Users must retain control over what data is shared and what actions are taken

So I suppose the model may suggest how things are shared but the user must be able to approve or override this suggestion.

But this is also strange because this assumes LLMs understand the concept of sessions and will know how to control them. That seems unlikely in the near term. Far more likely, I think, is the the MCP Host will understand which interactions belong in which "context" and will scope sessions accordingly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's just tool calls and approvals, nothing unusual there.

Example that already happens today using e.g. Notion MCP:

  • Model decides to call create_page, gets an ID.
  • Model decides to call update_page with an ID and new content.

or even more mundanely: using the CLI to create and update files.

That already happens today. Most MCP servers that provide access to documents and other resources work this way and it's completely natural and works well. All this SEP is proposing is that you can do a similar thing for things that might use sessions today (shopping parts, playwright servers, etc.)

@Randgalt
Copy link

FWIW - we already have MCP tasks for interactions that are not bound to sessions.


## Abstract

This proposal removes the protocol-level session concept from MCP entirely, replacing implicit session-scoped state with explicit, server-minted state handles that the model carries and threads through subsequent calls. Where [SEP-1442] makes sessions *optional* by defaulting to stateless operation, this proposal goes one step further and asks whether the opt-in needs to exist at all. The claim is that every legitimate use of session scoping today — application state, mutable tool lists, and resource subscriptions — is better served by explicit identifiers, and that the session abstraction itself introduces rigidity (fixed cardinality, undefined lifetime, uncacheable list endpoints) without corresponding benefit.
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's not clear to me that this SEP provides support for the claim "better served" is true -- I think "can be served" could be accurate, but any use case or design without a session could also be supported in a session as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I suppose people can reasonable disagree (which I welcome!) but I think I do make some key arguments that they are better served by explicit identifiers rather than sessions and are better in a few key ways:

  1. Sessions have fixed cardinality (you can only have one shopping cart). Explicit identifiers allow for any cardinality.
  2. Sessions are either shared or not shared by agents. I gave an example of sub-agents sharing a shopping cart "session" but having different computer-use "sessions" which I don't believe is possible with sessions.
  3. Caching becomes more efficient as remove the session from the cache key space.

There are downsides to this proposal, but I stand by that it makes a case for why the use cases are better served, and not just alternatively served.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sessions have fixed cardinality (you can only have one shopping cart). Explicit identifiers allow for any cardinality.

Right but my point is if you want that functionality, sessions being there doesn't prevent it. So a session doesn't make the design explicitly worse just by being present.

Choose a reason for hiding this comment

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

Sessions have fixed cardinality (you can only have one shopping cart)

I don't think this is right. A client could have as many sessions as it likes with a server. So if the client wanted mutple shopping carts, it could simply have multiple sessions, on per cart.

A client can start a new session any time it wants by calling "initialize" again. And then the client can choose to specify any of its active sessions on a new request to have that request grouped into that session.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The problem is that the host chooses when a session starts and ends. If you want the ShoppingMCP to be shared across two conversations but the ComputerUseMCP to not be shared then that's not practically possible since the host doesn't know what the model needs on a conversation-to-conversation basis. You could give the model access to this and it allow it to create sessions and specify sessions in tool calls, but that just brings us to this SEP and you could just get rid of sessions.

I think the question is here: we seem to accept that working around fixed cardinality can be done by just not using sessions (or adding machinery that effectively makes them into tool calls). Why are we defending sessions though? What are they getting you that tool calls and handles alone don't?


1. **Application state.** The canonical example is a shopping cart: `add_item()`, `add_item()`, `checkout()`, with the cart existing implicitly per-session. This generalizes to any stateful workflow — a Playwright browser instance, a database transaction, an open file descriptor.

2. **Mutable list endpoints.** `tools/list` (and `resources/list`, `prompts/list`) can legally return different results over a session's lifetime. For example, a server could expose an `enable_admin_tools` tool that mutates what subsequent `tools/list` calls return.
Copy link
Collaborator

Choose a reason for hiding this comment

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

another example here was exposing tools via a prompt -- e.g. you trigger /migrate_to_pg_17, and in addition to the playbook of steps being inject into your prompt, the tools would be activated.

However, Skills may serve the same role in that usecase (assuming some kind of Skills + MCP overlap)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I think this is a valid use case, but it would be better if the prompt/skill just returned the new tools rather than having mutable state in the tools list.


#### Session lifetime is undefined, and servers can't design around it

The spec does not say when a session begins or ends, because it depends on the host application. One chat interface creates a session per conversation; another per application launch. A subagent might share its parent's session or get its own. A page refresh might end the session or not. Different hosts reasonably do different things, and there is no obviously single correct answer that will suffice for any and all MCP applications.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we could put a definition around sessions as "a series of related calls with shared context." with the most common example being a conversation or chat thread. We could also narrow it down to just "conversation", but it seems likely there are non conversation applications where it might be relevant (e.g. some kind of dashboard).

It's not clear to me that the server needs the exact understanding of the session start/end as long as it knows it has one (e.g. tool_call_1 is related to tool_call_2 or prompt_call).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I give an example later (sub-agents sharing a shopping cart but wanting non-shared computer use) where no definition of a session start/end will suffice.


#### List endpoints cannot be cached across sessions

Because `tools/list` *might* be session-dependent, a client cannot assume a result fetched in one session is valid in the next. Every new session is forced to re-fetch — even when, as in the vast majority of deployments, the server's tool set is fixed at build time and never changes.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This would be solved be either TTLs or some mechanism to trigger a tool refresh in a session (e.g. the response could provide a field that indicates tools list should be refreshed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think TTLs solve this. If the session is part of the cache key then the TTL on one key does not help with another.

Let's suppose you have three sessions: A, B, and C; and we're in a world where tools/list can be different across sessions. Now let's imagine you have already fetched and cached the list for all three sessions and the TTL expires: do you fetch just once, or do you need to fetch 3 times? I think you need to fetch three times because each session could have a different tools/list (e.g. you've enabled admin tools, or run that prompt you mentioned earlier). Also, (perhaps more importantly), if you start session D, you also cannot re-use the list from any of the other sessions for the same reasons. You are forced to refetch tools for every new session.

Without sessions, you can have and maintain a single cache across all your conversations because there's no session in the cache key.


This applies to every list endpoint. Each must be treated as potentially session-scoped, so each must be re-fetched per session to be safe.

For hosts that regularly spawn subagents, this is not a marginal cost — it is a multiplier on the hot path. The mere *possibility* that a server is session-scoped forces `O(subagents × servers)` calls to `tools/list`: every subagent, for every server, every time, even if the underlying tool set hasn't changed since the orchestrator first connected. The client cannot skip the call because it cannot know in advance which servers are session-scoped and which aren't. Under this proposal the same workload is `O(servers)` — the orchestrator fetches each list once and every subagent reuses the cached result.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it be worth some kind of optimization that MCP primitives could be return per session?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure I understand. What I want to avoid is returning primitives per session since that's inefficient. I want a client to fetch primitives once per server across all conversations and only refetch on TTL.


#### Subagent fan-out pays per-session cost

An orchestrator agent spawning subagents is a common and growing pattern. If subagents get their own sessions — and there are isolation reasons they might — each spawn pays a per-server cost. [SEP-1442] removes the `initialize` handshake from that cost, which helps. But a `session/create` message is being proposed as the explicit replacement, and the forced `tools/list` re-fetch described above still applies regardless.
Copy link
Collaborator

Choose a reason for hiding this comment

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

tangent: we previously discussed a "session/clone" method for a similar usecase of forking the session state


An orchestrator agent spawning subagents is a common and growing pattern. If subagents get their own sessions — and there are isolation reasons they might — each spawn pays a per-server cost. [SEP-1442] removes the `initialize` handshake from that cost, which helps. But a `session/create` message is being proposed as the explicit replacement, and the forced `tools/list` re-fetch described above still applies regardless.

The cost per subagent is roughly `O(connected servers)` session-create messages plus `O(connected servers)` list re-fetches, even when most subagents never touch most servers. For an orchestrator spawning many short-lived subagents, this can amount to more protocol traffic than the actual tool calls.
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's not clear to me that a session should be re-called for subagents, specifically if the parent agent doesn't have a session. That might lower this to (O(connected servers that support sessions)).


The cost per subagent is roughly `O(connected servers)` session-create messages plus `O(connected servers)` list re-fetches, even when most subagents never touch most servers. For an orchestrator spawning many short-lived subagents, this can amount to more protocol traffic than the actual tool calls.

#### Cardinality is forced, and no single scope works for everything
Copy link
Collaborator

Choose a reason for hiding this comment

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

I pointed this out above, but will restate here: I don't think it is forced, as if you wanted to have explicit state you still could.

But a "shared session state" would enable a tool like list_carts() that would give you a list of all your potential carts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

if you wanted to have explicit state you still could.

Agree it's not forced, you don't have to use sessions and could instead use explicit state today. I'm arguing that if you need to avoid sessions to get this value then let's just make people always do this and get rid of sessions. It would be really unfortunate if people designed around sessions and then realized they don't like the fixed cardinality and have to change their API.


#### No way to initialize session state

Sessions begin empty. If state needs setup before it is usable, that setup has to happen through follow-up tool calls, which means the model has to know to make them and pay the round-trip for each.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This also seems incorrect. Couldn't session/create return an initial session state?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It depends if the initialization needs additional context that only the model has access to. For example with the shopping cart, imagine you need to know your address before adding items to the cart (since items may not be able to deliver to all addresses). With the session approach, you'd do:

session/create   // Called by host, doesn't know user's location
tool/call: add_item("Fish and chips")   // ERROR: must setup_cart first
tool/call: setup_cart("London")
tool/call: add_item("Fish and chips")

without sessions you'd do:

tool/call:  create_cart("London") -> cart_123
tool/call:  add_item(cart_123, "Fish and chips")

With sessions, the initial state is invalid and you have to work around by doing explicit checks. With explicit model-controlled creation it can make sure the cart is always in a valid state because the constructor mandates it and the type system prevents you from calling add_item without a valid cart.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think with that specific example, you could have setup_tool be the ONLY tool that's provided to prevent incorrect state.

You could also potentially solve it with session/create -> elicitation?


#### "Models have to carry the IDs forward"

With implicit session state, the model never tracks an identifier — the server does. With explicit IDs, the model is responsible for threading `basket_abc123` through every relevant call. Two failure modes: hallucinating a slightly-wrong ID, or the ID falling out of context when the conversation is compacted.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you help explain what concern you would like sessions to address?

This would be fine if sessions were purely a host concern. But *server authors* are the ones deciding what to scope to the session, and they need to know what "session" means to do that correctly. If I am writing a Playwright server and tying a browser instance to the session, I need to know whether "session" means one user turn, one agent process, or one chat that persists for weeks. The spec cannot tell me, and different hosts give different answers. I am designing against an abstraction whose semantics I do not control.

#### List endpoints cannot be cached across sessions

Choose a reason for hiding this comment

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

I think the argument in this section is the weakest of the reasons for eliminating sessions. Results of list endpoints cannot be considered 100% reliable even within sessions, since a server could change the list without sending a notification, or a notification was sent but lost or delayed by the network. So using a cached list from another session is really no different than using a cached list from the same session -- in either case the client must be able to handle the list being inaccurate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There is work planned by the WG to introduce caching/etag system to tools/list so that we have better defined semantics around what you are and aren't allowed to cache.

What I'm arguing here is that even with well-defined caching semantics, you cannot cache across sessions (because the lists can potentially be different per sessions). If we don't have sessions, then lists cannot be different across sessions and you can rely purely on the TTL/etag/list_changed semantics across all your chats/agents/whatever. It's much more efficient: O(1) vs O(n)

Choose a reason for hiding this comment

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

If we don't have sessions, then lists cannot be different across sessions

Well yes, trivially. If we didn't have lists, then lists couldn't be different between sessions either.

I think you missed my point. Yes, we could define cache semantics using eTags would allow the fetch of list to be satisified by a cached copy. But even with these there is no guarantee that calling in tool in the list will not fail with "tool not found". So apps must be prepared to handle "tool not found". And because of that, the value of well-defined caching semantics for tool lists or other lists is small.

But to be clear, I am supportive of this SEP based on the other rationale given, so it's not important to debate this point.


1. **Remove the session concept from the protocol.** There is no `session/create`, no `session/destroy`, and no `Mcp-Session-Id` header. The protocol is sessionless at every layer. (This supersedes the opt-in stateful path that [SEP-1442] retained.)

2. **List endpoints are session-independent.** The result of `tools/list`, `resources/list`, and `prompts/list` MUST NOT depend on per-connection, per-conversation, or prior-tool-call state. Lists can still change over time — a user upgrades their plan, a server ships new tools — but those changes happen at `(deployment, auth)` granularity, where they can be cache-managed and invalidated, rather than at session granularity, where they cannot. Caching mechanics are specified in separate work.
Copy link

Choose a reason for hiding this comment

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

I'd imagine there are use cases for this, though. User calls tool X and that unlocks tools Y and Z. Is this saying that would never be possible?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is saying you won't be able to use sessions for that.

I can imagine other mechanisms to enable it though. There's an example further down of e.g. enabling tools to return tools (not proposing that specifically, but just pointing out that there are many other ways we could enable that without having to introduce sessions).

@halter73
Copy link

halter73 commented Mar 19, 2026

I think this proposed SEP does a great job explaining the problems caused by introducing the concept of a protocol-level session when there was none previously, but we cannot put the genie back into the bottle. The fact is that clients today do make use of sessions as do servers.

In the case of interactive code editing tools like VS Code with GitHub Copilot or Cline, each instance starts a new Streamable HTTP session (optionally at the discretion of the server which is free to be stateless ofc) any time it would have (re)launched an MCP server process using the stdio transport.

Typically, this means that each VS Code instance gets its own session, and I know that GitHub Copilot and Cline provide very nice 🔄 UI to start a new session without having to restart the entire host like you do in Claude Code. Not that people haven't asked for a convenient /restart command for MCP servers in Claude Code as well, but I'm sure plenty of people run /exit then claude --resume already for exactly this use case anyway.

It's a useful concept for MCP servers to be able to distinguish between multiple concurrent and consecutive coding sessions by leveraging the Mcp-Session-Id. I'm not saying every server needs this, or even a lot, but some certainly do. I know we have samples for this in the C# SDK repo.

I know a lot of people have asked for example use cases, but the only thing that makes that hard is the sheer number of them. Imagine a note taking MCP server that wants a separate set of temporary notes per coding session. Or imagine a 3D editing MCP server that allows each parallel agent a different camera that's in a unique relative space when coding on a 3D project in parallel with other agents. And so on.

And I did read the SEP. I know the recommendation is to move the session ID up a layer, but why should I if what I have already works today? Maybe I don't want the model/agent to be in control of session creation. Maybe it would waste resources or just generally be counterproductive for my use case to allow the agent to create more than one session with the MCP server. Maybe instead of writing a 3D editing MCP server, I'm simulating a competitive 3D game where creating a new camera would be cheating. My harness should be allowed to create new cameras for each agent player (via the Mcp-Session-Id ofc), but my agent shouldn't be allowed to do that.

And then there's the elephant in the room. We just went to 1.0, and this is going to be a nightmare for SDK maintainers. The tier 1 SDKs just shipped their 1.0 releases which we promise are stable, and then in a few months we're going to break everything that references sessions?

It might be worth looking at modelcontextprotocol/csharp-sdk#1382 where I marked one lesser-used session-related API in the C# SDK "experimental" for unrelated reasons prior to 1.0 and got pushback. That isn't nearly as commonly used as say McpServer.SessionId or ConfigureSessionOptions.

@halter73
Copy link

FWIW - we already have MCP tasks for interactions that are not bound to sessions.

@Randgalt Where is this stated? I'm not seeing any language about sessions in https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/draft/basic/utilities/tasks.mdx

If you're just stating that there's no language tying tasks to sessions, I think that's fine. In the C# SDK we give the implementor of the IMcpTaskStore the Session ID as a parameter and let them decide whether it matters or not, but we shouldn't do this if it's disallowed.

Currently in our built-in InMemoryMcpTaskStore does track a unique set of Tasks per protocol-level session if the server is run in a stateful mode.

And of course, this is just yet another API we'll be forced to deprecate in the C# SDK if we go forward with this SEP, but at least that API was marked as experimental due to tasks being an experimental feature.

I also want to make very clear I'm not saying that most MCP servers should be relying on protocol level sessions.
The C# SDK currently defaults to using sessions, because it offers the richest possible feature set that makes multiple concurrent overlapping sessions per user possible similar to stdio with multiple processes, but I'm open to possibly changing the default or at least updating all of our samples and getting started docs that don't need sessions to use stateless mode.

What I'm saying is that some servers definitely can make use of the concept, and clients do fairly predictable things with the protocol-level sessions that's ultimately in control of the user or at least the developer that wields the SDK, and we shouldn't take that away just to make more work for our SDK maintainers and MCP app developers to try to find some alternative that hopefully works as well as what they had before.

@Randgalt
Copy link

@Randgalt Where is this stated? I'm not seeing any language about sessions in https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/draft/basic/utilities/tasks.mdx

@halter73 yes - that was my point. Tasks are independent of sessions. Of course, you could keep them in the session but it's not required.

@Randgalt
Copy link

Randgalt commented Mar 19, 2026

I re-read the SEP with fresh eyes and after the good discussion yesterday.

@pja-ant this SEP solves state between tools. What about resources? Per this proposal, toolX returns a value that toolY takes as an argument. This proposal calls that a "state handle". This is cooperative. What if I want state on any MCP call? When a resource is requested I'd like to have some context (we produce resources/prompts/completions programmatically). The point of a session is that it is present on every request (once established). A session is a way for a server to relate a series of requests that are related in terms of ID, client and time. For instance, we are running our MCP server inside of a very standard HTTP server that is primarily used for REST. Everything about our server is REST-y. Sessions integrate very nicely with that. The important point is: once created, there is a contract between client and server that the client will deliver the opaque value that the server created on every request until told not to. Note: this doesn't have to be implemented the way it is today. I only ask that i can give the client a value (a cookie!) that it will return on subsequent requests until I say stop. Maybe modelcontextprotocol/modelcontextprotocol#1655 is the answer.

I realize that this SEP doesn't necessarily describe how this should be implemented but I believe I've understood that there is a desire to not have the MCP protocol be prescriptive on this. I still think this is a loss. Please correct me if I'm missing something. All this state handle management is cooperative. Not standardizing the method is going to lead to a lot of confusion and feels that a potential for errors in the future.

More...

Another thing. As a server developer, I now have to pick apart the Tools message embedded in the JSON-RPC request in order to get a session handle that may or may not be there. I'm developing a general purpose MCP server that needs to work in many different contexts. Having to require users of my server to add a magic parameter to all requests is onerous.

Again, am I misunderstanding something?

@pja-ant
Copy link
Collaborator Author

pja-ant commented Mar 19, 2026

Typically, this means that each VS Code instance gets its own session, and I know that GitHub Copilot and Cline provide very nice 🔄 UI to start a new session without having to restart the entire host like you do in Claude Code. anthropics/claude-code#17675, but I'm sure plenty of people run /exit then claude --resume already for exactly this use case anyway.

What people are asking for in CC has nothing to do with MCP sessions. They just want to pick up new MCP servers (valid, but totally unrelated to the protocol).

It's a useful concept for MCP servers to be able to distinguish between multiple concurrent and consecutive coding sessions by leveraging the Mcp-Session-Id. I'm not saying every server needs this, or even a lot, but some certainly do. I know we have samples for this in the C# SDK repo.

Can you elaborate on why its useful? In that link you provided, I didn't see anything that mentioned MCP sessions, it just seems to be doing different things based on a route.

I know a lot of people have asked for example use cases, but the only thing that makes that hard is the sheer number of them. Imagine a note taking MCP server that wants a separate set of temporary notes per coding session. Or imagine a 3D editing MCP server that allows each parallel agent a different camera that's in a unique relative space when coding on a 3D project in parallel with other agents. And so on.

You can do all of these with this proposal, and in my opinion they are more valuable to users if you do NOT use MCP sessions for them.

And just to pick on one that perhaps illustrates the problem: "Imagine a note taking MCP server that wants a separate set of temporary notes per coding session." -- Claude Code (as one example) creates a new MCP session when you restart it, even if you resume a coding session. This use case doesn't work how you want it to work!

And then there's the elephant in the room. We just went to 1.0, and this is going to be a nightmare for SDK maintainers. The tier 1 SDKs just shipped their 1.0 releases which we promise are stable, and then in a few months we're going to break everything that references sessions?

I'm really sympathetic to this and really hate breaking things for people. The main reason I'm proposing this is because we are going to change sessions no matter what and I believe we can use that opportunity to do (what I see as) the right thing. The question is whether we introduce explicit session/create + session/destroy (or similar) or just remove them entirely. Both require changes. You could also make the new version opt-in so that people have a chance to upgrade.

@pja-ant
Copy link
Collaborator Author

pja-ant commented Mar 19, 2026

@Randgalt what exactly do you want to do with resources and sessions? I can imagine some things here, and I think there are better alternatives, but it depends on the specifics.

A session is a way for a server to relate a series of requests that are related in terms of ID, client and time.

The problem (as @markdroth rightly pointed out) is that no one agrees on the scope of a session. Would your use case still work if a client created a new session for every tool call? Would it still work if returning to a chat a week later created a new session? What about clients that have a single session per application launch, or single session per chat window, or single session per sub-agent launched? No applications agree on this stuff. It's impossible to design around, and we can't even comprehend what the AI applications of tomorrow look like.

We do want stateful interactions, and likely we'll want to introduce more things in future to help facilitate this, but we need to do it right and my conviction is that sessions are not the right way to do it.

The important point is: once created, there is a contract between client and server that the client will deliver the opaque value that the server created on every request until told not to.

This isn't true. The server can't tell a client to not call initialize and create a new session. In practice, clients will restart sessions at various times out of the server's control.

As a server developer, I now have to pick apart the Tools message embedded in the JSON-RPC request in order to get a session handle that may or may not be there.

You choose your own tool parameters, so you can guarantee they'll be there.

@Randgalt
Copy link

what exactly do you want to do with resources and sessions? I can imagine some things here, and I think there are better alternatives, but it depends on the specifics.

We may be talking past each other. My needs are not specific to MCP tools/resource/etc. interactions. Let's say I want to track a models usage of our MCP server over the course of a "session". In REST, I'd set a tracking cookie and then all of our monitoring systems could use that cookie to develop traces, etc. The major MCP SDKs (AFAIK) do not manage cookie JARs so I can't do that with MCP. The current MCP session, however, is perfect for this. I can set some context in the session that will be trackable cross JSON-RPC requests.

no one agrees on the scope of a session

This is true in any HTTP request/response. We have ways of managing this already.

In practice, clients will restart sessions at various times out of the server's control.

And so will servers!

In the end, I'm looking for something approximating an HTTP Cookie that we can use for tracking, correlation, etc.

@halter73
Copy link

halter73 commented Mar 19, 2026

What people are asking for in CC has nothing to do with MCP sessions. They just want to pick up new MCP servers (valid, but totally unrelated to the protocol).

That's not my reading of the issue. Here's what it actually says.

Proposed Solution

Add a built-in /restart command that:

  1. Saves the current session ID
  2. Re-execs the Claude Code process with --resume
  3. Automatically reconnects to updated MCP servers

Example Usage

/restart
Restarting Claude Code...
[Claude Code restarts and resumes the same session with refreshed MCP connections]

Additional Context

This would significantly improve the developer experience when:

  • Setting up new MCP servers
  • Debugging MCP connection issues
  • Switching between MCP configurations
  • Any scenario where MCP servers need to be reloaded

Beyond mentioning "Setting up new MCP servers", it also mentions "Debugging MCP connection issues", "Switching between MCP configurations" and "Any scenario where MCP servers need to be reloaded" emphasis mine.

Another thing to consider is that a lot of people use the Streamable HTTP transport to do local debugging of their MCP servers that typically use stdio in production. It's just a lot easier to attach a debugger that way, and it's easier to get log output via stdout. Giving them a mechanism for developers to at least simulate the same behavior they would get by restarting the process when clients like Claude Code, VS Code GitHub Copilot, Cline, Codex, etc... decide to refresh their connections.


You can do all of these with this proposal, and in my opinion they are more valuable to users if you do NOT use MCP sessions for them.

I admitted that. Why did you just quote the things that could theoretically be done with a higher-level handle while also ignoring the following paragraph where I mention the simulation scenario that cannot be achieved with a higher-level handle? This also ignores the argument that application developers shouldn't be forced to fix code that works regardless, even if it could work another way.

And I did read the SEP. I know the recommendation is to move the session ID up a layer, but why should I if what I have already works today? Maybe I don't want the model/agent to be in control of session creation. Maybe it would waste resources or just generally be counterproductive for my use case to allow the agent to create more than one session with the MCP server. Maybe instead of writing a 3D editing MCP server, I'm simulating a competitive 3D game where creating a new camera would be cheating. My harness should be allowed to create new cameras for each agent player (via the Mcp-Session-Id ofc), but my agent shouldn't be allowed to do that.


Claude Code (as one example) creates a new MCP session when you restart it, even if you resume a coding session. This use case doesn't work how you want it to work!

Nope. That's exactly how I want it to work. That's why I said this.

Not that people haven't asked for a convenient /restart command for MCP servers in Claude Code as well, but I'm sure plenty of people run /exit then claude --resume already for exactly this use case anyway.

Otherwise, it would be impossible to start a new MCP session with Claude code without restarting the model's session. Thankfully, Claude Code just like just about every other editing tool that supports MCP servers has a way to start a new MCP session in "Any scenario where MCP servers need to be reloaded"

Would it be also nice if there was an option to preserve MCP session state between claude code restarts (which is basically the inverse scenario)? Maybe, but that's less important to me. This is a feature you would be able to write with the C# SDK because we provide an McpClient.ResumeSessionAsync API at the request of consumers of our SDK. See modelcontextprotocol/csharp-sdk#946. I didn't add this API just because I felt like it or I enjoy doing extra work.


I'm really sympathetic to this and really hate breaking things for people. The main reason I'm proposing this is because we are going to change sessions no matter what and I believe we can use that opportunity to do (what I see as) the right thing. The question is whether we introduce explicit session/create + session/destroy (or similar) or just remove them entirely. Both require changes. You could also make the new version opt-in so that people have a chance to upgrade.

I'm asking for even more sympathy. This is going to create a lot of needless work for MCP app developers and SDK maintainers in their role supporting app developers for no discernable benefit aside from some sort of ivory tower purity. No one is forcing servers to use sessions if they don't want to. No one says that we need a crystal-clear definition of what a session means so long as they can be useful. What does the lifetime of a TCP connection mean? Well, it depends on the application.

If explicit JSON-RPC session/create + session/destroy methods do get add in the future and it conflates with the existing Mcp-Session-Id semantics we should address that then, but in the meantime, we shouldn't be making breaking changes lightly.

I definitely did not find the arguments made in the transport WG meeting yesterday that no one really uses the protocol yet, so we should be fine making major breaking changes compelling. I also didn't buy the argument that we should do this break just because we're making other breaking changes we should do them all at once. I've heard these arguments many times before by coworkers developing APIs and SDKs who want to break the old crusty versions that we've already shipped that "no one uses", and I have always regretted it.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants