From 18a5ff63640a7fc313c05ae48ed33c21a325ac72 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:26:02 +0000 Subject: [PATCH 01/35] proposals: strawman for data-layer sessions Lightweight discussion starter for application-level sessions in MCP. Includes: - Capability advertisement shape (experimental.session) - session/create, session/list, session/delete JSON-RPC methods - Cookie echo pattern via _meta["mcp/session"] - Revocation and error handling shapes - Selective enforcement discussion - Transport-layer vs data-layer comparison - Open questions to guide working group discussion Companion JSONC file with raw API shapes for quick reference. Based on experimentation with fast-agent and the sessions track brief. --- .../data-layer-sessions-api-shapes.jsonc | 144 ++++++++++ proposals/data-layer-sessions.md | 257 ++++++++++++++++++ 2 files changed, 401 insertions(+) create mode 100644 proposals/data-layer-sessions-api-shapes.jsonc create mode 100644 proposals/data-layer-sessions.md diff --git a/proposals/data-layer-sessions-api-shapes.jsonc b/proposals/data-layer-sessions-api-shapes.jsonc new file mode 100644 index 0000000..d74b4ff --- /dev/null +++ b/proposals/data-layer-sessions-api-shapes.jsonc @@ -0,0 +1,144 @@ +// Data-Layer Sessions — API Shape Quick Reference +// Companion to: proposals/data-layer-sessions.md +// Status: Strawman / Discussion Starter + +// ============================================================ +// 1. Capability Advertisement (in InitializeResult) +// ============================================================ + +{ + "capabilities": { + "experimental": { + "session": { + "version": 1, + "features": ["create", "list", "delete"] + } + } + } +} + +// ============================================================ +// 2. session/create +// ============================================================ + +// Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "session/create", + "params": { + "hints": { + "label": "my-agent-workspace", // human-readable label + "data": { "title": "Code Review" } // arbitrary key-value hints + } + } +} + +// Response — cookie returned in _meta +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review" } + } + } + } +} + +// ============================================================ +// 3. session/list +// ============================================================ + +// Request +{ "jsonrpc": "2.0", "id": 2, "method": "session/list" } + +// Response +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "sessions": [ + { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review" } + } + ] + } +} + +// ============================================================ +// 4. session/delete +// ============================================================ + +// Request — id in params, or inferred from current cookie +{ "jsonrpc": "2.0", "id": 3, "method": "session/delete", "params": { "id": "sess-a1b2c3d4e5f6" } } + +// Response — null cookie = revoked +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "deleted": true, + "_meta": { "mcp/session": null } + } +} + +// ============================================================ +// 5. Cookie Echo (on any request/response) +// ============================================================ + +// Client sends cookie in _meta on every request +{ + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": { + "name": "notebook_append", + "arguments": { "text": "remember this" }, + "_meta": { + "mcp/session": { "id": "sess-a1b2c3d4e5f6" } + } + } +} + +// Server echoes (possibly updated) cookie in response _meta +{ + "jsonrpc": "2.0", + "id": 4, + "result": { + "content": [{ "type": "text", "text": "appended" }], + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T15:00:00Z" + } + } + } +} + +// ============================================================ +// 6. Revocation (server-initiated) +// ============================================================ + +// Server returns null to revoke +{ + "_meta": { "mcp/session": null } +} + +// ============================================================ +// 7. Session-required error +// ============================================================ + +{ + "jsonrpc": "2.0", + "id": 5, + "error": { + "code": -32002, + "message": "Session required. Call session/create first." + } +} diff --git a/proposals/data-layer-sessions.md b/proposals/data-layer-sessions.md new file mode 100644 index 0000000..a3672c6 --- /dev/null +++ b/proposals/data-layer-sessions.md @@ -0,0 +1,257 @@ +# Data-Layer Sessions for MCP + +> **Status:** Strawman +> **Date:** 2026-02-23 +> **Track:** Sessions +> **Author(s):** Shaun Smith + +## Purpose + +This is a **discussion starter**, not a finished design. It proposes a +minimal set of JSON-RPC API shapes for application-level sessions in MCP, +decoupled from the transport layer. The goal is to give the working group +something concrete to react to. + +The core problem: MCP currently ties session identity to the transport +connection (`Mcp-Session-Id` header for Streamable HTTP, implicit for stdio). +When a connection drops, application state is lost. Servers that need +multi-turn state — scratch-pads, sandboxes, conversation context — have no +standard way to offer it. + +## Design Principles + +1. **Transport-agnostic.** Works identically over stdio and HTTP. +2. **Server-authoritative.** The server issues, updates, and revokes session + tokens. The client echoes them. (Adapted cookie semantics per RFC 6265.) +3. **Opt-in.** Sessions are discovered via capability negotiation during + `initialize`. Servers that don't need sessions don't advertise them. +4. **Incremental.** A server can require sessions globally, per-tool, or not + at all. + +## Capability Advertisement + +During `initialize`, a server that supports sessions includes an +`experimental` capability: + +```jsonc +// Server → Client (InitializeResult) +{ + "capabilities": { + "experimental": { + "session": { + "version": 1, + "features": ["create", "list", "delete"] + } + } + } +} +``` + +`features` lists the `session/*` methods the server supports. A minimal +server might only support `["create"]`. + +**Open question:** Should `version` be a single integer, or should this +use the spec's existing versioning approach? + +## Session Lifecycle Methods + +### `session/create` + +```jsonc +// Client → Server +{ + "jsonrpc": "2.0", + "id": 1, + "method": "session/create", + "params": { + "hints": { + "label": "my-agent-workspace", + "data": { "title": "Code Review Session" } + } + } +} + +// Server → Client +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review Session" } + } + } + } +} +``` + +### `session/delete` + +```jsonc +// Client → Server +{ + "jsonrpc": "2.0", + "id": 2, + "method": "session/delete", + "params": { "id": "sess-a1b2c3d4e5f6" } +} + +// Server → Client +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "deleted": true, + "_meta": { "mcp/session": null } + } +} +``` + +### `session/list` + +```jsonc +// Client → Server +{ + "jsonrpc": "2.0", + "id": 3, + "method": "session/list" +} + +// Server → Client +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "sessions": [ + { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review Session" } + } + ] + } +} +``` + +## Session Cookie Echo + +Once a session is established, the client includes the session cookie in +`_meta` on every request. The server echoes (or updates) it in every +response. + +```jsonc +// Client → Server (tools/call with session) +{ + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": { + "name": "notebook_append", + "arguments": { "text": "remember this" }, + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6" + } + } + } +} + +// Server → Client +{ + "jsonrpc": "2.0", + "id": 4, + "result": { + "content": [{ "type": "text", "text": "appended" }], + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T15:00:00Z" + } + } + } +} +``` + +### Revocation + +A server revokes a session by returning `"mcp/session": null`: + +```jsonc +{ + "_meta": { "mcp/session": null } +} +``` + +The client SHOULD clear its stored cookie and MAY re-establish a session. + +## Error Handling + +A server that requires a session for a particular operation returns a +JSON-RPC error: + +```jsonc +{ + "jsonrpc": "2.0", + "id": 5, + "error": { + "code": -32002, + "message": "Session required. Call session/create first." + } +} +``` + +**Open question:** Is `-32002` the right code? Should we define a +named error code in the spec? + +## Selective Enforcement + +Servers MAY require sessions for all tools, some tools, or no tools. The +mechanism for advertising which tools require sessions is left open: + +- Option A: A `sessionRequired` field in tool metadata. +- Option B: Servers just return `-32002` and clients react. +- Option C: A server-level policy declaration in capabilities. + +**Open question:** Which approach (or combination) best serves both +human developers and LLM-driven tool selection? + +## Interaction with Transport-Level Sessions + +Streamable HTTP already has `Mcp-Session-Id` for transport routing. This +proposal operates at a different layer: + +| Concern | Transport (`Mcp-Session-Id`) | Data-layer (`mcp/session`) | +|---|---|---| +| Scope | Single connection | Across connections | +| Set by | Transport layer | Application logic | +| Survives reconnect | No | Yes | +| Works over stdio | N/A | Yes | + +The two are complementary. A load balancer can route on `Mcp-Session-Id` +while the application maintains state via `mcp/session`. + +**Open question:** Should `mcp/session` be mirrored into an HTTP header +for routing affinity? If so, what are the size constraints? + +## Open Questions Summary + +1. **Versioning** — integer in capability vs. spec-level versioning? +2. **Error code** — `-32002` or a named constant? +3. **Selective enforcement** — how should servers declare per-tool requirements? +4. **HTTP header mirroring** — should `mcp/session` also appear as a header? +5. **Cookie size** — what constraints on the `data` field? +6. **Security** — signing/encryption of session tokens? Server-side only + vs. client-verifiable? +7. **Fork/branch** — should `session/fork` be in scope, or deferred? +8. **Relationship to MRTR** — how does this interact with the multi-round-trip + requests track's need for state passthrough? + +## Prior Art + +- **RFC 6265** (HTTP Cookies) — foundation for cookie semantics +- **Sessions Track Brief** (this repo) — working group discussion context +- **MRTR Track Brief** (this repo) — overlapping state-passthrough needs +- **`fast-agent` experimental sessions** — working prototype of this design + over both stdio and Streamable HTTP transports From f8281f01cb5fbf1b19d8012875d87130abf12969 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:30:11 +0000 Subject: [PATCH 02/35] fix: align cross-file consistency, add .gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Align title strings ('Code Review Session' in both files) - Reorder: create → list → delete (natural CRUD lifecycle) - Renumber JSON-RPC ids to match new order - Add .gitignore for local tooling artifacts --- .gitignore | 6 ++++ .../data-layer-sessions-api-shapes.jsonc | 6 ++-- proposals/data-layer-sessions.md | 29 +++++++++---------- 3 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83cc956 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.fast-agent/ +fastagent.jsonl +__pycache__/ +*.pyc +proposals/session-v2-experimental/ +proposals/session-v2-proposal.md diff --git a/proposals/data-layer-sessions-api-shapes.jsonc b/proposals/data-layer-sessions-api-shapes.jsonc index d74b4ff..f67b2c5 100644 --- a/proposals/data-layer-sessions-api-shapes.jsonc +++ b/proposals/data-layer-sessions-api-shapes.jsonc @@ -29,7 +29,7 @@ "params": { "hints": { "label": "my-agent-workspace", // human-readable label - "data": { "title": "Code Review" } // arbitrary key-value hints + "data": { "title": "Code Review Session" } // arbitrary key-value hints } } } @@ -43,7 +43,7 @@ "mcp/session": { "id": "sess-a1b2c3d4e5f6", "expiry": "2026-02-23T14:30:00Z", - "data": { "title": "Code Review" } + "data": { "title": "Code Review Session" } } } } @@ -65,7 +65,7 @@ { "id": "sess-a1b2c3d4e5f6", "expiry": "2026-02-23T14:30:00Z", - "data": { "title": "Code Review" } + "data": { "title": "Code Review Session" } } ] } diff --git a/proposals/data-layer-sessions.md b/proposals/data-layer-sessions.md index a3672c6..8c5f4cb 100644 --- a/proposals/data-layer-sessions.md +++ b/proposals/data-layer-sessions.md @@ -87,15 +87,14 @@ use the spec's existing versioning approach? } ``` -### `session/delete` +### `session/list` ```jsonc // Client → Server { "jsonrpc": "2.0", "id": 2, - "method": "session/delete", - "params": { "id": "sess-a1b2c3d4e5f6" } + "method": "session/list" } // Server → Client @@ -103,20 +102,25 @@ use the spec's existing versioning approach? "jsonrpc": "2.0", "id": 2, "result": { - "deleted": true, - "_meta": { "mcp/session": null } + "sessions": [ + { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review Session" } + } + ] } } ``` - -### `session/list` +### `session/delete` ```jsonc // Client → Server { "jsonrpc": "2.0", "id": 3, - "method": "session/list" + "method": "session/delete", + "params": { "id": "sess-a1b2c3d4e5f6" } } // Server → Client @@ -124,13 +128,8 @@ use the spec's existing versioning approach? "jsonrpc": "2.0", "id": 3, "result": { - "sessions": [ - { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z", - "data": { "title": "Code Review Session" } - } - ] + "deleted": true, + "_meta": { "mcp/session": null } } } ``` From 097f61c064e4bb922390327c64655bacb7c5d9ac Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:49:27 +0000 Subject: [PATCH 03/35] address schema consistency review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - session/list: add cursor/nextCursor pagination (matches tools/list, resources/list, prompts/list pattern from PaginatedRequestParams / PaginatedResult) - session/create: return session object in result body AND cookie in _meta (result body for inspection, _meta for echo cycle — consistent with Result having additionalProperties: {}) - Add 'Session Cookie: Placement' design discussion — named property (like progressToken) vs. convention key (mcp/session) in _meta bag, with trade-off table - Add 'Revocation via null' design note — null in _meta is schema-valid but novel; document alternatives (omit key, dedicated method) - Add Schema Compatibility Notes section documenting review against draft schema (DRAFT-2026-v1) - Expand open questions to 10 (add cookie placement + revocation signal) - Add companion file cross-reference - Align .jsonc with all .md changes --- .../data-layer-sessions-api-shapes.jsonc | 74 +++++++++-- proposals/data-layer-sessions.md | 119 ++++++++++++++++-- 2 files changed, 174 insertions(+), 19 deletions(-) diff --git a/proposals/data-layer-sessions-api-shapes.jsonc b/proposals/data-layer-sessions-api-shapes.jsonc index f67b2c5..a046d96 100644 --- a/proposals/data-layer-sessions-api-shapes.jsonc +++ b/proposals/data-layer-sessions-api-shapes.jsonc @@ -34,29 +34,45 @@ } } -// Response — cookie returned in _meta +// Response — session object in result body, cookie in _meta { "jsonrpc": "2.0", "id": 1, "result": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review Session" }, "_meta": { "mcp/session": { "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z", - "data": { "title": "Code Review Session" } + "expiry": "2026-02-23T14:30:00Z" } } } } +// NOTE: result body has full session object (for client inspection). +// _meta cookie is the opaque token the client echoes — server controls +// what goes in the cookie (may be a subset of the result body). + // ============================================================ -// 3. session/list +// 3. session/list (paginated — matches tools/list pattern) // ============================================================ -// Request +// Request (first page — params optional) { "jsonrpc": "2.0", "id": 2, "method": "session/list" } -// Response +// Request (subsequent page) +{ + "jsonrpc": "2.0", + "id": 2, + "method": "session/list", + "params": { + "cursor": "eyJvZmZzZXQiOjEwfQ==" + } +} + +// Response (with pagination) { "jsonrpc": "2.0", "id": 2, @@ -67,18 +83,27 @@ "expiry": "2026-02-23T14:30:00Z", "data": { "title": "Code Review Session" } } - ] + ], + "nextCursor": "eyJvZmZzZXQiOjEwfQ==" } } +// When nextCursor is absent, there are no more results. +// Follows PaginatedRequestParams / PaginatedResult convention. + // ============================================================ // 4. session/delete // ============================================================ -// Request — id in params, or inferred from current cookie -{ "jsonrpc": "2.0", "id": 3, "method": "session/delete", "params": { "id": "sess-a1b2c3d4e5f6" } } +// Request +{ + "jsonrpc": "2.0", + "id": 3, + "method": "session/delete", + "params": { "id": "sess-a1b2c3d4e5f6" } +} -// Response — null cookie = revoked +// Response — null cookie = revoked (see design note in .md) { "jsonrpc": "2.0", "id": 3, @@ -125,11 +150,16 @@ // 6. Revocation (server-initiated) // ============================================================ -// Server returns null to revoke +// Server returns null to revoke (design note: novel use of null in _meta) { "_meta": { "mcp/session": null } } +// Alternatives discussed in the proposal: +// Option A: null (current) — simple, expressive +// Option B: omit key entirely — ambiguous (absence vs. "no change") +// Option C: dedicated session/revoke notification — explicit but adds a method + // ============================================================ // 7. Session-required error // ============================================================ @@ -142,3 +172,25 @@ "message": "Session required. Call session/create first." } } + +// -32002 is in the JSON-RPC server-defined range (-32000 to -32099). +// MCP uses -32042 for URL_ELICITATION_REQUIRED. +// If adopted, would need a named constant (e.g. SESSION_REQUIRED). + +// ============================================================ +// 8. Cookie placement design question +// ============================================================ + +// OPTION A: Named property on RequestMetaObject (like progressToken) +// Would require schema changes — adds "session" as a typed field: +// +// RequestMetaObject.properties.session → SessionCookie schema +// MetaObject.properties.session → SessionCookie schema +// +// OPTION B: Convention key in _meta bag (current proposal) +// No schema changes — uses the open _meta extensibility point: +// +// "_meta": { "mcp/session": { ... } } +// +// The "mcp/" prefix is reserved for MCP spec use per MetaObject naming +// rules, so "mcp/session" is valid as a spec-defined key. diff --git a/proposals/data-layer-sessions.md b/proposals/data-layer-sessions.md index 8c5f4cb..771b15c 100644 --- a/proposals/data-layer-sessions.md +++ b/proposals/data-layer-sessions.md @@ -18,6 +18,9 @@ When a connection drops, application state is lost. Servers that need multi-turn state — scratch-pads, sandboxes, conversation context — have no standard way to offer it. +> **See also:** [`data-layer-sessions-api-shapes.jsonc`](data-layer-sessions-api-shapes.jsonc) +> — flat quick-reference of all wire shapes. + ## Design Principles 1. **Transport-agnostic.** Works identically over stdio and HTTP. @@ -57,6 +60,11 @@ use the spec's existing versioning approach? ### `session/create` +The `session/create` result returns the session object in the result body +(like any other method result) **and** sets the cookie in `_meta` for the +echo cycle. See [Session Cookie: Placement](#session-cookie-placement) for +the design discussion on where the cookie lives. + ```jsonc // Client → Server { @@ -76,27 +84,47 @@ use the spec's existing versioning approach? "jsonrpc": "2.0", "id": 1, "result": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review Session" }, "_meta": { "mcp/session": { "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z", - "data": { "title": "Code Review Session" } + "expiry": "2026-02-23T14:30:00Z" } } } } ``` +The result body contains the full session object (for the client to inspect). +The `_meta` cookie contains the opaque token the client echoes on subsequent +requests — the server controls what goes in the cookie and may include less +data than the result body. + ### `session/list` +Follows the standard MCP pagination pattern (`cursor` / `nextCursor`), +consistent with `tools/list`, `resources/list`, `prompts/list`, etc. + ```jsonc -// Client → Server +// Client → Server (first page) { "jsonrpc": "2.0", "id": 2, "method": "session/list" } +// Client → Server (subsequent page) +{ + "jsonrpc": "2.0", + "id": 2, + "method": "session/list", + "params": { + "cursor": "eyJvZmZzZXQiOjEwfQ==" + } +} + // Server → Client { "jsonrpc": "2.0", @@ -108,10 +136,15 @@ use the spec's existing versioning approach? "expiry": "2026-02-23T14:30:00Z", "data": { "title": "Code Review Session" } } - ] + ], + "nextCursor": "eyJvZmZzZXQiOjEwfQ==" } } ``` + +When no `nextCursor` is present, there are no more results. The `params` +object is optional on the first request (following `PaginatedRequestParams`). + ### `session/delete` ```jsonc @@ -134,6 +167,9 @@ use the spec's existing versioning approach? } ``` +See [Revocation via `null`](#revocation-via-null) for the design discussion +on using `null` as a signal. + ## Session Cookie Echo Once a session is established, the client includes the session cookie in @@ -173,7 +209,34 @@ response. } ``` -### Revocation +### Session Cookie: Placement + +The cookie echo mechanism uses `_meta` to carry session state across all +request/response pairs. This raises a design question about how the cookie +key is defined. + +The MCP schema today has **one** protocol-defined key in `_meta`: +`progressToken`, defined as a **named property** on `RequestMetaObject`. +This proposal uses a **convention key** (`"mcp/session"`) in the open +`_meta` bag instead. + +Both approaches are schema-legal. The trade-offs: + +| Approach | Pro | Con | +|---|---|---| +| **A: Named property** — add `session` to `RequestMetaObject` and `MetaObject` schema definitions, like `progressToken` | Schema-validatable; typed in SDKs; consistent with `progressToken` precedent | Requires schema changes; tighter coupling to spec release cycle | +| **B: Convention key** — use `"mcp/session"` as an opaque key in the `_meta` bag (current proposal) | No schema changes needed; works immediately as `experimental`; extensible | No schema validation; novel use of the extensibility bag for a protocol-level concept | + +The `mcp/` prefix is **reserved for MCP spec use** per the `MetaObject` +naming rules — so `"mcp/session"` is valid as a spec-defined key. Third +parties MUST NOT define keys under the `mcp/` prefix. + +**Open question:** If this moves from `experimental` to a first-class spec +feature, should the cookie become a named property (Path A)? Or is the +convention-key approach (Path B) sufficient given that `_meta` is explicitly +designed as an extensibility point? + +### Revocation via `null` A server revokes a session by returning `"mcp/session": null`: @@ -185,6 +248,17 @@ A server revokes a session by returning `"mcp/session": null`: The client SHOULD clear its stored cookie and MAY re-establish a session. +**Design note:** The `MetaObject` schema is `"type": "object"` with no +constraints on property value types, so `null` is technically valid. However, +no existing MCP usage puts `null` in `_meta` — this would be a novel +pattern. Alternatives: + +- **Option A (current):** `null` signals revocation. Simple, expressive. +- **Option B:** Omit `"mcp/session"` entirely to signal revocation. Ambiguous + — absence could mean "no change" rather than "revoked." +- **Option C:** Use a dedicated `session/revoke` notification. Explicit, but + adds a method. + ## Error Handling A server that requires a session for a particular operation returns a @@ -201,8 +275,13 @@ JSON-RPC error: } ``` -**Open question:** Is `-32002` the right code? Should we define a -named error code in the spec? +The code `-32002` is in the JSON-RPC server-defined range (`-32000` to +`-32099`). MCP already uses `-32042` for `URL_ELICITATION_REQUIRED`. If +adopted, this would need a named constant (e.g. `SESSION_REQUIRED`). + +**Open question:** Is `-32002` the right code? Should the error carry +structured `data` (like `URLElicitationRequiredError` does with its +`elicitations` array)? ## Selective Enforcement @@ -234,10 +313,30 @@ while the application maintains state via `mcp/session`. **Open question:** Should `mcp/session` be mirrored into an HTTP header for routing affinity? If so, what are the size constraints? +## Schema Compatibility Notes + +This proposal was reviewed against the MCP draft schema +(`schema/draft/schema.json`, DRAFT-2026-v1). Key compatibility points: + +- **Method naming** follows `{namespace}/{verb}` (`session/create`, + `session/list`, `session/delete`), consistent with `tools/call`, + `resources/read`, `tasks/cancel`. +- **`experimental` capability** is `Record` with + `additionalProperties: true` — the proposed shape is valid. +- **`_meta` on requests** (`RequestMetaObject`) and **results** + (`MetaObject`) are both open objects — arbitrary keys are allowed. +- **`Result`** has `"additionalProperties": {}` — custom fields like + `deleted`, `sessions`, `id`, `expiry` are valid. +- **`session/list`** uses `PaginatedRequestParams` / `nextCursor`, matching + all other list methods. +- **If formalized**, each method would need the standard 4-definition tuple + (`*Request`, `*RequestParams`, `*Result`, `*ResultResponse`) and + registration in the `ClientRequest` / `ServerResult` union types. + ## Open Questions Summary 1. **Versioning** — integer in capability vs. spec-level versioning? -2. **Error code** — `-32002` or a named constant? +2. **Error code** — `-32002` or a named constant? Structured `data` payload? 3. **Selective enforcement** — how should servers declare per-tool requirements? 4. **HTTP header mirroring** — should `mcp/session` also appear as a header? 5. **Cookie size** — what constraints on the `data` field? @@ -246,6 +345,10 @@ for routing affinity? If so, what are the size constraints? 7. **Fork/branch** — should `session/fork` be in scope, or deferred? 8. **Relationship to MRTR** — how does this interact with the multi-round-trip requests track's need for state passthrough? +9. **Cookie placement** — named `_meta` property (like `progressToken`) or + convention key (`"mcp/session"`)? See [Placement](#session-cookie-placement). +10. **Revocation signal** — `null` value, key absence, or dedicated method? + See [Revocation](#revocation-via-null). ## Prior Art From ad6c1931e8b3477d379d170481dc0b36e66818d7 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:24:40 +0000 Subject: [PATCH 04/35] proposal: phased strawman for data-layer sessions (phase 1) Lightweight discussion starter for application-level sessions in MCP. Phase 1 scope: - session/create - session/resume - session/delete - cookie echo/revocation via _meta["mcp/session"] Phase 2 (deferred): - session/list - session/recover Includes use cases, implementation-informed considerations, and open questions for working-group discussion, plus a companion JSONC quick-reference of wire shapes. --- .../data-layer-sessions-api-shapes.jsonc | 156 +++++++ proposals/data-layer-sessions.md | 429 ++++++++++++++++++ 2 files changed, 585 insertions(+) create mode 100644 proposals/data-layer-sessions-api-shapes.jsonc create mode 100644 proposals/data-layer-sessions.md diff --git a/proposals/data-layer-sessions-api-shapes.jsonc b/proposals/data-layer-sessions-api-shapes.jsonc new file mode 100644 index 0000000..d26aa0e --- /dev/null +++ b/proposals/data-layer-sessions-api-shapes.jsonc @@ -0,0 +1,156 @@ +// Data-Layer Sessions — API Shape Quick Reference +// Companion to: proposals/data-layer-sessions.md +// Status: Strawman / Discussion Starter +// Scope: Phase 1 only (create, resume, delete). Phase 2 (list, recover) deferred. + +// ============================================================ +// 1. Capability Advertisement (in InitializeResult) +// ============================================================ + +{ + "capabilities": { + "experimental": { + "session": { + "version": 1, + "features": ["create", "resume", "delete"] + } + } + } +} + +// ============================================================ +// 2. session/create +// ============================================================ + +// Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "session/create", + "params": { + "hints": { + "label": "my-agent-workspace", + "data": { "title": "Code Review Session" } + } + } +} + +// Response — session object in result body, cookie in _meta +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review Session" }, + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z" + } + } + } +} + +// ============================================================ +// 3. session/resume +// ============================================================ + +// Request +{ + "jsonrpc": "2.0", + "id": 2, + "method": "session/resume", + "params": { "id": "sess-a1b2c3d4e5f6" } +} + +// Response +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review Session" }, + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z" + } + } + } +} + +// If the session cannot be resumed, server returns an error. + +// ============================================================ +// 4. session/delete +// ============================================================ + +// Request +{ + "jsonrpc": "2.0", + "id": 3, + "method": "session/delete", + "params": { "id": "sess-a1b2c3d4e5f6" } +} + +// Response — null cookie = revoked +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "deleted": true, + "_meta": { "mcp/session": null } + } +} + +// ============================================================ +// 5. Cookie Echo (on any request/response) +// ============================================================ + +{ + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": { + "name": "notebook_append", + "arguments": { "text": "remember this" }, + "_meta": { + "mcp/session": { "id": "sess-a1b2c3d4e5f6" } + } + } +} + +{ + "jsonrpc": "2.0", + "id": 4, + "result": { + "content": [{ "type": "text", "text": "appended" }], + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T15:00:00Z" + } + } + } +} + +// ============================================================ +// 6. Revocation (server-initiated) +// ============================================================ + +{ "_meta": { "mcp/session": null } } + +// ============================================================ +// 7. Session-required error +// ============================================================ + +{ + "jsonrpc": "2.0", + "id": 5, + "error": { + "code": -32002, + "message": "Session required. Call session/create or session/resume first." + } +} diff --git a/proposals/data-layer-sessions.md b/proposals/data-layer-sessions.md new file mode 100644 index 0000000..2964c6c --- /dev/null +++ b/proposals/data-layer-sessions.md @@ -0,0 +1,429 @@ +# Data-Layer Sessions for MCP + +> **Status:** Strawman +> **Date:** 2026-02-23 +> **Track:** Sessions +> **Author(s):** Shaun Smith + +## Purpose + +This is a **discussion starter**, not a finished design. It proposes a +minimal set of JSON-RPC API shapes for application-level sessions in MCP, +decoupled from the transport layer. The goal is to give the working group +something concrete to react to. + +The core problem: MCP currently ties session identity to the transport +connection (`Mcp-Session-Id` header for Streamable HTTP, implicit for stdio). +When a connection drops, application state is lost. Servers that need +multi-turn state — scratch-pads, sandboxes, conversation context — have no +standard way to offer it. + +> **See also:** [`data-layer-sessions-api-shapes.jsonc`](data-layer-sessions-api-shapes.jsonc) +> — flat quick-reference of all wire shapes. + +## Design Principles + +1. **Transport-agnostic.** Works identically over stdio and HTTP. +2. **Server-authoritative.** The server issues, updates, and revokes session + tokens. The client echoes them. (Adapted cookie semantics per RFC 6265.) +3. **Opt-in.** Sessions are discovered via capability negotiation during + `initialize`. Servers that don't need sessions don't advertise them. +4. **Incremental.** A server can require sessions globally, per-tool, or not + at all. + + +## Use Cases + +The following are concrete scenarios from experimental client/server +integrations (including `fast-agent` + demo MCP servers) where data-layer +sessions are immediately useful: + +1. **Global session gatekeeping** + - Some servers require session establishment before any tool call. + - Example: policy-enforced systems that need an explicit server-issued + identity before tool execution. + +2. **Selective per-tool session policy** + - Public tools can run without a session, while stateful/sensitive tools + require one. + - Example: `public_echo` remains open; `session_counter_inc` requires a + valid session cookie. + +3. **Session-scoped stateful tools** + - Tools maintain per-session notebooks/KV data across multiple calls. + - Example: notebook append/read/clear and hash KV verify workflows. + +4. **Reconnect + resume semantics (same cookie, new transport)** + - Client disconnects/reconnects and resumes server state by reusing + `mcp/session` cookie. + - This is the core value beyond transport-local `Mcp-Session-Id`. + +5. **Session revocation + re-establishment** + - Server revokes cookie (`mcp/session = null`); client clears local cookie + and can create/select a new session. + +6. **Operator-driven session control** + - Runtime operators can create/resume/select/clear sessions explicitly + (e.g., for debugging, incident response, or workflow recovery). + +These use cases suggest sessions are not only a transport concern; they are a +practical application-layer primitive needed for real tool orchestration. + +## Capability Advertisement + +During `initialize`, a server that supports sessions includes an +`experimental` capability: + +```jsonc +// Server → Client (InitializeResult) +{ + "capabilities": { + "experimental": { + "session": { + "version": 1, + "features": ["create", "resume", "delete"] + } + } + } +} +``` + +`features` lists the `session/*` methods the server supports. A minimal +server might only support `["create"]`. + +**Open question:** Should `version` be a single integer, or should this +use the spec's existing versioning approach? + +## Phase Scope + +To keep this proposal straightforward for initial review, this draft splits +session functionality into two phases: + +- **Phase 1 (in scope for this draft):** `session/create`, `session/resume`, + `session/delete`, and cookie echo/revocation semantics. +- **Phase 2 (deferred):** `session/list` and `session/recover` semantics. + +This lets the group evaluate the core lifecycle first, then expand into +recovery/discovery workflows once the base contract is agreed. + +## Session Lifecycle Methods + +### `session/create` + +The `session/create` result returns the session object in the result body +(like any other method result) **and** sets the cookie in `_meta` for the +echo cycle. See [Session Cookie: Placement](#session-cookie-placement) for +the design discussion on where the cookie lives. + +```jsonc +// Client → Server +{ + "jsonrpc": "2.0", + "id": 1, + "method": "session/create", + "params": { + "hints": { + "label": "my-agent-workspace", + "data": { "title": "Code Review Session" } + } + } +} + +// Server → Client +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review Session" }, + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z" + } + } + } +} +``` + +The result body contains the full session object (for the client to inspect). +The `_meta` cookie contains the opaque token the client echoes on subsequent +requests — the server controls what goes in the cookie and may include less +data than the result body. + +### `session/resume` + +`session/resume` re-activates an existing session by ID and returns the +canonical cookie payload for subsequent echo. + +```jsonc +// Client → Server +{ + "jsonrpc": "2.0", + "id": 2, + "method": "session/resume", + "params": { + "id": "sess-a1b2c3d4e5f6" + } +} + +// Server → Client +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review Session" }, + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T14:30:00Z" + } + } + } +} +``` + +If the requested session cannot be resumed, the server SHOULD return an +error (e.g., session not found / invalid / expired). + +### `session/delete` + +```jsonc +// Client → Server +{ + "jsonrpc": "2.0", + "id": 3, + "method": "session/delete", + "params": { "id": "sess-a1b2c3d4e5f6" } +} + +// Server → Client +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "deleted": true, + "_meta": { "mcp/session": null } + } +} +``` + +See [Revocation via `null`](#revocation-via-null) for the design discussion +on using `null` as a signal. + +## Session Cookie Echo + +Once a session is established, the client includes the session cookie in +`_meta` on every request. The server echoes (or updates) it in every +response. + +```jsonc +// Client → Server (tools/call with session) +{ + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": { + "name": "notebook_append", + "arguments": { "text": "remember this" }, + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6" + } + } + } +} + +// Server → Client +{ + "jsonrpc": "2.0", + "id": 4, + "result": { + "content": [{ "type": "text", "text": "appended" }], + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T15:00:00Z" + } + } + } +} +``` + +### Session Cookie: Placement + +The cookie echo mechanism uses `_meta` to carry session state across all +request/response pairs. This raises a design question about how the cookie +key is defined. + +The MCP schema today has **one** protocol-defined key in `_meta`: +`progressToken`, defined as a **named property** on `RequestMetaObject`. +This proposal uses a **convention key** (`"mcp/session"`) in the open +`_meta` bag instead. + +Both approaches are schema-legal. The trade-offs: + +| Approach | Pro | Con | +|---|---|---| +| **A: Named property** — add `session` to `RequestMetaObject` and `MetaObject` schema definitions, like `progressToken` | Schema-validatable; typed in SDKs; consistent with `progressToken` precedent | Requires schema changes; tighter coupling to spec release cycle | +| **B: Convention key** — use `"mcp/session"` as an opaque key in the `_meta` bag (current proposal) | No schema changes needed; works immediately as `experimental`; extensible | No schema validation; novel use of the extensibility bag for a protocol-level concept | + +The `mcp/` prefix is **reserved for MCP spec use** per the `MetaObject` +naming rules — so `"mcp/session"` is valid as a spec-defined key. Third +parties MUST NOT define keys under the `mcp/` prefix. + +**Open question:** If this moves from `experimental` to a first-class spec +feature, should the cookie become a named property (Path A)? Or is the +convention-key approach (Path B) sufficient given that `_meta` is explicitly +designed as an extensibility point? + +### Revocation via `null` + +A server revokes a session by returning `"mcp/session": null`: + +```jsonc +{ + "_meta": { "mcp/session": null } +} +``` + +The client SHOULD clear its stored cookie and MAY re-establish a session. + +**Design note:** The `MetaObject` schema is `"type": "object"` with no +constraints on property value types, so `null` is technically valid. However, +no existing MCP usage puts `null` in `_meta` — this would be a novel +pattern. Alternatives: + +- **Option A (current):** `null` signals revocation. Simple, expressive. +- **Option B:** Omit `"mcp/session"` entirely to signal revocation. Ambiguous + — absence could mean "no change" rather than "revoked." +- **Option C:** Use a dedicated `session/revoke` notification. Explicit, but + adds a method. + +## Error Handling + +A server that requires a session for a particular operation returns a +JSON-RPC error: + +```jsonc +{ + "jsonrpc": "2.0", + "id": 5, + "error": { + "code": -32002, + "message": "Session required. Call session/create or session/resume first." + } +} +``` + +The code `-32002` is in the JSON-RPC server-defined range (`-32000` to +`-32099`). MCP already uses `-32042` for `URL_ELICITATION_REQUIRED`. If +adopted, this would need a named constant (e.g. `SESSION_REQUIRED`). + +**Open question:** Is `-32002` the right code? Should the error carry +structured `data` (like `URLElicitationRequiredError` does with its +`elicitations` array)? + +## Selective Enforcement + +Servers MAY require sessions for all tools, some tools, or no tools. The +mechanism for advertising which tools require sessions is left open: + +- Option A: A `sessionRequired` field in tool metadata. +- Option B: Servers just return `-32002` and clients react. +- Option C: A server-level policy declaration in capabilities. + +**Open question:** Which approach (or combination) best serves both +human developers and LLM-driven tool selection? + +## Interaction with Transport-Level Sessions + +Streamable HTTP already has `Mcp-Session-Id` for transport routing. This +proposal operates at a different layer: + +| Concern | Transport (`Mcp-Session-Id`) | Data-layer (`mcp/session`) | +|---|---|---| +| Scope | Single connection | Across connections | +| Set by | Transport layer | Application logic | +| Survives reconnect | No | Yes | +| Works over stdio | N/A | Yes | + +The two are complementary. A load balancer can route on `Mcp-Session-Id` +while the application maintains state via `mcp/session`. + +**Open question:** Should `mcp/session` be mirrored into an HTTP header +for routing affinity? If so, what are the size constraints? + +## Schema Compatibility Notes + +This proposal was reviewed against the MCP draft schema +(`schema/draft/schema.json`, DRAFT-2026-v1). Key compatibility points: + +- **Method naming** follows `{namespace}/{verb}` (`session/create`, + `session/resume`, `session/delete`), consistent with `tools/call`, + `resources/read`, `tasks/cancel`. +- **`experimental` capability** is `Record` with + `additionalProperties: true` — the proposed shape is valid. +- **`_meta` on requests** (`RequestMetaObject`) and **results** + (`MetaObject`) are both open objects — arbitrary keys are allowed. +- **`Result`** has `"additionalProperties": {}` — custom fields like + `deleted`, `id`, `expiry` are valid. +- **Phase 2 methods** (`session/list`, `session/recover`) are intentionally + deferred in this draft for scope control. +- **If formalized**, each method would need the standard 4-definition tuple + (`*Request`, `*RequestParams`, `*Result`, `*ResultResponse`) and + registration in the `ClientRequest` / `ServerResult` union types. + + +## Implementation-Informed Considerations + +Early implementation work suggests the following considerations (non-normative): + +- **Capability/version gating works in practice.** Clients can ignore unknown + session versions and continue normal MCP operation. +- **Auto-create + explicit controls both matter.** Automatic `session/create` + supports low-friction startup, while explicit controls (`create/resume/delete/clear`) + support operator workflows and debugging. +- **Client-side cookie persistence is valuable.** A local cookie jar enables + reconnect bootstrap and reduces redundant `session/create` calls. +- **Identity-aware storage helps multi-server environments.** Keying by server + identity (when available) reduces collisions and supports disconnected views. +- **Invalidation tracking is useful.** Marking rejected cookies as invalidated + avoids repeatedly selecting known-bad session IDs during resume. +- **Expiry is advisory unless enforced.** Demo servers stamp `expiry` metadata, + but enforcement policy remains server-defined. + +These considerations do **not** lock in protocol choices; they provide +practical guidance for SEP scope and interoperability testing. + +## Open Questions Summary + +1. **Versioning** — integer in capability vs. spec-level versioning? +2. **Error code** — `-32002` or a named constant? Structured `data` payload? +3. **Selective enforcement** — how should servers declare per-tool requirements? +4. **HTTP header mirroring** — should `mcp/session` also appear as a header? +5. **Cookie size** — what constraints on the `data` field? +6. **Security** — signing/encryption of session tokens? Server-side only + vs. client-verifiable? +7. **Phase 2 shape** — how should `session/list` and `session/recover` be + specified once Phase 1 stabilizes? +8. **Relationship to MRTR** — how does this interact with the multi-round-trip + requests track's need for state passthrough? +9. **Cookie placement** — named `_meta` property (like `progressToken`) or + convention key (`"mcp/session"`)? See [Placement](#session-cookie-placement). +10. **Revocation signal** — `null` value, key absence, or dedicated method? + See [Revocation](#revocation-via-null). +11. **Client persistence semantics** — should local cookie jars / resume behavior + be guidance-only, or should minimal interoperability expectations be defined? + +## Prior Art + +- **RFC 6265** (HTTP Cookies) — foundation for cookie semantics +- **Sessions Track Brief** (this repo) — working group discussion context +- **MRTR Track Brief** (this repo) — overlapping state-passthrough needs +- **`fast-agent` experimental sessions** — working prototype of this design + over both stdio and Streamable HTTP transports, including jar-based resume and + operator session controls From 8da5f88e8bc5e059aea81ebf2329fe81edb4cd4a Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:37:47 +0000 Subject: [PATCH 05/35] very early draft --- .../README.md | 25 ++++++ .../pyproject.toml | 22 +++++ .../src/mcp_sessions_server/__init__.py | 52 +++++++++++ .../src/mcp_sessions_server/model.py | 80 +++++++++++++++++ .../src/mcp_sessions_server/protocol.py | 61 +++++++++++++ .../src/mcp_sessions_server/server.py | 88 +++++++++++++++++++ .../mcp_sessions_server/server_handlers.py | 66 ++++++++++++++ .../src/mcp_sessions_server/store.py | 68 ++++++++++++++ 8 files changed, 462 insertions(+) create mode 100644 proposals/data-layer-sessions-server-python/README.md create mode 100644 proposals/data-layer-sessions-server-python/pyproject.toml create mode 100644 proposals/data-layer-sessions-server-python/src/mcp_sessions_server/__init__.py create mode 100644 proposals/data-layer-sessions-server-python/src/mcp_sessions_server/model.py create mode 100644 proposals/data-layer-sessions-server-python/src/mcp_sessions_server/protocol.py create mode 100644 proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server.py create mode 100644 proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server_handlers.py create mode 100644 proposals/data-layer-sessions-server-python/src/mcp_sessions_server/store.py diff --git a/proposals/data-layer-sessions-server-python/README.md b/proposals/data-layer-sessions-server-python/README.md new file mode 100644 index 0000000..ca88249 --- /dev/null +++ b/proposals/data-layer-sessions-server-python/README.md @@ -0,0 +1,25 @@ +# MCP Data-Layer Sessions (Server Reference Package) + +Small, self-contained reference package for the **server-side** of the +experimental MCP data-layer sessions proposal. + +This package demonstrates: + +- capability advertisement (`experimental.session`) +- extracting/validating incoming `_meta["mcp/session"]` +- issuing and revoking session cookies via response `_meta` +- explicit lifecycle handlers (`session/create`, `session/resume`, `session/delete`) + +It is intended for proposal review and inspection, not production use. + +## Layout + +```text +src/mcp_sessions_server/ + __init__.py + model.py + store.py + server.py + protocol.py + server_handlers.py +``` diff --git a/proposals/data-layer-sessions-server-python/pyproject.toml b/proposals/data-layer-sessions-server-python/pyproject.toml new file mode 100644 index 0000000..78ac68c --- /dev/null +++ b/proposals/data-layer-sessions-server-python/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["hatchling>=1.27.0"] +build-backend = "hatchling.build" + +[project] +name = "mcp-data-layer-sessions-server-ref" +version = "0.1.0" +description = "Reference server package for MCP data-layer sessions proposal" +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +authors = [{ name = "MCP Transports WG Contributors" }] +keywords = ["mcp", "sessions", "proposal", "reference"] +classifiers = [ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", +] +dependencies = ["mcp>=0.1.0"] + +[tool.hatch.build.targets.wheel] +packages = ["src/mcp_sessions_server"] diff --git a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/__init__.py b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/__init__.py new file mode 100644 index 0000000..c7ee390 --- /dev/null +++ b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/__init__.py @@ -0,0 +1,52 @@ +"""Reference server package for MCP data-layer sessions proposal.""" + +from .model import ( + SESSION_EXPERIMENTAL_KEY, + SESSION_META_KEY, + Session, + extract_session_capability, + extract_session_from_meta, + generate_session_id, + inject_session_into_meta, + session_capability, +) +from .protocol import ( + SessionCreateHints, + SessionCreateParams, + SessionCreateRequest, + SessionCreateResult, + SessionDeleteParams, + SessionDeleteRequest, + SessionDeleteResult, + SessionResumeParams, + SessionResumeRequest, + SessionResumeResult, +) +from .server import SessionServer +from .server_handlers import register_session_handlers +from .store import InMemorySessionStore, SessionStore + +__all__ = [ + "Session", + "SessionServer", + "SessionStore", + "InMemorySessionStore", + "register_session_handlers", + "SessionCreateRequest", + "SessionCreateParams", + "SessionCreateHints", + "SessionCreateResult", + "SessionResumeRequest", + "SessionResumeParams", + "SessionResumeResult", + "SessionDeleteRequest", + "SessionDeleteParams", + "SessionDeleteResult", + "generate_session_id", + "session_capability", + "extract_session_capability", + "inject_session_into_meta", + "extract_session_from_meta", + "SESSION_META_KEY", + "SESSION_EXPERIMENTAL_KEY", +] diff --git a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/model.py b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/model.py new file mode 100644 index 0000000..d893df6 --- /dev/null +++ b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/model.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import secrets +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Any + +SESSION_META_KEY = "mcp/session" +SESSION_EXPERIMENTAL_KEY = "session" + +@dataclass +class Session: + id: str + expiry: str | None = None + data: dict[str, str] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + result: dict[str, Any] = {"id": self.id} + if self.expiry is not None: + result["expiry"] = self.expiry + if self.data: + result["data"] = dict(self.data) + return result + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> Session: + return cls(id=d["id"], expiry=d.get("expiry"), data=d.get("data", {})) + + def is_expired(self) -> bool: + if self.expiry is None: + return False + try: + expiry_dt = datetime.fromisoformat(self.expiry) + now = datetime.now(timezone.utc) + if expiry_dt.tzinfo is None: + expiry_dt = expiry_dt.replace(tzinfo=timezone.utc) + return now > expiry_dt + except ValueError: + return False + + +def generate_session_id(prefix: str = "sess-") -> str: + return f"{prefix}{secrets.token_hex(8)}" + + +def session_capability(features: list[str] | None = None) -> dict[str, dict[str, Any]]: + cap: dict[str, Any] = {} + if features: + cap["features"] = features + return {SESSION_EXPERIMENTAL_KEY: cap} + + +def extract_session_capability( + experimental: dict[str, dict[str, Any]] | None, +) -> dict[str, Any] | None: + if experimental is None: + return None + cap = experimental.get(SESSION_EXPERIMENTAL_KEY) + if cap is None: + return None + return cap + + +def inject_session_into_meta( + session: Session | None, existing_meta: dict[str, Any] | None = None +) -> dict[str, Any]: + meta = dict(existing_meta) if existing_meta else {} + meta[SESSION_META_KEY] = None if session is None else session.to_dict() + return meta + + +def extract_session_from_meta(meta: dict[str, Any] | None) -> Session | None | bool: + if meta is None: + return None + if SESSION_META_KEY not in meta: + return None + value = meta[SESSION_META_KEY] + if value is None: + return False + return Session.from_dict(value) diff --git a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/protocol.py b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/protocol.py new file mode 100644 index 0000000..b028473 --- /dev/null +++ b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/protocol.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from typing import Literal + +from mcp.types import RequestParams, Result + +try: + from mcp.types._types import MCPModel # type: ignore[attr-defined] +except Exception: + try: + from mcp.types import MCPModel # type: ignore[attr-defined] + except Exception: + from pydantic import BaseModel as MCPModel + + +class SessionCreateHints(MCPModel): + label: str | None = None + data: dict[str, str] | None = None + + +class SessionCreateParams(RequestParams): + hints: SessionCreateHints | None = None + + +class SessionCreateRequest(MCPModel): + method: Literal["session/create"] = "session/create" + params: SessionCreateParams | None = None + + +class SessionCreateResult(Result): + id: str + expiry: str | None = None + data: dict[str, str] | None = None + + +class SessionResumeParams(RequestParams): + id: str + + +class SessionResumeRequest(MCPModel): + method: Literal["session/resume"] = "session/resume" + params: SessionResumeParams + + +class SessionResumeResult(Result): + id: str + expiry: str | None = None + data: dict[str, str] | None = None + + +class SessionDeleteParams(RequestParams): + id: str + + +class SessionDeleteRequest(MCPModel): + method: Literal["session/delete"] = "session/delete" + params: SessionDeleteParams + + +class SessionDeleteResult(Result): + deleted: bool diff --git a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server.py b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server.py new file mode 100644 index 0000000..e594032 --- /dev/null +++ b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import logging +from typing import Any + +from .model import ( + Session, + extract_session_capability, + extract_session_from_meta, + inject_session_into_meta, + session_capability, +) +from .store import InMemorySessionStore, SessionStore + +logger = logging.getLogger(__name__) + + +class SessionServer: + def __init__( + self, + store: SessionStore | None = None, + features: list[str] | None = None, + default_ttl_seconds: int = 3600, + ) -> None: + self._store = store or InMemorySessionStore() + self._features = features or ["create", "resume", "delete"] + self._default_ttl = default_ttl_seconds + + @property + def store(self) -> SessionStore: + return self._store + + def get_experimental_capabilities(self) -> dict[str, dict[str, Any]]: + return session_capability(self._features if self._features else None) + + def client_supports_sessions( + self, experimental: dict[str, dict[str, Any]] | None + ) -> bool: + return extract_session_capability(experimental) is not None + + def extract_request_session(self, meta: dict[str, Any] | None) -> Session | None: + extracted = extract_session_from_meta(meta) + if extracted is False or extracted is None: + return None + stored = self._store.get(extracted.id) + if stored is None: + logger.info("Rejected unknown/expired session: %s", extracted.id) + return None + return stored + + def create_session( + self, + owner: str | None = None, + data: dict[str, str] | None = None, + ttl_seconds: int | None = None, + ) -> Session: + ttl = ttl_seconds if ttl_seconds is not None else self._default_ttl + return self._store.create(owner=owner, data=data, ttl_seconds=ttl) + + def resume_session(self, session_id: str) -> Session | None: + return self._store.get(session_id) + + def prepare_response_meta( + self, + session: Session, + existing_meta: dict[str, Any] | None = None, + ) -> dict[str, Any]: + return inject_session_into_meta(session, existing_meta) + + def prepare_revocation_meta( + self, + session_id: str | None = None, + existing_meta: dict[str, Any] | None = None, + ) -> dict[str, Any]: + if session_id is not None: + self._store.delete(session_id) + return inject_session_into_meta(None, existing_meta) + + def get_or_create_session( + self, + meta: dict[str, Any] | None, + owner: str | None = None, + default_data: dict[str, str] | None = None, + ) -> tuple[Session, bool]: + session = self.extract_request_session(meta) + if session is not None: + return session, False + return self.create_session(owner=owner, data=default_data), True diff --git a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server_handlers.py b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server_handlers.py new file mode 100644 index 0000000..3758fa2 --- /dev/null +++ b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server_handlers.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from collections.abc import Callable + +from mcp.server.lowlevel.server import Server + +from .model import inject_session_into_meta +from .protocol import ( + SessionCreateRequest, + SessionCreateResult, + SessionDeleteRequest, + SessionDeleteResult, + SessionResumeRequest, + SessionResumeResult, +) +from .server import SessionServer + + +def register_session_handlers( + low_level_server: Server, + session_server: SessionServer, + get_owner: Callable[..., str | None] | None = None, +) -> None: + async def handle_session_create(req: SessionCreateRequest) -> SessionCreateResult: + owner = get_owner() if get_owner else None + hints = req.params.hints if req.params and req.params.hints else None + data = dict(hints.data) if hints and hints.data else {} + if hints and hints.label: + data.setdefault("label", hints.label) + + session = session_server.create_session(owner=owner, data=data if data else None) + meta = inject_session_into_meta(session) + return SessionCreateResult( + id=session.id, + expiry=session.expiry, + data=session.data if session.data else None, + **{"_meta": meta}, + ) + + low_level_server.request_handlers[SessionCreateRequest] = handle_session_create + + async def handle_session_resume(req: SessionResumeRequest) -> SessionResumeResult: + session = session_server.resume_session(req.params.id) + if session is None: + raise ValueError(f"Session not found: {req.params.id}") + meta = inject_session_into_meta(session) + return SessionResumeResult( + id=session.id, + expiry=session.expiry, + data=session.data if session.data else None, + **{"_meta": meta}, + ) + + low_level_server.request_handlers[SessionResumeRequest] = handle_session_resume + + async def handle_session_delete(req: SessionDeleteRequest) -> SessionDeleteResult: + existing = session_server.store.get(req.params.id) + if existing is not None: + session_server.store.delete(req.params.id) + deleted = True + else: + deleted = False + meta = inject_session_into_meta(None) + return SessionDeleteResult(deleted=deleted, **{"_meta": meta}) + + low_level_server.request_handlers[SessionDeleteRequest] = handle_session_delete diff --git a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/store.py b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/store.py new file mode 100644 index 0000000..6a02ce5 --- /dev/null +++ b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/store.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import threading +from datetime import datetime, timedelta, timezone +from typing import Protocol, runtime_checkable + +from .model import Session, generate_session_id + + +@runtime_checkable +class SessionStore(Protocol): + def create( + self, + owner: str | None = None, + data: dict[str, str] | None = None, + ttl_seconds: int = 3600, + ) -> Session: ... + + def get(self, session_id: str) -> Session | None: ... + + def update(self, session: Session) -> Session: ... + + def delete(self, session_id: str) -> None: ... + + +class InMemorySessionStore: + def __init__(self) -> None: + self._sessions: dict[str, Session] = {} + self._owners: dict[str, set[str]] = {} + self._lock = threading.Lock() + + def create( + self, + owner: str | None = None, + data: dict[str, str] | None = None, + ttl_seconds: int = 3600, + ) -> Session: + session_id = generate_session_id() + expiry = (datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)).isoformat() + session = Session(id=session_id, expiry=expiry, data=data or {}) + with self._lock: + self._sessions[session_id] = session + if owner is not None: + self._owners.setdefault(owner, set()).add(session_id) + return session + + def get(self, session_id: str) -> Session | None: + with self._lock: + session = self._sessions.get(session_id) + if session is None: + return None + if session.is_expired(): + self.delete(session_id) + return None + return session + + def update(self, session: Session) -> Session: + with self._lock: + if session.id not in self._sessions: + raise KeyError(f"Session {session.id} not found") + self._sessions[session.id] = session + return session + + def delete(self, session_id: str) -> None: + with self._lock: + self._sessions.pop(session_id, None) + for owner_sessions in self._owners.values(): + owner_sessions.discard(session_id) From a81cb580a02c53f9dad8b1273d3581795810c132 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:40:40 +0000 Subject: [PATCH 06/35] missing commit --- .../data-layer-sessions-api-shapes.jsonc | 3 +- proposals/data-layer-sessions.md | 227 ++++++++++++------ 2 files changed, 151 insertions(+), 79 deletions(-) diff --git a/proposals/data-layer-sessions-api-shapes.jsonc b/proposals/data-layer-sessions-api-shapes.jsonc index d26aa0e..3de5894 100644 --- a/proposals/data-layer-sessions-api-shapes.jsonc +++ b/proposals/data-layer-sessions-api-shapes.jsonc @@ -11,7 +11,6 @@ "capabilities": { "experimental": { "session": { - "version": 1, "features": ["create", "resume", "delete"] } } @@ -150,7 +149,7 @@ "jsonrpc": "2.0", "id": 5, "error": { - "code": -32002, + "code": -32043, "message": "Session required. Call session/create or session/resume first." } } diff --git a/proposals/data-layer-sessions.md b/proposals/data-layer-sessions.md index 2964c6c..d6de79a 100644 --- a/proposals/data-layer-sessions.md +++ b/proposals/data-layer-sessions.md @@ -1,8 +1,8 @@ # Data-Layer Sessions for MCP -> **Status:** Strawman +> **Status:** Early Draft > **Date:** 2026-02-23 -> **Track:** Sessions +> **Track:** transport-wg/sessions > **Author(s):** Shaun Smith ## Purpose @@ -14,23 +14,53 @@ something concrete to react to. The core problem: MCP currently ties session identity to the transport connection (`Mcp-Session-Id` header for Streamable HTTP, implicit for stdio). -When a connection drops, application state is lost. Servers that need -multi-turn state — scratch-pads, sandboxes, conversation context — have no -standard way to offer it. + +The proposal is to introduce a session concept within the MCP Data Layer, +using a lightweight _cookie_ style mechanism. > **See also:** [`data-layer-sessions-api-shapes.jsonc`](data-layer-sessions-api-shapes.jsonc) > — flat quick-reference of all wire shapes. +## Reference Packages (for review) + +To make discussion concrete, this proposal folder includes two small +Python reference packages that implement the data-layer session model: + +- [`data-layer-sessions-client-python/`](data-layer-sessions-client-python/) — + client-side cookie jar + request/response `_meta` handling. +- [`data-layer-sessions-server-python/`](data-layer-sessions-server-python/) — + server-side session issuer + `session/create`, `session/resume`, + `session/delete` handler registration. + +These are intentionally compact and self-contained so reviewers can inspect +implementation behavior alongside wire shapes. + +A simple Client/Server reference implementation is available. + ## Design Principles 1. **Transport-agnostic.** Works identically over stdio and HTTP. -2. **Server-authoritative.** The server issues, updates, and revokes session - tokens. The client echoes them. (Adapted cookie semantics per RFC 6265.) -3. **Opt-in.** Sessions are discovered via capability negotiation during +2. **Server-authoritative lifecycle, flexible payload ownership.** The server + issues, updates, accepts/rejects, and revokes session tokens. The client + echoes them. Session `data` may be server-defined, client-carried, or a + hybrid, depending on application policy. (Adapted cookie semantics per + RFC 6265.) +2. **Opt-in.** Sessions are discovered via capability negotiation during `initialize`. Servers that don't need sessions don't advertise them. -4. **Incremental.** A server can require sessions globally, per-tool, or not +2. **Incremental.** A server can require sessions globally, per-tool, or not at all. +## Phase Scope + +To keep this proposal straightforward for initial review, this draft splits +session functionality into two phases: + +- **Phase 1 (in scope for this draft):** `session/create`, `session/resume`, + `session/delete`, and cookie echo/revocation semantics. +- **Phase 2 (deferred):** `session/list` and `session/recover` semantics. + +We can evaluate the core lifecycle first, then expand into +recovery/discovery workflows if we think necessary. ## Use Cases @@ -49,26 +79,35 @@ sessions are immediately useful: - Example: `public_echo` remains open; `session_counter_inc` requires a valid session cookie. -3. **Session-scoped stateful tools** - - Tools maintain per-session notebooks/KV data across multiple calls. +2. **Session-scoped stateful tools** + - Tools maintain per-session state across multiple calls, either in + server-side storage or in cookie-carried `data` payloads. - Example: notebook append/read/clear and hash KV verify workflows. -4. **Reconnect + resume semantics (same cookie, new transport)** +2. **Client-carried user preferences (lightweight state transfer)** + - Clients can carry non-sensitive, low-volume preferences in session + `data`, and servers can apply them without additional lookup calls. + - Typical examples: `language`, `timezone`, display format preferences. + +2. **Reconnect + resume semantics (same cookie, new transport)** - Client disconnects/reconnects and resumes server state by reusing `mcp/session` cookie. - This is the core value beyond transport-local `Mcp-Session-Id`. -5. **Session revocation + re-establishment** +2. **Session revocation + re-establishment** - Server revokes cookie (`mcp/session = null`); client clears local cookie and can create/select a new session. -6. **Operator-driven session control** +2. **Operator-driven session control** - Runtime operators can create/resume/select/clear sessions explicitly (e.g., for debugging, incident response, or workflow recovery). These use cases suggest sessions are not only a transport concern; they are a practical application-layer primitive needed for real tool orchestration. +When using client-carried state in `data`, implementations should treat it as +advisory input unless explicitly trusted by policy. + ## Capability Advertisement During `initialize`, a server that supports sessions includes an @@ -80,7 +119,6 @@ During `initialize`, a server that supports sessions includes an "capabilities": { "experimental": { "session": { - "version": 1, "features": ["create", "resume", "delete"] } } @@ -91,20 +129,10 @@ During `initialize`, a server that supports sessions includes an `features` lists the `session/*` methods the server supports. A minimal server might only support `["create"]`. -**Open question:** Should `version` be a single integer, or should this -use the spec's existing versioning approach? - -## Phase Scope - -To keep this proposal straightforward for initial review, this draft splits -session functionality into two phases: - -- **Phase 1 (in scope for this draft):** `session/create`, `session/resume`, - `session/delete`, and cookie echo/revocation semantics. -- **Phase 2 (deferred):** `session/list` and `session/recover` semantics. - -This lets the group evaluate the core lifecycle first, then expand into -recovery/discovery workflows once the base contract is agreed. +No per-capability `version` field is included — no existing MCP capability +uses one. Versioning is handled at the protocol level via `protocolVersion` +during `initialize`. If the session capability shape needs breaking changes +in the future, those would be gated on a new protocol version. ## Session Lifecycle Methods @@ -255,6 +283,19 @@ response. ### Session Cookie: Placement +> **Implementation note:** The reference packages included with this +> proposal (`data-layer-sessions-client-python/`, +> `data-layer-sessions-server-python/`) are **overlay libraries** that +> layer on top of an unmodified MCP Python SDK. They inject and extract +> `_meta["mcp/session"]` by hand, without requiring any SDK changes. +> This is deliberate — it allows reviewers to evaluate the wire-level +> behaviour immediately, without gating on SDK or schema modifications. +> +> If this proposal progresses to a first-class spec feature, the +> expectation is that the session cookie would migrate from a convention +> key to a **named property** on `RequestMetaObject` and `MetaObject`, +> with typed support in the SDKs (see Path A below). + The cookie echo mechanism uses `_meta` to carry session state across all request/response pairs. This raises a design question about how the cookie key is defined. @@ -269,16 +310,18 @@ Both approaches are schema-legal. The trade-offs: | Approach | Pro | Con | |---|---|---| | **A: Named property** — add `session` to `RequestMetaObject` and `MetaObject` schema definitions, like `progressToken` | Schema-validatable; typed in SDKs; consistent with `progressToken` precedent | Requires schema changes; tighter coupling to spec release cycle | -| **B: Convention key** — use `"mcp/session"` as an opaque key in the `_meta` bag (current proposal) | No schema changes needed; works immediately as `experimental`; extensible | No schema validation; novel use of the extensibility bag for a protocol-level concept | +| **B: Convention key** — use `"mcp/session"` as an opaque key in the `_meta` bag (current proposal + demos) | No schema changes needed; works immediately as `experimental`; extensible; demos can run on stock SDK | No schema validation; novel use of the extensibility bag for a protocol-level concept | The `mcp/` prefix is **reserved for MCP spec use** per the `MetaObject` naming rules — so `"mcp/session"` is valid as a spec-defined key. Third parties MUST NOT define keys under the `mcp/` prefix. -**Open question:** If this moves from `experimental` to a first-class spec -feature, should the cookie become a named property (Path A)? Or is the -convention-key approach (Path B) sufficient given that `_meta` is explicitly -designed as an extensibility point? +**Recommended path:** Start with **Path B** (convention key under +`experimental`) for prototyping and interoperability testing, then promote +to **Path A** (named schema property) when the feature moves from +experimental to first-class. The reference packages are structured to make +this migration straightforward — the `_meta` injection/extraction is +isolated in `model.py` in both client and server packages. ### Revocation via `null` @@ -313,27 +356,47 @@ JSON-RPC error: "jsonrpc": "2.0", "id": 5, "error": { - "code": -32002, + "code": -32043, "message": "Session required. Call session/create or session/resume first." } } ``` -The code `-32002` is in the JSON-RPC server-defined range (`-32000` to -`-32099`). MCP already uses `-32042` for `URL_ELICITATION_REQUIRED`. If -adopted, this would need a named constant (e.g. `SESSION_REQUIRED`). +### Error Code Selection -**Open question:** Is `-32002` the right code? Should the error carry -structured `data` (like `URLElicitationRequiredError` does with its -`elicitations` array)? +The code `-32043` is in the JSON-RPC implementation-defined server error +range (`-32000` to `-32099`). The following codes in this range are already +allocated or claimed in the MCP ecosystem: + +| Code | Name | Where | Crosses wire? | +|---|---|---|---| +| `-32000` | `CONNECTION_CLOSED` | Python SDK | No (SDK-internal) | +| `-32001` | `REQUEST_TIMEOUT` | Python SDK, TS SDK | No (SDK-internal) | +| `-32002` | Resource not found | Spec docs (`server/resources.mdx`) | Yes | +| `-32042` | `URL_ELICITATION_REQUIRED` | Schema (`schema.ts`), both SDKs | Yes (formal) | + +The `-3204x` neighbourhood is used for **protocol-level conditions +requiring structured client action** (URL elicitation, session +establishment). This contrasts with `-3200x` which the SDKs have +informally claimed for internal transport/connection conditions, and +`-32002` which the spec docs already use for resource-not-found errors. + +If adopted, `-32043` would be defined as a named constant +(e.g. `SESSION_REQUIRED`) in `schema.ts` alongside +`URL_ELICITATION_REQUIRED`, and propagated to both SDKs. + +**Open question:** Should the error carry structured `data` (like +`URLElicitationRequiredError` does with its `elicitations` array)? +For example, the error `data` could include available session features +or a hint about which method(s) to call. ## Selective Enforcement Servers MAY require sessions for all tools, some tools, or no tools. The mechanism for advertising which tools require sessions is left open: -- Option A: A `sessionRequired` field in tool metadata. -- Option B: Servers just return `-32002` and clients react. +- Option A: Servers just return `-32043` and clients react. +- Option B: A `sessionRequired` field in tool metadata. - Option C: A server-level policy declaration in capabilities. **Open question:** Which approach (or combination) best serves both @@ -341,7 +404,7 @@ human developers and LLM-driven tool selection? ## Interaction with Transport-Level Sessions -Streamable HTTP already has `Mcp-Session-Id` for transport routing. This +Streamable HTTP currently has `Mcp-Session-Id` for transport routing. This proposal operates at a different layer: | Concern | Transport (`Mcp-Session-Id`) | Data-layer (`mcp/session`) | @@ -351,39 +414,43 @@ proposal operates at a different layer: | Survives reconnect | No | Yes | | Works over stdio | N/A | Yes | -The two are complementary. A load balancer can route on `Mcp-Session-Id` -while the application maintains state via `mcp/session`. - -**Open question:** Should `mcp/session` be mirrored into an HTTP header -for routing affinity? If so, what are the size constraints? - -## Schema Compatibility Notes - -This proposal was reviewed against the MCP draft schema -(`schema/draft/schema.json`, DRAFT-2026-v1). Key compatibility points: - -- **Method naming** follows `{namespace}/{verb}` (`session/create`, - `session/resume`, `session/delete`), consistent with `tools/call`, - `resources/read`, `tasks/cancel`. -- **`experimental` capability** is `Record` with - `additionalProperties: true` — the proposed shape is valid. -- **`_meta` on requests** (`RequestMetaObject`) and **results** - (`MetaObject`) are both open objects — arbitrary keys are allowed. -- **`Result`** has `"additionalProperties": {}` — custom fields like - `deleted`, `id`, `expiry` are valid. -- **Phase 2 methods** (`session/list`, `session/recover`) are intentionally - deferred in this draft for scope control. -- **If formalized**, each method would need the standard 4-definition tuple - (`*Request`, `*RequestParams`, `*Result`, `*ResultResponse`) and - registration in the `ClientRequest` / `ServerResult` union types. - +### Trajectory: Data-Layer Sessions Supersede Transport Sessions + +As MCP moves toward stateless transports, the transport-level +`Mcp-Session-Id` increasingly functions as a **routing hint** rather than +a session identity. This proposal's data-layer session ID is the natural +replacement for application-level session semantics. + +The intended evolution: + +1. **Today:** `Mcp-Session-Id` is both a routing key and a (fragile) + session identity. Losing the transport connection loses the session. +2. **With this proposal:** `mcp/session` carries durable session identity + in the JSON-RPC payload. `Mcp-Session-Id` is demoted to a + transport-routing concern only. +2. **Future:** The data-layer session ID is mirrored into an HTTP header + (e.g. `Mcp-Session-Id` itself, or a new `Mcp-Session` header) so that + load balancers and proxies can route on it without body parsing — + following the pattern established by + **[SEP-2243: HTTP Header Standardization](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)**. + +SEP-2243 defines the mechanism for surfacing JSON-RPC payload fields as +HTTP headers (`Mcp-Method`, `Mcp-Tool-Name`, etc.) and includes validation +rules for header/body consistency. The data-layer session ID is a natural +candidate for the same treatment: the client would include the session ID +both in `_meta["mcp/session"]` and in an HTTP header, enabling +infrastructure routing without deep packet inspection. + +**Open question:** Should the header reuse `Mcp-Session-Id` (replacing +the transport meaning) or introduce a new header name (e.g. +`Mcp-Data-Session`) to avoid ambiguity during the transition? ## Implementation-Informed Considerations Early implementation work suggests the following considerations (non-normative): -- **Capability/version gating works in practice.** Clients can ignore unknown - session versions and continue normal MCP operation. +- **Capability gating works in practice.** Clients can ignore unknown + experimental capabilities and continue normal MCP operation. - **Auto-create + explicit controls both matter.** Automatic `session/create` supports low-friction startup, while explicit controls (`create/resume/delete/clear`) support operator workflows and debugging. @@ -401,10 +468,12 @@ practical guidance for SEP scope and interoperability testing. ## Open Questions Summary -1. **Versioning** — integer in capability vs. spec-level versioning? -2. **Error code** — `-32002` or a named constant? Structured `data` payload? -3. **Selective enforcement** — how should servers declare per-tool requirements? -4. **HTTP header mirroring** — should `mcp/session` also appear as a header? +1. **Error code** — `-32043` (proposed) or a different code? Formal error code registry needed? Structured `data` payload? +2. **Selective enforcement** — how should servers declare per-tool requirements? +3. **HTTP header mirroring** — should `mcp/session` also appear as a header? +4. **SEP-2243 alignment** — should the data-layer session ID be mirrored + into an HTTP header following the SEP-2243 pattern? If so, reuse + `Mcp-Session-Id` or new header name? 5. **Cookie size** — what constraints on the `data` field? 6. **Security** — signing/encryption of session tokens? Server-side only vs. client-verifiable? @@ -413,7 +482,7 @@ practical guidance for SEP scope and interoperability testing. 8. **Relationship to MRTR** — how does this interact with the multi-round-trip requests track's need for state passthrough? 9. **Cookie placement** — named `_meta` property (like `progressToken`) or - convention key (`"mcp/session"`)? See [Placement](#session-cookie-placement). + convention key (`"mcp/session"`)? See [Placement](#session-cookie-placement). 10. **Revocation signal** — `null` value, key absence, or dedicated method? See [Revocation](#revocation-via-null). 11. **Client persistence semantics** — should local cookie jars / resume behavior @@ -424,6 +493,10 @@ practical guidance for SEP scope and interoperability testing. - **RFC 6265** (HTTP Cookies) — foundation for cookie semantics - **Sessions Track Brief** (this repo) — working group discussion context - **MRTR Track Brief** (this repo) — overlapping state-passthrough needs +- **[SEP-2243: HTTP Header Standardization](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)** — + defines the pattern for mirroring JSON-RPC fields into HTTP headers for + infrastructure routing; directly relevant for surfacing session IDs to + load balancers - **`fast-agent` experimental sessions** — working prototype of this design over both stdio and Streamable HTTP transports, including jar-based resume and operator session controls From 8f4b0b85a11c718f9b1d4bf6be4c1cca83ecf360 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:26:46 +0000 Subject: [PATCH 07/35] shapes: remove id/expiry duplication from session/create and session/resume result bodies --- .../data-layer-sessions-api-shapes.jsonc | 12 +++--- proposals/data-layer-sessions.md | 39 +++++++++++++------ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/proposals/data-layer-sessions-api-shapes.jsonc b/proposals/data-layer-sessions-api-shapes.jsonc index 3de5894..0d99d85 100644 --- a/proposals/data-layer-sessions-api-shapes.jsonc +++ b/proposals/data-layer-sessions-api-shapes.jsonc @@ -2,6 +2,10 @@ // Companion to: proposals/data-layer-sessions.md // Status: Strawman / Discussion Starter // Scope: Phase 1 only (create, resume, delete). Phase 2 (list, recover) deferred. +// +// NOTE: id and expiry are NOT duplicated in the result body. +// The SDK (or overlay library in the experiment phase) always reads id/expiry +// from _meta.mcp/session. The result body carries only the app data payload (data). // ============================================================ // 1. Capability Advertisement (in InitializeResult) @@ -34,13 +38,11 @@ } } -// Response — session object in result body, cookie in _meta +// Response — SDK/overlay surfaces id+expiry from _meta.mcp/session; result body carries app data only { "jsonrpc": "2.0", "id": 1, "result": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z", "data": { "title": "Code Review Session" }, "_meta": { "mcp/session": { @@ -63,13 +65,11 @@ "params": { "id": "sess-a1b2c3d4e5f6" } } -// Response +// Response — SDK/overlay surfaces id+expiry from _meta.mcp/session; result body carries app data only { "jsonrpc": "2.0", "id": 2, "result": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z", "data": { "title": "Code Review Session" }, "_meta": { "mcp/session": { diff --git a/proposals/data-layer-sessions.md b/proposals/data-layer-sessions.md index d6de79a..807a931 100644 --- a/proposals/data-layer-sessions.md +++ b/proposals/data-layer-sessions.md @@ -138,10 +138,10 @@ in the future, those would be gated on a new protocol version. ### `session/create` -The `session/create` result returns the session object in the result body -(like any other method result) **and** sets the cookie in `_meta` for the -echo cycle. See [Session Cookie: Placement](#session-cookie-placement) for -the design discussion on where the cookie lives. +The `session/create` result sets the session cookie in `_meta.mcp/session`. +The SDK (or overlay library in the experiment phase) surfaces `id` and +`expiry` to callers from there. See [Session Cookie: Placement](#session-cookie-placement) +for the design discussion on where the cookie lives. ```jsonc // Client → Server @@ -162,8 +162,6 @@ the design discussion on where the cookie lives. "jsonrpc": "2.0", "id": 1, "result": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z", "data": { "title": "Code Review Session" }, "_meta": { "mcp/session": { @@ -175,10 +173,11 @@ the design discussion on where the cookie lives. } ``` -The result body contains the full session object (for the client to inspect). -The `_meta` cookie contains the opaque token the client echoes on subsequent -requests — the server controls what goes in the cookie and may include less -data than the result body. +The result body contains the application data payload (`data`). The session +`id` and `expiry` are carried exclusively in `_meta.mcp/session` — the SDK +(or overlay library in the experiment phase) surfaces these to callers. There +is no duplication: `id`/`expiry` are not repeated in the top-level result +body. ### `session/resume` @@ -201,8 +200,6 @@ canonical cookie payload for subsequent echo. "jsonrpc": "2.0", "id": 2, "result": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z", "data": { "title": "Code Review Session" }, "_meta": { "mcp/session": { @@ -214,6 +211,12 @@ canonical cookie payload for subsequent echo. } ``` +The result body contains the application data payload (`data`). The session +`id` and `expiry` are carried exclusively in `_meta.mcp/session` — the SDK +(or overlay library in the experiment phase) surfaces these to callers. There +is no duplication: `id`/`expiry` are not repeated in the top-level result +body. + If the requested session cannot be resumed, the server SHOULD return an error (e.g., session not found / invalid / expired). @@ -323,6 +326,11 @@ experimental to first-class. The reference packages are structured to make this migration straightforward — the `_meta` injection/extraction is isolated in `model.py` in both client and server packages. +In both Path A and Path B, the result body shape is identical: `id`/`expiry` +live exclusively in `_meta.mcp/session`, and `data` carries the app payload. +The paths differ only in how the SDK exposes these fields to callers — via +typed accessors (Path A) or via direct dict access (Path B). + ### Revocation via `null` A server revokes a session by returning `"mcp/session": null`: @@ -462,6 +470,13 @@ Early implementation work suggests the following considerations (non-normative): avoids repeatedly selecting known-bad session IDs during resume. - **Expiry is advisory unless enforced.** Demo servers stamp `expiry` metadata, but enforcement policy remains server-defined. +- **`_meta.mcp/session` is the sole source of truth for `id`/`expiry`.** + Earlier prototype code included a fallback that read `id` directly from the + `result` body if `_meta.mcp/session` was absent. This fallback is + **removed from the target design**. Servers MUST populate `_meta.mcp/session` + on `session/create` and `session/resume` responses. The SDK (or overlay + library) reads `id` and `expiry` exclusively from `_meta.mcp/session`; the + result body carries only `data`. These considerations do **not** lock in protocol choices; they provide practical guidance for SEP scope and interoperability testing. From 7e1c6709883888c443ec80893a5c2bf13811c147 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:28:49 +0000 Subject: [PATCH 08/35] client: remove result-body fallback in process_create_or_resume_result --- .../src/mcp_sessions_client/client.py | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 proposals/data-layer-sessions-client-python/src/mcp_sessions_client/client.py diff --git a/proposals/data-layer-sessions-client-python/src/mcp_sessions_client/client.py b/proposals/data-layer-sessions-client-python/src/mcp_sessions_client/client.py new file mode 100644 index 0000000..9d4c804 --- /dev/null +++ b/proposals/data-layer-sessions-client-python/src/mcp_sessions_client/client.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import logging +from typing import Any + +from .model import ( + Session, + extract_session_capability, + extract_session_from_meta, + inject_session_into_meta, + session_capability, +) + +logger = logging.getLogger(__name__) + + +class SessionClient: + """In-memory cookie jar for data-layer sessions.""" + + def __init__(self) -> None: + self._session: Session | None = None + self._server_has_sessions = False + self._server_has_create = False + self._server_has_resume = False + self._server_has_delete = False + + @property + def session(self) -> Session | None: + return self._session + + def get_experimental_capabilities(self) -> dict[str, dict[str, Any]]: + return session_capability() + + def check_server_capabilities( + self, experimental: dict[str, dict[str, Any]] | None + ) -> bool: + cap = extract_session_capability(experimental) + self._server_has_sessions = cap is not None + features = cap.get("features", []) if cap else [] + self._server_has_create = "create" in features + self._server_has_resume = "resume" in features + self._server_has_delete = "delete" in features + return self._server_has_sessions + + def prepare_request_meta( + self, existing_meta: dict[str, Any] | None = None + ) -> dict[str, Any]: + if self._session is None: + return dict(existing_meta) if existing_meta else {} + return inject_session_into_meta(self._session, existing_meta) + + def process_response_meta(self, meta: dict[str, Any] | None) -> Session | None: + extracted = extract_session_from_meta(meta) + if extracted is False: + logger.info("Session revoked by server") + self._session = None + elif extracted is not None: + self._session = extracted + return self._session + + def build_create_request_params( + self, + label: str | None = None, + data: dict[str, str] | None = None, + ) -> dict[str, Any]: + hints: dict[str, Any] = {} + if label is not None: + hints["label"] = label + if data is not None: + hints["data"] = data + return {"hints": hints} if hints else {} + + def build_resume_request_params(self, session_id: str) -> dict[str, Any]: + return {"id": session_id} + + def build_delete_request_params(self, session_id: str | None = None) -> dict[str, Any]: + sid = session_id or (self._session.id if self._session else None) + if sid is None: + raise ValueError("No session_id provided and no session in jar") + return {"id": sid} + + def process_create_or_resume_result(self, result: dict[str, Any]) -> Session: + meta = result.get("_meta") or result.get("meta") + session = self.process_response_meta(meta) + if session is None: + raise ValueError( + "session/create or session/resume response is missing _meta.mcp/session. " + "Servers MUST populate _meta.mcp/session on these responses." + ) + return session + + def process_delete_result(self, result: dict[str, Any]) -> bool: + meta = result.get("_meta") or result.get("meta") + self.process_response_meta(meta) + return bool(result.get("deleted", False)) + + def set_session(self, session: Session | None) -> None: + self._session = session From fffe114221d8769612f1bea918bbf269c1a7e90a Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:40:21 +0000 Subject: [PATCH 09/35] shapes: add client-carried state model; move data into cookie; add 5b echo example --- .../data-layer-sessions-api-shapes.jsonc | 75 +++++++++-- proposals/data-layer-sessions.md | 124 +++++++++++++++--- 2 files changed, 171 insertions(+), 28 deletions(-) diff --git a/proposals/data-layer-sessions-api-shapes.jsonc b/proposals/data-layer-sessions-api-shapes.jsonc index 0d99d85..20f9ed0 100644 --- a/proposals/data-layer-sessions-api-shapes.jsonc +++ b/proposals/data-layer-sessions-api-shapes.jsonc @@ -3,9 +3,17 @@ // Status: Strawman / Discussion Starter // Scope: Phase 1 only (create, resume, delete). Phase 2 (list, recover) deferred. // -// NOTE: id and expiry are NOT duplicated in the result body. -// The SDK (or overlay library in the experiment phase) always reads id/expiry -// from _meta.mcp/session. The result body carries only the app data payload (data). +// NOTE: id, expiry, and data all live in _meta.mcp/session (the "cookie"). +// The result body is minimal — it carries only _meta. The SDK (or overlay +// library in the experiment phase) surfaces id, expiry, and data to callers +// from _meta.mcp/session. +// +// Two storage models differ in what the server does with inbound data: +// - Server-authoritative: server reads state from its own store keyed by id; +// client echoes carry only id; data is absent from client echoes. +// - Client-carried: server has no store for this data; it reads state from +// inbound _meta.mcp/session.data, mutates it, and returns the updated +// cookie. The cookie IS the state. // ============================================================ // 1. Capability Advertisement (in InitializeResult) @@ -38,16 +46,16 @@ } } -// Response — SDK/overlay surfaces id+expiry from _meta.mcp/session; result body carries app data only +// Response — cookie carries id, expiry, and data; SDK surfaces all three; result body is minimal { "jsonrpc": "2.0", "id": 1, "result": { - "data": { "title": "Code Review Session" }, "_meta": { "mcp/session": { "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z" + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review Session" } } } } @@ -65,16 +73,16 @@ "params": { "id": "sess-a1b2c3d4e5f6" } } -// Response — SDK/overlay surfaces id+expiry from _meta.mcp/session; result body carries app data only +// Response — cookie carries id, expiry, and data; SDK surfaces all three; result body is minimal { "jsonrpc": "2.0", "id": 2, "result": { - "data": { "title": "Code Review Session" }, "_meta": { "mcp/session": { "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z" + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review Session" } } } } @@ -108,6 +116,11 @@ // 5. Cookie Echo (on any request/response) // ============================================================ +// 5a. Server-authoritative echo +// Client echoes only the session id. Server looks up state from its own store. +// _meta.mcp/session.data is absent from client echoes — state lives server-side only. + +// Request { "jsonrpc": "2.0", "id": 4, @@ -121,6 +134,7 @@ } } +// Response { "jsonrpc": "2.0", "id": 4, @@ -135,6 +149,47 @@ } } +// 5b. Client-carried echo (stateless server — no server-side store for this data) +// Client echoes the full cookie including data. Server reconstructs state from +// inbound data, mutates it, and returns the updated cookie. The server never +// looks up a store — the cookie IS the state. +// +// Example: hash KV store where hashes are carried in data["hashes"]. + +// Request — client echoes full cookie including data from previous response +{ + "jsonrpc": "2.0", + "id": 5, + "method": "tools/call", + "params": { + "name": "hashcheck_store", + "arguments": { "key": "password", "text": "hunter2" }, + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T15:00:00Z", + "data": { "hashes": "{\"password\":\"abc123...\"}" } + } + } + } +} + +// Response — server updated data["hashes"] in-place; returns updated cookie +{ + "jsonrpc": "2.0", + "id": 5, + "result": { + "content": [{ "type": "text", "text": "Stored sha256('password') = def456..." }], + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T15:00:00Z", + "data": { "hashes": "{\"password\":\"def456...\"}" } + } + } + } +} + // ============================================================ // 6. Revocation (server-initiated) // ============================================================ @@ -147,7 +202,7 @@ { "jsonrpc": "2.0", - "id": 5, + "id": 6, "error": { "code": -32043, "message": "Session required. Call session/create or session/resume first." diff --git a/proposals/data-layer-sessions.md b/proposals/data-layer-sessions.md index 807a931..e0f2912 100644 --- a/proposals/data-layer-sessions.md +++ b/proposals/data-layer-sessions.md @@ -162,22 +162,21 @@ for the design discussion on where the cookie lives. "jsonrpc": "2.0", "id": 1, "result": { - "data": { "title": "Code Review Session" }, "_meta": { "mcp/session": { "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z" + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review Session" } } } } } ``` -The result body contains the application data payload (`data`). The session -`id` and `expiry` are carried exclusively in `_meta.mcp/session` — the SDK -(or overlay library in the experiment phase) surfaces these to callers. There -is no duplication: `id`/`expiry` are not repeated in the top-level result -body. +The session cookie (`_meta.mcp/session`) carries `id`, `expiry`, and `data` +together. The SDK (or overlay library in the experiment phase) surfaces all +three to callers. The result body is minimal — it contains only `_meta`. No +session fields appear in the top-level result body outside `_meta`. ### `session/resume` @@ -200,22 +199,21 @@ canonical cookie payload for subsequent echo. "jsonrpc": "2.0", "id": 2, "result": { - "data": { "title": "Code Review Session" }, "_meta": { "mcp/session": { "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z" + "expiry": "2026-02-23T14:30:00Z", + "data": { "title": "Code Review Session" } } } } } ``` -The result body contains the application data payload (`data`). The session -`id` and `expiry` are carried exclusively in `_meta.mcp/session` — the SDK -(or overlay library in the experiment phase) surfaces these to callers. There -is no duplication: `id`/`expiry` are not repeated in the top-level result -body. +The session cookie (`_meta.mcp/session`) carries `id`, `expiry`, and `data` +together. The SDK (or overlay library in the experiment phase) surfaces all +three to callers. The result body is minimal — it contains only `_meta`. No +session fields appear in the top-level result body outside `_meta`. If the requested session cannot be resumed, the server SHOULD return an error (e.g., session not found / invalid / expired). @@ -249,7 +247,14 @@ on using `null` as a signal. Once a session is established, the client includes the session cookie in `_meta` on every request. The server echoes (or updates) it in every -response. +response. The exact fields echoed depend on the **storage model** in use — +see [Session Storage Models](#session-storage-models) below. + +### 5a. Server-Authoritative Echo + +The client echoes only `id`. The server looks up state from its own store +and refreshes `expiry` in the response. `data` is absent from client echoes +— state lives server-side only. ```jsonc // Client → Server (tools/call with session) @@ -261,9 +266,7 @@ response. "name": "notebook_append", "arguments": { "text": "remember this" }, "_meta": { - "mcp/session": { - "id": "sess-a1b2c3d4e5f6" - } + "mcp/session": { "id": "sess-a1b2c3d4e5f6" } } } } @@ -284,6 +287,91 @@ response. } ``` +### 5b. Client-Carried Echo + +The client echoes the full cookie including `data`. The server reconstructs +state from inbound `data`, mutates it, and returns the updated cookie in the +response. The server never looks up a store — the cookie IS the state. + +```jsonc +// Client → Server — client echoes full cookie including data from previous response +{ + "jsonrpc": "2.0", + "id": 5, + "method": "tools/call", + "params": { + "name": "hashcheck_store", + "arguments": { "key": "password", "text": "hunter2" }, + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T15:00:00Z", + "data": { "hashes": "{"password":"abc123..."}" } + } + } + } +} + +// Server → Client — server updated data["hashes"] in-place; returns updated cookie +{ + "jsonrpc": "2.0", + "id": 5, + "result": { + "content": [{ "type": "text", "text": "Stored sha256('password') = def456..." }], + "_meta": { + "mcp/session": { + "id": "sess-a1b2c3d4e5f6", + "expiry": "2026-02-23T15:00:00Z", + "data": { "hashes": "{"password":"def456..."}" } + } + } + } +} +``` + +## Session Storage Models + +Two storage models are supported. They differ in where session state lives +and what the server does with the inbound `_meta.mcp/session.data` field. + +### Server-Authoritative + +The server stores state in its own store, keyed by session `id`. Inbound +client cookies carry only `id` (used as a lookup key). The server writes +`data` into the outbound cookie for informational purposes, but ignores any +`data` present in inbound cookies. + +- **State lives:** server-side store +- **Client echoes:** `{ id }` +- **Server reads:** store lookup by `id`; ignores inbound `data` +- **Server writes:** `{ id, expiry }` (and optionally `data` for + informational mirroring) +- **Statefulness requirement:** server must be stateful (or share a store + across replicas) +- **Trust:** `data` is server-controlled; client cannot forge meaningful state + +### Client-Carried + +The server has no store for this session's data. It reads state from the +inbound `_meta.mcp/session.data`, operates on it, and writes the updated +state back into `_meta.mcp/session.data` in the response. The cookie IS the +state — the server is effectively stateless with respect to this data. + +- **State lives:** inside the cookie, carried by the client +- **Client echoes:** `{ id, expiry, data }` (full cookie from last response) +- **Server reads:** inbound `data` directly — no store lookup +- **Server writes:** `{ id, expiry, data }` with mutated `data` +- **Statefulness requirement:** none — works with a fully stateless HTTP + server or serverless function +- **Trust implication:** the client can forge `data`. This is appropriate + when the client only affects themselves (e.g. a local hash KV store for + verification), not for sensitive or shared state. Implementations SHOULD + sign or encrypt `data` if tamper-resistance is required. + +> **Quick reference:** See the `.jsonc` companion +> ([`data-layer-sessions-api-shapes.jsonc`](data-layer-sessions-api-shapes.jsonc)) +> sections 5a and 5b for abbreviated wire shapes of each model. + ### Session Cookie: Placement > **Implementation note:** The reference packages included with this From 79598d2973eb4638aa00673d869213d99529c616 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:05:34 +0000 Subject: [PATCH 10/35] update --- proposals/0000-data-layer-sessions.md | 75 +++++++++++++++++++ .../data-layer-sessions-api-shapes.jsonc | 36 ++------- 2 files changed, 80 insertions(+), 31 deletions(-) create mode 100644 proposals/0000-data-layer-sessions.md diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md new file mode 100644 index 0000000..bbeead0 --- /dev/null +++ b/proposals/0000-data-layer-sessions.md @@ -0,0 +1,75 @@ +# Data-Layer Sessions for MCP + +> **Status:** Early Draft +> **Date:** 2026-02-23 +> **Track:** transport-wg/sessions +> **Author(s):** Shaun Smith + + + +## Abstract + +## Motivation + + +## Specification + +### Capabilities + +Clients and Servers that support Sessions expose the `sessions` capability. + + +### session/create + +request + +``` +{title} +``` + +response + +``` +{session id} +//hint +{expiry date} +{opaque value} +``` + +The Client SHOULD retain + +The expiry date is a hint. Can be refreshed `servers/discovery`. + + +{label} + +### session/delete + + +### request/* + +_meta may contain + +### response/* + +If the Session is not resumable + +## Rationale + +### HTTP Cookies vs. Custom Implementation + +To support non HTTP transports, an MCP Data Layer proposal has been selected. + +### Use + +## Backward Compatibility + +### Existing MCP Servers + +Some MCP Servers use SessionID for analytics (HF, GH). This usage is no longer . To associate tool calls + +### Session Guidance + +It is expected that + + diff --git a/proposals/data-layer-sessions-api-shapes.jsonc b/proposals/data-layer-sessions-api-shapes.jsonc index 20f9ed0..fb60c77 100644 --- a/proposals/data-layer-sessions-api-shapes.jsonc +++ b/proposals/data-layer-sessions-api-shapes.jsonc @@ -23,7 +23,7 @@ "capabilities": { "experimental": { "session": { - "features": ["create", "resume", "delete"] + "features": ["create", "delete"] } } } @@ -61,37 +61,11 @@ } } -// ============================================================ -// 3. session/resume -// ============================================================ - -// Request -{ - "jsonrpc": "2.0", - "id": 2, - "method": "session/resume", - "params": { "id": "sess-a1b2c3d4e5f6" } -} - -// Response — cookie carries id, expiry, and data; SDK surfaces all three; result body is minimal -{ - "jsonrpc": "2.0", - "id": 2, - "result": { - "_meta": { - "mcp/session": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z", - "data": { "title": "Code Review Session" } - } - } - } -} // If the session cannot be resumed, server returns an error. // ============================================================ -// 4. session/delete +// 3. session/delete // ============================================================ // Request @@ -113,10 +87,10 @@ } // ============================================================ -// 5. Cookie Echo (on any request/response) +// 4. Cookie Echo (on any request/response) // ============================================================ -// 5a. Server-authoritative echo +// 4a. Server-authoritative echo // Client echoes only the session id. Server looks up state from its own store. // _meta.mcp/session.data is absent from client echoes — state lives server-side only. @@ -149,7 +123,7 @@ } } -// 5b. Client-carried echo (stateless server — no server-side store for this data) +// 4b. Client-carried echo (stateless server — no server-side store for this data) // Client echoes the full cookie including data. Server reconstructs state from // inbound data, mutates it, and returns the updated cookie. The server never // looks up a store — the cookie IS the state. From 1bedee0a245f1b8fc26df638989518cd88043218 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:14:44 +0000 Subject: [PATCH 11/35] resume not needed for stateless :) --- proposals/0000-data-layer-sessions.md | 12 +++++++--- proposals/data-layer-sessions.md | 33 +-------------------------- 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index bbeead0..7eff0ff 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -5,8 +5,6 @@ > **Track:** transport-wg/sessions > **Author(s):** Shaun Smith - - ## Abstract ## Motivation @@ -45,6 +43,7 @@ The expiry date is a hint. Can be refreshed `servers/discovery`. ### session/delete +The Client SHOULD delete sessions where resources aren't required. ### request/* @@ -54,13 +53,20 @@ _meta may contain If the Session is not resumable + +### Tool Annotation + + + ## Rationale ### HTTP Cookies vs. Custom Implementation To support non HTTP transports, an MCP Data Layer proposal has been selected. -### Use +### Use of in-band Tool Call ID + +A ## Backward Compatibility diff --git a/proposals/data-layer-sessions.md b/proposals/data-layer-sessions.md index e0f2912..ce08f5e 100644 --- a/proposals/data-layer-sessions.md +++ b/proposals/data-layer-sessions.md @@ -119,7 +119,7 @@ During `initialize`, a server that supports sessions includes an "capabilities": { "experimental": { "session": { - "features": ["create", "resume", "delete"] + "features": ["create", "delete"] } } } @@ -178,37 +178,6 @@ together. The SDK (or overlay library in the experiment phase) surfaces all three to callers. The result body is minimal — it contains only `_meta`. No session fields appear in the top-level result body outside `_meta`. -### `session/resume` - -`session/resume` re-activates an existing session by ID and returns the -canonical cookie payload for subsequent echo. - -```jsonc -// Client → Server -{ - "jsonrpc": "2.0", - "id": 2, - "method": "session/resume", - "params": { - "id": "sess-a1b2c3d4e5f6" - } -} - -// Server → Client -{ - "jsonrpc": "2.0", - "id": 2, - "result": { - "_meta": { - "mcp/session": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z", - "data": { "title": "Code Review Session" } - } - } - } -} -``` The session cookie (`_meta.mcp/session`) carries `id`, `expiry`, and `data` together. The SDK (or overlay library in the experiment phase) surfaces all From 7a30022de3f45017317010a24ffe4e460c6822d4 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:36:58 +0000 Subject: [PATCH 12/35] interim commit --- proposals/0000-data-layer-sessions.md | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 7eff0ff..77a5994 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -7,18 +7,30 @@ ## Abstract +This proposal introduces a session concept within the MCP Data Layer, +using a lightweight _cookie_ style mechanism. This allows applications to + ## Motivation +MCP Sessions are currently either implicit (STDIO), or constructed as a side effect of the transport connection (Streamable HTTP). + +It is assumed (but not required) that Host applications rather than the LLM are responsible for Session management. ## Specification ### Capabilities -Clients and Servers that support Sessions expose the `sessions` capability. +MCP Servers that support sessions advertise a `sessions` capability, indicating that they support `session/create`, `session/delete` and associated request and response semantics. + +> For testing purposes, MCP Clients that support sessions use an `experimental/sessions` capability to simplify testing. ### session/create +Clients can begin a session with an MCP Server by calling `session/create`, optionally supplying a `title` for the session. The Server responds either by emitting a "cookie" style structure or returning an Error if session creation is not possible. + +The Error message **SHOULD** be descriptive of the reason for failure. + request ``` @@ -34,10 +46,16 @@ response {opaque value} ``` -The Client SHOULD retain +The Client SHOULD associate retained cookies with the issuing Server . + The expiry date is a hint. Can be refreshed `servers/discovery`. +- The session ID SHOULD be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash). +- The session ID MUST only contain visible ASCII characters (ranging from 0x21 to 0x7E). +- The client MUST handle the session ID in a secure manner, see Session Hijacking mitigations for more details. (TODO -- update this as data layer/stdio mitigations are different) + +The Session ID {label} @@ -66,7 +84,8 @@ To support non HTTP transports, an MCP Data Layer proposal has been selected. ### Use of in-band Tool Call ID -A +Session IDs are considered to be controlled by the Host application, rather than the Model - driving the design that identifiers are not revealed in tool calls etc. + ## Backward Compatibility From c65d0284e88127031d93810dcd5ce1f4711d4a3c Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:51:42 +0000 Subject: [PATCH 13/35] interim commit --- proposals/0000-data-layer-sessions.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 77a5994..bac3fdd 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -55,7 +55,6 @@ The expiry date is a hint. Can be refreshed `servers/discovery`. - The session ID MUST only contain visible ASCII characters (ranging from 0x21 to 0x7E). - The client MUST handle the session ID in a secure manner, see Session Hijacking mitigations for more details. (TODO -- update this as data layer/stdio mitigations are different) -The Session ID {label} @@ -65,11 +64,21 @@ The Client SHOULD delete sessions where resources aren't required. ### request/* +Any request can have a cookie contained within the _meta block. + +Clients SHOULD NOT send cookies to Servers that do not support the `session` capability. + +Clients MUST only send cookies to the Server that issued them. + +Servers SHOULD send an Error -34043 `Session not found` if the session is not recognized or valid. Clients SHOULD invalidate the Session. + + _meta may contain ### response/* -If the Session is not resumable +A response to a request containing a cookie MUST respond with a cookie that contains the same SessionID. + ### Tool Annotation From b176eec15f4b5f8ea680a1566bdee944dd5aef5e Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:14:03 +0000 Subject: [PATCH 14/35] for merge --- proposals/0000-data-layer-sessions.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 7eff0ff..b4538d9 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -9,6 +9,7 @@ ## Motivation +To allow state to be managed, typically at a ## Specification @@ -34,9 +35,11 @@ response {opaque value} ``` -The Client SHOULD retain -The expiry date is a hint. Can be refreshed `servers/discovery`. +The `expiry date` is a hint that allows the Client to indicate that a conversation is stale. Can be refreshed `servers/discovery`. + +KV Store. + - Expensive resource allocations {label} @@ -56,8 +59,6 @@ If the Session is not resumable ### Tool Annotation - - ## Rationale ### HTTP Cookies vs. Custom Implementation @@ -66,7 +67,7 @@ To support non HTTP transports, an MCP Data Layer proposal has been selected. ### Use of in-band Tool Call ID -A +It is possible for MCP Servers to simulate sessions by supplying a tool that generates an Id, and includes signatures ## Backward Compatibility From 86c53ddd3c89303fdfe46b7338492acdc7078d7e Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:19:49 +0000 Subject: [PATCH 15/35] interim --- proposals/0000-data-layer-sessions.md | 270 ++++++++++++++++++++++++-- 1 file changed, 251 insertions(+), 19 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index bac3fdd..fee6632 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -7,8 +7,9 @@ ## Abstract -This proposal introduces a session concept within the MCP Data Layer, -using a lightweight _cookie_ style mechanism. This allows applications to +This proposal introduces application level sessions within the MCP Data Layer. Sessions are created by the Client, and allow the Server to store an opaque state token. + +[Further context to be added] ## Motivation @@ -18,36 +19,225 @@ It is assumed (but not required) that Host applications rather than the LLM are ## Specification -### Capabilities +### User Interaction Model -MCP Servers that support sessions advertise a `sessions` capability, indicating that they support `session/create`, `session/delete` and associated request and response semantics. +Sessions are designed to be **application-driven**, with host applications determining how to establish sessions based on their need. -> For testing purposes, MCP Clients that support sessions use an `experimental/sessions` capability to simplify testing. +It is normally expected that applications will establish one session per conversation thread or task, but this is not required. +### Capabilities -### session/create +Servers that support sessions MUST declare the `sessions` capability: -Clients can begin a session with an MCP Server by calling `session/create`, optionally supplying a `title` for the session. The Server responds either by emitting a "cookie" style structure or returning an Error if session creation is not possible. +```json +{ + "capabilities": { + "sessions" : {} + } +} +``` -The Error message **SHOULD** be descriptive of the reason for failure. +> For testing purposes MCP Clients that support sessions declare an `experimental/sessions` capability to simplify testing. + +### Schema + +Session association metadata uses _meta["io.modelcontextprotocol/session"] with value type SessionMetadata. + +```ts + /** + * Describes an MCP Session. + * Include this in the `_meta` field under the key `io.modelcontextprotocol/session`. + */ +export interface SessionMetadata { + /** + * The session identifier. + */ + sessionId: string; + + /** + * ISO 8601 timestamp hint for session expiry + */ + expiresAt?: string; + + /** + * Opaque server-issued session state token. + * Clients MUST treat this value as opaque and MUST NOT inspect or modify it. + */ + state?: string; +} +``` -request +Sessions are created and deleted via `sessions/create` and `sessions/delete` requests: + +```json + /** + * A request to create a new session. + * + * @category `sessions/create` + */ + export interface CreateSessionRequest extends JSONRPCRequest { + method: "sessions/create"; + params?: RequestParams; + } + + /** + * The result returned by the server for a {@link CreateSessionRequest | sessions/create} request. + * + * @category `sessions/create` + */ + export interface CreateSessionResult extends Result { + session: SessionMetadata; + } + + /** + * A successful response from the server for a {@link CreateSessionRequest | sessions/create} request. + * + * @category `sessions/create` + */ + export interface CreateSessionResultResponse extends JSONRPCResultResponse { + result: CreateSessionResult; + } + + /** + * A request to delete an existing session. + * + * @category `sessions/delete` + */ + export interface DeleteSessionRequest extends JSONRPCRequest { + method: "sessions/delete"; + params?: RequestParams; + } + + /** + * A successful response from the server for a {@link DeleteSessionRequest | sessions/delete} request. + * + * @category `sessions/delete` + */ + export interface DeleteSessionResultResponse extends JSONRPCResultResponse { + result: EmptyResult; + } +``` +Clients use Sessions by including the SessionMetadata in `io.modelcontextprotocol/session` in _meta of the Request. + +When a requested Session is unknown by the Server it returns a `-32043 SESSION_NOT_FOUND` Error. + +```ts + /** @internal */ + export const SESSION_NOT_FOUND = -32043; + + /** + * An error response indicating that the supplied session does not exist. + * + * @example Session not found + * {@includeCode ./examples/SessionNotFoundError/session-not-found.json} + * + * @category Errors + */ + export interface SessionNotFoundError extends Omit { + error: Error & { + code: typeof SESSION_NOT_FOUND; + data: { + /** + * The session identifier provided by the caller. + */ + sessionId: string; + [key: string]: unknown; + }; + }; + } ``` -{title} + + +### Protocol Messages + + +#### Creating Sessions + +Clients begin a session with an MCP Server by calling `sessions/create`. + +**Request:** + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "sessions/create", +} ``` -response +**Response**: + +```json + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "session": { + "sessionId": "sess-a1b2c3d4e5f6", + "expiresAt": "2026-02-27T15:30:00Z" + }, + "_meta": { + "io.modelcontextprotocol/session": { + "sessionId": "sess-a1b2c3d4e5f6", + "expiresAt": "2026-02-27T15:30:00Z" + } + } + } + } ``` -{session id} -//hint -{expiry date} -{opaque value} + +The Client **MUST NOT** send `io.modelcontextprotocol/session` data with the sessions/create request. + +#### Deleting Sessions + +**Request:** + +```json + { + "jsonrpc": "2.0", + "id": 2, + "method": "sessions/delete", + "params": { + "_meta": { + "io.modelcontextprotocol/session": { + "sessionId": "sess-a1b2c3d4e5f6" + } + } + } + } ``` -The Client SHOULD associate retained cookies with the issuing Server . +**Response:** + +```json + { + "jsonrpc": "2.0", + "id": 2, + "result": {} + } +``` + + +Clients **SHOULD** delete sessions that are no longer required to allow the Server to reclaim unneeded resources. + +#### Using Sessions + +When a request includes `_meta["io.modelcontextprotocol/session"]`: + 1 The receiver MUST treat that sessionId as the session context for processing the request. + 2 Succesful response MUST include _meta["io.modelcontextprotocol/session"]. + 3 The sessionId in the response MUST exactly match the sessionId from the request. + 4 The receiver MUST NOT substitute, rotate, or rewrite sessionId in the response. + 5 If the supplied session is unknown, expired, or not accessible, the receiver MUST return an error (recommended: InvalidParams) and still include the same session metadata in _meta. + + +Clients MAY send a Session Id send requests with + +The Error message **SHOULD** be descriptive of the reason for failure. + +The Client SHOULD associate retained cookies with the issuing Server . The expiry date is a hint. Can be refreshed `servers/discovery`. @@ -58,9 +248,43 @@ The expiry date is a hint. Can be refreshed `servers/discovery`. {label} -### session/delete -The Client SHOULD delete sessions where resources aren't required. + { + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "search_code", + "arguments": { + "query": "SessionMetadata" + }, + "_meta": { + "io.modelcontextprotocol/session": { + "sessionId": "sess-a1b2c3d4e5f6" + } + } + } + } + +{ +"jsonrpc": "2.0", +"id": 3, +"result": { + "content": [ + { + "type": "text", + "text": "Found 3 matches." + } + ], + "_meta": { + "io.modelcontextprotocol/session": { + "sessionId": "sess-a1b2c3d4e5f6", + "expiresAt": "2026-02-27T15:30:00Z" + } + } +} +} + ### request/* @@ -83,6 +307,11 @@ A response to a request containing a cookie MUST respond with a cookie that cont ### Tool Annotation +An `allocatesResource` Tool Hint is proposed to indicate to the Client that a + + +## Other Work + ## Rationale @@ -95,12 +324,15 @@ To support non HTTP transports, an MCP Data Layer proposal has been selected. Session IDs are considered to be controlled by the Host application, rather than the Model - driving the design that identifiers are not revealed in tool calls etc. +### Use of single `state` value + +A single opaque "state" value mirrors the MRTR design, and reduces the chance of KV merge errors, and keeps client behaviour simple (echo bytes back). + ## Backward Compatibility ### Existing MCP Servers -Some MCP Servers use SessionID for analytics (HF, GH). This usage is no longer . To associate tool calls ### Session Guidance From 57480051c5b31e045f4909a0706e4c84e77d68a0 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:20:12 +0000 Subject: [PATCH 16/35] interim (replace) --- .../data-layer-sessions-api-shapes.jsonc | 184 ------ .../src/mcp_sessions_client/client.py | 98 --- .../README.md | 25 - .../pyproject.toml | 22 - .../src/mcp_sessions_server/__init__.py | 52 -- .../src/mcp_sessions_server/model.py | 80 --- .../src/mcp_sessions_server/protocol.py | 61 -- .../src/mcp_sessions_server/server.py | 88 --- .../mcp_sessions_server/server_handlers.py | 66 -- .../src/mcp_sessions_server/store.py | 68 --- proposals/data-layer-sessions.md | 574 ------------------ 11 files changed, 1318 deletions(-) delete mode 100644 proposals/data-layer-sessions-api-shapes.jsonc delete mode 100644 proposals/data-layer-sessions-client-python/src/mcp_sessions_client/client.py delete mode 100644 proposals/data-layer-sessions-server-python/README.md delete mode 100644 proposals/data-layer-sessions-server-python/pyproject.toml delete mode 100644 proposals/data-layer-sessions-server-python/src/mcp_sessions_server/__init__.py delete mode 100644 proposals/data-layer-sessions-server-python/src/mcp_sessions_server/model.py delete mode 100644 proposals/data-layer-sessions-server-python/src/mcp_sessions_server/protocol.py delete mode 100644 proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server.py delete mode 100644 proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server_handlers.py delete mode 100644 proposals/data-layer-sessions-server-python/src/mcp_sessions_server/store.py delete mode 100644 proposals/data-layer-sessions.md diff --git a/proposals/data-layer-sessions-api-shapes.jsonc b/proposals/data-layer-sessions-api-shapes.jsonc deleted file mode 100644 index fb60c77..0000000 --- a/proposals/data-layer-sessions-api-shapes.jsonc +++ /dev/null @@ -1,184 +0,0 @@ -// Data-Layer Sessions — API Shape Quick Reference -// Companion to: proposals/data-layer-sessions.md -// Status: Strawman / Discussion Starter -// Scope: Phase 1 only (create, resume, delete). Phase 2 (list, recover) deferred. -// -// NOTE: id, expiry, and data all live in _meta.mcp/session (the "cookie"). -// The result body is minimal — it carries only _meta. The SDK (or overlay -// library in the experiment phase) surfaces id, expiry, and data to callers -// from _meta.mcp/session. -// -// Two storage models differ in what the server does with inbound data: -// - Server-authoritative: server reads state from its own store keyed by id; -// client echoes carry only id; data is absent from client echoes. -// - Client-carried: server has no store for this data; it reads state from -// inbound _meta.mcp/session.data, mutates it, and returns the updated -// cookie. The cookie IS the state. - -// ============================================================ -// 1. Capability Advertisement (in InitializeResult) -// ============================================================ - -{ - "capabilities": { - "experimental": { - "session": { - "features": ["create", "delete"] - } - } - } -} - -// ============================================================ -// 2. session/create -// ============================================================ - -// Request -{ - "jsonrpc": "2.0", - "id": 1, - "method": "session/create", - "params": { - "hints": { - "label": "my-agent-workspace", - "data": { "title": "Code Review Session" } - } - } -} - -// Response — cookie carries id, expiry, and data; SDK surfaces all three; result body is minimal -{ - "jsonrpc": "2.0", - "id": 1, - "result": { - "_meta": { - "mcp/session": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z", - "data": { "title": "Code Review Session" } - } - } - } -} - - -// If the session cannot be resumed, server returns an error. - -// ============================================================ -// 3. session/delete -// ============================================================ - -// Request -{ - "jsonrpc": "2.0", - "id": 3, - "method": "session/delete", - "params": { "id": "sess-a1b2c3d4e5f6" } -} - -// Response — null cookie = revoked -{ - "jsonrpc": "2.0", - "id": 3, - "result": { - "deleted": true, - "_meta": { "mcp/session": null } - } -} - -// ============================================================ -// 4. Cookie Echo (on any request/response) -// ============================================================ - -// 4a. Server-authoritative echo -// Client echoes only the session id. Server looks up state from its own store. -// _meta.mcp/session.data is absent from client echoes — state lives server-side only. - -// Request -{ - "jsonrpc": "2.0", - "id": 4, - "method": "tools/call", - "params": { - "name": "notebook_append", - "arguments": { "text": "remember this" }, - "_meta": { - "mcp/session": { "id": "sess-a1b2c3d4e5f6" } - } - } -} - -// Response -{ - "jsonrpc": "2.0", - "id": 4, - "result": { - "content": [{ "type": "text", "text": "appended" }], - "_meta": { - "mcp/session": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T15:00:00Z" - } - } - } -} - -// 4b. Client-carried echo (stateless server — no server-side store for this data) -// Client echoes the full cookie including data. Server reconstructs state from -// inbound data, mutates it, and returns the updated cookie. The server never -// looks up a store — the cookie IS the state. -// -// Example: hash KV store where hashes are carried in data["hashes"]. - -// Request — client echoes full cookie including data from previous response -{ - "jsonrpc": "2.0", - "id": 5, - "method": "tools/call", - "params": { - "name": "hashcheck_store", - "arguments": { "key": "password", "text": "hunter2" }, - "_meta": { - "mcp/session": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T15:00:00Z", - "data": { "hashes": "{\"password\":\"abc123...\"}" } - } - } - } -} - -// Response — server updated data["hashes"] in-place; returns updated cookie -{ - "jsonrpc": "2.0", - "id": 5, - "result": { - "content": [{ "type": "text", "text": "Stored sha256('password') = def456..." }], - "_meta": { - "mcp/session": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T15:00:00Z", - "data": { "hashes": "{\"password\":\"def456...\"}" } - } - } - } -} - -// ============================================================ -// 6. Revocation (server-initiated) -// ============================================================ - -{ "_meta": { "mcp/session": null } } - -// ============================================================ -// 7. Session-required error -// ============================================================ - -{ - "jsonrpc": "2.0", - "id": 6, - "error": { - "code": -32043, - "message": "Session required. Call session/create or session/resume first." - } -} diff --git a/proposals/data-layer-sessions-client-python/src/mcp_sessions_client/client.py b/proposals/data-layer-sessions-client-python/src/mcp_sessions_client/client.py deleted file mode 100644 index 9d4c804..0000000 --- a/proposals/data-layer-sessions-client-python/src/mcp_sessions_client/client.py +++ /dev/null @@ -1,98 +0,0 @@ -from __future__ import annotations - -import logging -from typing import Any - -from .model import ( - Session, - extract_session_capability, - extract_session_from_meta, - inject_session_into_meta, - session_capability, -) - -logger = logging.getLogger(__name__) - - -class SessionClient: - """In-memory cookie jar for data-layer sessions.""" - - def __init__(self) -> None: - self._session: Session | None = None - self._server_has_sessions = False - self._server_has_create = False - self._server_has_resume = False - self._server_has_delete = False - - @property - def session(self) -> Session | None: - return self._session - - def get_experimental_capabilities(self) -> dict[str, dict[str, Any]]: - return session_capability() - - def check_server_capabilities( - self, experimental: dict[str, dict[str, Any]] | None - ) -> bool: - cap = extract_session_capability(experimental) - self._server_has_sessions = cap is not None - features = cap.get("features", []) if cap else [] - self._server_has_create = "create" in features - self._server_has_resume = "resume" in features - self._server_has_delete = "delete" in features - return self._server_has_sessions - - def prepare_request_meta( - self, existing_meta: dict[str, Any] | None = None - ) -> dict[str, Any]: - if self._session is None: - return dict(existing_meta) if existing_meta else {} - return inject_session_into_meta(self._session, existing_meta) - - def process_response_meta(self, meta: dict[str, Any] | None) -> Session | None: - extracted = extract_session_from_meta(meta) - if extracted is False: - logger.info("Session revoked by server") - self._session = None - elif extracted is not None: - self._session = extracted - return self._session - - def build_create_request_params( - self, - label: str | None = None, - data: dict[str, str] | None = None, - ) -> dict[str, Any]: - hints: dict[str, Any] = {} - if label is not None: - hints["label"] = label - if data is not None: - hints["data"] = data - return {"hints": hints} if hints else {} - - def build_resume_request_params(self, session_id: str) -> dict[str, Any]: - return {"id": session_id} - - def build_delete_request_params(self, session_id: str | None = None) -> dict[str, Any]: - sid = session_id or (self._session.id if self._session else None) - if sid is None: - raise ValueError("No session_id provided and no session in jar") - return {"id": sid} - - def process_create_or_resume_result(self, result: dict[str, Any]) -> Session: - meta = result.get("_meta") or result.get("meta") - session = self.process_response_meta(meta) - if session is None: - raise ValueError( - "session/create or session/resume response is missing _meta.mcp/session. " - "Servers MUST populate _meta.mcp/session on these responses." - ) - return session - - def process_delete_result(self, result: dict[str, Any]) -> bool: - meta = result.get("_meta") or result.get("meta") - self.process_response_meta(meta) - return bool(result.get("deleted", False)) - - def set_session(self, session: Session | None) -> None: - self._session = session diff --git a/proposals/data-layer-sessions-server-python/README.md b/proposals/data-layer-sessions-server-python/README.md deleted file mode 100644 index ca88249..0000000 --- a/proposals/data-layer-sessions-server-python/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# MCP Data-Layer Sessions (Server Reference Package) - -Small, self-contained reference package for the **server-side** of the -experimental MCP data-layer sessions proposal. - -This package demonstrates: - -- capability advertisement (`experimental.session`) -- extracting/validating incoming `_meta["mcp/session"]` -- issuing and revoking session cookies via response `_meta` -- explicit lifecycle handlers (`session/create`, `session/resume`, `session/delete`) - -It is intended for proposal review and inspection, not production use. - -## Layout - -```text -src/mcp_sessions_server/ - __init__.py - model.py - store.py - server.py - protocol.py - server_handlers.py -``` diff --git a/proposals/data-layer-sessions-server-python/pyproject.toml b/proposals/data-layer-sessions-server-python/pyproject.toml deleted file mode 100644 index 78ac68c..0000000 --- a/proposals/data-layer-sessions-server-python/pyproject.toml +++ /dev/null @@ -1,22 +0,0 @@ -[build-system] -requires = ["hatchling>=1.27.0"] -build-backend = "hatchling.build" - -[project] -name = "mcp-data-layer-sessions-server-ref" -version = "0.1.0" -description = "Reference server package for MCP data-layer sessions proposal" -readme = "README.md" -requires-python = ">=3.10" -license = { text = "MIT" } -authors = [{ name = "MCP Transports WG Contributors" }] -keywords = ["mcp", "sessions", "proposal", "reference"] -classifiers = [ - "Development Status :: 3 - Alpha", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", -] -dependencies = ["mcp>=0.1.0"] - -[tool.hatch.build.targets.wheel] -packages = ["src/mcp_sessions_server"] diff --git a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/__init__.py b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/__init__.py deleted file mode 100644 index c7ee390..0000000 --- a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Reference server package for MCP data-layer sessions proposal.""" - -from .model import ( - SESSION_EXPERIMENTAL_KEY, - SESSION_META_KEY, - Session, - extract_session_capability, - extract_session_from_meta, - generate_session_id, - inject_session_into_meta, - session_capability, -) -from .protocol import ( - SessionCreateHints, - SessionCreateParams, - SessionCreateRequest, - SessionCreateResult, - SessionDeleteParams, - SessionDeleteRequest, - SessionDeleteResult, - SessionResumeParams, - SessionResumeRequest, - SessionResumeResult, -) -from .server import SessionServer -from .server_handlers import register_session_handlers -from .store import InMemorySessionStore, SessionStore - -__all__ = [ - "Session", - "SessionServer", - "SessionStore", - "InMemorySessionStore", - "register_session_handlers", - "SessionCreateRequest", - "SessionCreateParams", - "SessionCreateHints", - "SessionCreateResult", - "SessionResumeRequest", - "SessionResumeParams", - "SessionResumeResult", - "SessionDeleteRequest", - "SessionDeleteParams", - "SessionDeleteResult", - "generate_session_id", - "session_capability", - "extract_session_capability", - "inject_session_into_meta", - "extract_session_from_meta", - "SESSION_META_KEY", - "SESSION_EXPERIMENTAL_KEY", -] diff --git a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/model.py b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/model.py deleted file mode 100644 index d893df6..0000000 --- a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/model.py +++ /dev/null @@ -1,80 +0,0 @@ -from __future__ import annotations - -import secrets -from dataclasses import dataclass, field -from datetime import datetime, timezone -from typing import Any - -SESSION_META_KEY = "mcp/session" -SESSION_EXPERIMENTAL_KEY = "session" - -@dataclass -class Session: - id: str - expiry: str | None = None - data: dict[str, str] = field(default_factory=dict) - - def to_dict(self) -> dict[str, Any]: - result: dict[str, Any] = {"id": self.id} - if self.expiry is not None: - result["expiry"] = self.expiry - if self.data: - result["data"] = dict(self.data) - return result - - @classmethod - def from_dict(cls, d: dict[str, Any]) -> Session: - return cls(id=d["id"], expiry=d.get("expiry"), data=d.get("data", {})) - - def is_expired(self) -> bool: - if self.expiry is None: - return False - try: - expiry_dt = datetime.fromisoformat(self.expiry) - now = datetime.now(timezone.utc) - if expiry_dt.tzinfo is None: - expiry_dt = expiry_dt.replace(tzinfo=timezone.utc) - return now > expiry_dt - except ValueError: - return False - - -def generate_session_id(prefix: str = "sess-") -> str: - return f"{prefix}{secrets.token_hex(8)}" - - -def session_capability(features: list[str] | None = None) -> dict[str, dict[str, Any]]: - cap: dict[str, Any] = {} - if features: - cap["features"] = features - return {SESSION_EXPERIMENTAL_KEY: cap} - - -def extract_session_capability( - experimental: dict[str, dict[str, Any]] | None, -) -> dict[str, Any] | None: - if experimental is None: - return None - cap = experimental.get(SESSION_EXPERIMENTAL_KEY) - if cap is None: - return None - return cap - - -def inject_session_into_meta( - session: Session | None, existing_meta: dict[str, Any] | None = None -) -> dict[str, Any]: - meta = dict(existing_meta) if existing_meta else {} - meta[SESSION_META_KEY] = None if session is None else session.to_dict() - return meta - - -def extract_session_from_meta(meta: dict[str, Any] | None) -> Session | None | bool: - if meta is None: - return None - if SESSION_META_KEY not in meta: - return None - value = meta[SESSION_META_KEY] - if value is None: - return False - return Session.from_dict(value) diff --git a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/protocol.py b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/protocol.py deleted file mode 100644 index b028473..0000000 --- a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/protocol.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import annotations - -from typing import Literal - -from mcp.types import RequestParams, Result - -try: - from mcp.types._types import MCPModel # type: ignore[attr-defined] -except Exception: - try: - from mcp.types import MCPModel # type: ignore[attr-defined] - except Exception: - from pydantic import BaseModel as MCPModel - - -class SessionCreateHints(MCPModel): - label: str | None = None - data: dict[str, str] | None = None - - -class SessionCreateParams(RequestParams): - hints: SessionCreateHints | None = None - - -class SessionCreateRequest(MCPModel): - method: Literal["session/create"] = "session/create" - params: SessionCreateParams | None = None - - -class SessionCreateResult(Result): - id: str - expiry: str | None = None - data: dict[str, str] | None = None - - -class SessionResumeParams(RequestParams): - id: str - - -class SessionResumeRequest(MCPModel): - method: Literal["session/resume"] = "session/resume" - params: SessionResumeParams - - -class SessionResumeResult(Result): - id: str - expiry: str | None = None - data: dict[str, str] | None = None - - -class SessionDeleteParams(RequestParams): - id: str - - -class SessionDeleteRequest(MCPModel): - method: Literal["session/delete"] = "session/delete" - params: SessionDeleteParams - - -class SessionDeleteResult(Result): - deleted: bool diff --git a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server.py b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server.py deleted file mode 100644 index e594032..0000000 --- a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server.py +++ /dev/null @@ -1,88 +0,0 @@ -from __future__ import annotations - -import logging -from typing import Any - -from .model import ( - Session, - extract_session_capability, - extract_session_from_meta, - inject_session_into_meta, - session_capability, -) -from .store import InMemorySessionStore, SessionStore - -logger = logging.getLogger(__name__) - - -class SessionServer: - def __init__( - self, - store: SessionStore | None = None, - features: list[str] | None = None, - default_ttl_seconds: int = 3600, - ) -> None: - self._store = store or InMemorySessionStore() - self._features = features or ["create", "resume", "delete"] - self._default_ttl = default_ttl_seconds - - @property - def store(self) -> SessionStore: - return self._store - - def get_experimental_capabilities(self) -> dict[str, dict[str, Any]]: - return session_capability(self._features if self._features else None) - - def client_supports_sessions( - self, experimental: dict[str, dict[str, Any]] | None - ) -> bool: - return extract_session_capability(experimental) is not None - - def extract_request_session(self, meta: dict[str, Any] | None) -> Session | None: - extracted = extract_session_from_meta(meta) - if extracted is False or extracted is None: - return None - stored = self._store.get(extracted.id) - if stored is None: - logger.info("Rejected unknown/expired session: %s", extracted.id) - return None - return stored - - def create_session( - self, - owner: str | None = None, - data: dict[str, str] | None = None, - ttl_seconds: int | None = None, - ) -> Session: - ttl = ttl_seconds if ttl_seconds is not None else self._default_ttl - return self._store.create(owner=owner, data=data, ttl_seconds=ttl) - - def resume_session(self, session_id: str) -> Session | None: - return self._store.get(session_id) - - def prepare_response_meta( - self, - session: Session, - existing_meta: dict[str, Any] | None = None, - ) -> dict[str, Any]: - return inject_session_into_meta(session, existing_meta) - - def prepare_revocation_meta( - self, - session_id: str | None = None, - existing_meta: dict[str, Any] | None = None, - ) -> dict[str, Any]: - if session_id is not None: - self._store.delete(session_id) - return inject_session_into_meta(None, existing_meta) - - def get_or_create_session( - self, - meta: dict[str, Any] | None, - owner: str | None = None, - default_data: dict[str, str] | None = None, - ) -> tuple[Session, bool]: - session = self.extract_request_session(meta) - if session is not None: - return session, False - return self.create_session(owner=owner, data=default_data), True diff --git a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server_handlers.py b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server_handlers.py deleted file mode 100644 index 3758fa2..0000000 --- a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/server_handlers.py +++ /dev/null @@ -1,66 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable - -from mcp.server.lowlevel.server import Server - -from .model import inject_session_into_meta -from .protocol import ( - SessionCreateRequest, - SessionCreateResult, - SessionDeleteRequest, - SessionDeleteResult, - SessionResumeRequest, - SessionResumeResult, -) -from .server import SessionServer - - -def register_session_handlers( - low_level_server: Server, - session_server: SessionServer, - get_owner: Callable[..., str | None] | None = None, -) -> None: - async def handle_session_create(req: SessionCreateRequest) -> SessionCreateResult: - owner = get_owner() if get_owner else None - hints = req.params.hints if req.params and req.params.hints else None - data = dict(hints.data) if hints and hints.data else {} - if hints and hints.label: - data.setdefault("label", hints.label) - - session = session_server.create_session(owner=owner, data=data if data else None) - meta = inject_session_into_meta(session) - return SessionCreateResult( - id=session.id, - expiry=session.expiry, - data=session.data if session.data else None, - **{"_meta": meta}, - ) - - low_level_server.request_handlers[SessionCreateRequest] = handle_session_create - - async def handle_session_resume(req: SessionResumeRequest) -> SessionResumeResult: - session = session_server.resume_session(req.params.id) - if session is None: - raise ValueError(f"Session not found: {req.params.id}") - meta = inject_session_into_meta(session) - return SessionResumeResult( - id=session.id, - expiry=session.expiry, - data=session.data if session.data else None, - **{"_meta": meta}, - ) - - low_level_server.request_handlers[SessionResumeRequest] = handle_session_resume - - async def handle_session_delete(req: SessionDeleteRequest) -> SessionDeleteResult: - existing = session_server.store.get(req.params.id) - if existing is not None: - session_server.store.delete(req.params.id) - deleted = True - else: - deleted = False - meta = inject_session_into_meta(None) - return SessionDeleteResult(deleted=deleted, **{"_meta": meta}) - - low_level_server.request_handlers[SessionDeleteRequest] = handle_session_delete diff --git a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/store.py b/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/store.py deleted file mode 100644 index 6a02ce5..0000000 --- a/proposals/data-layer-sessions-server-python/src/mcp_sessions_server/store.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -import threading -from datetime import datetime, timedelta, timezone -from typing import Protocol, runtime_checkable - -from .model import Session, generate_session_id - - -@runtime_checkable -class SessionStore(Protocol): - def create( - self, - owner: str | None = None, - data: dict[str, str] | None = None, - ttl_seconds: int = 3600, - ) -> Session: ... - - def get(self, session_id: str) -> Session | None: ... - - def update(self, session: Session) -> Session: ... - - def delete(self, session_id: str) -> None: ... - - -class InMemorySessionStore: - def __init__(self) -> None: - self._sessions: dict[str, Session] = {} - self._owners: dict[str, set[str]] = {} - self._lock = threading.Lock() - - def create( - self, - owner: str | None = None, - data: dict[str, str] | None = None, - ttl_seconds: int = 3600, - ) -> Session: - session_id = generate_session_id() - expiry = (datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)).isoformat() - session = Session(id=session_id, expiry=expiry, data=data or {}) - with self._lock: - self._sessions[session_id] = session - if owner is not None: - self._owners.setdefault(owner, set()).add(session_id) - return session - - def get(self, session_id: str) -> Session | None: - with self._lock: - session = self._sessions.get(session_id) - if session is None: - return None - if session.is_expired(): - self.delete(session_id) - return None - return session - - def update(self, session: Session) -> Session: - with self._lock: - if session.id not in self._sessions: - raise KeyError(f"Session {session.id} not found") - self._sessions[session.id] = session - return session - - def delete(self, session_id: str) -> None: - with self._lock: - self._sessions.pop(session_id, None) - for owner_sessions in self._owners.values(): - owner_sessions.discard(session_id) diff --git a/proposals/data-layer-sessions.md b/proposals/data-layer-sessions.md deleted file mode 100644 index ce08f5e..0000000 --- a/proposals/data-layer-sessions.md +++ /dev/null @@ -1,574 +0,0 @@ -# Data-Layer Sessions for MCP - -> **Status:** Early Draft -> **Date:** 2026-02-23 -> **Track:** transport-wg/sessions -> **Author(s):** Shaun Smith - -## Purpose - -This is a **discussion starter**, not a finished design. It proposes a -minimal set of JSON-RPC API shapes for application-level sessions in MCP, -decoupled from the transport layer. The goal is to give the working group -something concrete to react to. - -The core problem: MCP currently ties session identity to the transport -connection (`Mcp-Session-Id` header for Streamable HTTP, implicit for stdio). - -The proposal is to introduce a session concept within the MCP Data Layer, -using a lightweight _cookie_ style mechanism. - -> **See also:** [`data-layer-sessions-api-shapes.jsonc`](data-layer-sessions-api-shapes.jsonc) -> — flat quick-reference of all wire shapes. - -## Reference Packages (for review) - -To make discussion concrete, this proposal folder includes two small -Python reference packages that implement the data-layer session model: - -- [`data-layer-sessions-client-python/`](data-layer-sessions-client-python/) — - client-side cookie jar + request/response `_meta` handling. -- [`data-layer-sessions-server-python/`](data-layer-sessions-server-python/) — - server-side session issuer + `session/create`, `session/resume`, - `session/delete` handler registration. - -These are intentionally compact and self-contained so reviewers can inspect -implementation behavior alongside wire shapes. - -A simple Client/Server reference implementation is available. - -## Design Principles - -1. **Transport-agnostic.** Works identically over stdio and HTTP. -2. **Server-authoritative lifecycle, flexible payload ownership.** The server - issues, updates, accepts/rejects, and revokes session tokens. The client - echoes them. Session `data` may be server-defined, client-carried, or a - hybrid, depending on application policy. (Adapted cookie semantics per - RFC 6265.) -2. **Opt-in.** Sessions are discovered via capability negotiation during - `initialize`. Servers that don't need sessions don't advertise them. -2. **Incremental.** A server can require sessions globally, per-tool, or not - at all. - -## Phase Scope - -To keep this proposal straightforward for initial review, this draft splits -session functionality into two phases: - -- **Phase 1 (in scope for this draft):** `session/create`, `session/resume`, - `session/delete`, and cookie echo/revocation semantics. -- **Phase 2 (deferred):** `session/list` and `session/recover` semantics. - -We can evaluate the core lifecycle first, then expand into -recovery/discovery workflows if we think necessary. - -## Use Cases - -The following are concrete scenarios from experimental client/server -integrations (including `fast-agent` + demo MCP servers) where data-layer -sessions are immediately useful: - -1. **Global session gatekeeping** - - Some servers require session establishment before any tool call. - - Example: policy-enforced systems that need an explicit server-issued - identity before tool execution. - -2. **Selective per-tool session policy** - - Public tools can run without a session, while stateful/sensitive tools - require one. - - Example: `public_echo` remains open; `session_counter_inc` requires a - valid session cookie. - -2. **Session-scoped stateful tools** - - Tools maintain per-session state across multiple calls, either in - server-side storage or in cookie-carried `data` payloads. - - Example: notebook append/read/clear and hash KV verify workflows. - -2. **Client-carried user preferences (lightweight state transfer)** - - Clients can carry non-sensitive, low-volume preferences in session - `data`, and servers can apply them without additional lookup calls. - - Typical examples: `language`, `timezone`, display format preferences. - -2. **Reconnect + resume semantics (same cookie, new transport)** - - Client disconnects/reconnects and resumes server state by reusing - `mcp/session` cookie. - - This is the core value beyond transport-local `Mcp-Session-Id`. - -2. **Session revocation + re-establishment** - - Server revokes cookie (`mcp/session = null`); client clears local cookie - and can create/select a new session. - -2. **Operator-driven session control** - - Runtime operators can create/resume/select/clear sessions explicitly - (e.g., for debugging, incident response, or workflow recovery). - -These use cases suggest sessions are not only a transport concern; they are a -practical application-layer primitive needed for real tool orchestration. - -When using client-carried state in `data`, implementations should treat it as -advisory input unless explicitly trusted by policy. - -## Capability Advertisement - -During `initialize`, a server that supports sessions includes an -`experimental` capability: - -```jsonc -// Server → Client (InitializeResult) -{ - "capabilities": { - "experimental": { - "session": { - "features": ["create", "delete"] - } - } - } -} -``` - -`features` lists the `session/*` methods the server supports. A minimal -server might only support `["create"]`. - -No per-capability `version` field is included — no existing MCP capability -uses one. Versioning is handled at the protocol level via `protocolVersion` -during `initialize`. If the session capability shape needs breaking changes -in the future, those would be gated on a new protocol version. - -## Session Lifecycle Methods - -### `session/create` - -The `session/create` result sets the session cookie in `_meta.mcp/session`. -The SDK (or overlay library in the experiment phase) surfaces `id` and -`expiry` to callers from there. See [Session Cookie: Placement](#session-cookie-placement) -for the design discussion on where the cookie lives. - -```jsonc -// Client → Server -{ - "jsonrpc": "2.0", - "id": 1, - "method": "session/create", - "params": { - "hints": { - "label": "my-agent-workspace", - "data": { "title": "Code Review Session" } - } - } -} - -// Server → Client -{ - "jsonrpc": "2.0", - "id": 1, - "result": { - "_meta": { - "mcp/session": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T14:30:00Z", - "data": { "title": "Code Review Session" } - } - } - } -} -``` - -The session cookie (`_meta.mcp/session`) carries `id`, `expiry`, and `data` -together. The SDK (or overlay library in the experiment phase) surfaces all -three to callers. The result body is minimal — it contains only `_meta`. No -session fields appear in the top-level result body outside `_meta`. - - -The session cookie (`_meta.mcp/session`) carries `id`, `expiry`, and `data` -together. The SDK (or overlay library in the experiment phase) surfaces all -three to callers. The result body is minimal — it contains only `_meta`. No -session fields appear in the top-level result body outside `_meta`. - -If the requested session cannot be resumed, the server SHOULD return an -error (e.g., session not found / invalid / expired). - -### `session/delete` - -```jsonc -// Client → Server -{ - "jsonrpc": "2.0", - "id": 3, - "method": "session/delete", - "params": { "id": "sess-a1b2c3d4e5f6" } -} - -// Server → Client -{ - "jsonrpc": "2.0", - "id": 3, - "result": { - "deleted": true, - "_meta": { "mcp/session": null } - } -} -``` - -See [Revocation via `null`](#revocation-via-null) for the design discussion -on using `null` as a signal. - -## Session Cookie Echo - -Once a session is established, the client includes the session cookie in -`_meta` on every request. The server echoes (or updates) it in every -response. The exact fields echoed depend on the **storage model** in use — -see [Session Storage Models](#session-storage-models) below. - -### 5a. Server-Authoritative Echo - -The client echoes only `id`. The server looks up state from its own store -and refreshes `expiry` in the response. `data` is absent from client echoes -— state lives server-side only. - -```jsonc -// Client → Server (tools/call with session) -{ - "jsonrpc": "2.0", - "id": 4, - "method": "tools/call", - "params": { - "name": "notebook_append", - "arguments": { "text": "remember this" }, - "_meta": { - "mcp/session": { "id": "sess-a1b2c3d4e5f6" } - } - } -} - -// Server → Client -{ - "jsonrpc": "2.0", - "id": 4, - "result": { - "content": [{ "type": "text", "text": "appended" }], - "_meta": { - "mcp/session": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T15:00:00Z" - } - } - } -} -``` - -### 5b. Client-Carried Echo - -The client echoes the full cookie including `data`. The server reconstructs -state from inbound `data`, mutates it, and returns the updated cookie in the -response. The server never looks up a store — the cookie IS the state. - -```jsonc -// Client → Server — client echoes full cookie including data from previous response -{ - "jsonrpc": "2.0", - "id": 5, - "method": "tools/call", - "params": { - "name": "hashcheck_store", - "arguments": { "key": "password", "text": "hunter2" }, - "_meta": { - "mcp/session": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T15:00:00Z", - "data": { "hashes": "{"password":"abc123..."}" } - } - } - } -} - -// Server → Client — server updated data["hashes"] in-place; returns updated cookie -{ - "jsonrpc": "2.0", - "id": 5, - "result": { - "content": [{ "type": "text", "text": "Stored sha256('password') = def456..." }], - "_meta": { - "mcp/session": { - "id": "sess-a1b2c3d4e5f6", - "expiry": "2026-02-23T15:00:00Z", - "data": { "hashes": "{"password":"def456..."}" } - } - } - } -} -``` - -## Session Storage Models - -Two storage models are supported. They differ in where session state lives -and what the server does with the inbound `_meta.mcp/session.data` field. - -### Server-Authoritative - -The server stores state in its own store, keyed by session `id`. Inbound -client cookies carry only `id` (used as a lookup key). The server writes -`data` into the outbound cookie for informational purposes, but ignores any -`data` present in inbound cookies. - -- **State lives:** server-side store -- **Client echoes:** `{ id }` -- **Server reads:** store lookup by `id`; ignores inbound `data` -- **Server writes:** `{ id, expiry }` (and optionally `data` for - informational mirroring) -- **Statefulness requirement:** server must be stateful (or share a store - across replicas) -- **Trust:** `data` is server-controlled; client cannot forge meaningful state - -### Client-Carried - -The server has no store for this session's data. It reads state from the -inbound `_meta.mcp/session.data`, operates on it, and writes the updated -state back into `_meta.mcp/session.data` in the response. The cookie IS the -state — the server is effectively stateless with respect to this data. - -- **State lives:** inside the cookie, carried by the client -- **Client echoes:** `{ id, expiry, data }` (full cookie from last response) -- **Server reads:** inbound `data` directly — no store lookup -- **Server writes:** `{ id, expiry, data }` with mutated `data` -- **Statefulness requirement:** none — works with a fully stateless HTTP - server or serverless function -- **Trust implication:** the client can forge `data`. This is appropriate - when the client only affects themselves (e.g. a local hash KV store for - verification), not for sensitive or shared state. Implementations SHOULD - sign or encrypt `data` if tamper-resistance is required. - -> **Quick reference:** See the `.jsonc` companion -> ([`data-layer-sessions-api-shapes.jsonc`](data-layer-sessions-api-shapes.jsonc)) -> sections 5a and 5b for abbreviated wire shapes of each model. - -### Session Cookie: Placement - -> **Implementation note:** The reference packages included with this -> proposal (`data-layer-sessions-client-python/`, -> `data-layer-sessions-server-python/`) are **overlay libraries** that -> layer on top of an unmodified MCP Python SDK. They inject and extract -> `_meta["mcp/session"]` by hand, without requiring any SDK changes. -> This is deliberate — it allows reviewers to evaluate the wire-level -> behaviour immediately, without gating on SDK or schema modifications. -> -> If this proposal progresses to a first-class spec feature, the -> expectation is that the session cookie would migrate from a convention -> key to a **named property** on `RequestMetaObject` and `MetaObject`, -> with typed support in the SDKs (see Path A below). - -The cookie echo mechanism uses `_meta` to carry session state across all -request/response pairs. This raises a design question about how the cookie -key is defined. - -The MCP schema today has **one** protocol-defined key in `_meta`: -`progressToken`, defined as a **named property** on `RequestMetaObject`. -This proposal uses a **convention key** (`"mcp/session"`) in the open -`_meta` bag instead. - -Both approaches are schema-legal. The trade-offs: - -| Approach | Pro | Con | -|---|---|---| -| **A: Named property** — add `session` to `RequestMetaObject` and `MetaObject` schema definitions, like `progressToken` | Schema-validatable; typed in SDKs; consistent with `progressToken` precedent | Requires schema changes; tighter coupling to spec release cycle | -| **B: Convention key** — use `"mcp/session"` as an opaque key in the `_meta` bag (current proposal + demos) | No schema changes needed; works immediately as `experimental`; extensible; demos can run on stock SDK | No schema validation; novel use of the extensibility bag for a protocol-level concept | - -The `mcp/` prefix is **reserved for MCP spec use** per the `MetaObject` -naming rules — so `"mcp/session"` is valid as a spec-defined key. Third -parties MUST NOT define keys under the `mcp/` prefix. - -**Recommended path:** Start with **Path B** (convention key under -`experimental`) for prototyping and interoperability testing, then promote -to **Path A** (named schema property) when the feature moves from -experimental to first-class. The reference packages are structured to make -this migration straightforward — the `_meta` injection/extraction is -isolated in `model.py` in both client and server packages. - -In both Path A and Path B, the result body shape is identical: `id`/`expiry` -live exclusively in `_meta.mcp/session`, and `data` carries the app payload. -The paths differ only in how the SDK exposes these fields to callers — via -typed accessors (Path A) or via direct dict access (Path B). - -### Revocation via `null` - -A server revokes a session by returning `"mcp/session": null`: - -```jsonc -{ - "_meta": { "mcp/session": null } -} -``` - -The client SHOULD clear its stored cookie and MAY re-establish a session. - -**Design note:** The `MetaObject` schema is `"type": "object"` with no -constraints on property value types, so `null` is technically valid. However, -no existing MCP usage puts `null` in `_meta` — this would be a novel -pattern. Alternatives: - -- **Option A (current):** `null` signals revocation. Simple, expressive. -- **Option B:** Omit `"mcp/session"` entirely to signal revocation. Ambiguous - — absence could mean "no change" rather than "revoked." -- **Option C:** Use a dedicated `session/revoke` notification. Explicit, but - adds a method. - -## Error Handling - -A server that requires a session for a particular operation returns a -JSON-RPC error: - -```jsonc -{ - "jsonrpc": "2.0", - "id": 5, - "error": { - "code": -32043, - "message": "Session required. Call session/create or session/resume first." - } -} -``` - -### Error Code Selection - -The code `-32043` is in the JSON-RPC implementation-defined server error -range (`-32000` to `-32099`). The following codes in this range are already -allocated or claimed in the MCP ecosystem: - -| Code | Name | Where | Crosses wire? | -|---|---|---|---| -| `-32000` | `CONNECTION_CLOSED` | Python SDK | No (SDK-internal) | -| `-32001` | `REQUEST_TIMEOUT` | Python SDK, TS SDK | No (SDK-internal) | -| `-32002` | Resource not found | Spec docs (`server/resources.mdx`) | Yes | -| `-32042` | `URL_ELICITATION_REQUIRED` | Schema (`schema.ts`), both SDKs | Yes (formal) | - -The `-3204x` neighbourhood is used for **protocol-level conditions -requiring structured client action** (URL elicitation, session -establishment). This contrasts with `-3200x` which the SDKs have -informally claimed for internal transport/connection conditions, and -`-32002` which the spec docs already use for resource-not-found errors. - -If adopted, `-32043` would be defined as a named constant -(e.g. `SESSION_REQUIRED`) in `schema.ts` alongside -`URL_ELICITATION_REQUIRED`, and propagated to both SDKs. - -**Open question:** Should the error carry structured `data` (like -`URLElicitationRequiredError` does with its `elicitations` array)? -For example, the error `data` could include available session features -or a hint about which method(s) to call. - -## Selective Enforcement - -Servers MAY require sessions for all tools, some tools, or no tools. The -mechanism for advertising which tools require sessions is left open: - -- Option A: Servers just return `-32043` and clients react. -- Option B: A `sessionRequired` field in tool metadata. -- Option C: A server-level policy declaration in capabilities. - -**Open question:** Which approach (or combination) best serves both -human developers and LLM-driven tool selection? - -## Interaction with Transport-Level Sessions - -Streamable HTTP currently has `Mcp-Session-Id` for transport routing. This -proposal operates at a different layer: - -| Concern | Transport (`Mcp-Session-Id`) | Data-layer (`mcp/session`) | -|---|---|---| -| Scope | Single connection | Across connections | -| Set by | Transport layer | Application logic | -| Survives reconnect | No | Yes | -| Works over stdio | N/A | Yes | - -### Trajectory: Data-Layer Sessions Supersede Transport Sessions - -As MCP moves toward stateless transports, the transport-level -`Mcp-Session-Id` increasingly functions as a **routing hint** rather than -a session identity. This proposal's data-layer session ID is the natural -replacement for application-level session semantics. - -The intended evolution: - -1. **Today:** `Mcp-Session-Id` is both a routing key and a (fragile) - session identity. Losing the transport connection loses the session. -2. **With this proposal:** `mcp/session` carries durable session identity - in the JSON-RPC payload. `Mcp-Session-Id` is demoted to a - transport-routing concern only. -2. **Future:** The data-layer session ID is mirrored into an HTTP header - (e.g. `Mcp-Session-Id` itself, or a new `Mcp-Session` header) so that - load balancers and proxies can route on it without body parsing — - following the pattern established by - **[SEP-2243: HTTP Header Standardization](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)**. - -SEP-2243 defines the mechanism for surfacing JSON-RPC payload fields as -HTTP headers (`Mcp-Method`, `Mcp-Tool-Name`, etc.) and includes validation -rules for header/body consistency. The data-layer session ID is a natural -candidate for the same treatment: the client would include the session ID -both in `_meta["mcp/session"]` and in an HTTP header, enabling -infrastructure routing without deep packet inspection. - -**Open question:** Should the header reuse `Mcp-Session-Id` (replacing -the transport meaning) or introduce a new header name (e.g. -`Mcp-Data-Session`) to avoid ambiguity during the transition? - -## Implementation-Informed Considerations - -Early implementation work suggests the following considerations (non-normative): - -- **Capability gating works in practice.** Clients can ignore unknown - experimental capabilities and continue normal MCP operation. -- **Auto-create + explicit controls both matter.** Automatic `session/create` - supports low-friction startup, while explicit controls (`create/resume/delete/clear`) - support operator workflows and debugging. -- **Client-side cookie persistence is valuable.** A local cookie jar enables - reconnect bootstrap and reduces redundant `session/create` calls. -- **Identity-aware storage helps multi-server environments.** Keying by server - identity (when available) reduces collisions and supports disconnected views. -- **Invalidation tracking is useful.** Marking rejected cookies as invalidated - avoids repeatedly selecting known-bad session IDs during resume. -- **Expiry is advisory unless enforced.** Demo servers stamp `expiry` metadata, - but enforcement policy remains server-defined. -- **`_meta.mcp/session` is the sole source of truth for `id`/`expiry`.** - Earlier prototype code included a fallback that read `id` directly from the - `result` body if `_meta.mcp/session` was absent. This fallback is - **removed from the target design**. Servers MUST populate `_meta.mcp/session` - on `session/create` and `session/resume` responses. The SDK (or overlay - library) reads `id` and `expiry` exclusively from `_meta.mcp/session`; the - result body carries only `data`. - -These considerations do **not** lock in protocol choices; they provide -practical guidance for SEP scope and interoperability testing. - -## Open Questions Summary - -1. **Error code** — `-32043` (proposed) or a different code? Formal error code registry needed? Structured `data` payload? -2. **Selective enforcement** — how should servers declare per-tool requirements? -3. **HTTP header mirroring** — should `mcp/session` also appear as a header? -4. **SEP-2243 alignment** — should the data-layer session ID be mirrored - into an HTTP header following the SEP-2243 pattern? If so, reuse - `Mcp-Session-Id` or new header name? -5. **Cookie size** — what constraints on the `data` field? -6. **Security** — signing/encryption of session tokens? Server-side only - vs. client-verifiable? -7. **Phase 2 shape** — how should `session/list` and `session/recover` be - specified once Phase 1 stabilizes? -8. **Relationship to MRTR** — how does this interact with the multi-round-trip - requests track's need for state passthrough? -9. **Cookie placement** — named `_meta` property (like `progressToken`) or - convention key (`"mcp/session"`)? See [Placement](#session-cookie-placement). -10. **Revocation signal** — `null` value, key absence, or dedicated method? - See [Revocation](#revocation-via-null). -11. **Client persistence semantics** — should local cookie jars / resume behavior - be guidance-only, or should minimal interoperability expectations be defined? - -## Prior Art - -- **RFC 6265** (HTTP Cookies) — foundation for cookie semantics -- **Sessions Track Brief** (this repo) — working group discussion context -- **MRTR Track Brief** (this repo) — overlapping state-passthrough needs -- **[SEP-2243: HTTP Header Standardization](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)** — - defines the pattern for mirroring JSON-RPC fields into HTTP headers for - infrastructure routing; directly relevant for surfacing session IDs to - load balancers -- **`fast-agent` experimental sessions** — working prototype of this design - over both stdio and Streamable HTTP transports, including jar-based resume and - operator session controls From 3889b683d5e6f884eb9fa42b3d7def544e8bd212 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:59:34 +0000 Subject: [PATCH 17/35] interim --- proposals/0000-data-layer-sessions.md | 378 ++++++++++++-------------- 1 file changed, 176 insertions(+), 202 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index fee6632..5d789b4 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -3,7 +3,7 @@ > **Status:** Early Draft > **Date:** 2026-02-23 > **Track:** transport-wg/sessions -> **Author(s):** Shaun Smith +> **Author(s):** Shaun Smith ## Abstract @@ -13,9 +13,9 @@ This proposal introduces application level sessions within the MCP Data Layer. S ## Motivation -MCP Sessions are currently either implicit (STDIO), or constructed as a side effect of the transport connection (Streamable HTTP). +MCP Sessions are currently either implicit (STDIO), or constructed as a side effect of the transport connection (Streamable HTTP). -It is assumed (but not required) that Host applications rather than the LLM are responsible for Session management. +It is assumed (but not required) that Host applications rather than the LLM are responsible for Session management. ## Specification @@ -31,20 +31,153 @@ Servers that support sessions MUST declare the `sessions` capability: ```json { - "capabilities": { - "sessions" : {} - } + "capabilities": { + "sessions": {} + } } ``` > For testing purposes MCP Clients that support sessions declare an `experimental/sessions` capability to simplify testing. +### Protocol Messages + +#### Creating Sessions + +Clients begin a session with an MCP Server by calling `sessions/create`. + +**Request:** + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "sessions/create" +} +``` + +**Response**: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "session": { + "sessionId": "sess-a1b2c3d4e5f6", + "expiresAt": "2026-02-27T15:30:00Z" + }, + "_meta": { + "io.modelcontextprotocol/session": { + "sessionId": "sess-a1b2c3d4e5f6", + "expiresAt": "2026-02-27T15:30:00Z" + } + } + } +} +``` + +The Client **MUST NOT** send `io.modelcontextprotocol/session` data with the sessions/create request. + + +#### Using Sessions + +To use a Session the Client request includes `_meta["io.modelcontextprotocol/session"]`: + +1. The receiver MUST treat that sessionId as the session context for processing the request. +1. Succesful responses MUST include \_meta["io.modelcontextprotocol/session"]. +1. The `sessionId` in the response MUST exactly match the sessionId from the request. +1. The receiver MUST NOT substitute, rotate, or rewrite `sessionId` in the response. + +```json +{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "search_code", + "arguments": { + "query": "SessionMetadata" + }, + "_meta": { + "io.modelcontextprotocol/session": { + "sessionId": "sess-a1b2c3d4e5f6" + } + } + } +} +``` + + + +The Client SHOULD associate retained cookies with the issuing Server . + +The expiry date is a hint. Can be refreshed `servers/discovery`. + +- The session ID SHOULD be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash). +- The session ID MUST only contain visible ASCII characters (ranging from 0x21 to 0x7E). +- The client MUST handle the session ID in a secure manner, see Session Hijacking mitigations for more details. (TODO -- update this as data layer/stdio mitigations are different) + +The Error message **SHOULD** be descriptive of the reason for failure. + +```json +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "content": [ + { + "type": "text", + "text": "Found 3 matches." + } + ], + "_meta": { + "io.modelcontextprotocol/session": { + "sessionId": "sess-a1b2c3d4e5f6", + "expiresAt": "2026-02-27T15:30:00Z" + } + } + } +} +``` + + +#### Deleting Sessions + +**Request:** + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "method": "sessions/delete", + "params": { + "_meta": { + "io.modelcontextprotocol/session": { + "sessionId": "sess-a1b2c3d4e5f6" + } + } + } +} +``` + +**Response:** + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "result": {} +} +``` + +Clients **SHOULD** delete sessions that are no longer required to allow the Server to reclaim unneeded resources. + ### Schema -Session association metadata uses _meta["io.modelcontextprotocol/session"] with value type SessionMetadata. +Session association metadata uses `_meta["io.modelcontextprotocol/session"]` with value type SessionMetadata. ```ts - /** +/** * Describes an MCP Session. * Include this in the `_meta` field under the key `io.modelcontextprotocol/session`. */ @@ -55,7 +188,7 @@ export interface SessionMetadata { sessionId: string; /** - * ISO 8601 timestamp hint for session expiry + * ISO 8601 timestamp hint for session expiry */ expiresAt?: string; @@ -63,13 +196,13 @@ export interface SessionMetadata { * Opaque server-issued session state token. * Clients MUST treat this value as opaque and MUST NOT inspect or modify it. */ - state?: string; + state?: string; } ``` Sessions are created and deleted via `sessions/create` and `sessions/delete` requests: -```json +```ts /** * A request to create a new session. * @@ -118,201 +251,44 @@ Sessions are created and deleted via `sessions/create` and `sessions/delete` req } ``` -Clients use Sessions by including the SessionMetadata in `io.modelcontextprotocol/session` in _meta of the Request. +Clients use Sessions by including the SessionMetadata in `io.modelcontextprotocol/session` in \_meta of the Request. -When a requested Session is unknown by the Server it returns a `-32043 SESSION_NOT_FOUND` Error. +When a requested Session is unknown by the Server it returns a `-32043 SESSION_NOT_FOUND` Error. The Client **SHOULD** treat the Session as permanently invalidated. ```ts - /** @internal */ - export const SESSION_NOT_FOUND = -32043; - - /** - * An error response indicating that the supplied session does not exist. - * - * @example Session not found - * {@includeCode ./examples/SessionNotFoundError/session-not-found.json} - * - * @category Errors - */ - export interface SessionNotFoundError extends Omit { - error: Error & { - code: typeof SESSION_NOT_FOUND; - data: { - /** - * The session identifier provided by the caller. - */ - sessionId: string; - [key: string]: unknown; - }; - }; - } -``` - - -### Protocol Messages - - -#### Creating Sessions - -Clients begin a session with an MCP Server by calling `sessions/create`. - -**Request:** - -```json -{ - "jsonrpc": "2.0", - "id": 1, - "method": "sessions/create", +/** @internal */ +export const SESSION_NOT_FOUND = -32043; + +/** + * An error response indicating that the supplied session does not exist. + * + * @example Session not found + * {@includeCode ./examples/SessionNotFoundError/session-not-found.json} + * + * @category Errors + */ +export interface SessionNotFoundError extends Omit< + JSONRPCErrorResponse, + "error" +> { + error: Error & { + code: typeof SESSION_NOT_FOUND; + data: { + /** + * The session identifier provided by the caller. + */ + sessionId: string; + [key: string]: unknown; + }; + }; } ``` -**Response**: - -```json - { - "jsonrpc": "2.0", - "id": 1, - "result": { - "session": { - "sessionId": "sess-a1b2c3d4e5f6", - "expiresAt": "2026-02-27T15:30:00Z" - }, - "_meta": { - "io.modelcontextprotocol/session": { - "sessionId": "sess-a1b2c3d4e5f6", - "expiresAt": "2026-02-27T15:30:00Z" - } - } - } - } - -``` - -The Client **MUST NOT** send `io.modelcontextprotocol/session` data with the sessions/create request. - -#### Deleting Sessions - -**Request:** - -```json - { - "jsonrpc": "2.0", - "id": 2, - "method": "sessions/delete", - "params": { - "_meta": { - "io.modelcontextprotocol/session": { - "sessionId": "sess-a1b2c3d4e5f6" - } - } - } - } -``` - -**Response:** - -```json - { - "jsonrpc": "2.0", - "id": 2, - "result": {} - } -``` - - -Clients **SHOULD** delete sessions that are no longer required to allow the Server to reclaim unneeded resources. - -#### Using Sessions - -When a request includes `_meta["io.modelcontextprotocol/session"]`: - - 1 The receiver MUST treat that sessionId as the session context for processing the request. - 2 Succesful response MUST include _meta["io.modelcontextprotocol/session"]. - 3 The sessionId in the response MUST exactly match the sessionId from the request. - 4 The receiver MUST NOT substitute, rotate, or rewrite sessionId in the response. - 5 If the supplied session is unknown, expired, or not accessible, the receiver MUST return an error (recommended: InvalidParams) and still include the same session metadata in _meta. - - -Clients MAY send a Session Id send requests with - -The Error message **SHOULD** be descriptive of the reason for failure. - -The Client SHOULD associate retained cookies with the issuing Server . - -The expiry date is a hint. Can be refreshed `servers/discovery`. - -- The session ID SHOULD be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash). -- The session ID MUST only contain visible ASCII characters (ranging from 0x21 to 0x7E). -- The client MUST handle the session ID in a secure manner, see Session Hijacking mitigations for more details. (TODO -- update this as data layer/stdio mitigations are different) - - -{label} - - - { - "jsonrpc": "2.0", - "id": 3, - "method": "tools/call", - "params": { - "name": "search_code", - "arguments": { - "query": "SessionMetadata" - }, - "_meta": { - "io.modelcontextprotocol/session": { - "sessionId": "sess-a1b2c3d4e5f6" - } - } - } - } - -{ -"jsonrpc": "2.0", -"id": 3, -"result": { - "content": [ - { - "type": "text", - "text": "Found 3 matches." - } - ], - "_meta": { - "io.modelcontextprotocol/session": { - "sessionId": "sess-a1b2c3d4e5f6", - "expiresAt": "2026-02-27T15:30:00Z" - } - } -} -} - - -### request/* - -Any request can have a cookie contained within the _meta block. - -Clients SHOULD NOT send cookies to Servers that do not support the `session` capability. - -Clients MUST only send cookies to the Server that issued them. - -Servers SHOULD send an Error -34043 `Session not found` if the session is not recognized or valid. Clients SHOULD invalidate the Session. - - -_meta may contain - -### response/* - -A response to a request containing a cookie MUST respond with a cookie that contains the same SessionID. - - - -### Tool Annotation - -An `allocatesResource` Tool Hint is proposed to indicate to the Client that a - - ## Other Work +### SEP-2243 HTTP Standardization +When _meta["io.modelcontextprotocol/session"] is present and using the StreamableHttp transport, the sessionId should be included as `Mcp-Session-Id` in the HTTP Headers. ## Rationale @@ -322,13 +298,14 @@ To support non HTTP transports, an MCP Data Layer proposal has been selected. ### Use of in-band Tool Call ID -Session IDs are considered to be controlled by the Host application, rather than the Model - driving the design that identifiers are not revealed in tool calls etc. +A common workaround pattern is to use a session identifier within CallToolRequests to simulate sessions. This is often model controlled, with the session identifier being reproduced by the LLM. + +In practice, MCP Sessions may be used for other state control - for example availability of Tools, Prompts or Resources - therefore including the sessionId as part of the request/response cycle managed by the Host is the right choice. ### Use of single `state` value A single opaque "state" value mirrors the MRTR design, and reduces the chance of KV merge errors, and keeps client behaviour simple (echo bytes back). - ## Backward Compatibility ### Existing MCP Servers @@ -336,6 +313,3 @@ A single opaque "state" value mirrors the MRTR design, and reduces the chance of ### Session Guidance -It is expected that - - From 4ff50f863c22e556888a4136211658388fefb5ba Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:27:08 +0000 Subject: [PATCH 18/35] interim --- proposals/0000-data-layer-sessions.md | 53 +++++++++++++++------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 5d789b4..60513ed 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -64,30 +64,32 @@ Clients begin a session with an MCP Server by calling `sessions/create`. "result": { "session": { "sessionId": "sess-a1b2c3d4e5f6", - "expiresAt": "2026-02-27T15:30:00Z" + "expiresAt": "2026-02-27T15:30:00Z", + "state": "bGFuZ3VhZ2U9ZW4=" }, - "_meta": { - "io.modelcontextprotocol/session": { - "sessionId": "sess-a1b2c3d4e5f6", - "expiresAt": "2026-02-27T15:30:00Z" - } - } } } ``` -The Client **MUST NOT** send `io.modelcontextprotocol/session` data with the sessions/create request. +The Client **MUST NOT** send `io.modelcontextprotocol/session` data with the sessions/create request. + +The Client **MUST** associate retained sessionIds with the issuing Server. +`expiresAt` is a hint, and may be updated by the Server in future responses. The Host **MAY** use the `expiresAt` to indicate potentially stale sessions to the User. + +`state` **MUST** be retained by the Client and sent with future requests for that session. #### Using Sessions -To use a Session the Client request includes `_meta["io.modelcontextprotocol/session"]`: +To use a Session the Client request includes SessionMetadata in `_meta["io.modelcontextprotocol/session"]`: -1. The receiver MUST treat that sessionId as the session context for processing the request. +1. The Server MUST treat that sessionId as the session context for processing the request. 1. Succesful responses MUST include \_meta["io.modelcontextprotocol/session"]. 1. The `sessionId` in the response MUST exactly match the sessionId from the request. 1. The receiver MUST NOT substitute, rotate, or rewrite `sessionId` in the response. +**Request:** + ```json { "jsonrpc": "2.0", @@ -96,28 +98,19 @@ To use a Session the Client request includes `_meta["io.modelcontextprotocol/ses "params": { "name": "search_code", "arguments": { - "query": "SessionMetadata" + "query": "def fizz_buzz()" }, "_meta": { "io.modelcontextprotocol/session": { - "sessionId": "sess-a1b2c3d4e5f6" + "sessionId": "sess-a1b2c3d4e5f6", + "state": "bGFuZ3VhZ2U9ZW4=" } } } } ``` - - -The Client SHOULD associate retained cookies with the issuing Server . - -The expiry date is a hint. Can be refreshed `servers/discovery`. - -- The session ID SHOULD be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash). -- The session ID MUST only contain visible ASCII characters (ranging from 0x21 to 0x7E). -- The client MUST handle the session ID in a secure manner, see Session Hijacking mitigations for more details. (TODO -- update this as data layer/stdio mitigations are different) - -The Error message **SHOULD** be descriptive of the reason for failure. +**Response:** ```json { @@ -133,13 +126,18 @@ The Error message **SHOULD** be descriptive of the reason for failure. "_meta": { "io.modelcontextprotocol/session": { "sessionId": "sess-a1b2c3d4e5f6", - "expiresAt": "2026-02-27T15:30:00Z" + "state": "bGFuZ3VhZ2U9cHl0aG9u", + "expiresAt": "2026-03-31T23:59:00Z" } } } } ``` +1. The Client **MUST** update the `state` value if sent by the Server. +1. + +The Error message **SHOULD** be descriptive of the reason for failure. #### Deleting Sessions @@ -172,6 +170,12 @@ The Error message **SHOULD** be descriptive of the reason for failure. Clients **SHOULD** delete sessions that are no longer required to allow the Server to reclaim unneeded resources. +### Data Types + +- The `sessionId` **SHOULD** be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash). +- The `sessionId` MUST only contain visible ASCII characters (ranging from 0x21 to 0x7E). +- The client MUST handle the `sessionId` in a secure manner, see Session Hijacking mitigations for more details. ( + ### Schema Session association metadata uses `_meta["io.modelcontextprotocol/session"]` with value type SessionMetadata. @@ -200,6 +204,7 @@ export interface SessionMetadata { } ``` + Sessions are created and deleted via `sessions/create` and `sessions/delete` requests: ```ts From 8b0830276f3b6b70976cd04c368772ff51702923 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:20:35 +0000 Subject: [PATCH 19/35] MRTR1 --- proposals/0000-data-layer-sessions.md | 63 +++++++++++++++++++++------ 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 60513ed..1d0d53d 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -71,7 +71,7 @@ Clients begin a session with an MCP Server by calling `sessions/create`. } ``` -The Client **MUST NOT** send `io.modelcontextprotocol/session` data with the sessions/create request. +The Client **MUST NOT** send `io.modelcontextprotocol/session` with the sessions/create request. The Client **MUST** associate retained sessionIds with the issuing Server. @@ -134,13 +134,12 @@ To use a Session the Client request includes SessionMetadata in `_meta["io.model } ``` -1. The Client **MUST** update the `state` value if sent by the Server. -1. - -The Error message **SHOULD** be descriptive of the reason for failure. +1. The Client **MUST** update the `state` value if updated by the Server. See note on [Ordering](#session-update-sequencing)) #### Deleting Sessions +Clients **SHOULD** delete sessions that are no longer required to allow the Server to reclaim unneeded resources. + **Request:** ```json @@ -168,14 +167,47 @@ The Error message **SHOULD** be descriptive of the reason for failure. } ``` -Clients **SHOULD** delete sessions that are no longer required to allow the Server to reclaim unneeded resources. +#### Errors + +```json + { + "jsonrpc": "2.0", + "id": 42, + "error": { + "code": -32043, + "message": "Session not found", + "data": { + "sessionId": "sess-a1b2c3d4e5f6" + } + } + } +``` + +1. Unknown sessions **MUST** result in a `-32043 SESSION_NOT_FOUND` Error. The Client **SHOULD** treat the Session as permanently invalidated. +1. The Server **MAY** revoke a Session at any time by returning an `-32043 SESSION_NOT_FOUND` Error. ### Data Types -- The `sessionId` **SHOULD** be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash). -- The `sessionId` MUST only contain visible ASCII characters (ranging from 0x21 to 0x7E). -- The client MUST handle the `sessionId` in a secure manner, see Session Hijacking mitigations for more details. ( - +#### SessionMetadata + +_The following notes on sessionId are taken from the existing Streamable HTTP Transport guidance._ + +**sessionId:** +1. The `sessionId` **SHOULD** be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash). +1. The `sessionId` MUST only contain visible ASCII characters (ranging from 0x21 to 0x7E). +1. The client MUST handle the `sessionId` in a secure manner, see Session Hijacking mitigations for more details. ( + +**expiresAt:** +1. `expiresAt` is a hint that Clients **MAY** use to inform Users of potentially stale sessions. +1. Servers may update the `expiresAt` hint on any response. + +**state:** + +_The following notes on state are paraphrased from SEP (MRTR) + + + + ### Schema Session association metadata uses `_meta["io.modelcontextprotocol/session"]` with value type SessionMetadata. @@ -201,10 +233,10 @@ export interface SessionMetadata { * Clients MUST treat this value as opaque and MUST NOT inspect or modify it. */ state?: string; + } ``` - Sessions are created and deleted via `sessions/create` and `sessions/delete` requests: ```ts @@ -256,9 +288,6 @@ Sessions are created and deleted via `sessions/create` and `sessions/delete` req } ``` -Clients use Sessions by including the SessionMetadata in `io.modelcontextprotocol/session` in \_meta of the Request. - -When a requested Session is unknown by the Server it returns a `-32043 SESSION_NOT_FOUND` Error. The Client **SHOULD** treat the Session as permanently invalidated. ```ts /** @internal */ @@ -301,6 +330,12 @@ When _meta["io.modelcontextprotocol/session"] is present and using the Streamabl To support non HTTP transports, an MCP Data Layer proposal has been selected. +### Session Update Sequencing + +There are no ordering guarantees for requests/responses, meaning a Last-Write-Wins strategy by default. + +It is possible for the Server to send a monotonic state sequence to allow the client to identify the ordering of state content. + ### Use of in-band Tool Call ID A common workaround pattern is to use a session identifier within CallToolRequests to simulate sessions. This is often model controlled, with the session identifier being reproduced by the LLM. From 7a649023073b5d80abd9b2ccd53853cceb2a28cb Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:36:04 +0000 Subject: [PATCH 20/35] interim --- proposals/0000-data-layer-sessions.md | 66 +++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 1d0d53d..85c1fb6 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -9,6 +9,8 @@ This proposal introduces application level sessions within the MCP Data Layer. Sessions are created by the Client, and allow the Server to store an opaque state token. +This proposal should be reviewed alongside SEP-1442, and assumes that the `initialize` operation is deprecated. + [Further context to be added] ## Motivation @@ -203,11 +205,9 @@ _The following notes on sessionId are taken from the existing Streamable HTTP Tr **state:** -_The following notes on state are paraphrased from SEP (MRTR) - +_The following notes on state are paraphrased from SEP (MRTR)_ - ### Schema Session association metadata uses `_meta["io.modelcontextprotocol/session"]` with value type SessionMetadata. @@ -328,13 +328,15 @@ When _meta["io.modelcontextprotocol/session"] is present and using the Streamabl ### HTTP Cookies vs. Custom Implementation -To support non HTTP transports, an MCP Data Layer proposal has been selected. +HTTP cookies (RFC 6265) provide an existing stateless session mechanism with automatic client-side storage and per-request transmission. The header pattern is well-understood and battle-tested. + +This proposal adopts a similar pattern (server-issued opaque tokens that clients return unmodified) but implements it in the JSON-RPC message layer rather than HTTP Headers, enabling consistent session semantics across non-HTTP transports. ### Session Update Sequencing -There are no ordering guarantees for requests/responses, meaning a Last-Write-Wins strategy by default. +There are no ordering guarantees for requests/responses, meaning a Last-Write-Wins strategy by default. Servers should be aware of this potential race condition and include appropriate mitigations if needed. -It is possible for the Server to send a monotonic state sequence to allow the client to identify the ordering of state content. +A future design may introduce a monotonic state sequence to allow the client to identify the ordering of state content. ### Use of in-band Tool Call ID @@ -342,14 +344,60 @@ A common workaround pattern is to use a session identifier within CallToolReques In practice, MCP Sessions may be used for other state control - for example availability of Tools, Prompts or Resources - therefore including the sessionId as part of the request/response cycle managed by the Host is the right choice. -### Use of single `state` value +### Use of a single `state` value rather than KV store. + +A single opaque "state" value mirrors the MRTR design, reduces the chance of KV merge errors, and keeps client behaviour simple (simply echo bytes back). + + +### Scope of Sessions + +For 2025-11-25 specification STDIO servers, Sessions are inherent to the process lifecycle and all Requests and Responses are within the same "session scope". + +For 2025-11-25 specification Streamable HTTP servers, Sessions are typically managed on a "per connection" basis, with the MCP Server choosing session usage at Initialization time and enforcing with HTTP status codes. Although technically feasible to gate different operations to require sessions or not, in practice usage is "all" or "nothing". + -A single opaque "state" value mirrors the MRTR design, and reduces the chance of KV merge errors, and keeps client behaviour simple (echo bytes back). ## Backward Compatibility ### Existing MCP Servers -### Session Guidance + +## Test Vectors + +### Session Creation + +**Request:** +```json +{"jsonrpc":"2.0","id":1,"method":"sessions/create"} +``` + +**Valid Response:** +```json +{"jsonrpc":"2.0","id":1,"result":{"session":{"sessionId":"sess-abc123","expiresAt":"2026-03-01T00:00:00Z","state":"eyJrIjoidiJ9"}}} +``` + +### Session Usage + +**Request with session:** +```json +{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"msg":"hi"},"_meta":{"io.modelcontextprotocol/session":{"sessionId":"sess-abc123","state":"eyJrIjoidiJ9"}}}} +``` + +**Response with updated state:** +```json +{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"hi"}],"_meta":{"io.modelcontextprotocol/session":{"sessionId":"sess-abc123","state":"eyJrIjoidjIifQ==","expiresAt":"2026-03-01T00:00:00Z"}}}} +``` + +### Session Not Found + +**Request with invalid session:** +```json +{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{},"_meta":{"io.modelcontextprotocol/session":{"sessionId":"sess-invalid"}}}} +``` + +**Error Response:** +```json +{"jsonrpc":"2.0","id":3,"error":{"code":-32043,"message":"Session not found","data":{"sessionId":"sess-invalid"}}} +``` From 5698d8ef2057f659494d96c087a6f50ca76d7a33 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:15:02 +0000 Subject: [PATCH 21/35] update --- proposals/0000-data-layer-sessions.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 85c1fb6..f018bf4 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -207,6 +207,12 @@ _The following notes on sessionId are taken from the existing Streamable HTTP Tr _The following notes on state are paraphrased from SEP (MRTR)_ +state: + +1. state is an opaque, server-issued token that enables stateless server processing. +1. Clients MUST treat state as opaque and MUST echo the exact value on subsequent requests in that session. +1. Servers SHOULD protect state according to their security requirements, ranging from plaintext (development only) to signed or encrypted tokens (production). +1. See **SEP-XXXX Multi Round-Trip Requests** requestState guidance for canonical encoding, validation, size and security requirements. ### Schema @@ -348,19 +354,19 @@ In practice, MCP Sessions may be used for other state control - for example avai A single opaque "state" value mirrors the MRTR design, reduces the chance of KV merge errors, and keeps client behaviour simple (simply echo bytes back). - ### Scope of Sessions For 2025-11-25 specification STDIO servers, Sessions are inherent to the process lifecycle and all Requests and Responses are within the same "session scope". For 2025-11-25 specification Streamable HTTP servers, Sessions are typically managed on a "per connection" basis, with the MCP Server choosing session usage at Initialization time and enforcing with HTTP status codes. Although technically feasible to gate different operations to require sessions or not, in practice usage is "all" or "nothing". +With this design, it is possible for an MCP Server to support granualar session gating. +TODO -- enhance discussion here. ## Backward Compatibility -### Existing MCP Servers - +TODO -- incorporate support matrix. ## Test Vectors From 4477da795453d2b42d5fb5f6206666290bc3bb4a Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:15:57 +0000 Subject: [PATCH 22/35] update ignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 83cc956..cb63ac5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .fast-agent/ -fastagent.jsonl __pycache__/ *.pyc proposals/session-v2-experimental/ From cec93fb7fe2adae2be8130f460f7a31be9153548 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:27:17 +0000 Subject: [PATCH 23/35] enhance --- proposals/0000-data-layer-sessions.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index f018bf4..0707086 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -9,23 +9,25 @@ This proposal introduces application level sessions within the MCP Data Layer. Sessions are created by the Client, and allow the Server to store an opaque state token. -This proposal should be reviewed alongside SEP-1442, and assumes that the `initialize` operation is deprecated. +This proposal should be reviewed alongside SEP-1442, and assumes that the `initialize` operation and associated StreamableHTTP session creation is deprecated. [Further context to be added] ## Motivation -MCP Sessions are currently either implicit (STDIO), or constructed as a side effect of the transport connection (Streamable HTTP). +MCP Sessions are currently either implicit (STDIO), or constructed as a side effect of the transport connection (Streamable HTTP). -It is assumed (but not required) that Host applications rather than the LLM are responsible for Session management. +Migrating sessions to the MCP data layer allows MCP applications to handle sessions as part of their domain logic, decoupled from the transport layer. This enables predictable session semantics, especially for Hosts that handle multiple "threads" of context within the application. The ability for MCP Servers to allocate resources on a per-session basis, or be able to provide rich functionality without server-side storage makes MCP suitable for increasingly sophisticated and scaled deployments. ## Specification ### User Interaction Model -Sessions are designed to be **application-driven**, with host applications determining how to establish sessions based on their need. +Sessions are designed to be **application-driven**, with host applications determining how and when to establish sessions based on their need. -It is normally expected that applications will establish one session per conversation thread or task, but this is not required. +It is normally expected that applications will establish one session per conversation thread or task, but this is not required. + +MCP Servers may wish to offer capabilities in a mixture of authentication and session modalities. ### Capabilities @@ -75,7 +77,7 @@ Clients begin a session with an MCP Server by calling `sessions/create`. The Client **MUST NOT** send `io.modelcontextprotocol/session` with the sessions/create request. -The Client **MUST** associate retained sessionIds with the issuing Server. +The Client **MUST** securely associate retained sessions with the issuing Server. The Client will typically establish identity through a mixture of _connection target_ and _user identity_. `expiresAt` is a hint, and may be updated by the Server in future responses. The Host **MAY** use the `expiresAt` to indicate potentially stale sessions to the User. @@ -137,6 +139,7 @@ To use a Session the Client request includes SessionMetadata in `_meta["io.model ``` 1. The Client **MUST** update the `state` value if updated by the Server. See note on [Ordering](#session-update-sequencing)) +1. The Client **MAY** update the `expiresAt` value if updated by the Server. #### Deleting Sessions @@ -364,6 +367,10 @@ With this design, it is possible for an MCP Server to support granualar session TODO -- enhance discussion here. +### resourceAllocation Tool Hint + +**For discussion** - it may make sense to include a tool hint to indicate whether or not a Session is associated with expensive resources, hinting that deletion is preferred at the end of the immediate User interaction session. + ## Backward Compatibility TODO -- incorporate support matrix. From 838bf2a74deadbdd70fad5e2a1e849aaa3a448f5 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:35:26 +0100 Subject: [PATCH 24/35] small updates for sdk mentions --- proposals/0000-data-layer-sessions.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 0707086..d00c8dd 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -347,6 +347,8 @@ There are no ordering guarantees for requests/responses, meaning a Last-Write-Wi A future design may introduce a monotonic state sequence to allow the client to identify the ordering of state content. +Servers that have this as a critical requirement should manage session state at the server. + ### Use of in-band Tool Call ID A common workaround pattern is to use a session identifier within CallToolRequests to simulate sessions. This is often model controlled, with the session identifier being reproduced by the LLM. @@ -363,9 +365,18 @@ For 2025-11-25 specification STDIO servers, Sessions are inherent to the process For 2025-11-25 specification Streamable HTTP servers, Sessions are typically managed on a "per connection" basis, with the MCP Server choosing session usage at Initialization time and enforcing with HTTP status codes. Although technically feasible to gate different operations to require sessions or not, in practice usage is "all" or "nothing". -With this design, it is possible for an MCP Server to support granualar session gating. +With this design, it is possible for an MCP Server to support granular session gating. + +The TypeScript SDK in particular places a significant burden on the MCP Server developer to control "sessions". Client SDKs tend to combine the "connect" and current "initialize" operations leaving session establishment to the transport layer. + +For MCP Servers, developer experience could be simplified by using standard patterns in the SDK to suggest that Sessions are either: + - Required - the SDK will provide hooks and ensure that requests are completed within the context of a Session. + - Not Required - the SDK will not enforce sessions for MCP operations. + - Managed - the MCP Server Author will handle the allocation of sessions (similar to existing Typescript SDK) -TODO -- enhance discussion here. + Clients can use the following patterns, and discover whether Sessions are required by making a call or probing the proposed `/discover` endpoint: + - Single - the Client will provide a single Session for the connection, or no session if not required. This is similar to existing behaviour. + - Managed - the Client will provide an explicit `session.create` operation to return a token, an interface for storage and a reusable token for Client management. ### resourceAllocation Tool Hint From 92ff4ffda416da499e84d0726e152594decaad1b Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:46:01 +0100 Subject: [PATCH 25/35] some updates --- proposals/0000-data-layer-sessions.md | 154 ++++++++++++++++++++++++-- 1 file changed, 142 insertions(+), 12 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index d00c8dd..4f6f342 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -9,9 +9,7 @@ This proposal introduces application level sessions within the MCP Data Layer. Sessions are created by the Client, and allow the Server to store an opaque state token. -This proposal should be reviewed alongside SEP-1442, and assumes that the `initialize` operation and associated StreamableHTTP session creation is deprecated. - -[Further context to be added] +This proposal should be reviewed alongside SEP-1442. It assumes that the legacy initialize operation—and its side effect of implicit transport-level session creation (e.g., in Streamable HTTP)—is deprecated in favour of a stateless capability discovery mechanism (e.g., a /discover endpoint). ## Motivation @@ -88,7 +86,6 @@ The Client **MUST** securely associate retained sessions with the issuing Server To use a Session the Client request includes SessionMetadata in `_meta["io.modelcontextprotocol/session"]`: 1. The Server MUST treat that sessionId as the session context for processing the request. -1. Succesful responses MUST include \_meta["io.modelcontextprotocol/session"]. 1. The `sessionId` in the response MUST exactly match the sessionId from the request. 1. The receiver MUST NOT substitute, rotate, or rewrite `sessionId` in the response. @@ -116,6 +113,28 @@ To use a Session the Client request includes SessionMetadata in `_meta["io.model **Response:** +```json +{ + "jsonrpc": "2.0", + // JSON-RPC RequestId is used for session association + "id": 3, + "result": { + "content": [ + { + "type": "text", + "text": "Found 3 matches." + } + ], + } +} +``` + +#### Updating Session Metadata + +Servers **MAY** update Session Metadata by including _meta["io.modelcontextprotocol/session"] in a response to a Client request: + +**Response:** + ```json { "jsonrpc": "2.0", @@ -138,12 +157,111 @@ To use a Session the Client request includes SessionMetadata in `_meta["io.model } ``` -1. The Client **MUST** update the `state` value if updated by the Server. See note on [Ordering](#session-update-sequencing)) +1. The Client **MUST** attempt to update the `state` value if updated by the Server. See note on [Ordering](#session-update-sequencing)) +1. Servers **MUST NOT** include session metadata updates in notifications. 1. The Client **MAY** update the `expiresAt` value if updated by the Server. +#### Receiving Notifications + +Clients can subscribe to notifications associated with one or more sessions using `messages/listen`. + +Good catch on point 3. The "drop silently" language is in this sentence from my draft: + +> If no listen stream is open for the session, the server **MAY** drop notifications silently. The Client **SHOULD** re-fetch any subscribed resources when re-opening a listen stream. + +That's application-layer guidance that conflicts with what the transport already provides — Streamable HTTP's SSE supports `Last-Event-ID` for resumption, so the transport handles reconnection and missed-event recovery. We shouldn't be re-specifying that here. + +Here's the updated section with all three points addressed: + +--- + +#### Receiving Notifications + +Clients subscribe to server-initiated notifications using `messages/listen`. This opens a persistent delivery channel (SSE stream over HTTP/logical subscription over STDIO). + +A `messages/listen` request is scoped to one of: + +- **Single Session** — receives notifications relevant to that session. +- **Global** — receives broadcast notifications **not** scoped to any session. + +##### Opening a Notification Listener + +**Request (Session Scoped):** +```json +{ + "jsonrpc": "2.0", + "id": 10, + "method": "messages/listen", + "params": { + "_meta": { + "io.modelcontextprotocol/session": { + "sessionId": "sess-a1b2c3d4e5f6", + "state": "bGFuZ3VhZ2U9ZW4=" + } + } + } +} +``` + +**Request (Global):** + +```json +{ + "jsonrpc": "2.0", + "id": 11, + "method": "messages/listen", + "params": {} +} +``` + +The server confirms the stream is open with a `notifications/messages/listen` notification as the **first event**: + +```json +{ + "jsonrpc": "2.0", + "method": "notifications/messages/listen", +} +``` + +Subsequent events on this stream are notifications and server-to-client requests scoped to that session. For example, a resource update: + +```json +{ + "jsonrpc": "2.0", + "method": "notifications/resources/updated", + "params": { + "uri": "file:///project/config.yaml" + } +} +``` + + +Global listeners receive **only** broadcast notifications that are not scoped to any session — for example, `notifications/tools/list_changed`. Session-scoped notifications such as resource updates are **never** delivered on a global listener. + +##### Interaction with Resource Subscriptions + +`messages/listen` is the **delivery channel**; `resources/subscribe` is the **subscription mechanism**. + +Resource subscriptions **MAY** be associated with a session. If the `resources/subscribe` request includes session metadata, the resulting notifications/resources/updated notifications are delivered on that session's listen stream. If no session is specified, notifications are delivered on a global listener. + +Servers SHOULD define a lifecycle policy for subscriptions — for example, scoping them to a session if one exists, or applying a TTL for sessionless subscriptions. + +The typical flow: + +1. The Client creates a session (`sessions/create`). +2. The Client opens a listener (`messages/listen` scoped to that session). +3. The Client subscribes to a resource (`resources/subscribe` with the session in `_meta`). +4. When the resource changes, the Server sends `notifications/resources/updated` on the session's listen stream. + +Reconnection and missed-event recovery for the listen stream are handled at the **transport layer** (e.g., SSE `Last-Event-ID` for Streamable HTTP). + +##### STDIO Transport Behaviour + +For STDIO, `messages/listen` acts as a capabilities check. The Client sends the request; the Server responds with `notifications/messages/listen`. The Server **MAY** then send notifications for the declared scope at any time for the duration of the STDIO connection, as it does today. + #### Deleting Sessions -Clients **SHOULD** delete sessions that are no longer required to allow the Server to reclaim unneeded resources. +Clients **SHOULD** delete sessions that are no longer required to allow the Server to reclaim unneeded resources. **Request:** @@ -190,6 +308,7 @@ Clients **SHOULD** delete sessions that are no longer required to allow the Serv 1. Unknown sessions **MUST** result in a `-32043 SESSION_NOT_FOUND` Error. The Client **SHOULD** treat the Session as permanently invalidated. 1. The Server **MAY** revoke a Session at any time by returning an `-32043 SESSION_NOT_FOUND` Error. +1. The Server **SHOULD** implement a policy to remove stale Server maintained session state. ### Data Types @@ -343,11 +462,12 @@ This proposal adopts a similar pattern (server-issued opaque tokens that clients ### Session Update Sequencing -There are no ordering guarantees for requests/responses, meaning a Last-Write-Wins strategy by default. Servers should be aware of this potential race condition and include appropriate mitigations if needed. +Because MCP clients may execute multiple requests concurrently (e.g., parallel tool calling), there are no inherent ordering guarantees for request/response cycles. This creates a Last-Write-Wins race condition if the server relies entirely on the client-echoed state token to manage highly mutable data. Additionally Client state saving error conditions are not known to the Server. + +To resolve this, servers dealing with concurrent mutations SHOULD NOT rely on the state token. Instead, the server SHOULD use the sessionId as a lookup key for a server-side state management mechanism (e.g., a database, cache, or in-memory store). -A future design may introduce a monotonic state sequence to allow the client to identify the ordering of state content. +Using server-side state naturally delegates concurrency control to the server environment, keeping the MCP client implementation simple and eliminating the need for sequence tracking within the protocol layer. The state token remains available strictly for simple, stateless server deployments where concurrent mutations are not expected. -Servers that have this as a critical requirement should manage session state at the server. ### Use of in-band Tool Call ID @@ -378,14 +498,24 @@ For MCP Servers, developer experience could be simplified by using standard patt - Single - the Client will provide a single Session for the connection, or no session if not required. This is similar to existing behaviour. - Managed - the Client will provide an explicit `session.create` operation to return a token, an interface for storage and a reusable token for Client management. -### resourceAllocation Tool Hint +Because MCP clients may execute multiple requests concurrently (e.g., parallel tool calling), there are no inherent ordering guarantees for request/response cycles. This creates a Last-Write-Wins race condition if the server relies entirely on the client-echoed state token to manage highly mutable data. + +To resolve this, servers dealing with concurrent mutations SHOULD NOT rely on the state token. Instead, the server SHOULD use the sessionId as a lookup key for a server-side state management mechanism (e.g., a database, cache, or in-memory store). + +Using server-side state naturally delegates concurrency control to the server environment, keeping the MCP client implementation simple and eliminating the need for sequence tracking within the protocol layer. The state token remains available strictly for simple, stateless server deployments where concurrent mutations are not expected. +### Notifications for Multiple Sessions + +One `messages/listen` stream per session is a deliberate choice: -**For discussion** - it may make sense to include a tool hint to indicate whether or not a Session is associated with expensive resources, hinting that deletion is preferred at the end of the immediate User interaction session. +1. SSE streams are immutable once opened — session lists cannot be modified without reconnection. +1. HTTP/2 multiplexing makes concurrent streams inexpensive for HTTP transports. ## Backward Compatibility -TODO -- incorporate support matrix. +### Session Creation for pre SEP-1442 servers: +If a client attempts to invoke sessions/create or utilize session metadata on a server that does not support this extension, the server MUST reject the request with a standard JSON-RPC -32601 Method not found error. +ok ## Test Vectors From 920fc0f82a78a84f27ca0f3737bf8db36532e0fa Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:01:44 +0100 Subject: [PATCH 26/35] add session hijacking link --- proposals/0000-data-layer-sessions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 4f6f342..71ab5a6 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -319,7 +319,7 @@ _The following notes on sessionId are taken from the existing Streamable HTTP Tr **sessionId:** 1. The `sessionId` **SHOULD** be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash). 1. The `sessionId` MUST only contain visible ASCII characters (ranging from 0x21 to 0x7E). -1. The client MUST handle the `sessionId` in a secure manner, see Session Hijacking mitigations for more details. ( +1. The client MUST handle the `sessionId` in a secure manner, see [Session Hijacking](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices#session-hijacking) mitigations for more details. **expiresAt:** 1. `expiresAt` is a hint that Clients **MAY** use to inform Users of potentially stale sessions. From 62aa6aa0cf181f9ad0ecee2969f3759b22a11f97 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:31:45 +0000 Subject: [PATCH 27/35] commit from l/t push --- proposals/0000-data-layer-sessions.md | 31 ++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 71ab5a6..c2b2ca0 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -17,13 +17,42 @@ MCP Sessions are currently either implicit (STDIO), or constructed as a side eff Migrating sessions to the MCP data layer allows MCP applications to handle sessions as part of their domain logic, decoupled from the transport layer. This enables predictable session semantics, especially for Hosts that handle multiple "threads" of context within the application. The ability for MCP Servers to allocate resources on a per-session basis, or be able to provide rich functionality without server-side storage makes MCP suitable for increasingly sophisticated and scaled deployments. + +## Use Cases and Scope + +### Scope + +The current Transport specification defines sessions as follows: + +> An MCP “session” consists of logically related interactions between a client and a server, beginning with the initialization phase. To support servers which want to establish stateful sessions: + +Sessions allow Clients and Servers to maintain shared contextual state across sequences of Tool Calls + - Maintain shared contextual state between Tool Calls. + - Rehydrate application state on resumption. + - Servers may choose to customise MCP Server features based on the presence or absence of a Session. + - Servers may scope Notifications to specific sessions. + +Sessions are _not_ intended to provide a guarantee of MCP Protocol State. For example: + - Tools Lists + - Prompt Lists + - Resource Availability. + +### Use Cases + +Below are some sample use-cases where a session abstraction makes sense: + +- Shopping Cart. Maintaining integrity between different Chat Threads/Conversations. +- Contextual Documentation Retrieval, Conversational Subagent. A Server that adjusts its Tool Call Results based on earlier queries to avoid repetition of content. +- Playwright Testing Server. Being able to manage multiple parallel sessions without confusion. +- Managed Runtime Environment (sandbox). Allocating a stateful runtime environment to allow the LLM to coordinate and execute code and instructions. + ## Specification ### User Interaction Model Sessions are designed to be **application-driven**, with host applications determining how and when to establish sessions based on their need. -It is normally expected that applications will establish one session per conversation thread or task, but this is not required. +It is normally expected that applications will establish one session per context window, but this is not required. MCP Servers may wish to offer capabilities in a mixture of authentication and session modalities. From cc8737d5d377a7273001c2b2565f52071d653cef Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:07:16 +0000 Subject: [PATCH 28/35] session id requirement --- proposals/0000-data-layer-sessions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index c2b2ca0..3e60cd3 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -335,9 +335,9 @@ Clients **SHOULD** delete sessions that are no longer required to allow the Serv } ``` -1. Unknown sessions **MUST** result in a `-32043 SESSION_NOT_FOUND` Error. The Client **SHOULD** treat the Session as permanently invalidated. -1. The Server **MAY** revoke a Session at any time by returning an `-32043 SESSION_NOT_FOUND` Error. -1. The Server **SHOULD** implement a policy to remove stale Server maintained session state. +1. The Server **MAY** respond with a `-32043 SESSION_NOT_FOUND` Error if it it considers the Session identifier invalid. +1. Clients **SHOULD** consider the receipt of `-32043 SESSION_NOT_FOUND` to indicate that the Session is not recognised by the Server. +1. Servers and Clients **SHOULD** implement a policy to remove stale Server maintained session state. ### Data Types From b6361b96cff326751c29764a9075375aae9a0776 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:08:57 +0000 Subject: [PATCH 29/35] incorporate wording suggestion. --- proposals/0000-data-layer-sessions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 3e60cd3..b89a6f6 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -7,7 +7,7 @@ ## Abstract -This proposal introduces application level sessions within the MCP Data Layer. Sessions are created by the Client, and allow the Server to store an opaque state token. +This proposal introduces application level sessions within the MCP Data Layer. Sessions are created by the Client, and allow the Server to send to the Client an opaque state token that will be sent back for each relevant request. This proposal should be reviewed alongside SEP-1442. It assumes that the legacy initialize operation—and its side effect of implicit transport-level session creation (e.g., in Streamable HTTP)—is deprecated in favour of a stateless capability discovery mechanism (e.g., a /discover endpoint). From cdab978dd81ebd09ec53b7511eb38f0b3fdf396c Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:15:54 +0000 Subject: [PATCH 30/35] remove transport concern from message/listen, refer to 1442. --- proposals/0000-data-layer-sessions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index b89a6f6..8a3d24e 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -206,7 +206,7 @@ Here's the updated section with all three points addressed: #### Receiving Notifications -Clients subscribe to server-initiated notifications using `messages/listen`. This opens a persistent delivery channel (SSE stream over HTTP/logical subscription over STDIO). +Clients subscribe to server-initiated notifications using `messages/listen`. A `messages/listen` request is scoped to one of: From 4c5713a7d20554c420e0d15770f1460dd42d3496 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:00:41 +0000 Subject: [PATCH 31/35] clarify scoping, remove llmslop. --- proposals/0000-data-layer-sessions.md | 49 ++++++++++++--------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 8a3d24e..7293584 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -9,7 +9,7 @@ This proposal introduces application level sessions within the MCP Data Layer. Sessions are created by the Client, and allow the Server to send to the Client an opaque state token that will be sent back for each relevant request. -This proposal should be reviewed alongside SEP-1442. It assumes that the legacy initialize operation—and its side effect of implicit transport-level session creation (e.g., in Streamable HTTP)—is deprecated in favour of a stateless capability discovery mechanism (e.g., a /discover endpoint). +This proposal should be reviewed alongside SEP-1442. It is designed to align with stateless capability discovery and move session semantics into the MCP data layer. Deprecation or removal of legacy transport-level session establishment remains dependent on SEP-1442 (including the removal of `initialization` handshake). ## Motivation @@ -26,16 +26,18 @@ The current Transport specification defines sessions as follows: > An MCP “session” consists of logically related interactions between a client and a server, beginning with the initialization phase. To support servers which want to establish stateful sessions: -Sessions allow Clients and Servers to maintain shared contextual state across sequences of Tool Calls - - Maintain shared contextual state between Tool Calls. - - Rehydrate application state on resumption. - - Servers may choose to customise MCP Server features based on the presence or absence of a Session. - - Servers may scope Notifications to specific sessions. +Sessions allow Clients and Servers to bind a sequence of MCP requests into an application-defined context recognized by the Server. A session can scope: + - request processing state across multiple operations; + - server-managed resources or allocations associated with that context; + - subscriptions and delivery of server-initiated messages related to that context; and + - optional Server behaviour that depends on the presence of a Session (for example the unlocking of administration tools post elicitation). -Sessions are _not_ intended to provide a guarantee of MCP Protocol State. For example: - - Tools Lists - - Prompt Lists - - Resource Availability. +Sessions may influence how the Server evaluates requests and responses, but it does not provide a guarantee of MCP Protocol State, including: +- Tool Lists +- Prompt Lists +- Resource Availability. + +Existing `list_changed` notifications scoped to the Session continue to function as cache invalidation hints and not state transitions. ### Use Cases @@ -104,7 +106,7 @@ Clients begin a session with an MCP Server by calling `sessions/create`. The Client **MUST NOT** send `io.modelcontextprotocol/session` with the sessions/create request. -The Client **MUST** securely associate retained sessions with the issuing Server. The Client will typically establish identity through a mixture of _connection target_ and _user identity_. +The Client **MUST** securely associate retained sessions with the issuing Server. The Client will typically establish identity through a mixture of _connection target_ and _user identity_. In practice, that identity will typically be derived from configuration details such as server URL/origin, authentication context, and user/account identity. `expiresAt` is a hint, and may be updated by the Server in future responses. The Host **MAY** use the `expiresAt` to indicate potentially stale sessions to the User. @@ -194,16 +196,6 @@ Servers **MAY** update Session Metadata by including _meta["io.modelcontextproto Clients can subscribe to notifications associated with one or more sessions using `messages/listen`. -Good catch on point 3. The "drop silently" language is in this sentence from my draft: - -> If no listen stream is open for the session, the server **MAY** drop notifications silently. The Client **SHOULD** re-fetch any subscribed resources when re-opening a listen stream. - -That's application-layer guidance that conflicts with what the transport already provides — Streamable HTTP's SSE supports `Last-Event-ID` for resumption, so the transport handles reconnection and missed-event recovery. We shouldn't be re-specifying that here. - -Here's the updated section with all three points addressed: - ---- - #### Receiving Notifications Clients subscribe to server-initiated notifications using `messages/listen`. @@ -213,6 +205,8 @@ A `messages/listen` request is scoped to one of: - **Single Session** — receives notifications relevant to that session. - **Global** — receives broadcast notifications **not** scoped to any session. +The transport determines how those messages are delivered after registration. This proposal defines the logical scoping rules, not a transport-specific streaming mechanism. + ##### Opening a Notification Listener **Request (Session Scoped):** @@ -286,7 +280,7 @@ Reconnection and missed-event recovery for the listen stream are handled at the ##### STDIO Transport Behaviour -For STDIO, `messages/listen` acts as a capabilities check. The Client sends the request; the Server responds with `notifications/messages/listen`. The Server **MAY** then send notifications for the declared scope at any time for the duration of the STDIO connection, as it does today. +For STDIO, `messages/listen` does not create a separate transport channel; it registers notification scope on the existing connection. The Server acknowledges the registration with `notifications/messages/listen`, after which it **MAY** send server-initiated messages for that registered scope over the same STDIO connection. #### Deleting Sessions @@ -335,10 +329,11 @@ Clients **SHOULD** delete sessions that are no longer required to allow the Serv } ``` -1. The Server **MAY** respond with a `-32043 SESSION_NOT_FOUND` Error if it it considers the Session identifier invalid. +1. The Server **MAY** respond with a `-32043 SESSION_NOT_FOUND` Error if it considers the Session identifier invalid. 1. Clients **SHOULD** consider the receipt of `-32043 SESSION_NOT_FOUND` to indicate that the Session is not recognised by the Server. 1. Servers and Clients **SHOULD** implement a policy to remove stale Server maintained session state. + ### Data Types #### SessionMetadata @@ -541,10 +536,11 @@ One `messages/listen` stream per session is a deliberate choice: ## Backward Compatibility -### Session Creation for pre SEP-1442 servers: +### Servers without session support -If a client attempts to invoke sessions/create or utilize session metadata on a server that does not support this extension, the server MUST reject the request with a standard JSON-RPC -32601 Method not found error. -ok +If a client attempts to invoke `sessions/create` on a server that does not advertise the `sessions` capability, the server MUST reject the request with a standard JSON-RPC `-32601 Method not found` error. + +If a client sends session metadata to a server that does not support this extension, the server SHOULD reject the request using existing JSON-RPC or application-defined error handling. ## Test Vectors @@ -583,4 +579,3 @@ ok ```json {"jsonrpc":"2.0","id":3,"error":{"code":-32043,"message":"Session not found","data":{"sessionId":"sess-invalid"}}} ``` - From c0afe4aaf2fb834c93b5ebed79a3a93e8a861fe7 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:23:36 +0000 Subject: [PATCH 32/35] session example --- proposals/0000-data-layer-sessions.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 7293584..b55ff3e 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -24,19 +24,29 @@ Migrating sessions to the MCP data layer allows MCP applications to handle sessi The current Transport specification defines sessions as follows: -> An MCP “session” consists of logically related interactions between a client and a server, beginning with the initialization phase. To support servers which want to establish stateful sessions: +> An MCP “session” consists of logically related interactions between a client and a server, beginning with the initialization phase. + +With the removal of the `initalization` phase and associated lifecycle semantics, data-lyer sessions are scoped as follows: Sessions allow Clients and Servers to bind a sequence of MCP requests into an application-defined context recognized by the Server. A session can scope: - - request processing state across multiple operations; + - processing state across multiple operations; - server-managed resources or allocations associated with that context; - subscriptions and delivery of server-initiated messages related to that context; and - - optional Server behaviour that depends on the presence of a Session (for example the unlocking of administration tools post elicitation). Sessions may influence how the Server evaluates requests and responses, but it does not provide a guarantee of MCP Protocol State, including: - Tool Lists - Prompt Lists - Resource Availability. +Examples: + +- processing state across multiple operations; + - Entries in to a journal via an `add` tool +- server-managed resources or allocations associated with that context; + - A tool to create a remote sandbox that adds a `resource` entry which allows reading log tails from a specific URI. +- subscriptions and delivery of server-initiated messages related to that context + - Tool or Prompt list cache invalidation notifications + Existing `list_changed` notifications scoped to the Session continue to function as cache invalidation hints and not state transitions. ### Use Cases From a1812498ecede421b56028aec6f57f8a1d43101c Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:27:52 +0000 Subject: [PATCH 33/35] add note --- proposals/0000-data-layer-sessions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index b55ff3e..7af2f3d 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -28,7 +28,7 @@ The current Transport specification defines sessions as follows: With the removal of the `initalization` phase and associated lifecycle semantics, data-lyer sessions are scoped as follows: -Sessions allow Clients and Servers to bind a sequence of MCP requests into an application-defined context recognized by the Server. A session can scope: +Sessions allow Clients and Servers to bind a sequence of MCP requests into an application-defined context recognized by the Server. Sessions provide contextual association, not snapshot semantics. A session can scope: - processing state across multiple operations; - server-managed resources or allocations associated with that context; - subscriptions and delivery of server-initiated messages related to that context; and From f64e6defbb64047c067cfb43f8e93dd559381037 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:28:29 +0000 Subject: [PATCH 34/35] consistency addition --- proposals/0000-data-layer-sessions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 7af2f3d..474eb5f 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -28,7 +28,7 @@ The current Transport specification defines sessions as follows: With the removal of the `initalization` phase and associated lifecycle semantics, data-lyer sessions are scoped as follows: -Sessions allow Clients and Servers to bind a sequence of MCP requests into an application-defined context recognized by the Server. Sessions provide contextual association, not snapshot semantics. A session can scope: +Sessions allow Clients and Servers to bind a sequence of MCP requests into an application-defined context recognized by the Server. Sessions provide contextual association, not snapshot semantics or protocol state consistency guarantees. A session can scope: - processing state across multiple operations; - server-managed resources or allocations associated with that context; - subscriptions and delivery of server-initiated messages related to that context; and From 9566a1be1029d6a8c32736438f2e29f3541f32bd Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:00:23 +0000 Subject: [PATCH 35/35] include SESSION_REQUIRED --- proposals/0000-data-layer-sessions.md | 44 ++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/proposals/0000-data-layer-sessions.md b/proposals/0000-data-layer-sessions.md index 474eb5f..a9e3d40 100644 --- a/proposals/0000-data-layer-sessions.md +++ b/proposals/0000-data-layer-sessions.md @@ -128,7 +128,7 @@ To use a Session the Client request includes SessionMetadata in `_meta["io.model 1. The Server MUST treat that sessionId as the session context for processing the request. 1. The `sessionId` in the response MUST exactly match the sessionId from the request. -1. The receiver MUST NOT substitute, rotate, or rewrite `sessionId` in the response. +1. For operations that require a Session, if the request does not include `_meta["io.modelcontextprotocol/session"]`, the Server **MUST** reject the request with `SESSION_REQUIRED`. **Request:** @@ -201,6 +201,7 @@ Servers **MAY** update Session Metadata by including _meta["io.modelcontextproto 1. The Client **MUST** attempt to update the `state` value if updated by the Server. See note on [Ordering](#session-update-sequencing)) 1. Servers **MUST NOT** include session metadata updates in notifications. 1. The Client **MAY** update the `expiresAt` value if updated by the Server. +1. The receiver **MUST NOT** substitute, rotate, or rewrite `sessionId` in the response. #### Receiving Notifications @@ -325,6 +326,32 @@ Clients **SHOULD** delete sessions that are no longer required to allow the Serv #### Errors +##### Required + +```json +/** @internal */ +export const SESSION_REQUIRED = -32044; + +/** +* An error response indicating that the request requires a session. +* +* @category Errors +*/ +export interface SessionRequiredError extends Omit< + JSONRPCErrorResponse, + "error" +> { + error: Error & { + code: typeof SESSION_REQUIRED; + data?: { + [key: string]: unknown; + }; + }; +} +``` + +##### Not Found + ```json { "jsonrpc": "2.0", @@ -344,6 +371,8 @@ Clients **SHOULD** delete sessions that are no longer required to allow the Serv 1. Servers and Clients **SHOULD** implement a policy to remove stale Server maintained session state. + + ### Data Types #### SessionMetadata @@ -578,6 +607,19 @@ If a client sends session metadata to a server that does not support this extens {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"hi"}],"_meta":{"io.modelcontextprotocol/session":{"sessionId":"sess-abc123","state":"eyJrIjoidjIifQ==","expiresAt":"2026-03-01T00:00:00Z"}}}} ``` + +### Session Required + +**Request without a session:** +```json +{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"echo","arguments":{}}} +``` + +**Error Response:** +```json +{"jsonrpc":"2.0","id":4,"error":{"code":-32044,"message":"Session required"}} +``` + ### Session Not Found **Request with invalid session:**