Skip to content

Commit f764986

Browse files
committed
feat(telemetry): emit mcp_connect on the singular MCP connect path
1 parent e3d414c commit f764986

2 files changed

Lines changed: 54 additions & 17 deletions

File tree

‎src/mcp/client-telemetry.test.ts‎

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ await withMockedModule(
2525
}),
2626
);
2727

28-
const { connectMCPServers } = await import("./client.js");
28+
const { connectMCPServers, connectMCPServer } = await import("./client.js");
2929

3030
function recordingTelemetry(events: Record<string, unknown>[]): Telemetry {
3131
return {
@@ -62,11 +62,17 @@ describe("connectMCPServers telemetry", () => {
6262
expect(clients).toHaveLength(1);
6363
expect(warnings).toHaveLength(1);
6464
expect(events).toHaveLength(2);
65-
expect(events.map((event) => event.transport)).toEqual([
65+
// Order-insensitive: the batch fans out over Promise.all, and each
66+
// singular connect captures its own event on completion, so arrival
67+
// order follows settle order, not config order.
68+
expect(events.map((event) => event.transport).sort()).toEqual([
6669
"stdio",
6770
"stdio",
6871
]);
69-
expect(events.map((event) => event.result)).toEqual(["ok", "fail"]);
72+
expect(events.map((event) => event.result).sort()).toEqual([
73+
"fail",
74+
"ok",
75+
]);
7076
} finally {
7177
for (const client of clients) await client.close();
7278
}
@@ -92,3 +98,31 @@ describe("connectMCPServers telemetry", () => {
9298
}
9399
});
94100
});
101+
102+
describe("connectMCPServer telemetry", () => {
103+
beforeEach(() => {
104+
connectCalls = 0;
105+
failOnCall = -1;
106+
});
107+
108+
test("singular ok + fail emits one mcp_connect each, exactly once per attempt", async () => {
109+
const events: Record<string, unknown>[] = [];
110+
const telemetry = recordingTelemetry(events);
111+
const ok = await connectMCPServer(
112+
{ name: "a", type: "stdio", command: "test-server-a" },
113+
{ telemetry },
114+
);
115+
expect(ok.ok).toBe(true);
116+
if (ok.ok) await ok.client.close();
117+
failOnCall = 2;
118+
const failed = await connectMCPServer(
119+
{ name: "b", type: "stdio", command: "test-server-b" },
120+
{ telemetry },
121+
);
122+
expect(failed.ok).toBe(false);
123+
expect(connectCalls).toBe(2);
124+
expect(events).toHaveLength(connectCalls);
125+
expect(events.map((event) => event.transport)).toEqual(["stdio", "stdio"]);
126+
expect(events.map((event) => event.result)).toEqual(["ok", "fail"]);
127+
});
128+
});

‎src/mcp/client.ts‎

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -857,37 +857,40 @@ export async function connectMCPServer(
857857
config: ResolvedMCPServerConfig,
858858
options: MCPConnectOptions = {},
859859
): Promise<MCPConnectResult> {
860-
return isHttpServer(config)
860+
const telemetry = options.telemetry ?? NOOP_TELEMETRY;
861+
const result = await (isHttpServer(config)
861862
? connectHttp(config, options)
862-
: connectStdio(config, options);
863+
: connectStdio(config, options));
864+
// One enum-only event per outward connection outcome. Internal HTTP
865+
// auth-recovery re-dials stay inside connectHttp, so this wrapper fires
866+
// exactly once per connectMCPServer call. Server name, URL, and error
867+
// text never leave the process.
868+
captureMcpConnect(telemetry, {
869+
transport: isHttpServer(config) ? "http" : "stdio",
870+
result: classifyMcpConnectOutcome(result),
871+
});
872+
return result;
863873
}
864874

865875
export async function connectMCPServers(
866876
configs: ResolvedMCPServerConfig[],
867877
onWarning: (message: string) => void,
868878
options: MCPConnectOptions = {},
869879
): Promise<MCPClient[]> {
870-
const telemetry = options.telemetry ?? NOOP_TELEMETRY;
880+
// Telemetry flows through options into connectMCPServer, which captures
881+
// one mcp_connect per config.
871882
const results = await Promise.all(
872883
configs.map((c) => connectMCPServer(c, options)),
873884
);
874885
const clients: MCPClient[] = [];
875-
for (let index = 0; index < results.length; index += 1) {
876-
const result = results[index];
877-
if (result === undefined) continue;
886+
for (const result of results) {
878887
if (result.ok) clients.push(result.client);
879888
else
880889
onWarning(
881890
`[mcp] Warning: failed to connect to MCP server "${result.serverName}": ${result.error}`,
882891
);
883-
// One enum-only event per server: the transport that was dialed and the
884-
// outcome enum. Server name, URL, and error text never leave the process.
885-
const config = configs[index];
886-
captureMcpConnect(telemetry, {
887-
transport:
888-
config !== undefined && isHttpServer(config) ? "http" : "stdio",
889-
result: classifyMcpConnectOutcome(result),
890-
});
892+
// No emit here: connectMCPServer already captured one enum-only
893+
// mcp_connect per config. Emitting again would double-count.
891894
}
892895
return clients;
893896
}

0 commit comments

Comments
 (0)