Commit Graph
16 Commits
Author SHA1 Message Date
Edgar ArakelyanandClaude Opus 5.5 baff9d359e fix(mcp): apply the SDK's own Accept and Content-Type checks before a session takeover
Media types are case-insensitive in HTTP, so the takeover guard compared
them lower-cased, but the SDK's transport matches Accept as a
case-sensitive substring. A header such as Application/JSON passed the
guard, closed the live session, and then got 406 from the SDK. The guard
now also requires the SDK's Accept substrings and uses its exported
isJsonContentType, so it only replaces a session the SDK will accept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:42:30 -07:00
Edgar ArakelyanandClaude Opus 5.5 f56befc456 fix(mcp): split Accept ranges outside quoted strings before a session takeover
A comma or semicolon inside a quoted parameter is not a separator, so a
header such as application/json;note=", text/event-stream;x=1" holds a
single range. The takeover guard split on every comma and found a
text/event-stream range the client never sent. Ranges and their
parameters are now split outside quoted strings, and q is read from the
parsed parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:00:20 -07:00
Edgar ArakelyanandClaude Opus 5.5 5cf727cdc8 test(mcp): pin same-origin browser Origin acceptance on loopback listeners
A browser on the listener's own page sends Origin with the scheme and the
bound port. The SDK validator parses the Origin and compares its hostname
with the loopback allowlist, so the port never takes part; this test keeps
that true for the MCP endpoint and for composed routes, under 127.0.0.1
and localhost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:47:12 -07:00
Edgar ArakelyanandClaude Opus 5.5 2deed2eb77 fix(mcp): ignore zero-quality Accept ranges in the session takeover guard
Addresses a thirteenth automated review finding: `application/json;q=0`
explicitly refuses JSON, so it no longer counts toward the exact Accept ranges
a session takeover requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 13:27:04 -07:00
Edgar ArakelyanandClaude Fable 5.1 a1da7ffef1 fix(mcp): close the connection unless the incoming body was consumed to its end
Addresses the twelfth automated review finding: bodyUsed only means a body
was disturbed, so a route that read one chunk of an upload left the rest
paused on a keep-alive socket. The close decision now follows the incoming
message's end of stream.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 13:00:32 -07:00
Edgar ArakelyanandClaude Fable 5.1 10daa522e0 test(mcp): count both plain-route observations in the request-target regression
The previous commit added a second successful route call to the regression
but left the single-observation assertion in place.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 12:39:48 -07:00
Edgar ArakelyanandClaude Fable 5.1 d6b3b67458 fix(mcp): refuse request targets whose path URL parsing would rewrite
Addresses the eleventh automated review finding: dot segments such as
`/public/../mcp` were canonicalized to `/mcp` before routing. The bridge now
refuses any target whose path differs after parsing, so routing sees exactly
the path the client sent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 12:38:27 -07:00
Edgar ArakelyanandClaude Fable 5.1 0c9701334f fix(mcp): take request targets verbatim, require a lone initialize and keep failed hooks retryable
Addresses the tenth automated review round: the bridge joins an origin-form
request target onto the listener's origin so a network-path target stays a
path and absolute-form targets are refused; only a lone initialize can replace
an open session; and the shutdown hook records completion only after it
returns, so a hook that threw stays eligible for the next close.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 12:21:29 -07:00
Edgar ArakelyanandClaude Fable 5.1 24301be4fb fix(mcp): guard request conversion and close after any unread declared body
Addresses the ninth automated review round: a method Fetch refuses, such as
TRACE, is answered with 405 instead of escaping the request guard; the
unread-body decision now comes from the incoming message's own body
declaration, so a GET or HEAD carrying a body closes too; and the regression
uses a keep-alive client and checks that the socket actually closes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 11:22:42 -07:00
Edgar ArakelyanandClaude Fable 5.1 a23f5f1352 fix(mcp): close the connection after a reply sent before the body was read
Addresses the eighth automated review finding: a refused Host or Origin, an
unknown route or the startup 503 answered a POST while its body stayed unread
on the paused bridged stream; such replies now carry Connection: close so an
unread upload cannot keep a socket open.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 11:04:29 -07:00
Edgar ArakelyanandClaude Fable 5.1 174188279b fix(mcp): fire the shutdown hook once, stop awaiting the probe's tee and keep every Set-Cookie
Addresses the fifth automated review round: the instance's shutdown hook runs
exactly once even when the listener closes without a live session; the
initialize probe no longer awaits cancelling its cloned body, which would wait
for the original request to drain; and the node:http bridge passes repeated
Set-Cookie values through as an array.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 09:53:56 -07:00
Edgar ArakelyanandClaude Fable 5.1 fb22df370b fix(mcp): match Accept ranges exactly and keep a session whose shutdown failed
Addresses the fourth automated review round: a sessionless POST replaces a
session only when its Accept header lists application/json and
text/event-stream as exact media ranges, and a session whose transport
shutdown rejected stays owned so the next close retries it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 09:36:21 -07:00
Edgar ArakelyanandClaude Fable 5.1 b76f354d76 fix(mcp): classify loopback from the bound address and keep startup failures out of onerror
Addresses the third automated review round: loopback is classified from the
address the listener bound with the configured name added to the Host and
Origin allowlist; scoped IPv6 literals are refused before binding and a URL
that cannot be formed closes the listener; listener errors reach onerror only
after a successful bind; the MCP runtime keeps its factory owner as the single
owner of instance cleanup through `closeInstance: false`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 09:18:08 -07:00
Edgar ArakelyanandClaude Fable 5.1 9ed8f06f75 fix(mcp): compare the media type proper and bind before creating the server
Addresses the second automated review round: a sessionless POST replaces a
session only when its media type proper is application/json, and the server
instance is created after the listener binds so a bind failure leaves nothing
to clean up and a factory failure releases the listener.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 08:38:34 -07:00
Edgar ArakelyanandClaude Fable 5.1 b0ec63cc03 fix(mcp): harden the streamable HTTP listener's session replacement and shutdown
Addresses the automated review findings on the PR: refuse requests once the
session slot is closed and join an arming initialize during shutdown; retry
failed close stages instead of replaying the rejection; classify loopback
binds by address class and add the bind's own hostname to the Host and Origin
allowlist; let only a POST with the streamable headers replace an open
session; announce the bound URL at every log level; route `mcp -h --http` to
the HTTP entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 08:14:01 -07:00
Edgar ArakelyanandClaude Fable 5.1 46dd8f0e2b feat(mcp): serve the MCP server over streamable HTTP
Add rea mcp --http [--host] [--port]: the same server rea speaks over stdio,
served over MCP Streamable HTTP on a loopback listener by default, using the
SDK's web-standard transport already in the pinned @modelcontextprotocol/server
behind a small node:http bridge. One client and one session at a time; DELETE
ends the session without running provider teardown, a new client's initialize
replaces a session left open, foreign Host headers are refused on loopback
binds, and shutdown is signals-only. Stdio stays the default and registered
transport; server.json and client registrations are unchanged.

Refs morluto/rea#4933

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-17 21:05:10 -07:00