Standup hears “Laravel MCP is broken.” Someone pasted a JSON-RPC log. The first method is initialize. The package pin on the host is the new stable line. The junior files a server-outage ticket. They ping the person who shipped the bump. They tell the coding agent to wait for a rollback.
I stop the run there. A client that still opens with initialize is not a broken Laravel MCP server. Official upgrade notes for the 14 September 2026 GitHub tag, published 14:39 UTC, not a prerelease, name the field: the server now speaks protocol revision 2026-07-28 by default, and server/discover replaces the old handshake for modern clients. The same notes say clients that still open with initialize continue to work. The server answers that handshake with 2025-11-25 or 2025-06-18, whichever the client requested. [Source: https://github.com/laravel/mcp/releases/tag/v1.0.0] [Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md]
Laravel News, 15 September 2026, repeats the same desk rule in one sentence: clients that still connect with initialize continue to work. [Source: https://laravel-news.com/laravel-mcp-1-0]
I already refused to treat an every-turn HTTP 400 as a dead gateway in Every-Turn HTTP 400 on ANTHROPIC_BASE_URL Is Not a Dead Gateway. I already refused to treat a five-minute WebFetch fail as a hung model in A 300-Second WebFetch Fail Is Not a Hung Model. This post is the same desk rule for an old handshake on a new MCP server. Print the method. Print the pin. Name who owns both.
The question is not whether the new handshake demos well in Inspector. The question is whether the named owner can still tell an old client from a dead server.

The handshake that still answers
Juniors read initialize the way they read a 404. The method name is old. The package is new. They assume the bump killed the client.
MCP is not a Laravel-only toy. It is a protocol with a handshake. The old shape is initialize, then tools/list. The new shape for protocol 2026-07-28 is server/discover, then every later request carries protocol version and client capabilities in params._meta. Official upgrade notes print both payloads. [Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md]
That is a client dialect. It is not “the server died in composer.”
The GitHub release for tag v1.0.0 lists pull request 341 in the same body: serve legacy initialize clients alongside the modern protocol. That line is the ticket. A log that still says initialize is evidence the old path is in use. It is not evidence the new path is missing. [Source: https://github.com/laravel/mcp/releases/tag/v1.0.0] [Source: https://github.com/laravel/mcp/pull/341]
If the first method is initialize and the server answers with 2025-11-25 or 2025-06-18, you do not have a broken server. You have an old client on a server that still speaks the old handshake.
I do not invent a fake overnight outage. I use the public contract. Official notes also say clients requesting 2025-03-26 or 2024-11-05 are offered 2025-11-25 instead. A request that already carries io.modelcontextprotocol/protocolVersion or io.modelcontextprotocol/clientCapabilities in _meta is treated as a 2026-07-28 request and validated as such. Those are routing rules. They are not a license to delete the old client from the wiki card. [Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md]
Three tickets, one owner
Laravel plus Vue work on this desk still runs coding agents next to HTTP MCP: a Mcp::web() route, a client that still opens with initialize, a junior who sees the old method after a composer bump and pages the wrong channel. Mixing “old handshake,” “header mismatch,” and “server process down” into one “MCP is broken” thread burns an hour of backend people on a client they do not own.
| Ticket | What it means | What you do | Owner |
|---|---|---|---|
First method is initialize, server answers 2025-11-25 or 2025-06-18 | Old client on a server that still serves the legacy handshake | Copy the method. Print the pin. Do not file the server | Named human who pinned laravel/mcp and the client |
HTTP 400, JSON-RPC -32020 | Header mismatch on a modern request | Fix MCP-Protocol-Version, Mcp-Method, and Mcp-Name to match the body | Named human who owns the HTTP client and the tests |
| No process, no route, composer pin missing | Then it is a server install ticket | Use the install path, not the handshake path | Named human who owns composer.lock |
Do not paste one initialize screenshot and call the package dead. If the body names initialize and the server answered a protocol the client asked for, you are on a client ticket. If the body is a modern tools/call with a 400 and -32020, you are on a header ticket. If the route 404s and composer show laravel/mcp is empty, you are on an install ticket. See Every-Turn HTTP 400 on ANTHROPIC_BASE_URL Is Not a Dead Gateway for the sibling habit: a 400 that still answers is not a dead host, and an old handshake that still answers is not a dead MCP server.
Pins are evidence, not the hook
v1.0.0 for laravel/mcp published at 2026-09-14T14:39:00Z, prerelease false. Packagist recorded the same version at 2026-09-14T14:35:19+00:00. Laravel News covered the stable line on 15 September 2026. Print composer show laravel/mcp on the host. Do not treat Packagist time as GitHub published_at. Do not put those numbers in the title. [Source: https://github.com/laravel/mcp/releases/tag/v1.0.0] [Source: https://packagist.org/packages/laravel/mcp] [Source: https://laravel-news.com/laravel-mcp-1-0]
What initialize actually buys you
I do not invent a fake dashboard. I use the public payloads.
Official upgrade notes split two dialects. Do not mix them.
- Legacy client — first method
initialize, protocol2025-11-25or2025-06-18in the handshake params. No_meta. No MCP HTTP headers required for the rest of that client’s requests. The server still serves it. [Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md] - Modern client — first method
server/discover, protocol2026-07-28inparams._meta, and every later request repeats that_meta. HTTP clients also sendMCP-Protocol-VersionandMcp-Method. Tool, prompt, and resource calls also sendMcp-Name. [Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md]
If your application only defines Tool, Resource, Prompt, and Server classes, official notes say no application-class changes are required as long as the MCP client supports 2026-07-28. Both Laravel\Mcp\Client and the MCP Inspector support that revision. That sentence is for app code. It is not a claim that every third-party client already speaks discover. [Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md]
Here is the official before and after, copied as a working JSON-RPC trace, not as a bypass:
1{
2 "legacy": {
3 "initialize": {
4 "jsonrpc": "2.0",
5 "id": 1,
6 "method": "initialize",
7 "params": {
8 "protocolVersion": "2025-11-25",
9 "capabilities": {},
10 "clientInfo": {"name": "my-client", "version": "desk"}
11 }
12 },
13 "tools_list": {
14 "jsonrpc": "2.0",
15 "id": 2,
16 "method": "tools/list",
17 "params": {}
18 }
19 },
20 "modern": {
21 "discover": {
22 "jsonrpc": "2.0",
23 "id": 1,
24 "method": "server/discover",
25 "params": {
26 "_meta": {
27 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
28 "io.modelcontextprotocol/clientCapabilities": {}
29 }
30 }
31 }
32 }
33}
[Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md]
A junior who only greps for initialize after the bump will always get a hit on the old client. That grep is not a health check. The health check is: did the server answer, and which dialect did it answer in.
Stateless is a separate field. Official notes removed Request::sessionId(), Request::setSessionId(), the MCP-Session-Id header, and the SessionInitialized event. Every HTTP request and stdio message is processed independently. If you need to correlate calls, pass your own identifier through arguments or _meta. That is not this ticket. Do not file “initialize is broken” because a listener on SessionInitialized went quiet. The event is gone. The handshake is not. [Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md] [Source: https://laravel-news.com/laravel-mcp-1-0]
The header 400 that is not this ticket
Modern HTTP clients live under ValidateMcpHeaders. Official notes apply that middleware to every route registered via Mcp::web(...). Each POST must include MCP-Protocol-Version and Mcp-Method whose values match the request body. Requests for tools/call, prompts/get, and resources/read must also include Mcp-Name matching params.name or, for resources/read, params.uri. [Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md]
A mismatch returns HTTP 400 with JSON-RPC error code -32020. Laravel News states the same code. Older clients that use initialize and send no protocol metadata in _meta are exempt from header validation. [Source: https://laravel-news.com/laravel-mcp-1-0] [Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md]
That 400 is a schema reject. It is the sibling of Every-Turn HTTP 400 on ANTHROPIC_BASE_URL Is Not a Dead Gateway. The host answered. The request was illegal for that dialect. It is not proof initialize is dead, and it is not proof the package failed to install.
Tests that still postJson() a modern tools/call without headers will fail after the bump. Official notes show the repair. Use it in the test suite the named owner actually runs:
1public function test_modern_tools_call_sends_matching_mcp_headers(): void
2{
3 $message = [
4 'jsonrpc' => '2.0',
5 'id' => 1,
6 'method' => 'tools/call',
7 'params' => [
8 'name' => 'say-hi',
9 'arguments' => [],
10 '_meta' => [
11 'io.modelcontextprotocol/protocolVersion' => '2026-07-28',
12 'io.modelcontextprotocol/clientCapabilities' => [],
13 ],
14 ],
15 ];
16
17 $this->postJson('mcp-endpoint', $message, [
18 'MCP-Protocol-Version' => '2026-07-28',
19 'Mcp-Method' => 'tools/call',
20 'Mcp-Name' => 'say-hi',
21 ])->assertOk();
22}
[Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md]
Replace mcp-endpoint with the path you registered in Mcp::web(). Do not copy say-hi into production. The point is the three headers matching the body. A 400 with -32020 after this test still fails is a header ticket. A log that only shows initialize never entered this test.
I already refused to treat a wait line as a finished background agent in Waiting for Your Input Is Not a Finished Background Agent. Same instinct: a red test is a ticket, not a pass, and an old handshake in production is a client ticket, not a rollback.

Print the method before you file the server
I write the owner as a person, not a role. “Platform” is how these hosts stay half-watched until an old method ships as a dead package.
Print three artifacts on the ticket:
- The JSON-RPC
methodon the first request. - The installed
laravel/mcpversion from the lockfile the host actually runs. - The human who owns that lockfile.
This script reads composer.lock in the app root and prints the pinned version. It is the check I want on the ticket, not a Packagist screenshot:
1<?php
2declare(strict_types=1);
3
4$lockPath = $argv[1] ?? 'composer.lock';
5$raw = file_get_contents($lockPath);
6if ($raw === false) {
7 fwrite(STDERR, "cannot read {$lockPath}\n");
8 exit(1);
9}
10
11$lock = json_decode($raw, true);
12if (! is_array($lock)) {
13 fwrite(STDERR, "invalid composer.lock\n");
14 exit(1);
15}
16
17foreach ($lock['packages'] ?? [] as $package) {
18 if (($package['name'] ?? '') === 'laravel/mcp') {
19 fwrite(STDOUT, ($package['version'] ?? 'unknown') . PHP_EOL);
20 exit(0);
21 }
22}
23
24fwrite(STDERR, "laravel/mcp not in composer.lock\n");
25exit(1);
Run it on the host that serves Mcp::web(), not on a laptop that last ran composer update in January.
Then log the method the route actually received. Keep it as a probe, not as a new public API:
1use Illuminate\Http\Request;
2use Illuminate\Support\Facades\Log;
3
4public function beforeMcpDispatch(Request $request): void
5{
6 Log::info('mcp.jsonrpc.first_look', [
7 'method' => $request->input('method'),
8 'has_meta' => $request->input('params._meta') !== null,
9 'protocol_header' => $request->header('MCP-Protocol-Version'),
10 ]);
11}
If method is initialize and has_meta is false, you are on the legacy path the upgrade notes still serve. If method is server/discover, you are on the new path. If method is tools/call and the response is 400 with -32020, you are on the header path. Those three lines are the screenshot a junior can paste. [Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md]
Do not wrap this probe in a “compatibility shim” that rewrites initialize into server/discover on the wire. Official notes already keep the old clients. A homemade rewrite is a protocol-bypass. I will not put one in this post.
GSC this week still has no striking-distance query on the HTTP 400 post, the wait-line post, or the WebFetch post. I am not refreshing those URLs. This is a new field, not a synonym of Monday’s slug or yesterday’s fetch fail.
What you must not do
Forbidden:
- File a “Laravel MCP is broken” ticket from an
initializelog without printingcomposer show laravel/mcpand the JSON-RPC method. - Put the package version in the title or the first line. The version is evidence after the decision.
- Mix this field with a reconnect-gave-up
/mcpnotice, a whitespace MCP stall, or an idle MCP screenshot. Those are other tickets. - Treat a 400 with
-32020as proof the legacy handshake is dead. Official notes exemptinitializeclients from header validation. - Rewrite
initializeintoserver/discoverin a proxy so the log looks modern. That is a bypass. The server already answers the old method. - Delete listeners and then claim the handshake is gone because
SessionInitializedis gone. The event was removed. The legacy handshake was not. [Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md] - Recommend buying a new client, a new model, or a new host because the method name looks old.
Allowed:
- Print the JSON-RPC
methodon the ticket. - Print
composer show laravel/mcpor the lockfile script above. - Copy the protocol the server answered:
2025-11-25,2025-06-18, or2026-07-28. - Stop, and ping the named owner of the package pin and the MCP client.
- Keep the old client on a written exception until that owner moves it. Name the exception on the wiki card.

This is not yesterday’s fetch fail
Yesterday’s post is a download deadline. WebFetch goes red at about 300 seconds. The model was waiting on a socket. [Source: https://zemna.net/blog/a-300-second-webfetch-fail-is-not-a-hung-model/]
Monday’s post is a schema reject on a named ANTHROPIC_BASE_URL. The host answered. The request was illegal for that endpoint. [Source: https://zemna.net/blog/every-turn-http-400-is-not-a-dead-gateway/]
This post is a handshake dialect. The client still says initialize. The server still answers. The package is not the thing that failed.
Do not merge the three into “the agent stack is down.” A 400 wall still answers curl. A five-minute fetch fail still proves the CLI enforced a deadline. An initialize log on a current laravel/mcp pin still proves the server served the legacy path. All three need a named pin. They do not share a vendor-down channel.
If you need the broader habit, start at /ai-agent-operations/. Tool pins live under /developer-tools/. Laravel plus Vue notes live under /laravel-vue-saas/ when the agent is touching that stack. A first-week map is at /start-here/. Do not mix those hubs into the handshake row.
I already refused to treat a green package bump as ownership of the next repair. Same here. A changelog bullet about server/discover is not permission to skip composer show laravel/mcp.
What you should do Monday morning
- Open the host that actually serves
Mcp::web(). Runcomposer show laravel/mcp. Write the string on the ticket next to one human name. That person owns the package pin. - Capture one live JSON-RPC first request. Write the
method. If it isinitialize, write the protocol the server answered. If it isserver/discover, write that instead. Do not leave the method as a tribal screenshot in Slack. - Run the lockfile script in the app root the supervisor uses. Confirm it matches
composer show. A laptop lockfile is not the host. - If a modern HTTP test returns 400 with
-32020, add the three headers from the upgrade notes and re-run. Classify it: legacy-handshake, header-mismatch, or missing-package. Do not file a server-outage ticket for legacy-handshake. - Confirm coding-agent MCP configs on this desk still name the same URL the Laravel route serves. A client pointed at
/mcpon another box is a different ticket. Print the URL on the same card as the pin. - If the named pin is older than the 14 September 2026 GitHub tag and you needed the documented legacy path, the owner bumps the pin on purpose, with a written exception for every client that still sends
initialize. If the named pin is already on that tag andinitializestill answers, you have the expected path. You still do not have a broken server until the route is down and the lockfile is empty.
The question is not whether Inspector demos server/discover. The question is whether the old handshake survives maintenance, handoff, and a junior who wants to roll back the package.
Further reading
Source laravel/mcp upgrade guide — initialize clients continue to work
Source GitHub release v1.0.0 — PR 341 legacy initialize clients (14 September 2026)
