Skip to content

Commit 2b5663a

Browse files
fix(local-mcp): answer the relay MCP initialize handshake locally
A relayed server's real connection can be a slow-to-spawn stdio process on the user's machine. Relaying the MCP `initialize` handshake made connection establishment wait on a cross-machine round trip plus that spawn, blowing the client's init timeout so the server was dropped for the whole run ("no slack tool"). As a pass-through proxy the relay now answers `initialize` (and the paired `initialized` notification) locally — advertising tools capability and echoing the client's protocol version — so the connection is instant; `tools/list` and tool calls still relay for real with their generous per-request timeouts. Also log received `mcp_response` commands in the sandbox so the desktop's response behavior is diagnosable from the readable side, not just the (often invisible) Electron main-process logs. Generated-By: PostHog Code Task-Id: 7c87c3a4-0be6-475e-a8ec-269140ded301
1 parent b9d9928 commit 2b5663a

3 files changed

Lines changed: 70 additions & 4 deletions

File tree

packages/agent/src/server/agent-server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,13 @@ export class AgentServer {
12161216
const resolved = this.mcpRelayServer?.resolveResponse(
12171217
params as unknown as McpRelayResponse,
12181218
);
1219+
// Logged so the desktop's response behavior is visible from the
1220+
// readable sandbox side, not just the (often invisible) desktop logs.
1221+
this.logger.debug("MCP relay response received", {
1222+
requestId: String(params.requestId),
1223+
server: String(params.server),
1224+
resolved,
1225+
});
12191226
if (!resolved) {
12201227
throw new Error(
12211228
`No pending MCP relay request found for id: ${String(params.requestId)}`,

packages/agent/src/server/mcp-relay-server.test.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,41 @@ describe("McpRelayServer", () => {
179179
expect(events).toHaveLength(0);
180180
});
181181

182-
it("relays notifications fire-and-forget with a 202", async () => {
183-
const { events, post } = await startRelay();
182+
it("answers the initialize handshake locally without relaying", async () => {
183+
// Even with no reachable client, the connection must establish so the
184+
// real (slow-to-spawn) server isn't dropped during startup.
185+
const { events, post } = await startRelay({
186+
hasReachableClient: () => false,
187+
});
184188
const response = await post("slack", {
189+
jsonrpc: "2.0",
190+
id: 1,
191+
method: "initialize",
192+
params: { protocolVersion: "2025-06-18" },
193+
});
194+
expect(response.status).toBe(200);
195+
const body = (await response.json()) as {
196+
result: { protocolVersion: string; capabilities: { tools: unknown } };
197+
};
198+
expect(body.result.protocolVersion).toBe("2025-06-18");
199+
expect(body.result.capabilities.tools).toBeDefined();
200+
expect(events).toHaveLength(0);
201+
202+
// The paired initialized notification is also answered locally.
203+
const notified = await post("slack", {
185204
jsonrpc: "2.0",
186205
method: "notifications/initialized",
187206
});
207+
expect(notified.status).toBe(202);
208+
expect(events).toHaveLength(0);
209+
});
210+
211+
it("relays non-initialize notifications fire-and-forget with a 202", async () => {
212+
const { events, post } = await startRelay();
213+
const response = await post("slack", {
214+
jsonrpc: "2.0",
215+
method: "notifications/cancelled",
216+
});
188217
expect(response.status).toBe(202);
189218
await expect.poll(() => events.length).toBe(1);
190219
});
@@ -216,11 +245,11 @@ describe("McpRelayServer", () => {
216245
hasReachableClient: () => reachable,
217246
requestTimeoutMs: 2_000,
218247
});
219-
// Session-start handshake fires before the event relay attaches.
248+
// A relayed request (tools/list) fires before the event relay attaches.
220249
const responsePromise = post("slack", {
221250
jsonrpc: "2.0",
222251
id: 1,
223-
method: "initialize",
252+
method: "tools/list",
224253
});
225254
await expect.poll(() => events.length).toBe(1);
226255
const event = events[0] as { requestId: string };

packages/agent/src/server/mcp-relay-server.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,36 @@ export class McpRelayServer {
222222
});
223223
}
224224

225+
// Answer the MCP `initialize` handshake locally. The real server may be
226+
// a slow-to-spawn stdio process on the user's machine; making connection
227+
// establishment wait on a cross-machine round trip + process spawn blows
228+
// the client's init timeout, and the server is dropped for the run. As a
229+
// pass-through proxy we advertise tools capability and echo the client's
230+
// protocol version; `tools/list` and tool calls relay for real (with
231+
// generous per-request timeouts). The matching `initialized` notification
232+
// is likewise answered locally.
233+
if (payload.method === "initialize") {
234+
const params = payload.params as
235+
| { protocolVersion?: unknown }
236+
| undefined;
237+
const protocolVersion =
238+
typeof params?.protocolVersion === "string"
239+
? params.protocolVersion
240+
: "2025-06-18";
241+
return c.json({
242+
jsonrpc: "2.0",
243+
id: payload.id,
244+
result: {
245+
protocolVersion,
246+
capabilities: { tools: { listChanged: false } },
247+
serverInfo: { name: `relay:${server}`, version: "1.0.0" },
248+
},
249+
});
250+
}
251+
if (payload.method === "notifications/initialized") {
252+
return c.body(null, 202);
253+
}
254+
225255
const requestId = crypto.randomUUID();
226256
const timeoutMs =
227257
this.config.requestTimeoutMs ?? DEFAULT_RELAY_TIMEOUT_MS;

0 commit comments

Comments
 (0)