SEP-1372: Clarifying Semantics of Behavior Without Initialization#1372
SEP-1372: Clarifying Semantics of Behavior Without Initialization#1372ZachGerman wants to merge 12 commits into
Conversation
|
Is this behavior SDKs allow today? Would be interested in seeing at least one or two code references. |
The clearest example is from the Python SDK's FastMCP server using |
|
I'm in favor of this change. Although maybe we should go from MUST to SHOULD instead of MUST to MAY. The C# SDK currently does not enforce this behavior on the server either. However, the C# client will always send an initialization request to start a session. I think the Go SDK is similar, but @findleyr could answer for sure. I can see how it might be convenient in some cases to skip this message on the client, and it's definitely convenient for the server to not have to enforce it. With a "stateless" or at least a sessionless server, it's impossible for the server to enforce this requirement. We could enforce initialization in the C# SDK's Stateless mode as implemented todasy, because we currently still respond with an Mcp-Session-Id header. The Mcp-Session-Id produced by the C# SDK in Stateless mode is just an encrypted JSON payload containing client info received during initialization. However, we plan on getting rid of the Mcp-Session-Id header in Stateless mode in the near future (see modelcontextprotocol/csharp-sdk#682) so developers don't need to worry about sharing private data protection keys between server instances. And once we do that, enforcing initialization on the server really will be impossible. If we're afraid that dropping the initialization requirement for the client will encourage people to write client code that prevents stateful servers from learning potentially valuable information about the client during initialization, we could just clarify that the server does not need to enforce the initialization requirement but still insist the client MUST start server interaction with initialization. This is basically just codifying the state of the world today. But like I said, I'm in favor of just changing the MUST to SHOULD. The MCP-Protocol-Version request header should at least be enough for the server to reject any protocol versions it's not familiar with, although it would prevent gracefully downgrading unless we added a similar response header. |
|
@halter73 yes, the Go SDK behaves similarly: in stateless mode no lifecycle validation is performed (because it would defeat the purpose of stateless mode), but the client still always sends initialize. If we're going to change the wording from MUST to MAY or SHOULD, I think we also need to specify what the server assumes about the client in the case that no initialize params can be associated with the request. Probably this should be no capabilities and no client info. But what do we do about protocol version? There is clear discussion regarding protocol version for the streamable http transport, but if we're making this change to the lifecycle section of the spec, we also need to say how protocol version needs to be handled by stdio or custom transports. One option would be to say it defaults to 2025-XX-XX (the current or next draft), because that's the draft where we'd lift this restriction. But then what do we do about future drafts? Should it always default to the "latest draft"? That's introduces all sorts of problems when clients think they're speaking 2025-10-XX and server thinks otherwise. I don't think we should change this wording until we figure out what to do about protocol version semantics. On the other hand, I think we could codify the current stateless behavior by making it an addendum to the streamable transport section, with an asterisk in the lifecycle section saying something like "unless the transport supports implicit initialization". |
|
I think we should be prepared to make this change in the event that #1364 (or #1442, which also plans to eliminate the initialization phase) does not land in time for the next version of the spec (which may be soon?). In that spirit, I want to +1 earlier feedback that we should change "MUST" to "SHOULD" rather than "MAY". And I think a section should be added to explan the expectations for sessions that do not begin with an initialization phase. I suggest something like:
|
|
Agreed, both #1364 and #1442 seem rather large (and lacking consensus), so we should proceed with this effort.. I think we also need to clarify when requests are allowed, and update the streamable transport. How about something like the following wording changes to lifecycle (EDIT: I realized that I used 'MAY' here rather than 'SHOULD', but that decision is orthogonal to the rest of the wording).
...and then add a section to the streamable transport:
|
I don't think this is true. There are no mechanisms in either current transport to "determine" client capabilities. And I still think we need some statement that clients and servers must either have out-of-band knowledge of each other's capabilities and other metadata or make assumptions and be prepared to handle errors in those assumptions. |
I disagree, but only on semantics. I phrased it this way because there must be some convention for what the client capabilities are in stateless mode. Of course, the convention must be that there ARE no capabilities, but that's still a determination. I don't think "Make assumptions and be prepared to handle errors" is viable, because we'd also need to specify what those errors are, and how they should be handled. Since they will be HTTP errors, and (AFAICT) http headers are the way to transmit additional structured information with the error, this has to be left to the transport. |
|
This is an excellent discussion! There is so much to unpack here. I think the point of this SEP is to acknowledge that some/most/all? current SDKs do not enforce the requirement that connections start with an initialization phase, and that changing the SDKs to do so would be breaking, so we should instead weaken the language from MUST to SHOULD. In that spirit, I think the SEP should not be imposing new restrictions. When you say
are you saying that you believe this is how all SDKs work today? I'm not sure that that is true. In particular, I know that a server written with the C# SDK can send an elicitation or sampling request the client even if the client has not declared these capabilities. And if the client supports elicitation or sampling, it sends a successful response. Are you proposing that this be disallowed if there is no initialization phase in the connection? Also, if the server must determine that the client has no capabilities, would the same be true for clients and server capabilities? Meaning, without an initialization phase, must the client assume that the server has no capabilites? No tools, no prompts, no resources? I think that would be problematic and unlikely to jive with current practice. |
Agreed! And I suspect that as SDK maintainers we've been thinking about these questions in our silos. It's really interesting to compare notes.
The intent is not to impose restrictions on the ways SDKs currently behave in practice, but I think if we're going to change MUST to SHOULD or MAY, we need to also clarify the consequences when initialization doesn't occur. I agree that most SDKs have a 'stateless' mode that disables initialization validation, but the way stateless mode is supposed to behave is not at all clear around the edges. I think most of the use cases are "just let me call a tool". In fact, I think changing MUST to SHOULD or MAY is dangerous, because I think for most SDKs stateless mode is explicitly enabled on the server. This change to MAY means that all servers must allow uninitialized interactions, and there's no way for the server to reject the request to say "no, you really need to initialize".
Isn't that an error? Shouldn't we reject this request before even sending it to the client? (I thought we might already be doing this in the Go SDK, but we're not, so I suppose we're in the same boat as the C# SDK). Furthermore, if we're primarily talking about stateless streamable servers here, how would that client request ever get back to the server. If the client hasn't performed initialization, it doesn't have a session ID, and therefore has no way to address the session with its response to the elicitation.
I guess I was, but I'm OK with backing off wording. My assumption is that client capabilities only work if there's a stateful MCP session, and in that case we must have initialization. You're right to call out the asymmetry of this assertion. So, in summary, I'm OK with changing MUST to MAY or should but note:
|
I hope we can encourage other SDK maintainers to join this discussion!
I don't think connections without an initialization phase are the same as "stateless". I agree that they do not have the state that would be obtained from the initialization handshake -- capabilities, clent/sever info, server instructions -- but they may still have other state like tools / resources / prompts lists, notification configurations, etc. I think of connections without an initialization phase as "move fast and break things" connections. "Capabilities? We don't need no stinkin capabilities!". Again, based on the actual behavior of the C# SDK, a request (using streamable HTTP) sent on a connection that does not have an initialization phase currently gets a response with a sessionId. And subsequent requests that bear this sessionId are "part of the session" -- i.e. "logicially related" in the language of the spec. In short, I think some/most? SDKs support connections without an initialization phase and may also support stateful (as ill-defined as this may be in the spec) connections without an initialization phase. So we should endeavor in this SEP to clarify what this means. |
|
Coming back to this as Mike pointed out those other SEPs aren't nearing consensus, so we should be prepared for interim spec guidance regarding this. Will try to draft something today in complete SEP format with consideration for the discussion thus far. |
|
I believe I've accounted for both Python's FastMCP and the C# SDK variances with the changes now described in the SEP. There's definitely still room for discussion. I believe the updated wording will cleanly accommodate the currently agreed-upon aspects of the related initialization/session SEPs that allow for pre-discovered server support expectations. @mikekistler What do you think of the changes to Initialization? Do you think it's sufficient as-is? Should we take away the ", and initialization bypass support" and define specific error details for when bypassing initialization is not allowed by a server? Somewhat working under the assumption that the popular cases this accommodates are ones where clients are either:
Maybe just changing "MUST" to "SHOULD" is the way to go after all, but I wanted to try and clarify the reason for why initialization might be skipped. |
Sorry ... where is that exactly? I still only see one line changed in this PR. |
1524f92 to
ef09329
Compare
|
@mikekistler it was just in the SEP description above. I've now updated the PR to reflect the same. |
ef09329 to
d54a81c
Compare
|
@modelcontextprotocol/core-maintainers could we get one or two reviews on this? Or can we schedule this for a vote in the next core maintainers meeting? Or what is the next step? |
Should I mark you as the sponsor @mikekistler ? |
| securely generated UUID, a JWT, or a cryptographic hash). | ||
| - The session ID **MUST** only contain visible ASCII characters (ranging from 0x21 to | ||
| 0x7E). | ||
| 2. If an `Mcp-Session-Id` is returned by the server during initialization, clients using |
There was a problem hiding this comment.
Is it confusing that this still references initialization? If we don't edit this line, what is the meaning of the Mcp-Session-Id value returned for non-initialization requests?
There was a problem hiding this comment.
The difference is utilizing or not utilizing negotiation.
Initialization-phase-provided Session-Id:
- Negotiated protocol version
- Negotiated capabilities
- Logical grouping of requests
Non-Initialization-phase-provided Session-Id:
- Logical grouping of requests (which may utilize various protocol versions or capabilities)
There was a problem hiding this comment.
I think there is general consensus that we should make initialization optional, since most/maybe all the official SDKs do not actually require it.
But I think we'd be overstepping if we tried to make sessions exclusive to connections that include an initialization phase. I don't know how the other SDKs work, but the C# SDK will offer a sessionId in the response of any request that did not include one. The client is free to ignore this or use it to connect that requests with others it may send.
There was a problem hiding this comment.
Oh I absolutely agree that we shouldn't make sessions exclusive to connections that include an initialization phase. I just think we should say a bit more, because unless I missed it the new wording doesn't explicitly say how to treat non-initialization session IDs. This section says
If an
Mcp-Session-Idis, returned by the server during initialization, clients using the Streamable HTTP transport MUST include it in theMcp-Session-Idheader on all of their subsequent HTTP requests meant for that session.
It says nothing about Mcp-Session-Ids that are provided by non-initialization requests. It seems like we should say something, or adjust the wording here to cover all session IDs, not just those provided during initialization.
We could either drop "during initialization" here, or add something like "For session IDs originating from non-initialization requests, clients may choose to include them on subsequent requests if they want to take advantage of session-oriented features."
There was a problem hiding this comment.
I think dropping "during initialization" is reasonable, as the practice of including them in requests meant for that session applies in both cases.
There was a problem hiding this comment.
@mikekistler BTW, the Go SDK used to always offer a session ID, and upon a closer reading of the spec we intentionally changed it to only offer session IDs during initialization. But if this is accepted, I will happily change it back.
There was a problem hiding this comment.
the C# SDK will offer a sessionId in the response of any request that did not include one
A small clarification to make here is that this is the default behavior for the C# SDK, but not how it behaves in the opt-in "stateless" mode. The point still stands that we want to be able support sessions even when initialization gets skipped though.
In stateless mode, the C# SDK currently only responds with a session ID during initialization because it uses parts of the InitializeRequest to generate the ID which is really an encrypted JSON payload containing things like the client name and version. But after I merge modelcontextprotocol/csharp-sdk#760, stateless mode won't generate a session ID at all.
| to requests containing that session ID with HTTP 404 Not Found. | ||
| 4. When a client receives HTTP 404 in response to a request containing an | ||
| `Mcp-Session-Id`, it **MUST** start a new session by sending a new `InitializeRequest` | ||
| `Mcp-Session-Id` obtained through an [initialization phase](/specification/draft/basic/lifecycle), |
There was a problem hiding this comment.
Ditto here: what is the client meant to do with session IDs obtained from a non-initialization request?
It seems like we're just accommodating servers that happen to return session IDs for all requests, but why do we need to do that? If the only session IDs that matter are those assigned during initialization, do we even need to mention the others?
There was a problem hiding this comment.
What is the client meant to do with session IDs obtained from a non-initialization requests?
Use them or discard them based on whether they want to group them with subsequent requests and utilize the benefits of sessions. Sessions can still be useful without initialization when doing things such as establishing GET/Listening streams, utilizing resumability with SSE, etc.
Big emphasis on SSE last-event-id, which is unique per session-id. If my MCP client serves multiple application clients downstream, it may be useful for me to conduct each of my sub-clients' requests within a dedicated session-id for each one to allow for replaying SSE events on a per-sub-client basis (not having to resume/replay all and filter down to a specific sub-client's events).
An application that utilizes an MCP client to serve its own "sub-clients" is a great reason why initialization sometimes only needs to happen once (to get server's supported versions/capabilities), then create & manage multiple sessions without repeating the initialization process unnecessarily.
There was a problem hiding this comment.
Thanks. This all makes sense, and this is overall a very good change to the spec. I just think we should say something about session IDs obtained through non-initialization requests. The wording here makes sense though: I'll comment on the other thread.
|
Regarding:
The purpose of this SEP is simply to clarify the current state of the world, which is that most/maybe all the official MCP SDKs do not (on the server side) require a connection to start with an initialize request. The SEP started as a simple one-word change, from "MUST" to "SHOULD", but then we discovered this has ramifications in other parts of the spec so more changes wre needed. I think your comment above is pointing out one more that we missed previously. But this also has value because it removes one roadblock to allowing MCP to operating as a stateless protocol. In a stateless protocol, every request is independent and stands alone, so requiring an Initialize request before any other request is fundamentally at odds with that. |
| it **MUST** stop using that `Mcp-Sesssion-Id` and **SHOULD** start a new session by | ||
| sending a new `InitializeRequest` without a session ID attached. |
There was a problem hiding this comment.
@ZachGerman @simonrussell what do you think of this revision?
| it **MUST** stop using that `Mcp-Sesssion-Id` and **SHOULD** start a new session by | |
| sending a new `InitializeRequest` without a session ID attached. | |
| it **MUST** stop using that `Mcp-Sesssion-Id` and **SHOULD** start a new session for | |
| interactions that are logicially related. |
There was a problem hiding this comment.
I'm not sure that's quite right because the client can't start a new session, it can only stop using a previous session. I think it needs to be more along the lines of "sending a new request without a session ID attached"
There was a problem hiding this comment.
+1 on client's not really "starting" sessions, but "utilizing" them when an ID is offered by the server, which is being done either through initialization or by sending a request without a session ID attached. Maybe something like:
it **MUST** stop using that `Mcp-Sesssion-Id` and **SHOULD** utilize a new session for
subsequent interactions meant to be logicially related.
|
I am (fairly strongly) against this proposal being accepted. Process and Principle: Spec First, Not SDK FirstMy first objection is procedural. The core premise of a shared protocol like MCP is that the specification is the definitive source of truth. This principle ensures that a client written in any language can interoperate with any server, so long as both adhere to the spec. This SEP proposes we codify behavior that already exists in some SDKs, using that pre-existence as its main justification. This "SDK-first" approach fundamentally undermines the protocol's integrity. It risks making the SDKs the de facto standard, not the specification. We should evaluate proposed changes based on their independent technical merit and benefit to the entire ecosystem, not simply because a feature has been implemented somewhere in isolation. Insufficient Justification and Evidence of UseThe proposal does not make a compelling case that this behavior is frequently or consistently used in practice. If we are to accept the argument that "it exists in some SDKs today," the proposal should be specific about which SDKs and how popular those implementations are. For example, I see that FastMCP has a stateless_http mode, but it doesn't seem to be mentioned in the official docs or examples. Is this feature actually used frequently by the community, or is it an obscure, unsupported mode? Its mere existence isn't a strong argument for a protocol-level change. Discrepancy Between Proposed Spec and Existing ImplementationsThere also appears to be a mismatch between the behavior described in existing SDKs and the proposed spec change. Specifically, a comment from findleyr above mentions:
This is fundamentally different from what the spec proposes, which is that initialization "SHOULD be the first interaction between a client and a server" implying it "MAY be skipped." These are two distinct behaviors: one is "initialize but ignore the result," and the other is "do not initialize at all." This inconsistency makes it unclear what behavior we are actually trying to standardize. Technical Arguments Against Stateless SessionsBeyond the process issues, I believe the proposed behavior is a poor choice for the protocol for several reasons:
|
|
I initially took a look at the "single word change" to the spec thinking it would be a simple improvement, and enable "initialize free" MCP Server Usage. In practice as I read the spec, it clearly had a large ripple effect so I backed away... and I'd like to acknowledge @ZachGerman and others for the large effort of working through this one and it's details - it's a helpful exercise. re: SDKs - for example, the TypeScript SDK leaves 404 handling to the MCP Server Developer, and my guess is that a lot of MCP Server developers use the GUID generator and don't write the code to check session validity. Another example, the Python client SDK does not open the GET endpoint if an I agree with the proposal to go full "stateless" - where that means that every MCP interchange has what would have been in the initialization attached to it, and Session Management is explicit (will add the PR#s momentarily). |
|
Thanks @kurtisvg You make some very valid points, and I think it is right to consider whether we want to commit to this behavior when there are more changes to sessions on the horizon. I'll have to think about your points more, but for now have a few quick comments based on my experience, that may inform discussion: Regarding SDK support: I've seen this supported (inconsistently) across at least Python, Typescript, CSharp, and Go (the latter being my doing). For example, the typescript SDK enables stateless mode only if the sessionID is undefined. Part of the motivation for this SEP was (I think) to standardize what stateless mode actually means. Regarding evidence of use: I don't actually know how prevalent usage is, but I will say that I heard about this pretty quickly and loudly from users during the development of the Go SDK (for example in modelcontextprotocol/go-sdk#148 or in face-to-face discussion). The main use case is having a "distributable" server, which isn't possible with session ID validation, absent some very complicated intra-service communication. Additionally, users have indicated that they still want a session ID as the key to their logical application state. Regarding discrepancy between proposed spec and existing implementations: you pointed out that I noted the Go SDK behaves differently than C#. I think this is in true, in at least two ways:
In both of these cases, we'd change the behavior of the Go SDK as a result of this SEP: we'd offer a client option to bypass initialization, and we'd return session IDs on every HTTP request. |
I think part of my confusion here is I'm now noticing the PR description doesn't match the PR contents. The current PR description does not say anything about returning session id headers outside of the initialization handshake (but also looks like it hasn't been updated since Aug 20). Is this "return session-id" header behavior actually implemented in any SDKs right now? If not, it seems like we are even farther from "this is the current state of the world". |
That's fine, but for compatibility all clients should assume that servers require it right? Maybe the only change that needs to be made is actually removing the requirement for the "client initialized" notification, which does end up (if you're being strict about things) requiring that the server refuse any requests until it's received that. I think that could just be dropped from the HTTP transport (it still makes sense for stdio), and replaced with an implicit client initialize assumption if the client makes any future requests. I don't think that notification adds much value and yes does force statefulness if you're going to strictly follow the spec. (As an aside, it does seem very strange to have official SDKs that don't strictly follow the spec.)
I think there is value in some of those other proposals to unbundle session creation, protocol negotation and server capabilities, but having an "initialize" call on its own (sans the notification mentioned above) doesn't force statefulness on the server. |
|
@kurtisvg Just want to preface this with the fact that I don't see this as any sort of replacement for #1442, which I do hope is adopted ASAP. The original plan for this SEP was to simply change: "The initialization phase MUST be the first interaction between client and server." ...and it has always been my intention to simply clarify in the spec, for users of official SDKs, that the initialization phase isn't always the first interaction between client and server. Regardless of whether this SEP is adopted, I think it has already helped us prepare for writing updated specification documents if/when #1442 is accepted by highlighting some of the specific impacts that not requiring initialization will have on the existing spec. I think it will prove to have been a "helpful exercise", as @evalstate put it.
In my experience participating in SEP discussions for MCP, the most important question to everyone seems to me to be "why is this not achievable under the current specification"? The reason that we have good maintainers for SDKs who have permitted implementations that bypass initialization in non-compliance with the spec is because there's demand for functionality that is not achievable under the current specification. I don't believe that recognizing this with minimal change to the spec is "undermining the protocol's integrity", but bolstering it, as integrity is about being undivided, and having a spec that accounts for use-cases demanded by the community building the ecosystem in which it lives brings more integrity.
Existence in the SDKs by our good maintainers is already more "evidence of use" than the vast majority of accepted SEPs have provided. In fact, I can't recall another SEP that provided any "evidence of use" whatsoever (beyond example implementations required by SEP format). I could've added anecdotes regarding the chatter I see around FastMCP and how popular discussion surrounding it is in internal AWS discussions, but I don't think we want to start trying to use anecdotes as leverage for SEPs. I think you, of all people, know how important it is to developers to allow for stateless behavior with the spec, which inherently includes the ability to bypass the initialization phase as it's defined today. That's really all this SEP is advocating for: a single facet of your multi-faceted SEP (#1442) that already exists in SDKs due to demand for functionality we're all aware of.
It does, doesn't it? I suppose it's a consequence of a protocol being in a bleeding-edge space of tech with incredible amounts of money being thrown at development around it. Again, I'd personally rather see #1442 adopted so we don't have to worry about reconciling the use-cases that have popped up which this SEP addresses specifically, because #1442 addresses them as well as other important issues. I've been looking at this SEP as a temporary band-aid or stepping stone to #1442. |
If you're referring to the implied functionality of the spec change to: then yes, this is what's practiced in the C# SDK. |
Is that the SDK client-side accepting a fresh To answer this myself I had a dig around in the C# SDK code. I think the answer is yes, it will set a session ID on any request where there isn't a session ID, but given that clients will be sending the initialize request first, it probably works out fine. It seems like it would be a minor change to make it only issue new session IDs on So while yes it's true that the current C# SDK does set a fresh session ID on any response where the request didn't come in with one, this actually follows the spec behaviour as long as the client is following the spec and only ever doing this on I'd totally understand if the current SDKs didn't enforce "don't allow any request until the client initialize notification is received" as that's pretty annoying to implement (and I took the same shortcut in the very light SDK I built). It seems a little more necessary for stdio servers, but also it feels like it could probably be removed and very little would break. Without that requirement, there isn't really an "initialization phase" that the server needs state to support. (There is a need to keep the client capabilities around, if they're important to the server; just removing the |
I can only speak authoritatively about the Go SDK, but I believe it does strictly follow the spec: clients always follow the initialization lifecycle, and this is enforced by servers unless they are explicitly configured as stateless (which I don't believe violates the spec -- see below). My impression is that there are a large number of users who just want to serve a stateless tool from a scalable service, and the spec makes that hard or impossible if enforced strictly from the server SDK, so there has been a convention across SDKs to support this "just let me serve a tool" use-case. Being a relative latecomer, there was a lot of prior art and expectation when we considered this for the Go SDK. Allowing stateless servers hardly diverges from the current spec, in that clients still must follow the initialization flow, but servers don't (and can't) enforce it. The spec says "The client SHOULD NOT send requests other than pings before the server has responded to the initialize request.", which doesn't say anything about whether the server has to reject such requests. But the other aspect of this SEP is being able to avoid the unnecessary cost of the additional 'initialize' and 'notifications/initialized' messages, if the client knows a priori that the server is stateless. As I said, this is not currently possible in the Go SDK, but we'd make it possible as a result of this proposed change. Allowing clients to bypass initialization is a much bigger change, because it exposes the 'statelessness' bit to the spec, and now there must be systems in place for clients to track whether a server is stateless. This diverges from the presumed goal of the spec to define a protocol where clients and servers can communicate without any prior knowledge of each other. From discussions with other maintainers, I understood that the cost of initialization is a real concern, and the wording proposed here "Bypassing initialization is appropriate when the client has prior knowledge" seemed sufficiently specific such as to allow certain use cases without affecting the default behavior. With that said, perhaps we should be more cautious about hinting at such an under-specified use case. My initial thought was that we should just codify 'stateless' mode for server by adding a new section of the spec describing how it should work. This would not require any change to the rest of the spec, but also wouldn't address the use-case of bypassing initialization entirely. Maybe we should do that, or maybe we should do nothing at all, and allow "bypassing initialization" to be handle by another, more comprehensive change to the spec. |
The spec also says if the server responds with a session_id it MUST send that in every request after that. So if the Go client is making the initialization request but ignoring if it returns a session_id, then it's not compliant with the spec. This is also why stateless mode doesn't make sense to me. It keeps being said the server needs to decide if it's stateless or not (be returning a session_id), but then we say we need an option for the client to be stateless anyway. So which is it? |
But the Go client DOES use the session_id on subsequent requests. Go SDK clients are stateful, but as a result of this SEP we'd expose an option to bypass initialization, which I'm assuming is what you mean by stateless client. This is what I meant when I said "As I said, this is not currently possible in the Go SDK, but we'd make it possible as a result of this proposed change".
The server being stateless or not is
I'm confused by your confusion :), because I thought this was exactly the distinction I was trying to call out in my previous comment. In summary:
If it's not agreed upon that stateless clients are important, or that this is an acceptable way to enable them, then I would be fine doing nothing, or perhaps modifying this SEP to simply clarify the behavior of stateless servers. |
|
After a discussion with @kurtisvg, I agree with him that we should probably retract this SEP, but let me be explicit as to why because I’m coming at this from a relatively narrow perspective as an SDK maintainer. I apologize for what turned into a long winded response: TL;DR: the current stateless mode is problematic, and we shouldn't be further codifying or enabling it. BackgroundStateless servers: I first encountered statelessness when talking to users that “just need to host a simple tool”, and the current HTTP transports make this difficult/impossible if implemented strictly, due to the initialization handshake. It seems clear that the spec originally assumed one client process speaking to one server process (correct me if I'm wrong). After all how is a server supposed to use a client capability if the server process is not addressable to receive the response? But this was not clear. With the addition of the MCP-Protocol-Version header, the 2025-06-18 version of the spec seemed to confirm that there is meant to be an implicit stateless mode where we ignore client capabilities and initialization. I commented my confusion here, but we went on to add such a stateless mode in the Go SDK, initially implemented by a GetSessionID callback (if empty, enable stateless mode). Later, we separated the concept of statelessness from session id1, which may have been a mistake. As I posted above, I don’t think stateless servers are actually in conflict with the current spec, it’s just problematic that there are elements of the spec that clearly exist to enable stateless mode without spelling out how exactly stateless mode works. Stateless clients: However, I’ve also recently heard from other maintainers that when they know the server is stateless, they want to be able to skip the costly and pointless initialization, when they have prior knowledge of the server’s capabilities. I think that this SEP is really about enabling stateless clients, by relaxing conditions to allow skipping initialization. It seemed harmless to allow stateless clients when caveated with “when you have prior knowledge”, and probably does solve some real problems, but Kurtis convinced me otherwise. Rationale for not adopting this SEPThe reason I think we should abandon this SEP is that it doesn’t define stateless mode or sessions, or address any of the problems with stateless mode. Furthermore, it introduces yet another implicit mode of operation (this time for clients), which may have unintended consequences and make it harder to actually solve stateless serving in the future. Problems with stateless modeStateless servers are second-class citizens, and don’t have access to client capabilities. Therefore, server authors must have prior knowledge of their transport, which is (I believe) failing one of the design principles of MCP. Fundamentally, MCP defines client and server capabilities that enable rich interactions between AI host and server, and in some sense stateless mode abandons those capabilities, as well as the integrity of its data model, to solve a transport and hosting problem. Problems with skipping initializationSimply put, saying that clients can skip initialization turns an SDK problem into a spec problem (again, problems are going in the wrong direction!). If we codify that clients can skip initialization, we may close off some opportunities to fix stateless mode, and encourage use of MCP as a glorified RPC protocol. AlternativesI think we should consider other SEPs2 that try to define a stateless model for MCP in such a way that initialization or affinity is not necessary, and yet servers and clients can still have access to the full set of features offered by MCP. Such a SEP will necessarily have significant implications beyond the transport layer, because it will formalize the actual MCP server as stateless, which will have consequences for SDKs and users. However, if we accept that there is a need for stateless serving, it is better to map MCP capabilities onto the stateless model, than to sacrifice current and future MCP capabilities in an undefined and undiscoverable way. It also seems fine to adopt a bidirectional streaming transport technology (such as websockets) that has established hosting solutions. However, I don't know enough about this domain to say if this is a viable path forward. When I first learned about MCP, it reminded me of LSP, and that made sense to me, but it is morphing into something different. This transition is probably natural and inevitable, and it is better to address it explicitly. Thinking about SDK design, maybe there’s a way that a stateful server is just a special case of a stateless server, rather than the other way around 3. Footnotes
|
Just wanted to underline this one because it's very true -- session IDs only imply state if that's how you choose to implement them. You could choose to encode some client attributes from the initialize request in the session ID and use that in later requests, without having any storage requirements on the server. (I have suggested a few times that morphing session IDs into something closer to a cookie would be useful for this situation)
I'm really curious what the use-case for stateless clients is, or where the cost for the initialization call comes from.
They technically kind of are because of this line: "The server SHOULD NOT send requests other than pings and logging before receiving the initialized notification." But I think that could be relaxed or removed for the HTTP transport, or potentially completely. (And it is a should so...) But basically that would be it. If that was removed, then stateless servers are in every way completely fine, and very useful. |
|
@kurtisvg I wanted to address the three "technical argument" points you initially brought up, as I spent the majority of my last reply regarding a subjective disagreement on undermining integrity and "evidence of use" standards:
@findleyr I think your comment was very well stated, and points out the result (regardless of the intent) of this SEP, which is indeed enabling stateless clients (without really calling that out in the description, which I will change).
Interestingly enough, I see a similar comment [ref] for stateful servers in the discussion of #1442. This makes me question the intent Justin had in the comment referenced by @kurtisvg here when he used the words "...for now". Maybe we can get @jspahrsummers's thoughts on the matter. There seems to be desire for both stateful and stateless modes to be "first-class". Is it true that such a bifurcation would really mean that it "...might as well be two separate projects at that point"? I'm not convinced that the impact reaches far beyond session management changes outlined by this SEP.
WRT clients being unaware if server capabilities it used to know are still valid... This is not an issue and I'll detail why:
Allowing clients to choose whether they want to disclose their capabilities is not tantamount to "abandoning" them. It's enabling a more secure stance for clients contacting third-party servers.
Allowing non-disclosure of client capabilities and enabling sessions without unnecessary requests is not an SDK problem. I think it's a great SDK feature, and so do the others who are using it. Can't say it enough: For server ignorance of client-side capability support to be a total non-issue, they must declare their required capabilities for the resources they advertise so clients can choose whether they can and want to meet them! (#1385) |
|
Thanks for the thoughtful response, @ZachGerman.
I think this is a critical perspective to understand on both sides of the argument, because (1) statelessness enables scalable remote MCP servers which couldn't otherwise exist, yet (2) limit the types of interactions that are possible between client and server. I was originally in favor of this proposal because of (1), although I perhaps didn't sufficiently appreciate the complexity of enabling stateless clients. However, I changed my mind after speaking with @kurtisvg, who pointed out that there are lots of scalable use-cases where the server really wants to have access to client capabilities such as elicitation. If we double-down on this current model for statelessness, the world of MCP servers bifurcates into stateful servers, which are more like LSP sessions, and stateless servers, which are more like REST APIs. |
Again, I think that addressing that should be done by the server declaring that it wants to know that. Whether the client wants to allow it and continue down that road should be decided by the client based on whether that interaction is worth it to them. Server's can always say "Hey, I have this really cool thing I can do, but I need you to support elicitation for it to work", and clients can respond accordingly based on their level of trust for a given server and desire for that functionality, usually by initiating that functionality via a request that invokes it (or not). |
That all makes sense to me, but let me be sure we're on the same page about something: generally speaking stateless servers distributed across multiple processes can't use elicitation, because there's no way for the elicitation response (another POST message) to be routed to the same server. Or is there an established precedent for sticky routing of responses? (and if so, why do we need statelessness? :) ). We should not move toward a world where "MCP" means two different things depending on the hosting model. There are multiple paths that avoid such a future--websockets could be another alternative. This is similar to the sentiment expressed in the comment you linked. My concern with this SEP is that enabling stateless clients encourages this bifurcation into stateful and stateless. |
There are ways for servers to utilize unique request IDs for their requests and leverage the spec requirement:
...in order to achieve specific routing. |
mikekistler
left a comment
There was a problem hiding this comment.
We should revert the changes to permit sessionIds be returned from any request other than Initialize. That goes beyond the intent of this SEP to simply make Initialize optional.
| without a session ID attached. | ||
| 4. When a client receives HTTP 404 in response to a request containing an `Mcp-Session-Id`, | ||
| it **MUST** stop using that `Mcp-Sesssion-Id` and **SHOULD** start a new session by | ||
| sending a new `InitializeRequest` without a session ID attached. |
There was a problem hiding this comment.
The current spec only allows sessions to be established by InitializeRequest, and we should not expand that in this PR (I'm backing out changes that allowed this).
Co-authored-by: Mike Kistler <mikekistler@microsoft.com>
Co-authored-by: Mike Kistler <mikekistler@microsoft.com>
Co-authored-by: Mike Kistler <mikekistler@microsoft.com>
|
Committed those suggestions and revised the description accordingly. |
|
This is due for closure in favor of #1442 |
Preamble
Abstract
This SEP proposes a limited clarification to the MCP specification to describe the behavior of connections that do not begin with an initialization phase. It acknowledges that such connections are already supported by some SDKs and backends, while clarifying what guarantees are lost or retained when the initialization step is omitted.
Motivation
Several MCP SDKs and servers allow clients to send requests without performing the initialization handshake. While this practice is commonly followed for what is becoming known as "stateless MCP servers", this proposal is limited to the bypassing of the initialization lifecycle phase currently defined in the specification.
This flexibility is useful, but the current specification treats initialization as strictly required.
The goal of this SEP is to clarify:
Side note: This also allows for bypassing initialization when server details (supported protocol versions and capabilities) were already obtained through a registry.
Specification
Please see Files changed
Rationale
Backward Compatibility
Reference Implementation
Security Implications