Repository navigation
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Sessionless requests can leave persisted ephemeral sessions unreachable until expiry and expose the wrong protocol version to handlers.
2 open issues
What changed in this PR
Adds opt-in sessionless handling for handshake-era Streamable HTTP clients, allowing horizontally scaled servers to serve them without shared session storage.
Changes:
- Adds
Builder::withoutSessions()and request-local session handling, while preserving default behavior. - Omits session headers, rejects GET and DELETE, and fails client-directed requests immediately.
- Documents the trade-offs and adds unit and integration coverage.
| File | Description |
|---|---|
tests/Unit/Server/Transport/SessionlessHttpTest.php |
Tests sessionless HTTP requests, streaming, and client-request rejection. |
tests/Unit/Server/ProtocolTest.php |
Checks that other transports retain session requirements. |
tests/Integration/SessionlessHttpTest.php |
Tests sessionless client communication over HTTP. |
tests/Integration/Fixture/http.php |
Enables sessionless mode in the shared fixture. |
src/Server/Transport/StreamableHttpTransport.php |
Suppresses session headers and rejects unsupported methods. |
src/Server/Protocol.php |
Adds ephemeral sessions and client-request guards. |
src/Server/Builder.php |
Exposes and wires withoutSessions(). |
docs/run/sessions.md |
Explains sessionless configuration and limitations. |
docs/run/server-builder.md |
Lists the new builder option. |
CHANGELOG.md |
Records sessionless HTTP support. |
Review effort: Balanced
|
Hi @ruudk, on the one hand i'm a bit scared about some side-effects here, but i think this is worth going into, so thanks for bringing this up. Some thoughts:
should we maybe remove the fallback session mechanism and session only gets activated is cc @soyuka opinion on that call? one layer of rework is missing tho: a lot of feature don't work without session in handshake era and would need error handling (fail loud):
We'd need some tests around that as well 🙏 |
6fe6535 to
bca19b6
Compare
|
Went with your proposal: |
bca19b6 to
1478479
Compare
|
@chr-hertel could you take another look? |
| { | ||
| return json_decode((string) $response->getBody(), true, flags: \JSON_THROW_ON_ERROR); | ||
| } | ||
| } |
There was a problem hiding this comment.
not sure we need that many tests
There was a problem hiding this comment.
🤖 Ruud's Claude here
Trimmed the duplicates: the supports*() checks share one code path and are covered by a single test now, and the raw-wire integration test is gone, as the unit tests cover the same responses.
| * | ||
| * It lives as long as its request and is never stored, so it cannot be saved. | ||
| */ | ||
| final class EphemeralSession extends Session |
There was a problem hiding this comment.
either remove final or create an interface ?
There was a problem hiding this comment.
🤖 Ruud's Claude here
Added EphemeralSessionInterface, which EphemeralSession implements and every check uses, so the class stays final.
| $capabilities = ($this->configuration->capabilities ?? new ServerCapabilities())->withoutListChanged(); | ||
|
|
||
| // Updates of a subscribed resource reach the client outside a request, which needs a session. | ||
| if ($session instanceof EphemeralSession) { |
There was a problem hiding this comment.
i dislike that its a class i'd prefer an interface here
There was a problem hiding this comment.
🤖 Ruud's Claude here
It checks against EphemeralSessionInterface now, like the other places.
| progress and log messages, still reach the client on that request's stream. Servers that need the rest call | ||
| `setSession()`. | ||
|
|
||
| The modern (`2026-07-28`) era has no sessions to begin with and is not affected, and neither is stdio, where the |
There was a problem hiding this comment.
indeed maybe this feature could even help us with migrating pre-(2026-07-28) servers. +1 to add this
A session is optional in the handshake era, but the SDK always created one on initialize and refused every later request without it. So a horizontally scaled server had to share a session store and lock only to serve those clients, and the in-memory fallback was lost between requests under PHP-FPM anyway. Sessions over Streamable HTTP are now turned on by Builder::setSession() only. Without it, initialize is answered without an Mcp-Session-Id, every later request runs on an EphemeralSession that is never stored, and GET and DELETE are answered with 405. What needs a session fails loud: requests to the client, the client capability checks, queued messages and saving the session throw a LogicException; input-required results, resource subscriptions and logging/setLevel are answered with -32603. The protocol version comes from the MCP-Protocol-Version header. Stdio and the 2026-07-28 era are not affected.
1478479 to
42a953f
Compare

Closes #644.
A session is optional in the handshake era: a server "MAY assign a session ID at initialization time". The SDK always creates one on
initializeand refuses every later request without it, so a horizontally scaled server has to share a session store and lock only to serve handshake-era clients.Sessions over Streamable HTTP are now only turned on by
Builder::setSession(). Without it:initializeis answered withoutMcp-Session-Id, and a session id the client sends anyway is ignored;EphemeralSessionthat is never stored, with the protocol version taken from theMCP-Protocol-Versionheader;GETandDELETEare answered with405, which the spec allows for a server without a stream and for one that does not let clients end sessions.What needs a session fails loud: requests to the client (elicitation, sampling,
roots/list), thesupports*()client capability checks, messages sent outside a request and saving the session throw aLogicException; input-required results,resources/subscribe/unsubscribeandlogging/setLevelare answered with-32603, and thesubscriberesource capability is not advertised. Progress and other notifications sent during a request still reach the client. The TypeScript SDK serves handshake-era traffic the same way withsessionIdGenerator: undefined.This is a BC break for HTTP servers that relied on the in-memory fallback session, which was lost between requests under PHP-FPM or
php -Sanyway; they keep sessions withsetSession(new InMemorySessionStore()). stdio and the2026-07-28era are not affected.