diff --git a/packages/coding-agent/src/coordinator-mcp/server.ts b/packages/coding-agent/src/coordinator-mcp/server.ts index ec1084bb0e..27486eb18f 100644 --- a/packages/coding-agent/src/coordinator-mcp/server.ts +++ b/packages/coding-agent/src/coordinator-mcp/server.ts @@ -5698,6 +5698,9 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions }, }; } + if (request.method === "ping") { + return { jsonrpc: "2.0", id, result: {} }; + } if (request.method === "tools/list") { return { jsonrpc: "2.0", id, result: { tools: COORDINATOR_MCP_TOOL_NAMES.map(toolSchema) } }; } diff --git a/packages/coding-agent/test/coordinator-mcp-server.test.ts b/packages/coding-agent/test/coordinator-mcp-server.test.ts index 1f7e453b17..5e13e80aed 100644 --- a/packages/coding-agent/test/coordinator-mcp-server.test.ts +++ b/packages/coding-agent/test/coordinator-mcp-server.test.ts @@ -423,6 +423,56 @@ async function registerSdkSession(server: ReturnType { + async function pingServer(root: string) { + const server = createCoordinatorMcpServer({ + env: { + GJC_COORDINATOR_MCP_WORKDIR_ROOTS: root, + GJC_COORDINATOR_MCP_STATE_ROOT: path.join(root, ".gjc", "coordinator-state"), + GJC_COORDINATOR_MCP_PROFILE: "local", + GJC_COORDINATOR_MCP_REPO: "repo", + }, + services: { getAgentDir: () => path.join(root, "agent-global") }, + }); + return server; + } + + it("answers the MCP ping keepalive with an empty result instead of method-not-found", async () => { + const root = await tempRoot(); + const server = await pingServer(root); + const response = await server.handleJsonRpc({ jsonrpc: "2.0", id: 1, method: "ping" }); + expect(response).toEqual({ jsonrpc: "2.0", id: 1, result: {} }); + }); + + it("preserves a string request id in the ping response", async () => { + const root = await tempRoot(); + const server = await pingServer(root); + const response = await server.handleJsonRpc({ jsonrpc: "2.0", id: "keepalive-1", method: "ping" }); + expect(response).toEqual({ jsonrpc: "2.0", id: "keepalive-1", result: {} }); + }); + + it("answers ping with extra params by ignoring them (params carry no payload)", async () => { + const root = await tempRoot(); + const server = await pingServer(root); + const response = await server.handleJsonRpc({ + jsonrpc: "2.0", + id: 42, + method: "ping", + params: { unexpected: "ignored" }, + }); + expect(response).toEqual({ jsonrpc: "2.0", id: 42, result: {} }); + }); + + it("does not write any coordinator state files for a ping keepalive", async () => { + const root = await tempRoot(); + const stateRoot = path.join(root, ".gjc", "coordinator-state"); + const server = await pingServer(root); + await server.handleJsonRpc({ jsonrpc: "2.0", id: 1, method: "ping" }); + const exists = await fs + .stat(stateRoot) + .then(() => true) + .catch(() => false); + expect(exists).toBe(false); + }); it("uses agent-global SDK discovery and returns credential-free broker status", async () => { const root = await tempRoot(); const controls: SdkControl[] = []; diff --git a/packages/coding-agent/test/coordinator-mcp/pump.test.ts b/packages/coding-agent/test/coordinator-mcp/pump.test.ts index 0619f1d650..8b8f26d11a 100644 --- a/packages/coding-agent/test/coordinator-mcp/pump.test.ts +++ b/packages/coding-agent/test/coordinator-mcp/pump.test.ts @@ -352,4 +352,14 @@ describe("pumpCoordinatorMcpStream — frame handling", () => { await pumpCoordinatorMcpStream(handler as never, ch, l => void writes.push(JSON.parse(l))); expect(writes.map(w => w.id)).toEqual([7]); }); + it("answers a ping request but emits nothing for a ping notification (no id)", async () => { + const handler = async (req: Rpc): Promise => ({ jsonrpc: "2.0", id: req.id ?? null, result: {} }); + const writes: Rpc[] = []; + const ch = channel(); + ch.push(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" })}\n`); // request → answered + ch.push(`${JSON.stringify({ jsonrpc: "2.0", method: "ping" })}\n`); // notification (no id) → no response + ch.close(); + await pumpCoordinatorMcpStream(handler as never, ch, l => void writes.push(JSON.parse(l))); + expect(writes.map(w => w.id)).toEqual([1]); // only the request id gets a response + }); });