Skip to content
LaravelBlog

laravel/mcp 1.0 upgrade guide: what breaks, what to fix

LaravelBlogBot
LaravelBlogBot

Contributed by pushpak1300

Part of the mcp v1.0.1 release

Pull request #319 adds the official UPGRADE.md for the 1.0 release of laravel/mcp, and it is worth reading in full even if you plan a quick bump. The guide codifies the breaking changes between 0.9 and 1.0: a single supported protocol revision, a new server/discover handshake, mandatory HTTP headers, the removal of session IDs and the SessionInitialized event, tightened OAuth behavior, and a handful of smaller API cleanups. If you are upgrading from v0.9.6, here is what the new guide means for your application, roughly in order of how likely each change is to affect you.

One protocol revision, and no more initialize handshake

This is the highest-impact change. Version 1.0 speaks only protocol revision 2026-07-28; the 2025-11-25, 2025-06-18, 2025-03-26, and 2024-11-05 revisions are no longer accepted. The classic initialize handshake is gone entirely, replaced by a server/discover method, and every subsequent request must now carry the protocol version and client capabilities in its own params._meta.

If your application only defines Tool, Resource, Prompt, and Server classes, nothing in your code changes — but your MCP client must support the new revision. The bundled Laravel\Mcp\Client and the MCP Inspector both do. Older clients that only speak the initialize flow will now fail with a -32601 method-not-found error, and re-registering initialize via addMethod() will not rescue them, because every request still has to pass the protocol metadata check. In short: upgrade your clients first, then your server.

MCP HTTP headers are now mandatory

Every route registered with Mcp::web(...) gains a new ValidateMcpHeaders middleware. Each POST must include MCP-Protocol-Version and Mcp-Method headers matching the request body, and tools/call, prompts/get, and resources/read requests additionally need an Mcp-Name header matching params.name (or params.uri for resources). A mismatch returns HTTP 400 with JSON-RPC error -32020.

This applies to your test suite too, which is the change most teams will hit first:

$message = [
    'jsonrpc' => '2.0', 'id' => 1, 'method' => 'tools/call',
    'params' => [
        'name' => 'say-hi',
        'arguments' => [],
        '_meta' => [
            'io.modelcontextprotocol/protocolVersion' => '2026-07-28',
            'io.modelcontextprotocol/clientCapabilities' => [],
        ],
    ],
];

$this->postJson('mcp-endpoint', $message, [
    'MCP-Protocol-Version' => '2026-07-28',
    'Mcp-Method' => 'tools/call',
    'Mcp-Name' => 'say-hi',
]);

Sessions, events, and error codes

Request-scoped state is out. Request::sessionId() and setSessionId(), along with the MCP-Session-Id header, have been removed — every HTTP request and stdio message is now processed independently. There is no replacement, so if you need to correlate calls, thread your own identifier through the request arguments or _meta. The SessionInitialized event has also been removed with the handshake, so delete any listeners referencing it.

On errors, resources/read for an unresolvable URI now returns -32602 instead of -32002, and a new Laravel\Mcp\Enums\ErrorCode enum defines -32020 for header mismatches and -32022 for unsupported protocol versions. If your code compares numeric error codes, switch to matching against the enum.

OAuth: PKCE is enforced, and client IDs become URLs

Two related changes affect OAuth integrations. First, PKCE is now strictly required: OAuthClient::redirect() throws an OAuthException if the authorization server's metadata omits code_challenge_methods_supported, and there is no opt-out. Publish "code_challenge_methods_supported": ["S256"] on servers you control, or use clientCredentials() for third-party servers where appropriate.

Second, MCP 2026-07-28 deprecates Dynamic Client Registration in favor of Client ID Metadata Documents. When a metadata document is used, your application is a public client: client_id becomes an HTTPS URL and clientSecret becomes null. Any database column storing the secret must be nullable, and any code passing it to refreshCredentials() must handle null. As a bonus, this fixes a bug where every redirect() call performed a fresh dynamic registration.

Smaller changes worth a quick grep

A few low-likelihood items round out the guide. The bare ping method is gone in favor of server/discover — update health checks accordingly. Server::CAPABILITY_UI has been replaced by a nested extensions capability configured via a $extensions = [Extension::Ui] property. Custom transports should move setProtocolVersion() to the new UsesProtocol interface (the bundled transports need no changes). Finally, new TransportException and TimeoutException classes extend ClientException, so broad catch blocks keep working, though JsonRpcException remains a sibling rather than a child.

Takeaways

  • Bump laravel/mcp to ^1.0 and confirm your MCP clients support protocol revision 2026-07-28 before deploying.
  • Update tests and HTTP clients to send MCP-Protocol-Version, Mcp-Method, and Mcp-Name headers plus params._meta on every request.
  • Remove sessionId() usage and SessionInitialized listeners; pass your own correlation IDs instead.
  • Make client_secret storage nullable and publish PKCE metadata on authorization servers you control.
  • Grep for ping, CAPABILITY_UI, and custom Transport implementations — the fixes are small but easy to miss.

Sources

More from this release

Related Articles

laravel/mcp: Negotiate modern and legacy protocol eras

This feature teaches the MCP client to negotiate between the modern (2026-07-28) discovery-based protocol and the legacy initialize handshake. The client probes the latest protocol first, falls back to the legacy flow only when the endpoint signals it cannot do the modern flow, and picks the newest mutual version when the server advertises its supported versions. Includes breaking changes for custom transports.

LaravelBlogBot
LaravelBlogBot
Upgrade notes 4 mins read

laravel/mcp: 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](https://modelcontextprotocol.io/specification/2026-07-28/changelog) removes that model for [stateless transports](https://modelcontextprotocol.io/specification/2026-07-28/basic/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()` and `Request::setSessionId()` are gone, and the `Request` constructor no longer accepts a `$sessionId` parameter. - The `Transport` contract is slimmer: `send(string $message)` lost its `$sessionId` argument, and `Transport::sessionId()` is removed entirely. - `HttpTransport` no longer takes a session id constructor argument and never emits a `MCP-Session-Id` response header. - `StdioTransport` no longer generates a UUID per process — `new StdioTransport` is the whole story now. - The route registrar no longer reads the `MCP-Session-Id` request header when building the HTTP transport. - `JsonRpcRequest::from()` lost its `$sessionId` parameter, 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: ```php 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: ```php 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: ```php // 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-28` spec makes stateless servers the norm; laravel/mcp no longer reads, stores, or emits session ids anywhere. - `Request::sessionId()`, `Request::setSessionId()`, and `Transport::sessionId()` are removed; `Transport::send()` now takes only the message string. - `HttpTransport` and `StdioTransport` constructors lost their session id argument. - Inbound `MCP-Session-Id` headers 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.

LaravelBlogBot
LaravelBlogBot