Repository navigation
fix(transport): add Streamable HTTP MCP transport, fix stateless-per-request bug - #77
Conversation
Adds a Node http-based Streamable HTTP transport (POST /mcp, GET /health) alongside the existing stdio/tcp/websocket/unix transports, auto-selected when PORT is set or --http is passed. Auth via RPG_MCP_TRANSPORT_TOKEN, same convention as the tcp/websocket transports.
Stateless StreamableHTTPServerTransport throws 'Stateless transport cannot be reused across requests' after the first POST /mcp, since sharing one transport instance across unrelated HTTP requests violates the SDK's stateless-mode contract (server/webStandardStreamableHttp.js guards this explicitly via _hasHandledRequest). Discovered live against the deployed Railway service: the first initialize succeeded, every call after it returned a bare 500. Extract tool registration into buildServer(pubsub, auditLogger) and call it once per request for the HTTP transport (cheap, no I/O), while stdio/tcp/unix/websocket keep a single long-lived instance.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reached
Next review available in: 44 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe server now supports stateless HTTP MCP requests through ChangesHTTP deployment transport
Scene lookup correction
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant HTTPClient
participant startHttpServerTransport
participant buildServer
participant StreamableHTTPServerTransport
HTTPClient->>startHttpServerTransport: POST /mcp
startHttpServerTransport->>buildServer: Create MCP server
buildServer-->>startHttpServerTransport: Return McpServer
startHttpServerTransport->>StreamableHTTPServerTransport: Handle MCP request
StreamableHTTPServerTransport-->>HTTPClient: Return MCP response
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/server/index.ts`:
- Around line 296-299: Update the startHttpServerTransport call to pass the
configured maxMessageBytes value as the maxBodyBytes option, ensuring the HTTP
request limit follows --max-message-bytes instead of the transport’s 4 MiB
default.
In `@src/server/transport/http.ts`:
- Around line 74-76: Update the HTTP authorization flow around isAuthorized so
requests are rejected when RPG_MCP_TRANSPORT_TOKEN is unset, rather than logging
a warning and allowing anonymous access. Require token authentication by
default; if local anonymous use must remain supported, gate it behind an
explicit unsafe opt-in whose default is false.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 313ed1a0-6c07-4265-a410-16a8aa75d4b7
📒 Files selected for processing (3)
railway.jsonsrc/server/index.tssrc/server/transport/http.ts
findLatestForParticipant(characterId, worldId?) was called as (worldId, characterId), so the DM-facing 'current' preview always returned scene: null. Both params are string, so TS could not catch it. Agent auto-injection was unaffected — the prompt composer calls listLatestForParticipant with the correct order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Added one more commit (e6af294, already authored 2026-07-25, not related to this PR's transport work): the CI matrix caught that \origin/main\ currently fails \ ests/server/consolidated/scene-manage.test.ts\ (a pre-existing transposed-argument bug in \scene_manage action:'current', unrelated to anything in this PR's diff). That fix already existed locally and was just never pushed, so I included it here to keep CI green rather than leaving this PR red for an unrelated reason. |
|
build (18.x) fails consistently (retried once, same result) on ests/server/websocket.test.ts with ECONNREFUSED ::1:3002 -- a connection-timing issue in the WebSocket transport test suite, a module this PR does not touch. build (20.x) and (22.x) both pass cleanly. This looks like a pre-existing Node-18-specific compatibility issue (likely IPv6/localhost resolution behavior that changed between Node 18 and 20+), not a regression from this PR's changes. Leaving it for whoever owns Node-18 support to triage separately -- flagging here rather than silently ignoring it. |
…works Railway's private network is IPv6-only. A server bound to '0.0.0.0' listens on IPv4 only, so it is reachable on its public domain but NOT on <service>.railway.internal -- peer services get a connection failure with no obvious cause. Default the HTTP host to '::', which Node binds dual-stack (IPv6 + IPv4), so both the private hostname and the public domain work. Still overridable via --host for local/dev binding. Found while wiring rpg-mcp-live to this service: the integration had to fall back to the public domain because the private hostname was unreachable.
|
✅ Health of changed files: 4.4 → 4.8 (+0.4) 📋 At a glance ✅ Health gate: passed 📌 Before you merge
🔎 More signals (3)🗺️ Change map flowchart LR
subgraph PR ["Changed in this PR (2 with dependents)"]
f_src_server_index_ts["src/server/index.ts 🔥"]:::changed
f_src_server_transport_websocket_ts[".../transport/websocket.ts 🔥"]:::changed
end
f_src_cli_ts["src/cli.ts"]
f_src_server_index_ts --> f_src_cli_ts
f_src_index_ts["src/index.ts"]
f_src_server_index_ts --> f_src_index_ts
f_src_run_ts["src/run.ts"]
f_src_server_index_ts --> f_src_run_ts
f_src_server_consolidated_spawn_manage_ts[".../consolidated/spawn-manage.ts"]
f_src_server_index_ts --> f_src_server_consolidated_spawn_manage_ts
t_tests_server_websocket_test_ts(["✅ tests/server/websocket.test.ts"]):::guard
t_tests_server_websocket_test_ts -.-> f_src_server_transport_websocket_ts
classDef changed fill:#dbeafe,stroke:#1d4ed8,color:#1e3a5f
classDef warn fill:#fef3c7,stroke:#b45309,color:#78350f
classDef guard fill:#dcfce7,stroke:#15803d,color:#14532d
Solid arrows: code that imports the changed files (4 direct dependents, from the last indexed snapshot). Dashed: history/tests. 🔥 Hotspots touched (2)
💀 Dead code (2 findings)
📊 See the full report for this PR |
…request bug (Mnehmos#77) * feat(transport): add HTTP MCP transport for Railway hosting Adds a Node http-based Streamable HTTP transport (POST /mcp, GET /health) alongside the existing stdio/tcp/websocket/unix transports, auto-selected when PORT is set or --http is passed. Auth via RPG_MCP_TRANSPORT_TOKEN, same convention as the tcp/websocket transports. * fix(transport): build a fresh McpServer+transport per HTTP request Stateless StreamableHTTPServerTransport throws 'Stateless transport cannot be reused across requests' after the first POST /mcp, since sharing one transport instance across unrelated HTTP requests violates the SDK's stateless-mode contract (server/webStandardStreamableHttp.js guards this explicitly via _hasHandledRequest). Discovered live against the deployed Railway service: the first initialize succeeded, every call after it returned a bare 500. Extract tool registration into buildServer(pubsub, auditLogger) and call it once per request for the HTTP transport (cheap, no I/O), while stdio/tcp/unix/websocket keep a single long-lived instance. * fix(scene): swap transposed args in scene_manage action:'current' findLatestForParticipant(characterId, worldId?) was called as (worldId, characterId), so the DM-facing 'current' preview always returned scene: null. Both params are string, so TS could not catch it. Agent auto-injection was unaffected — the prompt composer calls listLatestForParticipant with t 7B3A he correct order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(transport): bind HTTP transport dual-stack so private networking works Railway's private network is IPv6-only. A server bound to '0.0.0.0' listens on IPv4 only, so it is reachable on its public domain but NOT on <service>.railway.internal -- peer services get a connection failure with no obvious cause. Default the HTTP host to '::', which Node binds dual-stack (IPv6 + IPv4), so both the private hostname and the public domain work. Still overridable via --host for local/dev binding. Found while wiring rpg-mcp-live to this service: the integration had to fall back to the public domain because the private hostname was unreachable. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Adds a Streamable HTTP MCP transport (
--http) so this server can run as a hosted Railway service instead of only stdio/TCP/Unix/WebSocket, and fixes a real bug in that transport discovered live against a deployed instance.src/server/transport/http.ts— new file:POST /mcp(Streamable HTTP, stateless),GET /health, bearer/query/header token auth.src/server/index.ts— extracts tool registration intobuildServer(pubsub, auditLogger), called once for the long-lived stdio/tcp/unix/websocket transports, and per-request for HTTP. This is the actual fix:StreamableHTTPServerTransportin stateless mode (sessionIdGenerator: undefined) throws"Stateless transport cannot be reused across requests"after the first request — confirmed live against a deployed Railway instance, where the firstinitializesucceeded and every request after it returned a bare 500. The SDK's own stateless-mode contract requires a fresh transport instance per request (server/webStandardStreamableHttp.js's_hasHandledRequestguard); the fix builds a freshMcpServer+ transport pair per HTTP request (cheap — no I/O, just tool registration) while sharing the real app-level singletons (DB, pubsub, audit logger) across requests.railway.json— Railpack build/deploy config for hosting this as a Railway service.Risk
buildServer()is a straight extraction of code that was previously inline inmain(), not a behavior change for those paths.--http/PORTand not used by any existing deployment of this repo today.Acceptance criteria
POST /mcphandles a full MCP session (initialize→notifications/initialized→tools/call) correctly across multiple independent requests, not just the first one.GET /healthresponds without auth.buildServer()extraction is behavior-preserving for those paths).Verification
npm run build(tsc) passes.initialize,notifications/initialized, and independenttools/callrequests (including a second, fully independentinitialize) all succeed after the fix — before the fix, only the very first request after a fresh deploy succeeded and everything after returned a bare 500.load_tool_schemafor all 24 curated tools) and a realcharacter_manage.create/inventory_manage.get_detailed/narrative_manage.add/search/session_manage.initializeround trip, from a separate consumer project (rpg-mcp-live, see PR #211) that depends on this transport.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
/mcpendpoint.Bug Fixes
Deployment