MoonshotAI / MoonshotAI/kimi-code
BUG: MCP reconnect reports connected status after clearing the active client
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 7.5k
- Forks
- 1.2k
- Avg merge
- 11h 53m
- Merged PRs (30d)
- 350
Description
What version of Kimi Code is running?
main@6b72345f8bb03487e3bcc05b541e65484818428c (@moonshot-ai/kimi-code 0.36.1; @moonshot-ai/agent-core 0.15.8)
Which open platform/subscription were you using?
Not applicable; the reproduction does not log in or make a provider request.
Which model were you using?
Not applicable; reproduced by directly invoking deterministic MCP connection-management code.
What platform is your computer?
Darwin 24.6.0 arm64 arm
What issue are you seeing?
In packages/agent-core/src/mcp/connection-manager.ts, McpConnectionManager.reconnect clears the active MCP client and awaits the old client's asynchronous close() before changing the server status to pending.
While that close is pending, the public APIs report inconsistent states for the same server:
get("slowClose"): { status: "connected", toolCount: 3 }
resolved("slowClose"): undefined
The manager therefore reports the server as connected after its client is no longer available. The pending transition is delayed until the old client's close completes.
What steps can reproduce the bug?
-
Check out Kimi Code
mainat commit6b72345f8bb03487e3bcc05b541e65484818428c. -
Add the following test inside the existing
describe("McpConnectionManager", ...)block inpackages/agent-core/test/mcp/connection-manager.test.ts. The file already importsvifromvitest.
it("marks a connected server pending before awaiting the old client close", async () => {
const cm = new McpConnectionManager();
let releaseClose = () => {};
try {
await cm.connectAll({ slowClose: stdioConfig() });
expect(cm.get("slowClose")).toMatchObject({
status: "connected",
toolCount: 3,
});
const internalEntry = (
cm as unknown as {
entries: Map<string, { client?: { close(): Promise<void> } }>;
}
).entries.get("slowClose");
const client = internalEntry?.client;
if (client === undefined) {
throw new Error("expected a connected client");
}
let notifyCloseStarted = () => {};
const closeStarted = new Promise<void>((resolve) => {
notifyCloseStarted = resolve;
});
const closeBlocked = new Promise<void>((resolve) => {
releaseClose = resolve;
});
const originalClose = client.close.bind(client);
vi.spyOn(client, "close").mockImplementation(async () => {
notifyCloseStarted();
await closeBlocked;
await originalClose();
});
const reconnect = cm.reconnect("slowClose");
await closeStarted;
const entryDuringClose = cm.get("slowClose");
const resolvedDuringClose = cm.resolved("slowClose");
releaseClose();
await reconnect;
expect(resolvedDuringClose).toBeUndefined();
expect(entryDuringClose).toMatchObject({
status: "pending",
toolCount: 0,
error: undefined,
});
} finally {
releaseClose();
await cm.shutdown();
}
}, 15000);
- Run:
pnpm --filter @moonshot-ai/agent-core exec vitest run test/mcp/connection-manager.test.ts
- Observe that the newly added assertion fails while the 43 existing tests pass.
What is the expected behavior?
Once reconnect() begins and the active client is cleared, the server should be exposed as pending with zero tools before the old client's asynchronous close is awaited:
cm.get("slowClose");
// Relevant fields: { status: "pending", toolCount: 0, error: undefined }
cm.resolved("slowClose");
// undefined
The pending status event should be emitted at that point so get(), resolved(), status consumers, and the session tool registry consistently represent the connection as unavailable during reconnect.
Additional information
The focused run reports:
FAIL |kimi-core| test/mcp/connection-manager.test.ts > McpConnectionManager > marks a connected server pending before awaiting the old client close
AssertionError: expected { name: 'slowClose', …(4) } to match object { status: 'pending', …(2) }
(2 matching properties omitted from actual)
- Expected
+ Received
{
"error": undefined,
- "status": "pending",
- "toolCount": 0,
+ "status": "connected",
+ "toolCount": 3,
}
Test Files 1 failed (1)
Tests 1 failed | 43 passed (44)
At the tested commit, McpConnectionManager.reconnect performs the operations in this order:
const attemptId = this.beginConnectAttempt(entry);
await this.closeClient(entry);
if (!this.isCurrent(entry, attemptId)) return;
entry.status = 'pending';
entry.tools = undefined;
entry.rawTools = undefined;
entry.enabledNames = undefined;
entry.error = undefined;
this.emit(entry);
closeClient removes the client reference before awaiting the asynchronous close:
const client = entry.client;
entry.client = undefined;
await this.closeRuntimeClient(client);
While closeRuntimeClient(client) is pending, get() still derives its public state from the unchanged connected status and enabled tool names, so it reports status: "connected" and toolCount: 3. In the same interval, resolved() returns undefined because entry.client has already been cleared.
No pending status event is emitted during this interval. ToolManager unregisters a server's tools when it receives a pending, disabled, or failed status, so the previously registered tools remain present until the old client's close completes.
packages/agent-core-v2/src/mcpCore/connection-manager.ts uses the same reconnect ordering: it awaits closeClient(entry) before changing the entry status to pending.
One possible fix direction is to change the entry to pending, clear its tool state, and emit the status update before awaiting the old client's close. The existing attempt-identity check should continue to prevent a superseded reconnect from proceeding to connectOne. Regression coverage should verify both the public state and emitted status events while the old client's close remains pending.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with packages/agent-core/src/mcp/connection-manager.ts and the matching agent-core-v2 connection manager, then run packages/agent-core/test/mcp/connection-manager.test.ts with the provided reproduction. Verify that reconnect exposes pending state, clears tools, returns undefined from resolved(), and emits the status update while the old client close is still blocked; preserve the existing attempt-identity behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend, cli
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100