SEP-1655: Client-Side State Management
Also see discussion: #1468
Outstanding Issues
Abstract
This proposal introduces a standardized mechanism for a Model Context Protocol (MCP) server to store and retrieve state information on an MCP client, analogous to the HTTP cookie standard. This allows for stateful, multi-turn interactions and promotes scalability and consistency. The proposal defines a way to set state in the client using the _meta property in server to client messages and have that state sent back in client to server messages. This is transport agnostic and works across both http and stdio transports.
Motivation
MCP’s stateless nature presents challenges for applications requiring context retention, such as user authentication, personalization, anti-abuse, and session tracking. A core mechanism in building consistency into highly distributed systems is storing state at the client. Without such state services must implement sticky sessions, which are incompatible with modern load balancing and auto scaling. Further, in globally distributed systems it is difficult to ensure highly consistent backend storage in a performant manner. Additional context is found in discussion 1468 and SEP-1442 echoes this same need.
The MCP protocol currently specifies two pieces of state that are regularly shared between a client and server: Mcp-Session-Id and OAuth Bearer tokens. Neither of these are general mechanisms for setting state in the client. The session id’s intended semantics are to follow the session as a random identifier. The session should end when the user leaves the application and the session-id should be globally unique and cryptographically secure. OAuth bearer tokens are only used for authorization purposes and are issued by an OAuth server, not the MCP server. In contrast this state mechanism is intended to be a general store with no assumption of the purpose or format of the state.
The Mcp-Session-Id and OAuth tokens are also only specified for the HTTP transport and not stdio (TODO: stdio is will be adding a session id as well, need reference). While a stdio server does not have the same difficulties in keeping consistent state as a globally distributed service, this mechanism can apply equally to stdio to avoid making http a special case and simplify implementing servers built for more than one transport.
Typical services will store multiple, logically distinct items in a key-value store. Supporting multiple items allows for the separation of concerns in backend systems, the ability to gradually evolve services and deprecate features. State should follow this key-value pattern, similar to web cookies. A key value store is straightforward to implement and makes keys independent of one another. This allows each key to have unique properties, such as expiration and security properties.
Such state is intended to be non-critical or recoverable, similar to how developers view web cookies. It is expected that clients may erase their state due to bugs, privacy concerns, reinstallation of a client, moving to a new device or client, etc.
State is scoped to different lifetimes: "session" and "server" ("request" scoping of state is being proposed in SEP-1685. Session scope bounds the lifetime of an MCP session and can be used for state pertinent to that conversation, such as previous results, shopping carts, etc. Server state has an unbounded lifetime and is intended to be used for tracking the long-term history of a client's interactions with a server, such as results from previous conversations and client profiling (eg. anti-abuse/trust).
While MCP’s state mechanism takes inspiration from web cookies, it can differ in several ways. First, and foremost, MCP state will not support functionality equivalent to “third-party cookies”; state will be tied to the server. Second, in web cookies the value is confined to a US-ASCII string, instead MCP state can use JSON to use a structure that is easier to inspect in protocol messages. Third, web cookies are scoped to a combination of a Domain and a Path. A client in MCP will not share state across http servers (even subdomains), and “Path” is not a well defined concept in MCP and thus state will be scoped to all requests.
Specification
Overview
The Model Context Protocol (MCP) provides a standardized way for servers to set state in the client to be accessed in subsequent interactions. Servers request set data as key-value data where the keys are strings and the values are JSON objects.
Capabilities
Clients that support state MUST declare the state capability during initialization:
{
"capabilities": {
"state": {}
}
}
Protocol
Setting State from the Server
State can be set in any server to client Result message.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"_meta": {
"modelcontextprotocol.io/state/session":
{
"Cart Contents": {
"items": ["cucumber", "tomato"],
"price": 100.0
},
"Coupon Code": "XYZZY"
}
}
"tools": [
...
],
}
}
State MUST be contained in a dictionary under one of two _meta keys: "http://modelcontextprotocol.io/state/session" and "http://modelcontextprotocol.io/state/server" (See the section on Session vs Server lifecycles)
The dictionary contains a key (the name for this item of state) which MUST be a string, and a value which is any valid JSON. State can be deleted by setting the value of a key to null.
State is additive in the client. If one Result message sets a state value with NameA and a later Result with just NameB then the values for both NameA and NameB will be sent by the client in Request messages. State set with the same key (name) as previous state overwrites the entire value for that state, it does not merge state other than at the top level of the dictionary.
Sending State from the Client
State MUST be set in all client to server Request messages.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"modelcontextprotocol.io/state/session": {
"Cart Contents": {
"items": ["cucumber", "tomato"],
"price": 100.0
}
}
}
"cursor": "optional-cursor-value"
}
}
If the client has no state to transmit, it SHOULD NOT send the "modelcontextprotocol.io/state/session" key.
Request, Session, Server State
There are two _meta properties that clients and servers can use:
"modelcontextprotocol.io/state/session"
"modelcontextprotocol.io/state/server"
The lifetime of any state included in the session path is tied to the client-server session. When the session ends or a new session starts with the same server this state should be discarded.
Server scope state persists across sessions until the server removes it. Similar to the behavior on the web, it should be expected the clients may sometimes clear state for a variety of reasons (privacy concerns, re-installation, working around a bug, etc). and state may be cleared unexpectedly.
Schema
See sample schema
State Scope
Clients MUST only send state to the server that set it. There is no support for third-party access to state.
For HTTP servers the server name is defined by the fully qualified domain name, not including the protocol, port, or final '.'. Thus state set by www.example.com is not sent to other.example.com Clients MUST NOT store state for HTTP servers at localhost or by IP address and only for registry controlled domains.
For stdio servers the server name is defined by the file path of the server, such as "path/to/your_mcp_server.py"
Error Handling
This proposal does not add any additional errors beyond those specified already.
Rationale
Alternatives
Using an explicit set and get operations
Rather than setting state by piggybacking on the _meta property of Responses and Requests, we could define explicit GetState and SetState server-client protocol messages. This mechanism is more cumbersome to use and requires an extra round trip to get the necessary state before an operation.
State Attributes
Setting state could support a number of attributes that would dictate their behavior, such as:
server-scope: The domain or stdio path the state is valid for
method-scope: The method requests the client would include state with
max-age: The number of seconds until the state expires.
When overwriting previous state with the same name, the attributes would be overwritten with the new values.
Attribute: server-scope
Server-scope CAN be specified, with defaults used as specified here.
Domain server-scope for http transports:
"attributes: {
server-scope: “example.com”
}
A domain server-scope follows the same rules as https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#domaindomain-value
Notably:
- Omitting the server-scope on the http transport will default to the hostname of the MCP server.
- If the hostname is an IP Address then that will be the default server-scope
- Subdomains are matched according to the same rules as HTTP Cookies
- Leading “.” is ignored
server-scope for stdio transport MUST NOT be set. The client should scope state to suitable identifier from the stdio path or npm package name (see security considerations below). The use cases for server-scope are not clear at this time, but it could be layered in later without backwards compatibility issues.
Attribute: max-age
"attributes: {
max-age: 86400
}
The maximum value for max-age is 34560000 (400 days). The default value is the same as the maximum value. This matches recent developments in web cookie maximum ages. Values larger than that, 0, or negative values are errors and an invalid value error SHOULD be returned. The use cases for max-age are not strong enough at this time, but it could be layered in later without backwards compatibility issues.
Attribute: method-scope
Method–scope can be specified to restrict state to being sent only with certain MCP methods. The method-scope is a prefix match, so “tools” (or “tools/”) will match tools/list, but not "notifications/tools/list_changed”
Examples:
"attributes: {
method-scope: "tools"
}
or
"attributes: {
method-scope: "tools/list"
}
Method scope is mostly a bandwidth optimization technique to restrict what state is sent in requests, if a server wants to restrict the use of state to one tool or another, the name of the tool can be stored in the state itself and then filtered server side. Depending on how state is used in practice, method scope may be useful in the future and can be layered on top without backwards compatibility issues.
Path scoping
One possible addition is to expand the method-scope to include deeper elements in the message, such as the name (similar to Path in web cookies). This would reduce the amount of state sent to the server if it is only need for certain tools etc. The problem is that while some MCP messages can clearly be turned into a path:
"tools/list" -> “tools/list”
“tools/call” (always contains a name) -> “tools/call/[name]”
Others are more difficult, such as:
"completion/complete" (contains a ref) -> “tools/call/[name]”
Is perhaps ambiguous as a "ref/prompt" could share a name with another type of ref, so it should be:
"completion/complete" (contains a ref) -> “tools/call/ref/prompt/[name]”
And others seem very difficult, such as cursors which would require the client to record which path the original request the cursor is related to and look up its path to decide to include the state.
Request scope
The protocol can support a scope of a single request/response sequence. While there are a number of similarities, clients will only need to store state ephemeneraly to support request scope. Request scope has been separated into SEP-1685
Not supporting a session scope
State could additionally be supported in one global server scope rather than additionally defining a session state. The session state does not add any functionality that couldn’t be achieved by inserting the session id into the state object and then having the server lazily delete or update it on the next session. However, we believe that the session scope will be frequently used and the implicit lifecycle management makes state easier to use.
Session scope does rely on having sessions be a property of the data layer, rather than the transport as only the http transport currently supports session ids. With three proposals [1359, 1364, 1442], this seems likely.
State storage
Instead of separating the scopes by the key path, such as ``"modelcontextprotocol.io/state/session", and "modelcontextprotocol.io/state/server"`, we could instead use one path: `"modelcontextprotocol.io/state"` and add an attribute to the state that indicates its scope.
Or the state could be elevated to a non-namespaced key, such as "state" under the _meta object.
Including state as an http header instead of the _meta object.
Including the state as an http header instead of in the protocol itself would make it easier for http servers to read headers and take actions (such as blocking requests) without fully parsing the JSON-RPC messages. However, http headers are limited to ascii strings so state has to be encoded before sending (typical in the web), which makes it difficult to read and debug. This makes things inconsistent across transports as the state is in different places and placing it within the protocol itself is consistent and cleaner.
Storage requirements
The standard could specify the minimum amount of storage a client should support, such as "Clients SHOULD store at least 4096 bytes of state per key, inclusive of the name and the value. Clients SHOULD store at least 300 unique pieces of state and at least 20 per unique server-scope." These numbers come from RFC2109. It is felt that this is too specific and clients should feel free to manage storage as needed.
Backward Compatibility
This SEP does not introduce any backward compatibility issues.
Reference Implementation
TBD
Security Implications
The privacy of state is dependent on the client trusting the DNS infrastructure. This is the same assumption made in web cookies.
If a client uses two stdio servers with the same path at different times, state will be leaked to the second server.
SEP-1655: Client-Side State Management
Also see discussion: #1468
Outstanding Issues
Abstract
This proposal introduces a standardized mechanism for a Model Context Protocol (MCP) server to store and retrieve state information on an MCP client, analogous to the HTTP cookie standard. This allows for stateful, multi-turn interactions and promotes scalability and consistency. The proposal defines a way to set state in the client using the _meta property in server to client messages and have that state sent back in client to server messages. This is transport agnostic and works across both
httpandstdiotransports.Motivation
MCP’s stateless nature presents challenges for applications requiring context retention, such as user authentication, personalization, anti-abuse, and session tracking. A core mechanism in building consistency into highly distributed systems is storing state at the client. Without such state services must implement sticky sessions, which are incompatible with modern load balancing and auto scaling. Further, in globally distributed systems it is difficult to ensure highly consistent backend storage in a performant manner. Additional context is found in discussion 1468 and SEP-1442 echoes this same need.
The MCP protocol currently specifies two pieces of state that are regularly shared between a client and server: Mcp-Session-Id and OAuth Bearer tokens. Neither of these are general mechanisms for setting state in the client. The session id’s intended semantics are to follow the session as a random identifier. The session should end when the user leaves the application and the session-id should be globally unique and cryptographically secure. OAuth bearer tokens are only used for authorization purposes and are issued by an OAuth server, not the MCP server. In contrast this state mechanism is intended to be a general store with no assumption of the purpose or format of the state.
The Mcp-Session-Id and OAuth tokens are also only specified for the HTTP transport and not stdio (TODO: stdio is will be adding a session id as well, need reference). While a stdio server does not have the same difficulties in keeping consistent state as a globally distributed service, this mechanism can apply equally to stdio to avoid making http a special case and simplify implementing servers built for more than one transport.
Typical services will store multiple, logically distinct items in a key-value store. Supporting multiple items allows for the separation of concerns in backend systems, the ability to gradually evolve services and deprecate features. State should follow this key-value pattern, similar to web cookies. A key value store is straightforward to implement and makes keys independent of one another. This allows each key to have unique properties, such as expiration and security properties.
Such state is intended to be non-critical or recoverable, similar to how developers view web cookies. It is expected that clients may erase their state due to bugs, privacy concerns, reinstallation of a client, moving to a new device or client, etc.
State is scoped to different lifetimes: "session" and "server" ("request" scoping of state is being proposed in SEP-1685. Session scope bounds the lifetime of an MCP session and can be used for state pertinent to that conversation, such as previous results, shopping carts, etc. Server state has an unbounded lifetime and is intended to be used for tracking the long-term history of a client's interactions with a server, such as results from previous conversations and client profiling (eg. anti-abuse/trust).
While MCP’s state mechanism takes inspiration from web cookies, it can differ in several ways. First, and foremost, MCP state will not support functionality equivalent to “third-party cookies”; state will be tied to the server. Second, in web cookies the value is confined to a US-ASCII string, instead MCP state can use JSON to use a structure that is easier to inspect in protocol messages. Third, web cookies are scoped to a combination of a Domain and a Path. A client in MCP will not share state across http servers (even subdomains), and “Path” is not a well defined concept in MCP and thus state will be scoped to all requests.
Specification
Overview
The Model Context Protocol (MCP) provides a standardized way for servers to set state in the client to be accessed in subsequent interactions. Servers request set data as key-value data where the keys are strings and the values are JSON objects.
Capabilities
Clients that support state MUST declare the state capability during initialization:
Protocol
Setting State from the Server
State can be set in any server to client Result message.
State MUST be contained in a dictionary under one of two _meta keys: "http://modelcontextprotocol.io/state/session" and "http://modelcontextprotocol.io/state/server" (See the section on Session vs Server lifecycles)
The dictionary contains a key (the name for this item of state) which MUST be a string, and a value which is any valid JSON. State can be deleted by setting the value of a key to null.
State is additive in the client. If one Result message sets a state value with NameA and a later Result with just NameB then the values for both NameA and NameB will be sent by the client in Request messages. State set with the same key (name) as previous state overwrites the entire value for that state, it does not merge state other than at the top level of the dictionary.
Sending State from the Client
State MUST be set in all client to server Request messages.
If the client has no state to transmit, it SHOULD NOT send the "modelcontextprotocol.io/state/session" key.
Request, Session, Server State
There are two _meta properties that clients and servers can use:
"modelcontextprotocol.io/state/session""modelcontextprotocol.io/state/server"The lifetime of any state included in the session path is tied to the client-server session. When the session ends or a new session starts with the same server this state should be discarded.
Server scope state persists across sessions until the server removes it. Similar to the behavior on the web, it should be expected the clients may sometimes clear state for a variety of reasons (privacy concerns, re-installation, working around a bug, etc). and state may be cleared unexpectedly.
Schema
See sample schema
State Scope
Clients MUST only send state to the server that set it. There is no support for third-party access to state.
For HTTP servers the server name is defined by the fully qualified domain name, not including the protocol, port, or final '.'. Thus state set by www.example.com is not sent to other.example.com Clients MUST NOT store state for HTTP servers at localhost or by IP address and only for registry controlled domains.
For stdio servers the server name is defined by the file path of the server, such as "path/to/your_mcp_server.py"
Error Handling
This proposal does not add any additional errors beyond those specified already.
Rationale
Alternatives
Using an explicit set and get operations
Rather than setting state by piggybacking on the _meta property of Responses and Requests, we could define explicit GetState and SetState server-client protocol messages. This mechanism is more cumbersome to use and requires an extra round trip to get the necessary state before an operation.
State Attributes
Setting state could support a number of attributes that would dictate their behavior, such as:
server-scope: The domain or stdio path the state is valid formethod-scope: The method requests the client would include state withmax-age: The number of seconds until the state expires.When overwriting previous state with the same name, the attributes would be overwritten with the new values.
Attribute: server-scope
Server-scope CAN be specified, with defaults used as specified here.
Domain server-scope for http transports:
A domain server-scope follows the same rules as https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#domaindomain-value
Notably:
server-scope for stdio transport MUST NOT be set. The client should scope state to suitable identifier from the stdio path or npm package name (see security considerations below). The use cases for server-scope are not clear at this time, but it could be layered in later without backwards compatibility issues.
Attribute: max-age
The maximum value for max-age is 34560000 (400 days). The default value is the same as the maximum value. This matches recent developments in web cookie maximum ages. Values larger than that, 0, or negative values are errors and an invalid value error SHOULD be returned. The use cases for max-age are not strong enough at this time, but it could be layered in later without backwards compatibility issues.
Attribute: method-scope
Method–scope can be specified to restrict state to being sent only with certain MCP methods. The method-scope is a prefix match, so “tools” (or “tools/”) will match tools/list, but not "notifications/tools/list_changed”
Examples:
or
Method scope is mostly a bandwidth optimization technique to restrict what state is sent in requests, if a server wants to restrict the use of state to one tool or another, the name of the tool can be stored in the state itself and then filtered server side. Depending on how state is used in practice, method scope may be useful in the future and can be layered on top without backwards compatibility issues.
Path scoping
One possible addition is to expand the method-scope to include deeper elements in the message, such as the name (similar to Path in web cookies). This would reduce the amount of state sent to the server if it is only need for certain tools etc. The problem is that while some MCP messages can clearly be turned into a path:
"tools/list" -> “tools/list”
“tools/call” (always contains a name) -> “tools/call/[name]”
Others are more difficult, such as:
"completion/complete" (contains a ref) -> “tools/call/[name]”
Is perhaps ambiguous as a "ref/prompt" could share a name with another type of ref, so it should be:
"completion/complete" (contains a ref) -> “tools/call/ref/prompt/[name]”
And others seem very difficult, such as cursors which would require the client to record which path the original request the cursor is related to and look up its path to decide to include the state.
Request scope
The protocol can support a scope of a single request/response sequence. While there are a number of similarities, clients will only need to store state ephemeneraly to support request scope. Request scope has been separated into SEP-1685
Not supporting a session scope
State could additionally be supported in one global server scope rather than additionally defining a session state. The session state does not add any functionality that couldn’t be achieved by inserting the session id into the state object and then having the server lazily delete or update it on the next session. However, we believe that the session scope will be frequently used and the implicit lifecycle management makes state easier to use.
Session scope does rely on having sessions be a property of the data layer, rather than the transport as only the http transport currently supports session ids. With three proposals [1359, 1364, 1442], this seems likely.
State storage
Instead of separating the scopes by the key path, such as ``"modelcontextprotocol.io/state/session"
, and"modelcontextprotocol.io/state/server"`, we could instead use one path: `"modelcontextprotocol.io/state"` and add an attribute to the state that indicates its scope.Or the state could be elevated to a non-namespaced key, such as "state" under the _meta object.
Including state as an http header instead of the _meta object.
Including the state as an http header instead of in the protocol itself would make it easier for http servers to read headers and take actions (such as blocking requests) without fully parsing the JSON-RPC messages. However, http headers are limited to ascii strings so state has to be encoded before sending (typical in the web), which makes it difficult to read and debug. This makes things inconsistent across transports as the state is in different places and placing it within the protocol itself is consistent and cleaner.
Storage requirements
The standard could specify the minimum amount of storage a client should support, such as "Clients SHOULD store at least 4096 bytes of state per key, inclusive of the name and the value. Clients SHOULD store at least 300 unique pieces of state and at least 20 per unique server-scope." These numbers come from RFC2109. It is felt that this is too specific and clients should feel free to manage storage as needed.
Backward Compatibility
This SEP does not introduce any backward compatibility issues.
Reference Implementation
TBD
Security Implications
The privacy of state is dependent on the client trusting the DNS infrastructure. This is the same assumption made in web cookies.
If a client uses two stdio servers with the same path at different times, state will be leaked to the second server.