Skip to content

fix(agent-core): reconnect dropped streamable-HTTP MCP sessions on tool call - #2748

Open
tarikermis wants to merge 2 commits into
MoonshotAI:mainfrom
tarikermis:fix/mcp-http-session-reconnect
Open

fix(agent-core): reconnect dropped streamable-HTTP MCP sessions on tool call#2748
tarikermis wants to merge 2 commits into
MoonshotAI:mainfrom
tarikermis:fix/mcp-http-session-reconnect

Conversation

@tarikermis

Copy link
Copy Markdown

Related Issue

Resolve #2742

Problem

When a streamable-HTTP MCP server's session drops mid-session (e.g. a server restart), the legacy agent-core engine never re-established it. Every call to that server's tools failed for the rest of the session — the SDK transport keeps POSTing with the stale session id and the restarted server answers Bad Request: Server not initialized — and the failed server's tools were unregistered from the model's tool list, so the model retried its work through other servers' tools instead of getting a loud, server-named error (the "different tools execute" symptom in the issue).

Note on engine coverage: the default agent-core-v2 engine already recovers exactly this way (reconnect-and-retry on tool-call failure, #1991) — I verified a server-restart scenario recovers transparently there. The defect was specific to the legacy engine (KIMI_CODE_LEGACY_FLAG, SDK v1 client), which this PR brings to parity.

Reproduction evidence (legacy engine, this branch before the fix): connect a real in-process streamable-HTTP MCP server through McpConnectionManager + ToolManager, restart it mid-session, then call its tool again — the call throws Streamable HTTP error: ... "Bad Request: Server not initialized" on every subsequent attempt, the entry stays connected, and no reconnect is ever attempted. After the fix, the same scenario reconnects transparently and the retried call returns the correct payload.

What changed

Ports the agent-core-v2 recovery pattern to the legacy engine, reusing its exact failure classification so the two engines behave identically:

  • mcp/client-shared.ts: add isMcpConnectionClosedError / isMcpTransportFailure / isMcpMalformedResultError / probeMcpLiveness (ported from agent-core-v2/src/mcpCore/client-shared.ts), and MCPClient.ping (bounded by a 5s probe timeout) implemented by the stdio/SSE/HTTP clients.
  • mcp/connection-manager.ts: add reconnectAndJoin(name) so several parallel failing calls to a dropped server collapse into one reconnect (same as v2).
  • agent/tool/index.ts: the wrapped MCP tool call now (1) rethrows server-answered errors (reconnecting would not change the answer), (2) probes liveness on ambiguous transport failures and retries once in place when the server is alive, and (3) on a provably dead transport drives one shared reconnect and retries on the fresh client. A server that flips to failed keeps its tools registered, so the next call can drive the reconnect and calls fail with the server's own error in the meantime instead of the tools silently vanishing from the tool list.

Retries are at-least-once (same accepted trade-off as v2): if the transport died after the server processed the call but before the response arrived, the retry may duplicate side effects.

Deliberate boundaries, kept for parity with the v2 behavior: a call-time RequestTimeout is still classified as a server answer (no reconnect), and there is no negative caching — each call to a permanently dead server pays one bounded probe plus one failed reconnect attempt before erroring loudly.

Tests: test/mcp/tool-manager-mcp.test.ts gains unit coverage of the classification/retry matrix (dead transport → reconnect + retry on fresh client, transient blip → in-place retry, server-answered and malformed-result errors → rethrow, failed reconnect → combined error, aborted call → no reconnect, connection-closed → probe skipped, reconnect yielding no fresh client → original error, failed status keeps tools registered) plus a real-HTTP regression test for the issue scenario (server restart mid-session → the same tool on the same server answers). test/mcp/connection-manager.test.ts gains reconnectAndJoin dedup/unknown-server coverage.

Checks run (Node 24.15.0, pnpm 10.33.0): full @moonshot-ai/agent-core suite — 4143 passed / 225 files; tsc --noEmit clean for agent-core, @moonshot-ai/kimi-code-sdk, and @moonshot-ai/kimi-code; oxlint --type-aware — 0 errors (only pre-existing repo warnings). The full diff was reviewed by a second model via kiro-cli (claude-opus-5); its actionable findings (untangled port-rebind race in the test helper, a missing type annotation, extra classification test cases) are incorporated.

Testing limitations: the reproduction uses in-process streamable-HTTP MCP servers (SDK StreamableHTTPServerTransport), not a specific production server; the reporter's exact engine (legacy flag vs. SDK embedding) is inferred from the symptoms rather than confirmed, and the wire-level "tool substitution" is addressed by keeping the failed server's tools registered and failing loudly — no evidence of a name-level dispatch bug was found (dispatch is by exact tool name).

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

…ol call

When a streamable-HTTP MCP server's session drops mid-session (e.g. a
server restart), the legacy engine never re-established it: every call
to that server's tools failed for the rest of the session, and the
failed server's tools were unregistered, so the model retried its work
through other servers' tools instead of getting a loud, server-named
error (MoonshotAI#2742).

Port the agent-core-v2 recovery pattern (MoonshotAI#1991) to the legacy engine:

- The wrapped MCP tool call now classifies the failure: server-answered
  errors are rethrown, ambiguous transport failures are probed with a
  ping and retried once in place when alive, and a dead transport
  triggers one shared reconnect through the connection manager before
  retrying on the fresh client.
- McpConnectionManager gains reconnectAndJoin so parallel failing calls
  collapse into a single reconnect.
- A server that flips to failed keeps its tools registered, so the next
  call can drive the reconnect and calls fail with the server's own
  error in the meantime.

The default agent-core-v2 engine already recovers this way; this brings
the legacy engine (KIMI_CODE_LEGACY_FLAG, SDK v1 client) to parity.
@changeset-bot

changeset-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6db3f53

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 858b9104c9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 403 to 410
if (entry.status === 'failed') {
this.unregisterMcpServer(entry.name);
this.agent.emitEvent({
type: 'tool.list.updated',
reason: 'mcp.failed',
serverName: entry.name,
});
// Keep the tools registered: a dropped connection is recovered through
// the wrapped call's reconnect-and-retry path, and until then calls
// fail loudly with the server's error instead of vanishing from the
// tool list (which made the model retry through *other* servers'
// tools — see #2742). The tool list itself did not change, so no
// `tool.list.updated` event is emitted.
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep tools registered through the pending reconnect state

When the first call-driven reconnect happens while the server is still unavailable, McpConnectionManager.reconnect() emits pending before ultimately emitting failed; the pending branch below unregisters the tools, so this failed no-op has nothing left to preserve. Consequently, one unsuccessful recovery attempt removes the server's tools for the rest of the session and later calls cannot drive another reconnect—the exact failure mode this change intends to prevent. Preserve existing tools during reconnect-style pending transitions as well, or otherwise distinguish initial/configuration pending states from recovery.

Useful? React with 👍 / 👎.

…state

Addresses a P1 review finding on PR MoonshotAI#2748: McpConnectionManager.reconnect()
emits 'pending' before 'failed', and the v1 ToolManager unregistered a
server's tools on 'pending' too — so one failed call-driven recovery
stranded the session without tools and later calls could never drive
another reconnect (the exact bug the PR fixes, MoonshotAI#2742).

Only 'disabled' now unregisters tools and emits mcp.disconnected; both
re-registration paths (connected, needs-auth) already start by
unregistering, so keeping tools through pending never leaves stale
entries behind.

Regression coverage: a real-HTTP-server test drives a call through a
failed reconnect (server down), asserts the tools stay registered, no
tool-list event fires, and the transport's own error surfaces
byte-equivalently, then brings the server back and verifies the next
call re-drives the reconnect and heals.
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.

Dropped streamable-HTTP MCP session: no auto-reconnect, and calls to the dead server execute different tools

1 participant