Stateless servers, searchable tool catalogs, and OAuth with mandatory PKCE — what the first stable release actually changes for anyone exposing app data to agents.
A staging MCP server that worked perfectly on laravel/mcp 0.9 returns HTTP 400 on every single tools/call after a routine composer update. The JSON-RPC error code is -32020. Nothing in the application code changed — the tools are the same classes, the server is the same server, the agent connecting to it is the same agent. What changed is that 1.0 validates a set of mirrored request headers on every POST that 0.9 never asked for, and a client that doesn’t send matching MCP-Protocol-Version and Mcp-Method headers — plus an Mcp-Name header on calls like tools/call — is now rejected outright rather than accommodated.
That’s the shape of this release in one example: laravel/mcp 1.0 isn’t a feature release with a version number attached, it’s a protocol migration. It ships MCP revision 2026-07-28, which replaces the initialize handshake with server/discover, removes session tracking entirely, mandates PKCE for OAuth, and introduces searchable tool catalogs that change what an agent actually sees when it connects. Anyone running an MCP server on 0.x is not doing a version bump. They’re migrating to a different protocol that happens to share a package name.
Stateless Servers — The Breaking Change That Isn’t Optional
The single largest structural change: under protocol 2026-07-28, the server processes each request independently, with no server-side session state at all. Every HTTP request and stdio message now carries the protocol version and the client’s supported features in params._meta, rather than establishing that once during an initialize exchange and having the server remember it.
That means a specific set of APIs is simply gone:
// ❌ All of these were removed in 1.0 — not deprecated, removed
$request->sessionId();
$request->setSessionId($id);
// The MCP-Session-Id header is no longer sent or read
// The SessionInitialized event no longer fires — any listener
// registered against it is now dead code that will never run
// ✅ Tracking related calls now means carrying your own identifier
// through the request arguments or _meta, explicitly
class GenerateReport extends Tool
{
public function handle(Request $request): ToolResult
{
// Pass a correlation ID as a normal tool argument, or read it
// from _meta — the server has no memory of a prior request
$correlationId = $request->get('correlation_id')
?? $request->meta('correlation_id');
// ... your logic, with whatever grouping you need done
// explicitly rather than inferred from a server-held session
}
}
The reason this is worth understanding rather than just patching around: any MCP server built on 0.x that relied on session state to carry context between calls — accumulating results across a multi-step agent interaction, caching an expensive lookup for the duration of a session, tracking which tools an agent had already invoked — has that assumption broken, not bent. The protocol no longer has a concept of “this request and that request belong to the same conversation” unless the application itself puts an identifier in the payload and carries it forward. This is a more honest architecture for a distributed, horizontally-scaled deployment (any server instance can handle any request, no sticky sessions, no shared session store) and it is genuinely more work for anyone who was leaning on session state to avoid passing context explicitly.
The header validation from the opening is the enforcement mechanism for this: POST requests under the new protocol need matching MCP-Protocol-Version and Mcp-Method headers, with Mcp-Name additionally required on calls like tools/call. A mismatch returns HTTP 400 with JSON-RPC error -32020 — loudly, immediately, rather than silently degrading, which is the right call for a protocol change this fundamental.
Legacy clients aren’t stranded, though. The release includes protocol era negotiation — a client sending initialize with an older protocol revision (2025-06-18, 2025-11-25) still negotiates as it did before, served alongside the modern protocol path. This is genuinely important for anyone whose server is consumed by agents they don’t control: upgrading the server to 1.0 doesn’t immediately break every older client pointed at it, even though the server’s own internal handling is now stateless.
Searchable Tool Catalogs — The Change Most Likely to Improve Real Agent Behavior
The feature with the largest practical effect on how well an agent actually performs against your server, and the one easiest to under-read as a minor convenience: instead of dumping the complete list of every tool a server exposes into the model’s context window at connection time, 1.0 lets an agent discover tools on demand through search_tools and execute_tools.
// A server with a partial default catalog — only the most commonly
// needed tools are listed up front; the rest are discoverable
class AppServer extends Server
{
public array $tools = [
// Listed immediately on connect — the entry points an agent
// almost always needs
SearchOrders::class,
GetCustomer::class,
];
// Everything else remains reachable via search_tools / execute_tools
// without consuming context window on every single connection
}
Why this matters more than it sounds: every tool listed at connection time costs context window — its name, description, and full input schema, for every tool, on every connection, before the agent has done anything at all. A server exposing forty tools burns a meaningful fraction of the model’s available context on a catalog it will mostly never touch during any given task, and that cost is paid on every single interaction. Worse, a large undifferentiated tool list measurably degrades tool selection accuracy — a model choosing between forty similarly-described tools makes worse choices than one choosing between four, which is a well-understood failure mode in agent design that has nothing to do with Laravel specifically.
Third-party packages have already adopted this pattern deliberately since 1.0 landed: the Statamic MCP server, for one, now lists only three tools on connect and exposes the other eight through search_tools and execute_tools — an explicit design choice to keep the connection-time context cost low rather than an accident of implementation.
Cache hints land alongside this, letting the server tell the client which responses are reusable and for how long, including privacy scope. For a tool returning data that genuinely doesn’t change per-request — a schema description, a static configuration lookup, a reference table — this is a direct reduction in redundant round trips, and Laravel’s own MCP client honors these hints when consuming another server.
OAuth: PKCE Is Now Mandatory, and Dynamic Client Registration Is on the Way Out
The OAuth changes are the ones most likely to break an integration with a third-party authorization server rather than your own code, and they’re stricter in a specific, deliberate way.
// Under 0.x: a server that listed code_challenge_methods_supported
// WITHOUT S256 was rejected. A server that omitted the field entirely
// was accepted.
// Under 1.0: OAuthClient::redirect() throws an OAuthException if the
// authorization server's metadata omits code_challenge_methods_supported
// at all — omission is no longer treated as "probably fine"
This closes a real gap. An authorization server that doesn’t advertise PKCE support might genuinely not support it, and proceeding on the assumption that silence means compatibility is exactly the kind of optimistic default that produces an authorization flow that appears to work while providing none of the protection PKCE exists to give. Failing at redirect() — before any user is sent anywhere — is the correct place to surface that.
Client ID Metadata Documents replace Dynamic Client Registration as the preferred path, with DCR deprecated under MCP 2026-07-28. For anyone whose OAuth flow currently relies on dynamic registration, this is the migration to plan for — not urgent today, since deprecated isn’t removed, but it’s the clearly signposted direction and it’s cheaper to move deliberately than under deadline pressure once removal actually lands.
The Upgrade, in the Order That Actually Works
1. Read the 1.0 upgrade guide first — it ships with the release, and
this is a protocol migration, not a version bump
2. Grep for the removed APIs — these are hard failures, not deprecations:
grep -rn "sessionId()\|setSessionId(\|SessionInitialized" app/
3. For every hit: replace session-derived context with an explicit
identifier carried in tool arguments or _meta
4. Audit any OAuth integration against a third-party authorization
server — confirm its metadata advertises code_challenge_methods_supported,
or redirect() will now throw where it previously proceeded
5. Decide your tool catalog shape deliberately — which tools are worth
context window on every connection, and which belong behind
search_tools? This is a design decision, not a default to accept
6. Test against BOTH protocol eras if external clients consume your
server — legacy initialize clients still negotiate, and that path
deserves its own test rather than an assumption
The step most likely to be skipped is step 5, because a tool catalog that “works” requires no attention — every tool listed, every connection, exactly as it behaved on 0.x. That’s the default, and it’s the one that quietly costs context window and tool-selection accuracy on every single agent interaction for as long as nobody revisits it. Treating the catalog shape as a deliberate design decision is the single highest-leverage thing in this entire upgrade that isn’t strictly forced by the protocol.
The One Rule
The subtitle’s framing — your app is now an AI tool whether you planned for it or not — is less about the release forcing anything on anyone and more about what a 1.0 actually signals: the experimental phase is over, the protocol has a stable shape, and exposing application data to agents has moved from something teams were prototyping to something teams are expected to run in production with real auth, real scale, and real failure modes. The stateless requirement, the mandatory PKCE, the header validation that returns a hard 400 instead of quietly accommodating a mismatch — none of those are conveniences. They’re the things a protocol adds when it stops assuming everyone using it is experimenting and starts assuming some of them are exposing a production database to an agent they don’t control. Upgrading is a migration. Treating it as a composer update is how a staging environment starts returning -32020 on every call with nothing in the diff to explain it.
