Refuse GET /mcp instead of answering with a closed stream - #343
Merged
Conversation
A GET opens the stream a server uses to speak to a client unprompted. The engine never speaks first, so it answered with an empty stream that closed at once, which a client reads as a dropped connection and reconnects. The pair then loops for as long as both are up: one instance took 71 644 GETs in 21 hours, and since each wake ran a garbage collection over several gigabytes of loaded data, it burned two and a half cores while idle. 405 is what the protocol asks of a server that offers no such stream, and the endpoint already answered every other method that way.
A 405 has to carry an Allow header naming the methods the resource does take (RFC 9110). The arm answered without one, which mattered little while it only ever saw a stray method, and matters now that every GET lands there: a GET is the request an off-the-shelf client probes with, and the header is what tells it to try POST instead. The CHANGELOG entry also states how the change reads for the next release number, since the release step decides that by what a release removes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What happens today
GET /mcpis the stream a server uses to speak to a client unprompted. VoLCA never speaks first, so the endpoint answered with an SSE response whose body was empty and already closed.A client reads a closed stream as a dropped connection and reconnects. The server closes again. The two loop for as long as both are up.
Measured on a live instance holding Agribalyse 3.2, BAFU 2026 v1 and two EF 3.1 method sets, with an MCP client attached: 71 644 GETs in 21 hours, one about every second. The requests themselves are trivial, but each one wakes a process whose live heap is around 6 GB, and the idle collector then runs a full collection. The engine sat at 250 % CPU on a four-core machine, permanently, having done no work at all.
The change
Drop the
GETarm. Every other method already fell through to a 405, which is what the protocol asks of a server that offers no server-initiated stream, and a conformant client stops asking after it.DELETEwas already answered that way and stays so.Why not keep the stream open instead
Holding it open with a periodic keep-alive would also stop the loop, but it means maintaining a stream that never carries anything. 405 states the same fact and costs nothing.
One thing to settle before merge
The GET arm arrived in
e087b55, a commit that added five things at once for compatibility with the OpenAI Responses API: the protocol version, the session header, SSE on POST, the 405 for other methods, and this stream. Which of the five that client actually needed was never recorded, and I have not tested against it.The spec obliges a client to accept a 405 at this endpoint, and a stream returned already closed cannot have carried anything, so the likely answer is that the connector needed the version and the session header. But it is a guess. If someone can point an OpenAI MCP connector at an engine built from this branch, that closes the question.
The same question decides the next release number:
docs/release.mdreads a removal a working client relies on as minor. The CHANGELOG entry states the judgment that no working client relied on the old answer, since the one behaviour ever observed was the reconnect loop. If the OpenAI check contradicts that, the entry and the number both need revisiting.