laravel/mcp: Remove server session state for the stateless transports
Contributed by pushpak1300
Part of the mcp v1.0.1 release
Remove server session state for the stateless transports
PR #285 strips every trace of session state from laravel/mcp's server transports, aligning the package with the stateless model in the MCP 2026-07-28 specification. In that revision, a server has no session to hold onto: each message stands alone, and the session id that used to travel with a request has nothing left to identify. If you are upgrading from v0.9.6, this is a breaking change — but a mechanical one. Unless you built a custom transport or read the session id inside a tool, the migration mostly consists of deleting code.
Why the spec dropped sessions
Under earlier revisions of the MCP spec, an HTTP client called initialize, received a MCP-Session-Id response header, and echoed that id back on every subsequent request so the server could look up its state. The 2026-07-28 changelog removes that model for stateless transports: no handshake, no negotiated id, no server-side session store.
Laravel's MCP servers already handled each request independently through the framework's request lifecycle, so the session plumbing was bookkeeping with no payoff. It also forced clients to perform an initialization round trip that served no purpose before real work could start.
What was removed
Request::sessionId()andRequest::setSessionId()are gone, and theRequestconstructor no longer accepts a$sessionIdparameter.- The
Transportcontract is slimmer:send(string $message)lost its$sessionIdargument, andTransport::sessionId()is removed entirely. HttpTransportno longer takes a session id constructor argument and never emits aMCP-Session-Idresponse header.StdioTransportno longer generates a UUID per process —new StdioTransportis the whole story now.- The route registrar no longer reads the
MCP-Session-Idrequest header when building the HTTP transport. JsonRpcRequest::from()lost its$sessionIdparameter, so session ids never enter the message pipeline in the first place.
Runtime behavior after the upgrade
New feature tests lock in the stateless contract. Posting a request with a stale MCP-Session-Id header still returns 200 — the header is simply ignored — and the response asserts assertHeaderMissing('MCP-Session-Id'). A second test fires two requests back to back with no prior handshake and both succeed, so stateless clients (and clients still sending the old header out of habit) keep working uninterrupted.
The PR also confirms nothing else needed removing: Last-Event-ID and SSE event ids were never implemented in the first place, and GET/DELETE requests on an MCP route already answer 405, which the stateless profile expects.
Upgrading tool code
Any tool that touched the session id will break once you upgrade:
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
class OrderHistoryTool extends Tool
{
public function handle(Request $request): Response
{
return Response::text('Session: '.$request->sessionId()); // fatal: method removed
}
}
Remove the call. If you were using the session id as a proxy for client identity, lean on Laravel authentication instead — $request->user() is available inside tools:
public function handle(Request $request): Response
{
return Response::text('Orders for: '.$request->user()?->email);
}
Upgrading custom transports
If you implement Laravel\Mcp\Server\Contracts\Transport, two changes are required:
// Before
public function send(string $message, ?string $sessionId = null): void;
public function sessionId(): ?string;
// After
public function send(string $message): void;
Likewise, drop the constructor argument wherever transports are built directly — new HttpTransport(request()) in a route or test, new StdioTransport for local servers. The package's own FakeTransporter and ArrayTransport test fixtures were updated the same way, so they make a handy reference for the minimal compliant implementation.
Takeaways
- The MCP
2026-07-28spec makes stateless servers the norm; laravel/mcp no longer reads, stores, or emits session ids anywhere. Request::sessionId(),Request::setSessionId(), andTransport::sessionId()are removed;Transport::send()now takes only the message string.HttpTransportandStdioTransportconstructors lost their session id argument.- Inbound
MCP-Session-Idheaders are silently ignored and never echoed back; no handshake is required before calling tools, resources, or prompts. - Replace session-based identity with
$request->user()or your own authentication layer. - Thanks to @JordanDalton, whose #275 mapped out the 2026-07-28 work this PR completes.