Skip to content

fix: route MCP sessions to their owning Fly machine and 404 lost sessions - #290

Closed
hyochan wants to merge 1 commit into
mainfrom
fix/mcp-session-affinity-287
Closed

hyochan wants to merge 1 commit into
mainfrom
fix/mcp-session-affinity-287

Conversation

@hyochan

@hyochan hyochan commented Aug 5, 2026 •

Copy link
Copy Markdown
Member

Summary

Fixes #287 — kit.openiap.dev/mcp intermittently rejected a valid, freshly issued mcp-session-id with 400 "initialize first" (13/20 failures in the issue repro).

Root cause (verified): createIapKitWebMcpHandler keeps StreamableHTTP transports in a per-process Map (web.ts). fly.toml sets min_machines_running = 1, which is a floor, not a cap — with N machines round-robining behind the Fly proxy, only the machine that served initialize recognizes the session. A ~35% success rate matches N=3 exactly. The SDK registers the session before the initialize response is sent, and no code path evicts sessions, so instance split-brain is the only mechanism consistent with the interleaved 200/400 pattern.

Fix

  1. Machine-affinity session ids — session ids are minted as <FLY_MACHINE_ID>.<uuid>. A request for a session owned by a sibling machine is answered with a fly-replay: instance=<id> header, so Fly's proxy re-sends the original request to the owner. No shared store or infra change; a live SSE transport can't be serialized into Redis anyway.
    • Loop-safe: never replays a request that already carries fly-replay-src, never replays to itself, and validates the attacker-controlled prefix (^[A-Za-z0-9]{1,32}$) before echoing it into a response header.
    • Off Fly (no FLY_MACHINE_ID), ids stay plain UUIDs and behavior is unchanged.
  2. 404 instead of 400 for lost sessions — per the MCP spec, clients transparently re-initialize on 404 (-32001 Session not found). The old 400 broke that recovery path, which is why clients hard-failed instead of self-healing after machine restarts/deploys.
  3. Deploy-coupling fix — deploy-kit.yml did not trigger on packages/mcp-server/**, yet kit's Fly binary imports the MCP handler from source. An MCP-only fix would merge and silently never ship. Paths added, and the verify job now also runs the MCP server's own vitest suite (previously it ran in no CI at all).

Both entry points are covered: the production web handler (web.ts, mounted by kit's Hono server) and the standalone Node server (http.ts).

Tests

  • packages/mcp-server: 44 tests pass (bun run lint + bun run test) — new suites for the routing rules and for the web handler end-to-end (prefixed ids, replay, replay-once guard, self-restart 404, off-Fly 404, GET/DELETE parity, unchanged 400s for missing-session requests).
  • packages/kit: 913 tests pass (kit mounts this handler in server/mcp.ts).

Post-merge verification

After deploy, rerun the issue's repro (20× tools/list on one session) — expect 20/20, with cross-machine hops served via replay.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved MCP session reliability across multiple machines by routing requests to the machine that created the session.
    • Added automatic session affinity and recovery for requests involving remote sessions.
    • Added clearer handling for unavailable or invalid sessions, including appropriate 404 and 400 responses.
  • Bug Fixes

    • Prevented session replay loops and inconsistent behavior across POST, GET, and DELETE requests.
  • Tests

    • Expanded coverage for local, remote, invalid, and unavailable session scenarios.

…ions

The hosted kit.openiap.dev/mcp endpoint keeps StreamableHTTP sessions in
a per-process Map, so with more than one Fly machine behind the proxy a
valid mcp-session-id was rejected with 400 "initialize first" whenever
the request landed on a sibling machine (~65% of calls in the issue
repro, consistent with 3-machine round-robin).

- Prefix session ids with FLY_MACHINE_ID and answer requests for a
  foreign machine's session with a fly-replay header so Fly's proxy
  re-routes them to the owner. No shared store needed; the transport
  object holds live SSE state and cannot be serialized anyway.
- Never replay twice (fly-replay-src guard) and never replay to self,
  so stale machine ids after a deploy cannot loop.
- Answer 404 (-32001 Session not found) instead of 400 for a session
  this process genuinely cannot serve — the MCP spec makes clients
  transparently re-initialize on 404, so restarts now self-heal.
- Add packages/mcp-server/** to deploy-kit.yml triggers: kit's Fly
  binary imports the MCP handler from source, so MCP fixes previously
  merged without ever deploying. Also run the MCP server's own vitest
  suite in the verify job — no CI ran it before.

Fixes #287

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The MCP server now supports Fly machine-affinity session IDs. Unknown sessions can replay requests to their owning machine. Unavailable sessions return 404 responses, while invalid requests retain their existing 400 responses. CI verifies the MCP server package.

Changes

MCP session routing

Layer / File(s) Summary
Session routing contract
packages/mcp-server/src/session-routing.ts
Adds Fly machine ID validation, machine-prefixed session IDs, and replay or not-found routing results.
HTTP and web handler integration
packages/mcp-server/src/http.ts, packages/mcp-server/src/web.ts
Adds optional machine configuration, generates machine-affine sessions, and routes unknown POST, GET, and DELETE sessions through Fly replay or 404 responses.
Routing validation and CI
packages/mcp-server/test/*, .github/workflows/deploy-kit.yml
Adds routing and handler regression tests. CI runs MCP server lint and test commands and responds to MCP server changes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant MCPHandler
  participant routeUnknownSession
  participant OwningMachine
  MCPClient->>MCPHandler: Send request with machine-prefixed session ID
  MCPHandler->>routeUnknownSession: Resolve unknown session
  routeUnknownSession-->>MCPHandler: Return replay target or not-found
  MCPHandler->>OwningMachine: Request Fly replay when target exists
  OwningMachine-->>MCPClient: Process request or return 404
Loading

Possibly related PRs

Suggested labels: 💨 ci

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: routing MCP sessions to their owning Fly machine and returning 404 for lost sessions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mcp-session-affinity-287

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @.github/workflows/deploy-kit.yml:
- Around line 19-22: Update the pull_request.paths configuration in
deploy-kit.yml to mirror the push path filters by adding bun.lock, package.json,
and .github/workflows/deploy-kit.yml, while preserving the existing packages/kit
and packages/mcp-server entries.

In `@packages/mcp-server/src/http.ts`:
- Line 87: Normalize the configured machine ID before storing it in both
handlers: update the machine ID assignment in packages/mcp-server/src/http.ts at
lines 87-87 and packages/mcp-server/src/web.ts at lines 48-48 to apply the
existing normalizer to options.machineId while preserving currentMachineId()
fallback behavior.
🪄 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: 44dc84e2-ea4c-4263-adae-60e77d660fbc

📥 Commits

Reviewing files that changed from the base of the PR and between 3bc71df and 1f54f36.

📒 Files selected for processing (7)
  • .github/workflows/deploy-kit.yml
  • packages/mcp-server/src/http.ts
  • packages/mcp-server/src/session-routing.ts
  • packages/mcp-server/src/web.ts
  • packages/mcp-server/test/http.test.ts
  • packages/mcp-server/test/session-routing.test.ts
  • packages/mcp-server/test/web.test.ts

Comment on lines 19 to +22
pull_request:
paths:
- "packages/kit/**"
- "packages/mcp-server/**"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Mirror the dependency filters for pull requests.

The pull_request.paths list omits bun.lock, package.json, and .github/workflows/deploy-kit.yml. A pull request that changes only one of these files skips this workflow, including the MCP server suite. Add the same dependency and workflow paths used by push.

🤖 Prompt for 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.

In @.github/workflows/deploy-kit.yml around lines 19 - 22, Update the
pull_request.paths configuration in deploy-kit.yml to mirror the push path
filters by adding bun.lock, package.json, and .github/workflows/deploy-kit.yml,
while preserving the existing packages/kit and packages/mcp-server entries.

const allowedOrigins =
options.allowedOrigins ??
parseAllowedOrigins(process.env.IAPKIT_MCP_ALLOWED_ORIGINS);
const machineId = options.machineId ?? currentMachineId();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate configured machine IDs in both handlers.

Both handlers accept an explicit machineId without applying the validation used for FLY_MACHINE_ID. A value containing . creates an ID that resolves to a different replay target.

  • packages/mcp-server/src/http.ts#L87-L87: normalize options.machineId before storing it.
  • packages/mcp-server/src/web.ts#L48-L48: use the same normalizer before storing options.machineId.
📍 Affects 2 files
  • packages/mcp-server/src/http.ts#L87-L87 (this comment)
  • packages/mcp-server/src/web.ts#L48-L48
🤖 Prompt for 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.

In `@packages/mcp-server/src/http.ts` at line 87, Normalize the configured machine
ID before storing it in both handlers: update the machine ID assignment in
packages/mcp-server/src/http.ts at lines 87-87 and
packages/mcp-server/src/web.ts at lines 48-48 to apply the existing normalizer
to options.machineId while preserving currentMachineId() fallback behavior.

@hyochan

hyochan commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Superseded by #292, which bundles this with the #288 and #289 fixes in one PR.

@hyochan hyochan closed this Aug 5, 2026
@hyochan
hyochan deleted the fix/mcp-session-affinity-287 branch August 17, 2026 09:17
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.

kit.openiap.dev MCP: valid session id intermittently rejected with "initialize first" (13/20 failure rate)

1 participant