[Streamable HTTP][Server] GET should open a server→client SSE listener
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
Research direction
Start in vendor/mcp/sdk/src/Server/Transport/StreamableHttpTransport.php, tracing handleRequest(), handlePostRequest(), createStreamedResponse(), CallbackStream, and flushOutgoingMessages(). Reuse the existing session validation and streaming paths as applicable, then verify that GET requires a valid session, produces an SSE response, drains queued messages, and terminates cleanly on disconnect.
Written by the indexing model from the issue text.
Description
Problem
StreamableHttpTransport only handles OPTIONS, POST, and DELETE. GET falls into the match default arm and gets a 405 Method Not Allowed with Allow: POST, DELETE, OPTIONS.
vendor/mcp/sdk/src/Server/Transport/StreamableHttpTransport.php (current main):
return match ($request->getMethod()) {
'OPTIONS' => $this->handleOptionsRequest(),
'POST' => $this->handlePostRequest(),
'DELETE' => $this->handleDeleteRequest(),
default => $this->createErrorResponse(Error::forInvalidRequest('Method Not Allowed'), 405),
};
This contradicts both the MCP spec and the SDK's own CORS advertisement on the same class:
'Access-Control-Allow-Methods' => 'GET, POST, DELETE, OPTIONS',
Per the MCP Streamable HTTP transport spec, a client may issue GET against the MCP endpoint to open a long-lived SSE channel for server→client messages (notifications and server-initiated requests outside the request/response loop). Refusing the GET breaks that channel.
Observed impact
Spec-conformant clients open this listener immediately after initialize. The 405 kills the channel. In our deployment we see two session rows created in the session store on every fresh client startup — one for the working POST request/response loop, one orphaned from the failed listener that the client retries under a new session id.
Proposal
Add handleGetRequest() to StreamableHttpTransport and route GET to it from the match in handleRequest().
Behavior:
- Require
Mcp-Session-Id; without it return400(consistent withhandleDeleteRequest()). - Validate the session exists in the configured
SessionStoreInterface; on miss return404. - Open an SSE response (
Content-Type: text/event-stream) and stream:- any queued outgoing messages for this session (the same queue
flushOutgoingMessages()already drains increateStreamedResponse()); - server-initiated requests/notifications produced via the existing
Protocol/Fibermachinery.
- any queued outgoing messages for this session (the same queue
- Honor
Last-Event-IDfor resumption per the spec (can land in a follow-up; the initial PR can document the gap). - Cleanly terminate when the client disconnects.
The mechanics already exist — CallbackStream and flushOutgoingMessages() from PR #109 do the streaming half inside handlePostRequest(). The new method is essentially the same loop without a triggering POST body.
Backward compatibility
Purely additive. Clients that never issue GET see no change. The Access-Control-Allow-Methods header already advertises GET, so the surface is unchanged from the client's perspective — only the server's response to a method it claims to accept changes from 405 to a valid SSE stream.
Related
- PR #109 — added
CallbackStream+flushOutgoingMessages()for SSE insidehandlePostRequest. Provides the streaming primitives this proposal reuses. - Issue #226 — fixed the previous
500response for unsupported methods to405 + Allow. That work treatedGETas legitimately unsupported; this issue arguesGETis actually supported by the spec and should be wired up. - Issue #275 — separate concurrency race on the per-session outgoing message queue. Independent, but a working
GETlistener will exercise the same queue and would benefit from the same fix.
- Dominant language
- PHP
- Stars
- 1.6k
- Forks
- 173
- Avg merge
- 2d 49m
- Merged PRs (30d)
- 23
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from modelcontextprotocol/php-sdk
-
[Server] Handler type uses bare Closure, hard to decorate RegistryInterface under strict PHPStan OpenServer
Difficulty 1/5 Under an hour Newbie friendliness 78/100
modelcontextprotocol/php-sdk#468 · 2 comments ·
-
needs confirmation needs maintainer action Server
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
modelcontextprotocol/php-sdk#398 · 1 reaction ·
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
modelcontextprotocol/php-sdk#370 ·
-
enhancement
Difficulty 4/5 3-5 days Newbie friendliness 55/100
modelcontextprotocol/php-sdk#510 · 1 comment ·
-
bug
Difficulty 4/5 3-5 days Newbie friendliness 45/100
modelcontextprotocol/php-sdk#504 ·
All issues in modelcontextprotocol/php-sdk
Similar issues
-
priority: p3
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
googleapis/librarian#7636 ·
-
0. Needs triage bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
nextcloud/fulltextsearch#1011 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
phpstan/phpstan-doctrine#794 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Automattic/static-site-importer#1767 ·