Skip to content

Comments

fix: mark client initialized after initialize handshake#79

Open
pascualmg wants to merge 1 commit intophp-mcp:mainfrom
pascualmg:fix/mark-initialized-after-handshake
Open

fix: mark client initialized after initialize handshake#79
pascualmg wants to merge 1 commit intophp-mcp:mainfrom
pascualmg:fix/mark-initialized-after-handshake

Conversation

@pascualmg
Copy link

@pascualmg pascualmg commented Feb 20, 2026

Summary

Marks the client session as initialized immediately after a successful initialize response, instead of waiting for the notifications/initialized notification that many real-world clients never send.

This is a one-line change ($session->set('initialized', true) in Dispatcher::handleInitialize()). It fixes "Client session not initialized" errors for Claude Code, Gemini/Antigravity, and any other MCP client that treats notifications/initialized as optional.

The problem

Protocol::processRequest() gates every method call behind assertSessionInitialized(), but initialized is only set to true in Dispatcher::handleNotificationInitialized(). Clients that skip the notification are permanently locked out after a successful handshake.

I'm using php-mcp/server on my blog at pascualmg.dev/blog with SSE transport, and my beta testers reported that MCP tools were failing with "Client session not initialized" errors right after a successful initialize handshake.

Steps to reproduce

Start any php-mcp/server instance with SSE transport:

# 1. Open SSE stream, capture clientId from endpoint event
# 2. Initialize (succeeds)
curl -s -X POST "$MCP_URL" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"test","version":"1.0"}}}'

# 3. Skip notifications/initialized, go straight to tools/list
curl -s -X POST "$MCP_URL" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
# Result: {"jsonrpc":"2.0","id":2,"error":{"code":-32600,"message":"Client session not initialized."}}

Affected clients

  • Claude Code (Anthropic) — does not send notifications/initialized over SSE
  • Gemini / Antigravity (Google) — does not send notifications/initialized
  • Any custom client that treats the notification as optional

The fix

--- a/src/Dispatcher.php
+++ b/src/Dispatcher.php
@@ -128,6 +128,12 @@
         $session->set('client_info', $request->clientInfo->toArray());
         $session->set('protocol_version', $protocolVersion);
 
+        // Mark client as initialized immediately after successful handshake.
+        // Many real-world MCP clients (Claude Code, Gemini) skip
+        // the notifications/initialized step, causing "Client session not
+        // initialized" errors on all subsequent requests.
+        $session->set('initialized', true);
+
         $serverInfo = $this->configuration->serverInfo;

Backwards compatibility

Fully backwards compatible. If a client still sends notifications/initialized after initialize, handleNotificationInitialized() calls $session->set('initialized', true) again — a harmless no-op on an already-initialized session.

No changes to SessionInterface, no new config, no breaking API changes.

Context

Discovered while running a production MCP server (Cohete) that serves Claude Code and other MCP clients over the SSE transport. We've been running with this fix patched locally via cweagans/composer-patches and it resolves the issue for all clients.

🤖 Generated with Claude Code

@pascualmg pascualmg force-pushed the fix/mark-initialized-after-handshake branch from 5374356 to 606704b Compare February 21, 2026 00:01
Many real-world MCP clients (Claude Code, Gemini/Antigravity)
complete the initialize handshake and immediately call tools/list or
tools/call without sending the notifications/initialized notification.

Currently, initialized is only set to true in
handleNotificationInitialized(), so these clients get permanently
locked out with "Client session not initialized" errors.

This change marks the session as initialized right after a successful
initialize response. If a client still sends notifications/initialized,
handleNotificationInitialized() sets the flag again — a harmless no-op.

Discovered while running a production MCP server (Cohete blog) that
serves Claude Code and other MCP clients over SSE transport.
@pascualmg pascualmg force-pushed the fix/mark-initialized-after-handshake branch from 606704b to de7c087 Compare February 21, 2026 00:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant