feat: report MCP client telemetry (#242)

This commit is contained in:
Andrey Avtomonov 2026-05-30 18:00:25 +02:00 committed by GitHub
parent 25f639fba2
commit 2e5f7f25aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 216 additions and 29 deletions

View file

@ -157,7 +157,9 @@
"outcome",
"durationMs",
"errorClass",
"sampleRate"
"sampleRate",
"mcpClientName",
"mcpClientVersion"
]
},
{
@ -1131,7 +1133,13 @@
},
"sampleRate": {
"type": "number",
"const": 0.1
"const": 1
},
"mcpClientName": {
"type": "string"
},
"mcpClientVersion": {
"type": "string"
}
},
"required": [

View file

@ -156,7 +156,12 @@ const mcpRequestCompletedSchema = telemetryCommonEnvelopeSchema
outcome: outcomeSchema,
durationMs: z.number().nonnegative(),
errorClass: z.string().optional(),
sampleRate: z.literal(0.1),
sampleRate: z.literal(1),
// Raw, client-tool-controlled identity from the MCP initialize handshake
// (clientInfo.name/version). Optional: clients may omit clientInfo. Stored
// verbatim — normalize the free-form names at query time, not at write time.
mcpClientName: z.string().optional(),
mcpClientVersion: z.string().optional(),
})
.strict();
@ -325,7 +330,7 @@ export const telemetryEventCatalog = [
{
name: 'mcp_request_completed',
description: 'Emitted for sampled MCP tool requests.',
fields: ['toolName', 'outcome', 'durationMs', 'errorClass', 'sampleRate'],
fields: ['toolName', 'outcome', 'durationMs', 'errorClass', 'sampleRate', 'mcpClientName', 'mcpClientVersion'],
},
{
name: 'daemon_started',

View file

@ -75,17 +75,14 @@ export async function loadTelemetryIdentity(options: LoadTelemetryIdentityOption
const env = options.env ?? process.env;
const path = telemetryPath(options.homeDir ?? homedir());
if (envDisablesTelemetry(env) || options.stdoutIsTTY !== true) {
const existing = await readTelemetryFile(path);
return {
installId: existing?.installId,
enabled: false,
createdFile: false,
noticeShown: false,
path,
};
if (envDisablesTelemetry(env)) {
return { enabled: false, createdFile: false, noticeShown: false, path };
}
// Honor an already-consented identity regardless of the current surface.
// Telemetry enablement follows the persisted decision and opt-out env vars,
// not whether this invocation happens to own a TTY — MCP servers always run
// headless (stdio stubs stdout; the HTTP server runs detached).
const existing = await readTelemetryFile(path);
if (existing) {
return {
@ -97,6 +94,13 @@ export async function loadTelemetryIdentity(options: LoadTelemetryIdentityOption
};
}
// No identity yet. Minting one means showing the one-time opt-out notice, so
// first-run creation requires an interactive surface; a headless first run
// stays disabled and defers enablement until the next interactive run.
if (options.stdoutIsTTY !== true) {
return { enabled: false, createdFile: false, noticeShown: false, path };
}
const timestamp = (options.now ?? (() => new Date()))().toISOString();
const next = {
installId: randomUUID(),

View file

@ -52,7 +52,11 @@ type TelemetryEventFields<Name extends TelemetryEventName> = Omit<
>;
const emittedProjectSnapshots = new Set<string>();
const MCP_SAMPLE_RATE = 0.1 as const;
// MCP tool calls are captured at full rate while ktx is early-stage: at current
// install counts any sampling below 1.0 yields too few events to be useful, and
// the recorded sampleRate lets us dial this down (and reweight history) once
// per-session call volume justifies it.
const MCP_SAMPLE_RATE = 1 as const;
let mcpSampled: boolean | undefined;
function telemetryDebugEnabled(): boolean {
@ -64,7 +68,7 @@ export function shouldEmitMcpTelemetry(): boolean {
return mcpSampled;
}
export function mcpTelemetrySampleRate(): 0.1 {
export function mcpTelemetrySampleRate(): 1 {
return MCP_SAMPLE_RATE;
}