mcpfy-pulse 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/mcpfy-proxy.js.map +1 -1
- package/dist/src/core/batcher.d.ts +21 -0
- package/dist/src/core/batcher.d.ts.map +1 -1
- package/dist/src/index.cjs +23 -0
- package/dist/src/index.cjs.map +1 -1
- package/dist/src/index.js +23 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/wrap-transport.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/proxy/run.ts","../../src/config.ts","../../src/core/classify.ts","../../src/core/batcher.ts","../../src/bin/mcpfy-proxy.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { resolveConfig } from \"../config.js\";\nimport { MessageClassifier } from \"../core/classify.js\";\nimport { TelemetryBatcher } from \"../core/batcher.js\";\n\nfunction tryParse(line: string): any {\n const trimmed = line.trim();\n if (!trimmed) return undefined;\n try {\n return JSON.parse(trimmed);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Forwards raw chunks to `dest` immediately and unchanged, then independently\n * re-buffers a copy into newline-delimited lines for classification. The pass-through\n * path never waits on parsing — a bug in classification can never corrupt or delay\n * the actual MCP traffic.\n */\nfunction pipeLines(source: NodeJS.ReadableStream, dest: NodeJS.WritableStream, onLine: (line: string) => void): void {\n let buffer = \"\";\n source.on(\"data\", (chunk: Buffer) => {\n dest.write(chunk);\n buffer += chunk.toString(\"utf8\");\n let newlineIndex: number;\n while ((newlineIndex = buffer.indexOf(\"\\n\")) !== -1) {\n const line = buffer.slice(0, newlineIndex);\n buffer = buffer.slice(newlineIndex + 1);\n onLine(line);\n }\n });\n}\n\n/**\n * `mcpfy-proxy -- <command> [args...]`\n *\n * Spawns <command> as a child process and sits in its stdin/stdout pipe. Client-to-server\n * messages (parent stdin -> child stdin) are classified as \"incoming\"; server-to-client\n * messages (child stdout -> parent stdout) are classified as \"outgoing\", mirroring\n * onmessage/send in the in-process wrapper. Works for any language — the proxy never\n * parses anything beyond newline-delimited JSON-RPC framing.\n */\nexport async function runProxy(argv: string[]): Promise<void> {\n const sepIndex = argv.indexOf(\"--\");\n if (sepIndex === -1 || sepIndex === argv.length - 1) {\n process.stderr.write(\n \"Usage: mcpfy-proxy -- <command> [args...]\\n\" +\n \"Example: mcpfy-proxy -- npx -y @modelcontextprotocol/server-github\\n\"\n );\n process.exitCode = 1;\n return;\n }\n\n const [command, ...args] = argv.slice(sepIndex + 1);\n const config = resolveConfig();\n const classifier = new MessageClassifier();\n const batcher = config.apiKey\n ? new TelemetryBatcher(config, { sdkName: \"unknown\", installMode: \"stdio-proxy\" })\n : undefined;\n\n const child = spawn(command, args, {\n stdio: [\"pipe\", \"pipe\", \"inherit\"],\n env: process.env,\n });\n\n child.on(\"error\", (err) => {\n process.stderr.write(`mcpfy-proxy: failed to start \"${command}\": ${err.message}\\n`);\n process.exitCode = 1;\n });\n\n pipeLines(process.stdin, child.stdin!, (line) => {\n const message = tryParse(line);\n if (!message) return;\n const event = classifier.onIncoming(message);\n if (event && batcher) batcher.push(event);\n });\n\n pipeLines(child.stdout!, process.stdout, (line) => {\n const message = tryParse(line);\n if (!message) return;\n const event = classifier.onOutgoing(message);\n if (event && batcher) batcher.push(event);\n });\n\n process.on(\"SIGINT\", () => child.kill(\"SIGINT\"));\n process.on(\"SIGTERM\", () => child.kill(\"SIGTERM\"));\n\n await new Promise<void>((resolvePromise) => {\n child.on(\"exit\", (code, signal) => {\n void batcher?.close().finally(() => {\n if (signal) {\n process.kill(process.pid, signal);\n } else {\n process.exitCode = code ?? 0;\n }\n resolvePromise();\n });\n });\n });\n}\n","import type { TelemetryOptions } from \"./types.js\";\n\n// Path matches the real route on cloudmcp-nest: POST /v1/telemetry/ingest.\nconst DEFAULT_ENDPOINT = \"https://api.mcpfy.ai/v1/telemetry/ingest\";\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000;\nconst DEFAULT_MAX_BATCH_SIZE = 500;\n\nexport interface ResolvedConfig {\n apiKey: string | undefined;\n endpoint: string;\n flushIntervalMs: number;\n maxBatchSize: number;\n}\n\nexport function resolveConfig(options?: TelemetryOptions): ResolvedConfig {\n return {\n apiKey: options?.apiKey ?? process.env.MCPFY_API_KEY,\n endpoint: options?.endpoint ?? process.env.MCPFY_TELEMETRY_ENDPOINT ?? DEFAULT_ENDPOINT,\n flushIntervalMs: options?.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,\n maxBatchSize: options?.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,\n };\n}\n","import type { DeclaredToolMeta, TelemetryEvent } from \"../types.js\";\n\ninterface PendingEntry {\n method: string;\n startedAt: number;\n argsBytes: number;\n extra: Partial<TelemetryEvent>;\n progressToken?: string;\n progressCount: number;\n}\n\nfunction byteLength(value: unknown): number {\n // `undefined` (no `params` key at all) and `null` (an explicit `\"params\": null`)\n // both mean \"no meaningful params\" — collapsed to the same 0 here so argsBytes\n // doesn't swing on a client's JSON-serialization choice. Matches classify.py's\n // `_byte_length`, which can't tell the two apart once parsed into a dict anyway.\n if (value === undefined || value === null) return 0;\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return 0;\n }\n}\n\n/**\n * Reduces a `tools/list` response's `result.tools` array to non-content metadata —\n * whether/how-long each description is, and how many of its params are individually\n * documented, plus structured-output/annotation presence. Never reads the description\n * text or schema into the event itself.\n */\nfunction extractDeclaredTools(tools: unknown): DeclaredToolMeta[] | undefined {\n if (!Array.isArray(tools)) return undefined;\n const declared: DeclaredToolMeta[] = [];\n for (const tool of tools) {\n if (!tool || typeof tool.name !== \"string\") continue;\n const description = typeof tool.description === \"string\" ? tool.description : \"\";\n const properties = tool.inputSchema?.properties;\n const paramNames = properties && typeof properties === \"object\" ? Object.keys(properties) : [];\n const paramsWithDescriptionCount = paramNames.filter(\n (p) => typeof properties[p]?.description === \"string\" && properties[p].description.trim().length > 0,\n ).length;\n const annotations = tool.annotations && typeof tool.annotations === \"object\" ? tool.annotations : undefined;\n declared.push({\n name: tool.name,\n hasDescription: description.trim().length > 0,\n descriptionLength: description.length,\n paramCount: paramNames.length,\n paramsWithDescriptionCount,\n hasOutputSchema: tool.outputSchema != null && typeof tool.outputSchema === \"object\",\n readOnlyHint: typeof annotations?.readOnlyHint === \"boolean\" ? annotations.readOnlyHint : undefined,\n destructiveHint: typeof annotations?.destructiveHint === \"boolean\" ? annotations.destructiveHint : undefined,\n idempotentHint: typeof annotations?.idempotentHint === \"boolean\" ? annotations.idempotentHint : undefined,\n openWorldHint: typeof annotations?.openWorldHint === \"boolean\" ? annotations.openWorldHint : undefined,\n });\n }\n return declared;\n}\n\n/**\n * Flattens a `ClientCapabilities`/`ServerCapabilities` object to a list of dotted\n * capability paths that are actually declared — group-presence for capabilities with\n * no meaningful sub-flags, `<group>.<subflag>` for the handful that have one worth\n * surfacing (resources/prompts/tools listChanged, resources.subscribe). `experimental`\n * is deliberately skipped (arbitrary custom capability names, out of scope).\n */\nfunction extractCapabilities(capabilities: unknown): string[] | undefined {\n if (!capabilities || typeof capabilities !== \"object\") return undefined;\n const caps = capabilities as Record<string, any>;\n const out: string[] = [];\n const GROUPS = [\"sampling\", \"elicitation\", \"roots\", \"tasks\", \"logging\", \"prompts\", \"resources\", \"tools\", \"completions\"];\n for (const group of GROUPS) {\n const value = caps[group];\n if (value === undefined || value === null) continue;\n out.push(group);\n if (typeof value === \"object\") {\n if (value.listChanged === true) out.push(`${group}.listChanged`);\n if (group === \"resources\" && value.subscribe === true) out.push(`${group}.subscribe`);\n }\n }\n return out.length > 0 ? out : undefined;\n}\n\nfunction extractLabel(method: string, params: any): Partial<TelemetryEvent> {\n switch (method) {\n case \"tools/call\":\n return { toolName: params?.name };\n case \"prompts/get\":\n return { promptName: params?.name };\n case \"resources/read\":\n return { resourceUri: params?.uri };\n case \"initialize\": {\n // Protocol handshake metadata, not user data — same fields the spec itself exchanges in the clear.\n const label: Partial<TelemetryEvent> = {\n clientName: params?.clientInfo?.name,\n clientVersion: params?.clientInfo?.version,\n protocolVersion: params?.protocolVersion,\n };\n const clientCapabilities = extractCapabilities(params?.capabilities);\n if (clientCapabilities) label.clientCapabilities = clientCapabilities;\n return label;\n }\n default:\n return {};\n }\n}\n\n/**\n * Tracks request/response pairs across the two seams every Transport exposes\n * (onmessage = incoming, send = outgoing) and emits one event per completed\n * request. Only method names, byte counts, timing, and outcome are captured —\n * argument values and result content are never read beyond their byte length.\n * The one exception is `tools/list`, where per-tool description/param *presence\n * and length* are captured (never the description text or schema) — see\n * `extractDeclaredTools`.\n *\n * Also tracks a handful of notification types now: `notifications/cancelled` (turns\n * into a completed event with `outcome:\"cancelled\"`, since the original request never\n * gets a response), `notifications/progress` (counted per request, never the progress\n * values themselves), and `notifications/message` plus the three list_changed\n * notifications (emitted as standalone `type:\"notification\"` events). Everything else server-initiated\n * (sampling, elicitation, roots) is still intentionally not tracked — out of scope for\n * this pass.\n */\nexport class MessageClassifier {\n private pending = new Map<string | number, PendingEntry>();\n private pendingByToken = new Map<string, string | number>();\n\n private forgetPending(id: string | number, pending: PendingEntry): void {\n this.pending.delete(id);\n if (pending.progressToken !== undefined) this.pendingByToken.delete(pending.progressToken);\n }\n\n /**\n * Returns an event only for the one incoming case that completes a request without\n * ever seeing a response: a client cancelling its own still-pending request. Every\n * other incoming message (ordinary requests, `notifications/initialized`, and any\n * other notification) returns `undefined`, matching prior behavior exactly.\n */\n onIncoming(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, params } = message;\n\n if (id === undefined && method === \"notifications/cancelled\") {\n // Never read params.reason — free-text, same \"never content\" boundary as everything else.\n const requestId = params?.requestId;\n if (requestId === undefined) return undefined;\n const pending = this.pending.get(requestId);\n if (!pending) return undefined;\n this.forgetPending(requestId, pending);\n return {\n type: \"request\",\n method: pending.method,\n argsBytes: pending.argsBytes,\n durationMs: Date.now() - pending.startedAt,\n outcome: \"cancelled\",\n progressUpdateCount: pending.progressCount,\n timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n }\n\n if (id === undefined || !method) return undefined; // only requests carry both an id and a method\n\n const progressToken = params?._meta?.progressToken;\n const entry: PendingEntry = {\n method,\n startedAt: Date.now(),\n argsBytes: byteLength(params),\n extra: extractLabel(method, params),\n progressCount: 0,\n };\n if (typeof progressToken === \"string\" || typeof progressToken === \"number\") {\n entry.progressToken = String(progressToken);\n this.pendingByToken.set(entry.progressToken, id);\n }\n this.pending.set(id, entry);\n return undefined;\n }\n\n onOutgoing(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, params, result, error } = message;\n\n if (method && id === undefined) {\n // An outgoing notification (server -> client) — not a response.\n if (method === \"notifications/progress\") {\n const pendingId = this.pendingByToken.get(String(params?.progressToken));\n if (pendingId !== undefined) {\n const pending = this.pending.get(pendingId);\n if (pending) pending.progressCount += 1;\n }\n return undefined;\n }\n if (method === \"notifications/message\") {\n return {\n type: \"notification\",\n method,\n logLevel: typeof params?.level === \"string\" ? params.level : undefined,\n timestamp: new Date().toISOString(),\n };\n }\n if (\n method === \"notifications/tools/list_changed\" ||\n method === \"notifications/resources/list_changed\" ||\n method === \"notifications/prompts/list_changed\"\n ) {\n return { type: \"notification\", method, timestamp: new Date().toISOString() };\n }\n return undefined; // other notifications / server-initiated requests — still out of scope\n }\n\n if (method || id === undefined) return undefined;\n\n const pending = this.pending.get(id);\n if (!pending) return undefined;\n this.forgetPending(id, pending);\n\n const event: TelemetryEvent = {\n type: \"request\",\n method: pending.method,\n argsBytes: pending.argsBytes,\n resultBytes: byteLength(error ?? result),\n durationMs: Date.now() - pending.startedAt,\n outcome: error ? \"error\" : \"ok\",\n errorCode: error?.code,\n progressUpdateCount: pending.progressCount,\n timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n\n if (pending.method === \"initialize\" && !error) {\n if (result?.serverInfo) {\n event.serverName = result.serverInfo.name;\n event.serverVersion = result.serverInfo.version;\n }\n const serverCapabilities = extractCapabilities(result?.capabilities);\n if (serverCapabilities) event.serverCapabilities = serverCapabilities;\n }\n\n if (pending.method === \"tools/call\" && !error && result?.isError === true) {\n event.resultIsError = true;\n }\n\n if (pending.method === \"tools/list\" && !error) {\n const declaredTools = extractDeclaredTools(result?.tools);\n if (declaredTools) event.declaredTools = declaredTools;\n }\n\n return event;\n }\n}\n","import type { TelemetryEvent, SdkName, InstallMode } from \"../types.js\";\nimport type { ResolvedConfig } from \"../config.js\";\n\nexport interface BatchMeta {\n serverName?: string;\n serverVersion?: string;\n sdkName: SdkName;\n sdkVersion?: string;\n installMode: InstallMode;\n}\n\n/**\n * Ring-buffer batcher: queues events, flushes on a timer or when full, and never\n * throws or retries indefinitely — a failed or unreachable ingest endpoint is a\n * silent no-op, by design (telemetry must never affect the server's own behavior,\n * and the endpoint may not exist yet — see telemetry-master-plan.md §4/§7).\n */\nexport class TelemetryBatcher {\n private queue: TelemetryEvent[] = [];\n private timer: ReturnType<typeof setInterval>;\n\n constructor(\n private readonly config: ResolvedConfig,\n private readonly meta: BatchMeta\n ) {\n this.timer = setInterval(() => void this.flush(), config.flushIntervalMs);\n this.timer.unref?.(); // telemetry must never be the reason a process stays alive\n }\n\n push(event: TelemetryEvent): void {\n this.queue.push(event);\n if (this.queue.length >= this.config.maxBatchSize) void this.flush();\n }\n\n async flush(): Promise<void> {\n if (this.queue.length === 0) return;\n const events = this.queue.splice(0, this.queue.length);\n try {\n const response = await fetch(this.config.endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.config.apiKey ?? \"\"}`,\n },\n body: JSON.stringify({\n serverName: this.meta.serverName,\n serverVersion: this.meta.serverVersion,\n sdkName: this.meta.sdkName,\n sdkVersion: this.meta.sdkVersion,\n installMode: this.meta.installMode,\n events,\n }),\n });\n // Never throws on a non-2xx (silent drop is still the contract — see class doc),\n // but a warning is the difference between \"found the bug in 30 seconds\" and\n // \"found it three days later\": a 404/401/500 here means events are being\n // discarded even though fetch() itself didn't throw.\n if (!response.ok) {\n console.warn(\n `[mcpfy-pulse] telemetry flush failed: ${response.status} ${response.statusText} (${this.config.endpoint}) — ${events.length} event(s) dropped`\n );\n }\n } catch (err) {\n console.warn(\n `[mcpfy-pulse] telemetry flush failed: ${err instanceof Error ? err.message : String(err)} (${this.config.endpoint}) — ${events.length} event(s) dropped`\n );\n }\n }\n\n async close(): Promise<void> {\n clearInterval(this.timer);\n await this.flush();\n }\n}\n","#!/usr/bin/env node\nimport { runProxy } from \"../proxy/run.js\";\n\nrunProxy(process.argv.slice(2)).catch((err) => {\n process.stderr.write(`mcpfy-proxy: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exitCode = 1;\n});\n"],"mappings":"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","names":["pending"]}
|
|
1
|
+
{"version":3,"sources":["../../src/proxy/run.ts","../../src/config.ts","../../src/core/classify.ts","../../src/core/batcher.ts","../../src/bin/mcpfy-proxy.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { resolveConfig } from \"../config.js\";\nimport { MessageClassifier } from \"../core/classify.js\";\nimport { TelemetryBatcher } from \"../core/batcher.js\";\n\nfunction tryParse(line: string): any {\n const trimmed = line.trim();\n if (!trimmed) return undefined;\n try {\n return JSON.parse(trimmed);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Forwards raw chunks to `dest` immediately and unchanged, then independently\n * re-buffers a copy into newline-delimited lines for classification. The pass-through\n * path never waits on parsing — a bug in classification can never corrupt or delay\n * the actual MCP traffic.\n */\nfunction pipeLines(source: NodeJS.ReadableStream, dest: NodeJS.WritableStream, onLine: (line: string) => void): void {\n let buffer = \"\";\n source.on(\"data\", (chunk: Buffer) => {\n dest.write(chunk);\n buffer += chunk.toString(\"utf8\");\n let newlineIndex: number;\n while ((newlineIndex = buffer.indexOf(\"\\n\")) !== -1) {\n const line = buffer.slice(0, newlineIndex);\n buffer = buffer.slice(newlineIndex + 1);\n onLine(line);\n }\n });\n}\n\n/**\n * `mcpfy-proxy -- <command> [args...]`\n *\n * Spawns <command> as a child process and sits in its stdin/stdout pipe. Client-to-server\n * messages (parent stdin -> child stdin) are classified as \"incoming\"; server-to-client\n * messages (child stdout -> parent stdout) are classified as \"outgoing\", mirroring\n * onmessage/send in the in-process wrapper. Works for any language — the proxy never\n * parses anything beyond newline-delimited JSON-RPC framing.\n */\nexport async function runProxy(argv: string[]): Promise<void> {\n const sepIndex = argv.indexOf(\"--\");\n if (sepIndex === -1 || sepIndex === argv.length - 1) {\n process.stderr.write(\n \"Usage: mcpfy-proxy -- <command> [args...]\\n\" +\n \"Example: mcpfy-proxy -- npx -y @modelcontextprotocol/server-github\\n\"\n );\n process.exitCode = 1;\n return;\n }\n\n const [command, ...args] = argv.slice(sepIndex + 1);\n const config = resolveConfig();\n const classifier = new MessageClassifier();\n const batcher = config.apiKey\n ? new TelemetryBatcher(config, { sdkName: \"unknown\", installMode: \"stdio-proxy\" })\n : undefined;\n\n const child = spawn(command, args, {\n stdio: [\"pipe\", \"pipe\", \"inherit\"],\n env: process.env,\n });\n\n child.on(\"error\", (err) => {\n process.stderr.write(`mcpfy-proxy: failed to start \"${command}\": ${err.message}\\n`);\n process.exitCode = 1;\n });\n\n pipeLines(process.stdin, child.stdin!, (line) => {\n const message = tryParse(line);\n if (!message) return;\n const event = classifier.onIncoming(message);\n if (event && batcher) batcher.push(event);\n });\n\n pipeLines(child.stdout!, process.stdout, (line) => {\n const message = tryParse(line);\n if (!message) return;\n const event = classifier.onOutgoing(message);\n if (event && batcher) batcher.push(event);\n });\n\n process.on(\"SIGINT\", () => child.kill(\"SIGINT\"));\n process.on(\"SIGTERM\", () => child.kill(\"SIGTERM\"));\n\n await new Promise<void>((resolvePromise) => {\n child.on(\"exit\", (code, signal) => {\n void batcher?.close().finally(() => {\n if (signal) {\n process.kill(process.pid, signal);\n } else {\n process.exitCode = code ?? 0;\n }\n resolvePromise();\n });\n });\n });\n}\n","import type { TelemetryOptions } from \"./types.js\";\n\n// Path matches the real route on cloudmcp-nest: POST /v1/telemetry/ingest.\nconst DEFAULT_ENDPOINT = \"https://api.mcpfy.ai/v1/telemetry/ingest\";\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000;\nconst DEFAULT_MAX_BATCH_SIZE = 500;\n\nexport interface ResolvedConfig {\n apiKey: string | undefined;\n endpoint: string;\n flushIntervalMs: number;\n maxBatchSize: number;\n}\n\nexport function resolveConfig(options?: TelemetryOptions): ResolvedConfig {\n return {\n apiKey: options?.apiKey ?? process.env.MCPFY_API_KEY,\n endpoint: options?.endpoint ?? process.env.MCPFY_TELEMETRY_ENDPOINT ?? DEFAULT_ENDPOINT,\n flushIntervalMs: options?.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,\n maxBatchSize: options?.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,\n };\n}\n","import type { DeclaredToolMeta, TelemetryEvent } from \"../types.js\";\n\ninterface PendingEntry {\n method: string;\n startedAt: number;\n argsBytes: number;\n extra: Partial<TelemetryEvent>;\n progressToken?: string;\n progressCount: number;\n}\n\nfunction byteLength(value: unknown): number {\n // `undefined` (no `params` key at all) and `null` (an explicit `\"params\": null`)\n // both mean \"no meaningful params\" — collapsed to the same 0 here so argsBytes\n // doesn't swing on a client's JSON-serialization choice. Matches classify.py's\n // `_byte_length`, which can't tell the two apart once parsed into a dict anyway.\n if (value === undefined || value === null) return 0;\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return 0;\n }\n}\n\n/**\n * Reduces a `tools/list` response's `result.tools` array to non-content metadata —\n * whether/how-long each description is, and how many of its params are individually\n * documented, plus structured-output/annotation presence. Never reads the description\n * text or schema into the event itself.\n */\nfunction extractDeclaredTools(tools: unknown): DeclaredToolMeta[] | undefined {\n if (!Array.isArray(tools)) return undefined;\n const declared: DeclaredToolMeta[] = [];\n for (const tool of tools) {\n if (!tool || typeof tool.name !== \"string\") continue;\n const description = typeof tool.description === \"string\" ? tool.description : \"\";\n const properties = tool.inputSchema?.properties;\n const paramNames = properties && typeof properties === \"object\" ? Object.keys(properties) : [];\n const paramsWithDescriptionCount = paramNames.filter(\n (p) => typeof properties[p]?.description === \"string\" && properties[p].description.trim().length > 0,\n ).length;\n const annotations = tool.annotations && typeof tool.annotations === \"object\" ? tool.annotations : undefined;\n declared.push({\n name: tool.name,\n hasDescription: description.trim().length > 0,\n descriptionLength: description.length,\n paramCount: paramNames.length,\n paramsWithDescriptionCount,\n hasOutputSchema: tool.outputSchema != null && typeof tool.outputSchema === \"object\",\n readOnlyHint: typeof annotations?.readOnlyHint === \"boolean\" ? annotations.readOnlyHint : undefined,\n destructiveHint: typeof annotations?.destructiveHint === \"boolean\" ? annotations.destructiveHint : undefined,\n idempotentHint: typeof annotations?.idempotentHint === \"boolean\" ? annotations.idempotentHint : undefined,\n openWorldHint: typeof annotations?.openWorldHint === \"boolean\" ? annotations.openWorldHint : undefined,\n });\n }\n return declared;\n}\n\n/**\n * Flattens a `ClientCapabilities`/`ServerCapabilities` object to a list of dotted\n * capability paths that are actually declared — group-presence for capabilities with\n * no meaningful sub-flags, `<group>.<subflag>` for the handful that have one worth\n * surfacing (resources/prompts/tools listChanged, resources.subscribe). `experimental`\n * is deliberately skipped (arbitrary custom capability names, out of scope).\n */\nfunction extractCapabilities(capabilities: unknown): string[] | undefined {\n if (!capabilities || typeof capabilities !== \"object\") return undefined;\n const caps = capabilities as Record<string, any>;\n const out: string[] = [];\n const GROUPS = [\"sampling\", \"elicitation\", \"roots\", \"tasks\", \"logging\", \"prompts\", \"resources\", \"tools\", \"completions\"];\n for (const group of GROUPS) {\n const value = caps[group];\n if (value === undefined || value === null) continue;\n out.push(group);\n if (typeof value === \"object\") {\n if (value.listChanged === true) out.push(`${group}.listChanged`);\n if (group === \"resources\" && value.subscribe === true) out.push(`${group}.subscribe`);\n }\n }\n return out.length > 0 ? out : undefined;\n}\n\nfunction extractLabel(method: string, params: any): Partial<TelemetryEvent> {\n switch (method) {\n case \"tools/call\":\n return { toolName: params?.name };\n case \"prompts/get\":\n return { promptName: params?.name };\n case \"resources/read\":\n return { resourceUri: params?.uri };\n case \"initialize\": {\n // Protocol handshake metadata, not user data — same fields the spec itself exchanges in the clear.\n const label: Partial<TelemetryEvent> = {\n clientName: params?.clientInfo?.name,\n clientVersion: params?.clientInfo?.version,\n protocolVersion: params?.protocolVersion,\n };\n const clientCapabilities = extractCapabilities(params?.capabilities);\n if (clientCapabilities) label.clientCapabilities = clientCapabilities;\n return label;\n }\n default:\n return {};\n }\n}\n\n/**\n * Tracks request/response pairs across the two seams every Transport exposes\n * (onmessage = incoming, send = outgoing) and emits one event per completed\n * request. Only method names, byte counts, timing, and outcome are captured —\n * argument values and result content are never read beyond their byte length.\n * The one exception is `tools/list`, where per-tool description/param *presence\n * and length* are captured (never the description text or schema) — see\n * `extractDeclaredTools`.\n *\n * Also tracks a handful of notification types now: `notifications/cancelled` (turns\n * into a completed event with `outcome:\"cancelled\"`, since the original request never\n * gets a response), `notifications/progress` (counted per request, never the progress\n * values themselves), and `notifications/message` plus the three list_changed\n * notifications (emitted as standalone `type:\"notification\"` events). Everything else server-initiated\n * (sampling, elicitation, roots) is still intentionally not tracked — out of scope for\n * this pass.\n */\nexport class MessageClassifier {\n private pending = new Map<string | number, PendingEntry>();\n private pendingByToken = new Map<string, string | number>();\n\n private forgetPending(id: string | number, pending: PendingEntry): void {\n this.pending.delete(id);\n if (pending.progressToken !== undefined) this.pendingByToken.delete(pending.progressToken);\n }\n\n /**\n * Returns an event only for the one incoming case that completes a request without\n * ever seeing a response: a client cancelling its own still-pending request. Every\n * other incoming message (ordinary requests, `notifications/initialized`, and any\n * other notification) returns `undefined`, matching prior behavior exactly.\n */\n onIncoming(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, params } = message;\n\n if (id === undefined && method === \"notifications/cancelled\") {\n // Never read params.reason — free-text, same \"never content\" boundary as everything else.\n const requestId = params?.requestId;\n if (requestId === undefined) return undefined;\n const pending = this.pending.get(requestId);\n if (!pending) return undefined;\n this.forgetPending(requestId, pending);\n return {\n type: \"request\",\n method: pending.method,\n argsBytes: pending.argsBytes,\n durationMs: Date.now() - pending.startedAt,\n outcome: \"cancelled\",\n progressUpdateCount: pending.progressCount,\n timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n }\n\n if (id === undefined || !method) return undefined; // only requests carry both an id and a method\n\n const progressToken = params?._meta?.progressToken;\n const entry: PendingEntry = {\n method,\n startedAt: Date.now(),\n argsBytes: byteLength(params),\n extra: extractLabel(method, params),\n progressCount: 0,\n };\n if (typeof progressToken === \"string\" || typeof progressToken === \"number\") {\n entry.progressToken = String(progressToken);\n this.pendingByToken.set(entry.progressToken, id);\n }\n this.pending.set(id, entry);\n return undefined;\n }\n\n onOutgoing(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, params, result, error } = message;\n\n if (method && id === undefined) {\n // An outgoing notification (server -> client) — not a response.\n if (method === \"notifications/progress\") {\n const pendingId = this.pendingByToken.get(String(params?.progressToken));\n if (pendingId !== undefined) {\n const pending = this.pending.get(pendingId);\n if (pending) pending.progressCount += 1;\n }\n return undefined;\n }\n if (method === \"notifications/message\") {\n return {\n type: \"notification\",\n method,\n logLevel: typeof params?.level === \"string\" ? params.level : undefined,\n timestamp: new Date().toISOString(),\n };\n }\n if (\n method === \"notifications/tools/list_changed\" ||\n method === \"notifications/resources/list_changed\" ||\n method === \"notifications/prompts/list_changed\"\n ) {\n return { type: \"notification\", method, timestamp: new Date().toISOString() };\n }\n return undefined; // other notifications / server-initiated requests — still out of scope\n }\n\n if (method || id === undefined) return undefined;\n\n const pending = this.pending.get(id);\n if (!pending) return undefined;\n this.forgetPending(id, pending);\n\n const event: TelemetryEvent = {\n type: \"request\",\n method: pending.method,\n argsBytes: pending.argsBytes,\n resultBytes: byteLength(error ?? result),\n durationMs: Date.now() - pending.startedAt,\n outcome: error ? \"error\" : \"ok\",\n errorCode: error?.code,\n progressUpdateCount: pending.progressCount,\n timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n\n if (pending.method === \"initialize\" && !error) {\n if (result?.serverInfo) {\n event.serverName = result.serverInfo.name;\n event.serverVersion = result.serverInfo.version;\n }\n const serverCapabilities = extractCapabilities(result?.capabilities);\n if (serverCapabilities) event.serverCapabilities = serverCapabilities;\n }\n\n if (pending.method === \"tools/call\" && !error && result?.isError === true) {\n event.resultIsError = true;\n }\n\n if (pending.method === \"tools/list\" && !error) {\n const declaredTools = extractDeclaredTools(result?.tools);\n if (declaredTools) event.declaredTools = declaredTools;\n }\n\n return event;\n }\n}\n","import type { TelemetryEvent, SdkName, InstallMode } from \"../types.js\";\nimport type { ResolvedConfig } from \"../config.js\";\n\nexport interface BatchMeta {\n serverName?: string;\n serverVersion?: string;\n sdkName: SdkName;\n sdkVersion?: string;\n installMode: InstallMode;\n}\n\n/**\n * Ring-buffer batcher: queues events, flushes on a timer or when full, and never\n * throws or retries indefinitely — a failed or unreachable ingest endpoint is a\n * silent no-op, by design (telemetry must never affect the server's own behavior,\n * and the endpoint may not exist yet — see telemetry-master-plan.md §4/§7).\n */\nexport class TelemetryBatcher {\n private queue: TelemetryEvent[] = [];\n private timer: ReturnType<typeof setInterval>;\n\n constructor(\n private readonly config: ResolvedConfig,\n private readonly meta: BatchMeta\n ) {\n this.timer = setInterval(() => void this.flush(), config.flushIntervalMs);\n this.timer.unref?.(); // telemetry must never be the reason a process stays alive\n }\n\n push(event: TelemetryEvent): void {\n this.queue.push(event);\n if (this.queue.length >= this.config.maxBatchSize) void this.flush();\n }\n\n async flush(): Promise<void> {\n if (this.queue.length === 0) return;\n const events = this.queue.splice(0, this.queue.length);\n try {\n const response = await fetch(this.config.endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.config.apiKey ?? \"\"}`,\n },\n body: JSON.stringify({\n serverName: this.meta.serverName,\n serverVersion: this.meta.serverVersion,\n sdkName: this.meta.sdkName,\n sdkVersion: this.meta.sdkVersion,\n installMode: this.meta.installMode,\n events,\n }),\n });\n // Never throws on a non-2xx (silent drop is still the contract — see class doc),\n // but a warning is the difference between \"found the bug in 30 seconds\" and\n // \"found it three days later\": a 404/401/500 here means events are being\n // discarded even though fetch() itself didn't throw.\n if (!response.ok) {\n console.warn(\n `[mcpfy-pulse] telemetry flush failed: ${response.status} ${response.statusText} (${this.config.endpoint}) — ${events.length} event(s) dropped`\n );\n }\n } catch (err) {\n console.warn(\n `[mcpfy-pulse] telemetry flush failed: ${err instanceof Error ? err.message : String(err)} (${this.config.endpoint}) — ${events.length} event(s) dropped`\n );\n }\n }\n\n async close(): Promise<void> {\n clearInterval(this.timer);\n await this.flush();\n }\n}\n\nconst SHUTDOWN_SIGNALS: NodeJS.Signals[] = [\"SIGTERM\", \"SIGINT\"];\nconst SHUTDOWN_FLUSH_TIMEOUT_MS = 3000;\n\n/**\n * Installs a best-effort \"flush before we die\" hook for a batcher that has no\n * other lifecycle event to hang a flush off of. `withMcpfyTelemetry` wraps a\n * transport inside the *same process* as the server's own code — unlike the stdio\n * proxy, there's no child process it can wait on and forward signals to. Without\n * this, a real MCP session shorter than the flush interval loses every event, via\n * two independent paths, both confirmed against real client/server sessions:\n *\n * - A real MCP client's normal graceful shutdown (close stdin, wait, escalate to\n * SIGTERM if the process is still alive) kills the process via an unhandled\n * signal — instant, no `exit` event, no flush.\n * - Some server setups (e.g. the low-level `Server` class, as opposed to\n * `McpServer`) simply exit *naturally* on stdin EOF — no signal at all, just\n * Node draining an event loop with nothing left to do — which an unhandled\n * signal listener does nothing to catch.\n *\n * Both are covered here: `beforeExit` for the natural-exit case, `SIGTERM`/\n * `SIGINT` for the signal case, sharing one flush bounded by a timeout so a slow\n * or unreachable ingest endpoint can never hang process shutdown either way.\n */\nexport function installShutdownFlush(batcher: TelemetryBatcher): void {\n let flushed = false;\n const flushOnce = (): Promise<void> => {\n if (flushed) return Promise.resolve();\n flushed = true;\n const timeout = new Promise<void>((resolve) => setTimeout(resolve, SHUTDOWN_FLUSH_TIMEOUT_MS).unref());\n return Promise.race([batcher.close(), timeout]).catch(() => {});\n };\n\n // Natural exit: nothing else queued on the event loop, no signal involved.\n // Scheduling async work here (the flush) delays the actual exit until it\n // settles, without ever holding the process open when there's truly nothing\n // to flush — `flush()` itself no-ops instantly on an empty queue.\n process.once(\"beforeExit\", () => {\n void flushOnce();\n });\n\n for (const signal of SHUTDOWN_SIGNALS) {\n const handler = () => {\n // Remove ourselves before re-raising the same signal below, so that re-raise\n // falls through to the default action (or any other listener) instead of\n // looping back into this handler.\n process.removeListener(signal, handler);\n void flushOnce().finally(() => process.kill(process.pid, signal));\n };\n process.on(signal, handler);\n }\n}\n","#!/usr/bin/env node\nimport { runProxy } from \"../proxy/run.js\";\n\nrunProxy(process.argv.slice(2)).catch((err) => {\n process.stderr.write(`mcpfy-proxy: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exitCode = 1;\n});\n"],"mappings":"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","names":["pending"]}
|
|
@@ -23,4 +23,25 @@ export declare class TelemetryBatcher {
|
|
|
23
23
|
flush(): Promise<void>;
|
|
24
24
|
close(): Promise<void>;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Installs a best-effort "flush before we die" hook for a batcher that has no
|
|
28
|
+
* other lifecycle event to hang a flush off of. `withMcpfyTelemetry` wraps a
|
|
29
|
+
* transport inside the *same process* as the server's own code — unlike the stdio
|
|
30
|
+
* proxy, there's no child process it can wait on and forward signals to. Without
|
|
31
|
+
* this, a real MCP session shorter than the flush interval loses every event, via
|
|
32
|
+
* two independent paths, both confirmed against real client/server sessions:
|
|
33
|
+
*
|
|
34
|
+
* - A real MCP client's normal graceful shutdown (close stdin, wait, escalate to
|
|
35
|
+
* SIGTERM if the process is still alive) kills the process via an unhandled
|
|
36
|
+
* signal — instant, no `exit` event, no flush.
|
|
37
|
+
* - Some server setups (e.g. the low-level `Server` class, as opposed to
|
|
38
|
+
* `McpServer`) simply exit *naturally* on stdin EOF — no signal at all, just
|
|
39
|
+
* Node draining an event loop with nothing left to do — which an unhandled
|
|
40
|
+
* signal listener does nothing to catch.
|
|
41
|
+
*
|
|
42
|
+
* Both are covered here: `beforeExit` for the natural-exit case, `SIGTERM`/
|
|
43
|
+
* `SIGINT` for the signal case, sharing one flush bounded by a timeout so a slow
|
|
44
|
+
* or unreachable ingest endpoint can never hang process shutdown either way.
|
|
45
|
+
*/
|
|
46
|
+
export declare function installShutdownFlush(batcher: TelemetryBatcher): void;
|
|
26
47
|
//# sourceMappingURL=batcher.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batcher.d.ts","sourceRoot":"","sources":["../../../src/core/batcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED;;;;;GAKG;AACH,qBAAa,gBAAgB;IAKzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI;IALvB,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,KAAK,CAAiC;gBAG3B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,SAAS;IAMlC,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAK3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}
|
|
1
|
+
{"version":3,"file":"batcher.d.ts","sourceRoot":"","sources":["../../../src/core/batcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED;;;;;GAKG;AACH,qBAAa,gBAAgB;IAKzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI;IALvB,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,KAAK,CAAiC;gBAG3B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,SAAS;IAMlC,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAK3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B;AAKD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CA2BpE"}
|
package/dist/src/index.cjs
CHANGED
|
@@ -271,6 +271,28 @@ var TelemetryBatcher = class {
|
|
|
271
271
|
await this.flush();
|
|
272
272
|
}
|
|
273
273
|
};
|
|
274
|
+
var SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
|
|
275
|
+
var SHUTDOWN_FLUSH_TIMEOUT_MS = 3e3;
|
|
276
|
+
function installShutdownFlush(batcher) {
|
|
277
|
+
let flushed = false;
|
|
278
|
+
const flushOnce = () => {
|
|
279
|
+
if (flushed) return Promise.resolve();
|
|
280
|
+
flushed = true;
|
|
281
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, SHUTDOWN_FLUSH_TIMEOUT_MS).unref());
|
|
282
|
+
return Promise.race([batcher.close(), timeout]).catch(() => {
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
process.once("beforeExit", () => {
|
|
286
|
+
void flushOnce();
|
|
287
|
+
});
|
|
288
|
+
for (const signal of SHUTDOWN_SIGNALS) {
|
|
289
|
+
const handler = () => {
|
|
290
|
+
process.removeListener(signal, handler);
|
|
291
|
+
void flushOnce().finally(() => process.kill(process.pid, signal));
|
|
292
|
+
};
|
|
293
|
+
process.on(signal, handler);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
274
296
|
|
|
275
297
|
// src/wrap-transport.ts
|
|
276
298
|
function withMcpfyTelemetry(transport, options) {
|
|
@@ -284,6 +306,7 @@ function withMcpfyTelemetry(transport, options) {
|
|
|
284
306
|
sdkVersion: options?.sdkVersion,
|
|
285
307
|
installMode: options?.installMode ?? "sdk-wrapper"
|
|
286
308
|
});
|
|
309
|
+
installShutdownFlush(batcher);
|
|
287
310
|
const wrapped = {
|
|
288
311
|
start: () => transport.start(),
|
|
289
312
|
close: () => {
|
package/dist/src/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/index.ts","../../src/config.ts","../../src/core/classify.ts","../../src/core/batcher.ts","../../src/wrap-transport.ts"],"sourcesContent":["export { withMcpfyTelemetry } from \"./wrap-transport.js\";\nexport type { TelemetryOptions, TelemetryEvent, MinimalTransport, SdkName, InstallMode } from \"./types.js\";\n","import type { TelemetryOptions } from \"./types.js\";\n\n// Path matches the real route on cloudmcp-nest: POST /v1/telemetry/ingest.\nconst DEFAULT_ENDPOINT = \"https://api.mcpfy.ai/v1/telemetry/ingest\";\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000;\nconst DEFAULT_MAX_BATCH_SIZE = 500;\n\nexport interface ResolvedConfig {\n apiKey: string | undefined;\n endpoint: string;\n flushIntervalMs: number;\n maxBatchSize: number;\n}\n\nexport function resolveConfig(options?: TelemetryOptions): ResolvedConfig {\n return {\n apiKey: options?.apiKey ?? process.env.MCPFY_API_KEY,\n endpoint: options?.endpoint ?? process.env.MCPFY_TELEMETRY_ENDPOINT ?? DEFAULT_ENDPOINT,\n flushIntervalMs: options?.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,\n maxBatchSize: options?.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,\n };\n}\n","import type { DeclaredToolMeta, TelemetryEvent } from \"../types.js\";\n\ninterface PendingEntry {\n method: string;\n startedAt: number;\n argsBytes: number;\n extra: Partial<TelemetryEvent>;\n progressToken?: string;\n progressCount: number;\n}\n\nfunction byteLength(value: unknown): number {\n // `undefined` (no `params` key at all) and `null` (an explicit `\"params\": null`)\n // both mean \"no meaningful params\" — collapsed to the same 0 here so argsBytes\n // doesn't swing on a client's JSON-serialization choice. Matches classify.py's\n // `_byte_length`, which can't tell the two apart once parsed into a dict anyway.\n if (value === undefined || value === null) return 0;\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return 0;\n }\n}\n\n/**\n * Reduces a `tools/list` response's `result.tools` array to non-content metadata —\n * whether/how-long each description is, and how many of its params are individually\n * documented, plus structured-output/annotation presence. Never reads the description\n * text or schema into the event itself.\n */\nfunction extractDeclaredTools(tools: unknown): DeclaredToolMeta[] | undefined {\n if (!Array.isArray(tools)) return undefined;\n const declared: DeclaredToolMeta[] = [];\n for (const tool of tools) {\n if (!tool || typeof tool.name !== \"string\") continue;\n const description = typeof tool.description === \"string\" ? tool.description : \"\";\n const properties = tool.inputSchema?.properties;\n const paramNames = properties && typeof properties === \"object\" ? Object.keys(properties) : [];\n const paramsWithDescriptionCount = paramNames.filter(\n (p) => typeof properties[p]?.description === \"string\" && properties[p].description.trim().length > 0,\n ).length;\n const annotations = tool.annotations && typeof tool.annotations === \"object\" ? tool.annotations : undefined;\n declared.push({\n name: tool.name,\n hasDescription: description.trim().length > 0,\n descriptionLength: description.length,\n paramCount: paramNames.length,\n paramsWithDescriptionCount,\n hasOutputSchema: tool.outputSchema != null && typeof tool.outputSchema === \"object\",\n readOnlyHint: typeof annotations?.readOnlyHint === \"boolean\" ? annotations.readOnlyHint : undefined,\n destructiveHint: typeof annotations?.destructiveHint === \"boolean\" ? annotations.destructiveHint : undefined,\n idempotentHint: typeof annotations?.idempotentHint === \"boolean\" ? annotations.idempotentHint : undefined,\n openWorldHint: typeof annotations?.openWorldHint === \"boolean\" ? annotations.openWorldHint : undefined,\n });\n }\n return declared;\n}\n\n/**\n * Flattens a `ClientCapabilities`/`ServerCapabilities` object to a list of dotted\n * capability paths that are actually declared — group-presence for capabilities with\n * no meaningful sub-flags, `<group>.<subflag>` for the handful that have one worth\n * surfacing (resources/prompts/tools listChanged, resources.subscribe). `experimental`\n * is deliberately skipped (arbitrary custom capability names, out of scope).\n */\nfunction extractCapabilities(capabilities: unknown): string[] | undefined {\n if (!capabilities || typeof capabilities !== \"object\") return undefined;\n const caps = capabilities as Record<string, any>;\n const out: string[] = [];\n const GROUPS = [\"sampling\", \"elicitation\", \"roots\", \"tasks\", \"logging\", \"prompts\", \"resources\", \"tools\", \"completions\"];\n for (const group of GROUPS) {\n const value = caps[group];\n if (value === undefined || value === null) continue;\n out.push(group);\n if (typeof value === \"object\") {\n if (value.listChanged === true) out.push(`${group}.listChanged`);\n if (group === \"resources\" && value.subscribe === true) out.push(`${group}.subscribe`);\n }\n }\n return out.length > 0 ? out : undefined;\n}\n\nfunction extractLabel(method: string, params: any): Partial<TelemetryEvent> {\n switch (method) {\n case \"tools/call\":\n return { toolName: params?.name };\n case \"prompts/get\":\n return { promptName: params?.name };\n case \"resources/read\":\n return { resourceUri: params?.uri };\n case \"initialize\": {\n // Protocol handshake metadata, not user data — same fields the spec itself exchanges in the clear.\n const label: Partial<TelemetryEvent> = {\n clientName: params?.clientInfo?.name,\n clientVersion: params?.clientInfo?.version,\n protocolVersion: params?.protocolVersion,\n };\n const clientCapabilities = extractCapabilities(params?.capabilities);\n if (clientCapabilities) label.clientCapabilities = clientCapabilities;\n return label;\n }\n default:\n return {};\n }\n}\n\n/**\n * Tracks request/response pairs across the two seams every Transport exposes\n * (onmessage = incoming, send = outgoing) and emits one event per completed\n * request. Only method names, byte counts, timing, and outcome are captured —\n * argument values and result content are never read beyond their byte length.\n * The one exception is `tools/list`, where per-tool description/param *presence\n * and length* are captured (never the description text or schema) — see\n * `extractDeclaredTools`.\n *\n * Also tracks a handful of notification types now: `notifications/cancelled` (turns\n * into a completed event with `outcome:\"cancelled\"`, since the original request never\n * gets a response), `notifications/progress` (counted per request, never the progress\n * values themselves), and `notifications/message` plus the three list_changed\n * notifications (emitted as standalone `type:\"notification\"` events). Everything else server-initiated\n * (sampling, elicitation, roots) is still intentionally not tracked — out of scope for\n * this pass.\n */\nexport class MessageClassifier {\n private pending = new Map<string | number, PendingEntry>();\n private pendingByToken = new Map<string, string | number>();\n\n private forgetPending(id: string | number, pending: PendingEntry): void {\n this.pending.delete(id);\n if (pending.progressToken !== undefined) this.pendingByToken.delete(pending.progressToken);\n }\n\n /**\n * Returns an event only for the one incoming case that completes a request without\n * ever seeing a response: a client cancelling its own still-pending request. Every\n * other incoming message (ordinary requests, `notifications/initialized`, and any\n * other notification) returns `undefined`, matching prior behavior exactly.\n */\n onIncoming(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, params } = message;\n\n if (id === undefined && method === \"notifications/cancelled\") {\n // Never read params.reason — free-text, same \"never content\" boundary as everything else.\n const requestId = params?.requestId;\n if (requestId === undefined) return undefined;\n const pending = this.pending.get(requestId);\n if (!pending) return undefined;\n this.forgetPending(requestId, pending);\n return {\n type: \"request\",\n method: pending.method,\n argsBytes: pending.argsBytes,\n durationMs: Date.now() - pending.startedAt,\n outcome: \"cancelled\",\n progressUpdateCount: pending.progressCount,\n timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n }\n\n if (id === undefined || !method) return undefined; // only requests carry both an id and a method\n\n const progressToken = params?._meta?.progressToken;\n const entry: PendingEntry = {\n method,\n startedAt: Date.now(),\n argsBytes: byteLength(params),\n extra: extractLabel(method, params),\n progressCount: 0,\n };\n if (typeof progressToken === \"string\" || typeof progressToken === \"number\") {\n entry.progressToken = String(progressToken);\n this.pendingByToken.set(entry.progressToken, id);\n }\n this.pending.set(id, entry);\n return undefined;\n }\n\n onOutgoing(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, params, result, error } = message;\n\n if (method && id === undefined) {\n // An outgoing notification (server -> client) — not a response.\n if (method === \"notifications/progress\") {\n const pendingId = this.pendingByToken.get(String(params?.progressToken));\n if (pendingId !== undefined) {\n const pending = this.pending.get(pendingId);\n if (pending) pending.progressCount += 1;\n }\n return undefined;\n }\n if (method === \"notifications/message\") {\n return {\n type: \"notification\",\n method,\n logLevel: typeof params?.level === \"string\" ? params.level : undefined,\n timestamp: new Date().toISOString(),\n };\n }\n if (\n method === \"notifications/tools/list_changed\" ||\n method === \"notifications/resources/list_changed\" ||\n method === \"notifications/prompts/list_changed\"\n ) {\n return { type: \"notification\", method, timestamp: new Date().toISOString() };\n }\n return undefined; // other notifications / server-initiated requests — still out of scope\n }\n\n if (method || id === undefined) return undefined;\n\n const pending = this.pending.get(id);\n if (!pending) return undefined;\n this.forgetPending(id, pending);\n\n const event: TelemetryEvent = {\n type: \"request\",\n method: pending.method,\n argsBytes: pending.argsBytes,\n resultBytes: byteLength(error ?? result),\n durationMs: Date.now() - pending.startedAt,\n outcome: error ? \"error\" : \"ok\",\n errorCode: error?.code,\n progressUpdateCount: pending.progressCount,\n timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n\n if (pending.method === \"initialize\" && !error) {\n if (result?.serverInfo) {\n event.serverName = result.serverInfo.name;\n event.serverVersion = result.serverInfo.version;\n }\n const serverCapabilities = extractCapabilities(result?.capabilities);\n if (serverCapabilities) event.serverCapabilities = serverCapabilities;\n }\n\n if (pending.method === \"tools/call\" && !error && result?.isError === true) {\n event.resultIsError = true;\n }\n\n if (pending.method === \"tools/list\" && !error) {\n const declaredTools = extractDeclaredTools(result?.tools);\n if (declaredTools) event.declaredTools = declaredTools;\n }\n\n return event;\n }\n}\n","import type { TelemetryEvent, SdkName, InstallMode } from \"../types.js\";\nimport type { ResolvedConfig } from \"../config.js\";\n\nexport interface BatchMeta {\n serverName?: string;\n serverVersion?: string;\n sdkName: SdkName;\n sdkVersion?: string;\n installMode: InstallMode;\n}\n\n/**\n * Ring-buffer batcher: queues events, flushes on a timer or when full, and never\n * throws or retries indefinitely — a failed or unreachable ingest endpoint is a\n * silent no-op, by design (telemetry must never affect the server's own behavior,\n * and the endpoint may not exist yet — see telemetry-master-plan.md §4/§7).\n */\nexport class TelemetryBatcher {\n private queue: TelemetryEvent[] = [];\n private timer: ReturnType<typeof setInterval>;\n\n constructor(\n private readonly config: ResolvedConfig,\n private readonly meta: BatchMeta\n ) {\n this.timer = setInterval(() => void this.flush(), config.flushIntervalMs);\n this.timer.unref?.(); // telemetry must never be the reason a process stays alive\n }\n\n push(event: TelemetryEvent): void {\n this.queue.push(event);\n if (this.queue.length >= this.config.maxBatchSize) void this.flush();\n }\n\n async flush(): Promise<void> {\n if (this.queue.length === 0) return;\n const events = this.queue.splice(0, this.queue.length);\n try {\n const response = await fetch(this.config.endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.config.apiKey ?? \"\"}`,\n },\n body: JSON.stringify({\n serverName: this.meta.serverName,\n serverVersion: this.meta.serverVersion,\n sdkName: this.meta.sdkName,\n sdkVersion: this.meta.sdkVersion,\n installMode: this.meta.installMode,\n events,\n }),\n });\n // Never throws on a non-2xx (silent drop is still the contract — see class doc),\n // but a warning is the difference between \"found the bug in 30 seconds\" and\n // \"found it three days later\": a 404/401/500 here means events are being\n // discarded even though fetch() itself didn't throw.\n if (!response.ok) {\n console.warn(\n `[mcpfy-pulse] telemetry flush failed: ${response.status} ${response.statusText} (${this.config.endpoint}) — ${events.length} event(s) dropped`\n );\n }\n } catch (err) {\n console.warn(\n `[mcpfy-pulse] telemetry flush failed: ${err instanceof Error ? err.message : String(err)} (${this.config.endpoint}) — ${events.length} event(s) dropped`\n );\n }\n }\n\n async close(): Promise<void> {\n clearInterval(this.timer);\n await this.flush();\n }\n}\n","import { resolveConfig } from \"./config.js\";\nimport { MessageClassifier } from \"./core/classify.js\";\nimport { TelemetryBatcher } from \"./core/batcher.js\";\nimport type { MinimalTransport, TelemetryOptions } from \"./types.js\";\n\n/**\n * Wraps a Transport's `onmessage`/`send` seam to capture telemetry, then delegates\n * everything through to the real transport unchanged. If no API key is configured\n * (via `options.apiKey` or the MCPFY_API_KEY env var), this is a complete no-op —\n * the original transport is returned untouched.\n *\n * Usage (add these lines yourself — nothing here edits your files for you):\n *\n * import { withMcpfyTelemetry } from \"mcpfy-pulse\";\n * const transport = new StdioServerTransport();\n * await server.connect(withMcpfyTelemetry(transport, { apiKey: process.env.MCPFY_API_KEY }));\n */\nexport function withMcpfyTelemetry<T extends MinimalTransport>(transport: T, options?: TelemetryOptions): T {\n const config = resolveConfig(options);\n if (!config.apiKey) return transport;\n\n const classifier = new MessageClassifier();\n const batcher = new TelemetryBatcher(config, {\n serverName: options?.serverName,\n serverVersion: options?.serverVersion,\n sdkName: options?.sdkName ?? \"@modelcontextprotocol/sdk\",\n sdkVersion: options?.sdkVersion,\n installMode: options?.installMode ?? \"sdk-wrapper\",\n });\n\n const wrapped: MinimalTransport = {\n start: () => transport.start(),\n close: () => {\n void batcher.close();\n return transport.close();\n },\n send: (message: any, sendOptions?: any) => {\n const event = classifier.onOutgoing(message);\n if (event) batcher.push(event);\n return transport.send(message, sendOptions);\n },\n get sessionId() {\n return transport.sessionId;\n },\n get onclose() {\n return transport.onclose;\n },\n set onclose(fn) {\n transport.onclose = fn;\n },\n get onerror() {\n return transport.onerror;\n },\n set onerror(fn) {\n transport.onerror = fn;\n },\n get onmessage() {\n return transport.onmessage;\n },\n set onmessage(handler) {\n transport.onmessage = (message: any, extra?: any) => {\n const event = classifier.onIncoming(message);\n if (event) batcher.push(event);\n handler?.(message, extra);\n };\n },\n };\n\n return wrapped as T;\n}\n"],"mappings":"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","names":["pending"]}
|
|
1
|
+
{"version":3,"sources":["../../src/index.ts","../../src/config.ts","../../src/core/classify.ts","../../src/core/batcher.ts","../../src/wrap-transport.ts"],"sourcesContent":["export { withMcpfyTelemetry } from \"./wrap-transport.js\";\nexport type { TelemetryOptions, TelemetryEvent, MinimalTransport, SdkName, InstallMode } from \"./types.js\";\n","import type { TelemetryOptions } from \"./types.js\";\n\n// Path matches the real route on cloudmcp-nest: POST /v1/telemetry/ingest.\nconst DEFAULT_ENDPOINT = \"https://api.mcpfy.ai/v1/telemetry/ingest\";\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000;\nconst DEFAULT_MAX_BATCH_SIZE = 500;\n\nexport interface ResolvedConfig {\n apiKey: string | undefined;\n endpoint: string;\n flushIntervalMs: number;\n maxBatchSize: number;\n}\n\nexport function resolveConfig(options?: TelemetryOptions): ResolvedConfig {\n return {\n apiKey: options?.apiKey ?? process.env.MCPFY_API_KEY,\n endpoint: options?.endpoint ?? process.env.MCPFY_TELEMETRY_ENDPOINT ?? DEFAULT_ENDPOINT,\n flushIntervalMs: options?.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,\n maxBatchSize: options?.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,\n };\n}\n","import type { DeclaredToolMeta, TelemetryEvent } from \"../types.js\";\n\ninterface PendingEntry {\n method: string;\n startedAt: number;\n argsBytes: number;\n extra: Partial<TelemetryEvent>;\n progressToken?: string;\n progressCount: number;\n}\n\nfunction byteLength(value: unknown): number {\n // `undefined` (no `params` key at all) and `null` (an explicit `\"params\": null`)\n // both mean \"no meaningful params\" — collapsed to the same 0 here so argsBytes\n // doesn't swing on a client's JSON-serialization choice. Matches classify.py's\n // `_byte_length`, which can't tell the two apart once parsed into a dict anyway.\n if (value === undefined || value === null) return 0;\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return 0;\n }\n}\n\n/**\n * Reduces a `tools/list` response's `result.tools` array to non-content metadata —\n * whether/how-long each description is, and how many of its params are individually\n * documented, plus structured-output/annotation presence. Never reads the description\n * text or schema into the event itself.\n */\nfunction extractDeclaredTools(tools: unknown): DeclaredToolMeta[] | undefined {\n if (!Array.isArray(tools)) return undefined;\n const declared: DeclaredToolMeta[] = [];\n for (const tool of tools) {\n if (!tool || typeof tool.name !== \"string\") continue;\n const description = typeof tool.description === \"string\" ? tool.description : \"\";\n const properties = tool.inputSchema?.properties;\n const paramNames = properties && typeof properties === \"object\" ? Object.keys(properties) : [];\n const paramsWithDescriptionCount = paramNames.filter(\n (p) => typeof properties[p]?.description === \"string\" && properties[p].description.trim().length > 0,\n ).length;\n const annotations = tool.annotations && typeof tool.annotations === \"object\" ? tool.annotations : undefined;\n declared.push({\n name: tool.name,\n hasDescription: description.trim().length > 0,\n descriptionLength: description.length,\n paramCount: paramNames.length,\n paramsWithDescriptionCount,\n hasOutputSchema: tool.outputSchema != null && typeof tool.outputSchema === \"object\",\n readOnlyHint: typeof annotations?.readOnlyHint === \"boolean\" ? annotations.readOnlyHint : undefined,\n destructiveHint: typeof annotations?.destructiveHint === \"boolean\" ? annotations.destructiveHint : undefined,\n idempotentHint: typeof annotations?.idempotentHint === \"boolean\" ? annotations.idempotentHint : undefined,\n openWorldHint: typeof annotations?.openWorldHint === \"boolean\" ? annotations.openWorldHint : undefined,\n });\n }\n return declared;\n}\n\n/**\n * Flattens a `ClientCapabilities`/`ServerCapabilities` object to a list of dotted\n * capability paths that are actually declared — group-presence for capabilities with\n * no meaningful sub-flags, `<group>.<subflag>` for the handful that have one worth\n * surfacing (resources/prompts/tools listChanged, resources.subscribe). `experimental`\n * is deliberately skipped (arbitrary custom capability names, out of scope).\n */\nfunction extractCapabilities(capabilities: unknown): string[] | undefined {\n if (!capabilities || typeof capabilities !== \"object\") return undefined;\n const caps = capabilities as Record<string, any>;\n const out: string[] = [];\n const GROUPS = [\"sampling\", \"elicitation\", \"roots\", \"tasks\", \"logging\", \"prompts\", \"resources\", \"tools\", \"completions\"];\n for (const group of GROUPS) {\n const value = caps[group];\n if (value === undefined || value === null) continue;\n out.push(group);\n if (typeof value === \"object\") {\n if (value.listChanged === true) out.push(`${group}.listChanged`);\n if (group === \"resources\" && value.subscribe === true) out.push(`${group}.subscribe`);\n }\n }\n return out.length > 0 ? out : undefined;\n}\n\nfunction extractLabel(method: string, params: any): Partial<TelemetryEvent> {\n switch (method) {\n case \"tools/call\":\n return { toolName: params?.name };\n case \"prompts/get\":\n return { promptName: params?.name };\n case \"resources/read\":\n return { resourceUri: params?.uri };\n case \"initialize\": {\n // Protocol handshake metadata, not user data — same fields the spec itself exchanges in the clear.\n const label: Partial<TelemetryEvent> = {\n clientName: params?.clientInfo?.name,\n clientVersion: params?.clientInfo?.version,\n protocolVersion: params?.protocolVersion,\n };\n const clientCapabilities = extractCapabilities(params?.capabilities);\n if (clientCapabilities) label.clientCapabilities = clientCapabilities;\n return label;\n }\n default:\n return {};\n }\n}\n\n/**\n * Tracks request/response pairs across the two seams every Transport exposes\n * (onmessage = incoming, send = outgoing) and emits one event per completed\n * request. Only method names, byte counts, timing, and outcome are captured —\n * argument values and result content are never read beyond their byte length.\n * The one exception is `tools/list`, where per-tool description/param *presence\n * and length* are captured (never the description text or schema) — see\n * `extractDeclaredTools`.\n *\n * Also tracks a handful of notification types now: `notifications/cancelled` (turns\n * into a completed event with `outcome:\"cancelled\"`, since the original request never\n * gets a response), `notifications/progress` (counted per request, never the progress\n * values themselves), and `notifications/message` plus the three list_changed\n * notifications (emitted as standalone `type:\"notification\"` events). Everything else server-initiated\n * (sampling, elicitation, roots) is still intentionally not tracked — out of scope for\n * this pass.\n */\nexport class MessageClassifier {\n private pending = new Map<string | number, PendingEntry>();\n private pendingByToken = new Map<string, string | number>();\n\n private forgetPending(id: string | number, pending: PendingEntry): void {\n this.pending.delete(id);\n if (pending.progressToken !== undefined) this.pendingByToken.delete(pending.progressToken);\n }\n\n /**\n * Returns an event only for the one incoming case that completes a request without\n * ever seeing a response: a client cancelling its own still-pending request. Every\n * other incoming message (ordinary requests, `notifications/initialized`, and any\n * other notification) returns `undefined`, matching prior behavior exactly.\n */\n onIncoming(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, params } = message;\n\n if (id === undefined && method === \"notifications/cancelled\") {\n // Never read params.reason — free-text, same \"never content\" boundary as everything else.\n const requestId = params?.requestId;\n if (requestId === undefined) return undefined;\n const pending = this.pending.get(requestId);\n if (!pending) return undefined;\n this.forgetPending(requestId, pending);\n return {\n type: \"request\",\n method: pending.method,\n argsBytes: pending.argsBytes,\n durationMs: Date.now() - pending.startedAt,\n outcome: \"cancelled\",\n progressUpdateCount: pending.progressCount,\n timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n }\n\n if (id === undefined || !method) return undefined; // only requests carry both an id and a method\n\n const progressToken = params?._meta?.progressToken;\n const entry: PendingEntry = {\n method,\n startedAt: Date.now(),\n argsBytes: byteLength(params),\n extra: extractLabel(method, params),\n progressCount: 0,\n };\n if (typeof progressToken === \"string\" || typeof progressToken === \"number\") {\n entry.progressToken = String(progressToken);\n this.pendingByToken.set(entry.progressToken, id);\n }\n this.pending.set(id, entry);\n return undefined;\n }\n\n onOutgoing(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, params, result, error } = message;\n\n if (method && id === undefined) {\n // An outgoing notification (server -> client) — not a response.\n if (method === \"notifications/progress\") {\n const pendingId = this.pendingByToken.get(String(params?.progressToken));\n if (pendingId !== undefined) {\n const pending = this.pending.get(pendingId);\n if (pending) pending.progressCount += 1;\n }\n return undefined;\n }\n if (method === \"notifications/message\") {\n return {\n type: \"notification\",\n method,\n logLevel: typeof params?.level === \"string\" ? params.level : undefined,\n timestamp: new Date().toISOString(),\n };\n }\n if (\n method === \"notifications/tools/list_changed\" ||\n method === \"notifications/resources/list_changed\" ||\n method === \"notifications/prompts/list_changed\"\n ) {\n return { type: \"notification\", method, timestamp: new Date().toISOString() };\n }\n return undefined; // other notifications / server-initiated requests — still out of scope\n }\n\n if (method || id === undefined) return undefined;\n\n const pending = this.pending.get(id);\n if (!pending) return undefined;\n this.forgetPending(id, pending);\n\n const event: TelemetryEvent = {\n type: \"request\",\n method: pending.method,\n argsBytes: pending.argsBytes,\n resultBytes: byteLength(error ?? result),\n durationMs: Date.now() - pending.startedAt,\n outcome: error ? \"error\" : \"ok\",\n errorCode: error?.code,\n progressUpdateCount: pending.progressCount,\n timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n\n if (pending.method === \"initialize\" && !error) {\n if (result?.serverInfo) {\n event.serverName = result.serverInfo.name;\n event.serverVersion = result.serverInfo.version;\n }\n const serverCapabilities = extractCapabilities(result?.capabilities);\n if (serverCapabilities) event.serverCapabilities = serverCapabilities;\n }\n\n if (pending.method === \"tools/call\" && !error && result?.isError === true) {\n event.resultIsError = true;\n }\n\n if (pending.method === \"tools/list\" && !error) {\n const declaredTools = extractDeclaredTools(result?.tools);\n if (declaredTools) event.declaredTools = declaredTools;\n }\n\n return event;\n }\n}\n","import type { TelemetryEvent, SdkName, InstallMode } from \"../types.js\";\nimport type { ResolvedConfig } from \"../config.js\";\n\nexport interface BatchMeta {\n serverName?: string;\n serverVersion?: string;\n sdkName: SdkName;\n sdkVersion?: string;\n installMode: InstallMode;\n}\n\n/**\n * Ring-buffer batcher: queues events, flushes on a timer or when full, and never\n * throws or retries indefinitely — a failed or unreachable ingest endpoint is a\n * silent no-op, by design (telemetry must never affect the server's own behavior,\n * and the endpoint may not exist yet — see telemetry-master-plan.md §4/§7).\n */\nexport class TelemetryBatcher {\n private queue: TelemetryEvent[] = [];\n private timer: ReturnType<typeof setInterval>;\n\n constructor(\n private readonly config: ResolvedConfig,\n private readonly meta: BatchMeta\n ) {\n this.timer = setInterval(() => void this.flush(), config.flushIntervalMs);\n this.timer.unref?.(); // telemetry must never be the reason a process stays alive\n }\n\n push(event: TelemetryEvent): void {\n this.queue.push(event);\n if (this.queue.length >= this.config.maxBatchSize) void this.flush();\n }\n\n async flush(): Promise<void> {\n if (this.queue.length === 0) return;\n const events = this.queue.splice(0, this.queue.length);\n try {\n const response = await fetch(this.config.endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.config.apiKey ?? \"\"}`,\n },\n body: JSON.stringify({\n serverName: this.meta.serverName,\n serverVersion: this.meta.serverVersion,\n sdkName: this.meta.sdkName,\n sdkVersion: this.meta.sdkVersion,\n installMode: this.meta.installMode,\n events,\n }),\n });\n // Never throws on a non-2xx (silent drop is still the contract — see class doc),\n // but a warning is the difference between \"found the bug in 30 seconds\" and\n // \"found it three days later\": a 404/401/500 here means events are being\n // discarded even though fetch() itself didn't throw.\n if (!response.ok) {\n console.warn(\n `[mcpfy-pulse] telemetry flush failed: ${response.status} ${response.statusText} (${this.config.endpoint}) — ${events.length} event(s) dropped`\n );\n }\n } catch (err) {\n console.warn(\n `[mcpfy-pulse] telemetry flush failed: ${err instanceof Error ? err.message : String(err)} (${this.config.endpoint}) — ${events.length} event(s) dropped`\n );\n }\n }\n\n async close(): Promise<void> {\n clearInterval(this.timer);\n await this.flush();\n }\n}\n\nconst SHUTDOWN_SIGNALS: NodeJS.Signals[] = [\"SIGTERM\", \"SIGINT\"];\nconst SHUTDOWN_FLUSH_TIMEOUT_MS = 3000;\n\n/**\n * Installs a best-effort \"flush before we die\" hook for a batcher that has no\n * other lifecycle event to hang a flush off of. `withMcpfyTelemetry` wraps a\n * transport inside the *same process* as the server's own code — unlike the stdio\n * proxy, there's no child process it can wait on and forward signals to. Without\n * this, a real MCP session shorter than the flush interval loses every event, via\n * two independent paths, both confirmed against real client/server sessions:\n *\n * - A real MCP client's normal graceful shutdown (close stdin, wait, escalate to\n * SIGTERM if the process is still alive) kills the process via an unhandled\n * signal — instant, no `exit` event, no flush.\n * - Some server setups (e.g. the low-level `Server` class, as opposed to\n * `McpServer`) simply exit *naturally* on stdin EOF — no signal at all, just\n * Node draining an event loop with nothing left to do — which an unhandled\n * signal listener does nothing to catch.\n *\n * Both are covered here: `beforeExit` for the natural-exit case, `SIGTERM`/\n * `SIGINT` for the signal case, sharing one flush bounded by a timeout so a slow\n * or unreachable ingest endpoint can never hang process shutdown either way.\n */\nexport function installShutdownFlush(batcher: TelemetryBatcher): void {\n let flushed = false;\n const flushOnce = (): Promise<void> => {\n if (flushed) return Promise.resolve();\n flushed = true;\n const timeout = new Promise<void>((resolve) => setTimeout(resolve, SHUTDOWN_FLUSH_TIMEOUT_MS).unref());\n return Promise.race([batcher.close(), timeout]).catch(() => {});\n };\n\n // Natural exit: nothing else queued on the event loop, no signal involved.\n // Scheduling async work here (the flush) delays the actual exit until it\n // settles, without ever holding the process open when there's truly nothing\n // to flush — `flush()` itself no-ops instantly on an empty queue.\n process.once(\"beforeExit\", () => {\n void flushOnce();\n });\n\n for (const signal of SHUTDOWN_SIGNALS) {\n const handler = () => {\n // Remove ourselves before re-raising the same signal below, so that re-raise\n // falls through to the default action (or any other listener) instead of\n // looping back into this handler.\n process.removeListener(signal, handler);\n void flushOnce().finally(() => process.kill(process.pid, signal));\n };\n process.on(signal, handler);\n }\n}\n","import { resolveConfig } from \"./config.js\";\nimport { MessageClassifier } from \"./core/classify.js\";\nimport { TelemetryBatcher, installShutdownFlush } from \"./core/batcher.js\";\nimport type { MinimalTransport, TelemetryOptions } from \"./types.js\";\n\n/**\n * Wraps a Transport's `onmessage`/`send` seam to capture telemetry, then delegates\n * everything through to the real transport unchanged. If no API key is configured\n * (via `options.apiKey` or the MCPFY_API_KEY env var), this is a complete no-op —\n * the original transport is returned untouched.\n *\n * Usage (add these lines yourself — nothing here edits your files for you):\n *\n * import { withMcpfyTelemetry } from \"mcpfy-pulse\";\n * const transport = new StdioServerTransport();\n * await server.connect(withMcpfyTelemetry(transport, { apiKey: process.env.MCPFY_API_KEY }));\n */\nexport function withMcpfyTelemetry<T extends MinimalTransport>(transport: T, options?: TelemetryOptions): T {\n const config = resolveConfig(options);\n if (!config.apiKey) return transport;\n\n const classifier = new MessageClassifier();\n const batcher = new TelemetryBatcher(config, {\n serverName: options?.serverName,\n serverVersion: options?.serverVersion,\n sdkName: options?.sdkName ?? \"@modelcontextprotocol/sdk\",\n sdkVersion: options?.sdkVersion,\n installMode: options?.installMode ?? \"sdk-wrapper\",\n });\n installShutdownFlush(batcher);\n\n const wrapped: MinimalTransport = {\n start: () => transport.start(),\n close: () => {\n void batcher.close();\n return transport.close();\n },\n send: (message: any, sendOptions?: any) => {\n const event = classifier.onOutgoing(message);\n if (event) batcher.push(event);\n return transport.send(message, sendOptions);\n },\n get sessionId() {\n return transport.sessionId;\n },\n get onclose() {\n return transport.onclose;\n },\n set onclose(fn) {\n transport.onclose = fn;\n },\n get onerror() {\n return transport.onerror;\n },\n set onerror(fn) {\n transport.onerror = fn;\n },\n get onmessage() {\n return transport.onmessage;\n },\n set onmessage(handler) {\n transport.onmessage = (message: any, extra?: any) => {\n const event = classifier.onIncoming(message);\n if (event) batcher.push(event);\n handler?.(message, extra);\n };\n },\n };\n\n return wrapped as T;\n}\n"],"mappings":"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","names":["pending"]}
|
package/dist/src/index.js
CHANGED
|
@@ -245,6 +245,28 @@ var TelemetryBatcher = class {
|
|
|
245
245
|
await this.flush();
|
|
246
246
|
}
|
|
247
247
|
};
|
|
248
|
+
var SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
|
|
249
|
+
var SHUTDOWN_FLUSH_TIMEOUT_MS = 3e3;
|
|
250
|
+
function installShutdownFlush(batcher) {
|
|
251
|
+
let flushed = false;
|
|
252
|
+
const flushOnce = () => {
|
|
253
|
+
if (flushed) return Promise.resolve();
|
|
254
|
+
flushed = true;
|
|
255
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, SHUTDOWN_FLUSH_TIMEOUT_MS).unref());
|
|
256
|
+
return Promise.race([batcher.close(), timeout]).catch(() => {
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
process.once("beforeExit", () => {
|
|
260
|
+
void flushOnce();
|
|
261
|
+
});
|
|
262
|
+
for (const signal of SHUTDOWN_SIGNALS) {
|
|
263
|
+
const handler = () => {
|
|
264
|
+
process.removeListener(signal, handler);
|
|
265
|
+
void flushOnce().finally(() => process.kill(process.pid, signal));
|
|
266
|
+
};
|
|
267
|
+
process.on(signal, handler);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
248
270
|
|
|
249
271
|
// src/wrap-transport.ts
|
|
250
272
|
function withMcpfyTelemetry(transport, options) {
|
|
@@ -258,6 +280,7 @@ function withMcpfyTelemetry(transport, options) {
|
|
|
258
280
|
sdkVersion: options?.sdkVersion,
|
|
259
281
|
installMode: options?.installMode ?? "sdk-wrapper"
|
|
260
282
|
});
|
|
283
|
+
installShutdownFlush(batcher);
|
|
261
284
|
const wrapped = {
|
|
262
285
|
start: () => transport.start(),
|
|
263
286
|
close: () => {
|
package/dist/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/config.ts","../../src/core/classify.ts","../../src/core/batcher.ts","../../src/wrap-transport.ts"],"sourcesContent":["import type { TelemetryOptions } from \"./types.js\";\n\n// Path matches the real route on cloudmcp-nest: POST /v1/telemetry/ingest.\nconst DEFAULT_ENDPOINT = \"https://api.mcpfy.ai/v1/telemetry/ingest\";\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000;\nconst DEFAULT_MAX_BATCH_SIZE = 500;\n\nexport interface ResolvedConfig {\n apiKey: string | undefined;\n endpoint: string;\n flushIntervalMs: number;\n maxBatchSize: number;\n}\n\nexport function resolveConfig(options?: TelemetryOptions): ResolvedConfig {\n return {\n apiKey: options?.apiKey ?? process.env.MCPFY_API_KEY,\n endpoint: options?.endpoint ?? process.env.MCPFY_TELEMETRY_ENDPOINT ?? DEFAULT_ENDPOINT,\n flushIntervalMs: options?.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,\n maxBatchSize: options?.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,\n };\n}\n","import type { DeclaredToolMeta, TelemetryEvent } from \"../types.js\";\n\ninterface PendingEntry {\n method: string;\n startedAt: number;\n argsBytes: number;\n extra: Partial<TelemetryEvent>;\n progressToken?: string;\n progressCount: number;\n}\n\nfunction byteLength(value: unknown): number {\n // `undefined` (no `params` key at all) and `null` (an explicit `\"params\": null`)\n // both mean \"no meaningful params\" — collapsed to the same 0 here so argsBytes\n // doesn't swing on a client's JSON-serialization choice. Matches classify.py's\n // `_byte_length`, which can't tell the two apart once parsed into a dict anyway.\n if (value === undefined || value === null) return 0;\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return 0;\n }\n}\n\n/**\n * Reduces a `tools/list` response's `result.tools` array to non-content metadata —\n * whether/how-long each description is, and how many of its params are individually\n * documented, plus structured-output/annotation presence. Never reads the description\n * text or schema into the event itself.\n */\nfunction extractDeclaredTools(tools: unknown): DeclaredToolMeta[] | undefined {\n if (!Array.isArray(tools)) return undefined;\n const declared: DeclaredToolMeta[] = [];\n for (const tool of tools) {\n if (!tool || typeof tool.name !== \"string\") continue;\n const description = typeof tool.description === \"string\" ? tool.description : \"\";\n const properties = tool.inputSchema?.properties;\n const paramNames = properties && typeof properties === \"object\" ? Object.keys(properties) : [];\n const paramsWithDescriptionCount = paramNames.filter(\n (p) => typeof properties[p]?.description === \"string\" && properties[p].description.trim().length > 0,\n ).length;\n const annotations = tool.annotations && typeof tool.annotations === \"object\" ? tool.annotations : undefined;\n declared.push({\n name: tool.name,\n hasDescription: description.trim().length > 0,\n descriptionLength: description.length,\n paramCount: paramNames.length,\n paramsWithDescriptionCount,\n hasOutputSchema: tool.outputSchema != null && typeof tool.outputSchema === \"object\",\n readOnlyHint: typeof annotations?.readOnlyHint === \"boolean\" ? annotations.readOnlyHint : undefined,\n destructiveHint: typeof annotations?.destructiveHint === \"boolean\" ? annotations.destructiveHint : undefined,\n idempotentHint: typeof annotations?.idempotentHint === \"boolean\" ? annotations.idempotentHint : undefined,\n openWorldHint: typeof annotations?.openWorldHint === \"boolean\" ? annotations.openWorldHint : undefined,\n });\n }\n return declared;\n}\n\n/**\n * Flattens a `ClientCapabilities`/`ServerCapabilities` object to a list of dotted\n * capability paths that are actually declared — group-presence for capabilities with\n * no meaningful sub-flags, `<group>.<subflag>` for the handful that have one worth\n * surfacing (resources/prompts/tools listChanged, resources.subscribe). `experimental`\n * is deliberately skipped (arbitrary custom capability names, out of scope).\n */\nfunction extractCapabilities(capabilities: unknown): string[] | undefined {\n if (!capabilities || typeof capabilities !== \"object\") return undefined;\n const caps = capabilities as Record<string, any>;\n const out: string[] = [];\n const GROUPS = [\"sampling\", \"elicitation\", \"roots\", \"tasks\", \"logging\", \"prompts\", \"resources\", \"tools\", \"completions\"];\n for (const group of GROUPS) {\n const value = caps[group];\n if (value === undefined || value === null) continue;\n out.push(group);\n if (typeof value === \"object\") {\n if (value.listChanged === true) out.push(`${group}.listChanged`);\n if (group === \"resources\" && value.subscribe === true) out.push(`${group}.subscribe`);\n }\n }\n return out.length > 0 ? out : undefined;\n}\n\nfunction extractLabel(method: string, params: any): Partial<TelemetryEvent> {\n switch (method) {\n case \"tools/call\":\n return { toolName: params?.name };\n case \"prompts/get\":\n return { promptName: params?.name };\n case \"resources/read\":\n return { resourceUri: params?.uri };\n case \"initialize\": {\n // Protocol handshake metadata, not user data — same fields the spec itself exchanges in the clear.\n const label: Partial<TelemetryEvent> = {\n clientName: params?.clientInfo?.name,\n clientVersion: params?.clientInfo?.version,\n protocolVersion: params?.protocolVersion,\n };\n const clientCapabilities = extractCapabilities(params?.capabilities);\n if (clientCapabilities) label.clientCapabilities = clientCapabilities;\n return label;\n }\n default:\n return {};\n }\n}\n\n/**\n * Tracks request/response pairs across the two seams every Transport exposes\n * (onmessage = incoming, send = outgoing) and emits one event per completed\n * request. Only method names, byte counts, timing, and outcome are captured —\n * argument values and result content are never read beyond their byte length.\n * The one exception is `tools/list`, where per-tool description/param *presence\n * and length* are captured (never the description text or schema) — see\n * `extractDeclaredTools`.\n *\n * Also tracks a handful of notification types now: `notifications/cancelled` (turns\n * into a completed event with `outcome:\"cancelled\"`, since the original request never\n * gets a response), `notifications/progress` (counted per request, never the progress\n * values themselves), and `notifications/message` plus the three list_changed\n * notifications (emitted as standalone `type:\"notification\"` events). Everything else server-initiated\n * (sampling, elicitation, roots) is still intentionally not tracked — out of scope for\n * this pass.\n */\nexport class MessageClassifier {\n private pending = new Map<string | number, PendingEntry>();\n private pendingByToken = new Map<string, string | number>();\n\n private forgetPending(id: string | number, pending: PendingEntry): void {\n this.pending.delete(id);\n if (pending.progressToken !== undefined) this.pendingByToken.delete(pending.progressToken);\n }\n\n /**\n * Returns an event only for the one incoming case that completes a request without\n * ever seeing a response: a client cancelling its own still-pending request. Every\n * other incoming message (ordinary requests, `notifications/initialized`, and any\n * other notification) returns `undefined`, matching prior behavior exactly.\n */\n onIncoming(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, params } = message;\n\n if (id === undefined && method === \"notifications/cancelled\") {\n // Never read params.reason — free-text, same \"never content\" boundary as everything else.\n const requestId = params?.requestId;\n if (requestId === undefined) return undefined;\n const pending = this.pending.get(requestId);\n if (!pending) return undefined;\n this.forgetPending(requestId, pending);\n return {\n type: \"request\",\n method: pending.method,\n argsBytes: pending.argsBytes,\n durationMs: Date.now() - pending.startedAt,\n outcome: \"cancelled\",\n progressUpdateCount: pending.progressCount,\n timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n }\n\n if (id === undefined || !method) return undefined; // only requests carry both an id and a method\n\n const progressToken = params?._meta?.progressToken;\n const entry: PendingEntry = {\n method,\n startedAt: Date.now(),\n argsBytes: byteLength(params),\n extra: extractLabel(method, params),\n progressCount: 0,\n };\n if (typeof progressToken === \"string\" || typeof progressToken === \"number\") {\n entry.progressToken = String(progressToken);\n this.pendingByToken.set(entry.progressToken, id);\n }\n this.pending.set(id, entry);\n return undefined;\n }\n\n onOutgoing(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, params, result, error } = message;\n\n if (method && id === undefined) {\n // An outgoing notification (server -> client) — not a response.\n if (method === \"notifications/progress\") {\n const pendingId = this.pendingByToken.get(String(params?.progressToken));\n if (pendingId !== undefined) {\n const pending = this.pending.get(pendingId);\n if (pending) pending.progressCount += 1;\n }\n return undefined;\n }\n if (method === \"notifications/message\") {\n return {\n type: \"notification\",\n method,\n logLevel: typeof params?.level === \"string\" ? params.level : undefined,\n timestamp: new Date().toISOString(),\n };\n }\n if (\n method === \"notifications/tools/list_changed\" ||\n method === \"notifications/resources/list_changed\" ||\n method === \"notifications/prompts/list_changed\"\n ) {\n return { type: \"notification\", method, timestamp: new Date().toISOString() };\n }\n return undefined; // other notifications / server-initiated requests — still out of scope\n }\n\n if (method || id === undefined) return undefined;\n\n const pending = this.pending.get(id);\n if (!pending) return undefined;\n this.forgetPending(id, pending);\n\n const event: TelemetryEvent = {\n type: \"request\",\n method: pending.method,\n argsBytes: pending.argsBytes,\n resultBytes: byteLength(error ?? result),\n durationMs: Date.now() - pending.startedAt,\n outcome: error ? \"error\" : \"ok\",\n errorCode: error?.code,\n progressUpdateCount: pending.progressCount,\n timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n\n if (pending.method === \"initialize\" && !error) {\n if (result?.serverInfo) {\n event.serverName = result.serverInfo.name;\n event.serverVersion = result.serverInfo.version;\n }\n const serverCapabilities = extractCapabilities(result?.capabilities);\n if (serverCapabilities) event.serverCapabilities = serverCapabilities;\n }\n\n if (pending.method === \"tools/call\" && !error && result?.isError === true) {\n event.resultIsError = true;\n }\n\n if (pending.method === \"tools/list\" && !error) {\n const declaredTools = extractDeclaredTools(result?.tools);\n if (declaredTools) event.declaredTools = declaredTools;\n }\n\n return event;\n }\n}\n","import type { TelemetryEvent, SdkName, InstallMode } from \"../types.js\";\nimport type { ResolvedConfig } from \"../config.js\";\n\nexport interface BatchMeta {\n serverName?: string;\n serverVersion?: string;\n sdkName: SdkName;\n sdkVersion?: string;\n installMode: InstallMode;\n}\n\n/**\n * Ring-buffer batcher: queues events, flushes on a timer or when full, and never\n * throws or retries indefinitely — a failed or unreachable ingest endpoint is a\n * silent no-op, by design (telemetry must never affect the server's own behavior,\n * and the endpoint may not exist yet — see telemetry-master-plan.md §4/§7).\n */\nexport class TelemetryBatcher {\n private queue: TelemetryEvent[] = [];\n private timer: ReturnType<typeof setInterval>;\n\n constructor(\n private readonly config: ResolvedConfig,\n private readonly meta: BatchMeta\n ) {\n this.timer = setInterval(() => void this.flush(), config.flushIntervalMs);\n this.timer.unref?.(); // telemetry must never be the reason a process stays alive\n }\n\n push(event: TelemetryEvent): void {\n this.queue.push(event);\n if (this.queue.length >= this.config.maxBatchSize) void this.flush();\n }\n\n async flush(): Promise<void> {\n if (this.queue.length === 0) return;\n const events = this.queue.splice(0, this.queue.length);\n try {\n const response = await fetch(this.config.endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.config.apiKey ?? \"\"}`,\n },\n body: JSON.stringify({\n serverName: this.meta.serverName,\n serverVersion: this.meta.serverVersion,\n sdkName: this.meta.sdkName,\n sdkVersion: this.meta.sdkVersion,\n installMode: this.meta.installMode,\n events,\n }),\n });\n // Never throws on a non-2xx (silent drop is still the contract — see class doc),\n // but a warning is the difference between \"found the bug in 30 seconds\" and\n // \"found it three days later\": a 404/401/500 here means events are being\n // discarded even though fetch() itself didn't throw.\n if (!response.ok) {\n console.warn(\n `[mcpfy-pulse] telemetry flush failed: ${response.status} ${response.statusText} (${this.config.endpoint}) — ${events.length} event(s) dropped`\n );\n }\n } catch (err) {\n console.warn(\n `[mcpfy-pulse] telemetry flush failed: ${err instanceof Error ? err.message : String(err)} (${this.config.endpoint}) — ${events.length} event(s) dropped`\n );\n }\n }\n\n async close(): Promise<void> {\n clearInterval(this.timer);\n await this.flush();\n }\n}\n","import { resolveConfig } from \"./config.js\";\nimport { MessageClassifier } from \"./core/classify.js\";\nimport { TelemetryBatcher } from \"./core/batcher.js\";\nimport type { MinimalTransport, TelemetryOptions } from \"./types.js\";\n\n/**\n * Wraps a Transport's `onmessage`/`send` seam to capture telemetry, then delegates\n * everything through to the real transport unchanged. If no API key is configured\n * (via `options.apiKey` or the MCPFY_API_KEY env var), this is a complete no-op —\n * the original transport is returned untouched.\n *\n * Usage (add these lines yourself — nothing here edits your files for you):\n *\n * import { withMcpfyTelemetry } from \"mcpfy-pulse\";\n * const transport = new StdioServerTransport();\n * await server.connect(withMcpfyTelemetry(transport, { apiKey: process.env.MCPFY_API_KEY }));\n */\nexport function withMcpfyTelemetry<T extends MinimalTransport>(transport: T, options?: TelemetryOptions): T {\n const config = resolveConfig(options);\n if (!config.apiKey) return transport;\n\n const classifier = new MessageClassifier();\n const batcher = new TelemetryBatcher(config, {\n serverName: options?.serverName,\n serverVersion: options?.serverVersion,\n sdkName: options?.sdkName ?? \"@modelcontextprotocol/sdk\",\n sdkVersion: options?.sdkVersion,\n installMode: options?.installMode ?? \"sdk-wrapper\",\n });\n\n const wrapped: MinimalTransport = {\n start: () => transport.start(),\n close: () => {\n void batcher.close();\n return transport.close();\n },\n send: (message: any, sendOptions?: any) => {\n const event = classifier.onOutgoing(message);\n if (event) batcher.push(event);\n return transport.send(message, sendOptions);\n },\n get sessionId() {\n return transport.sessionId;\n },\n get onclose() {\n return transport.onclose;\n },\n set onclose(fn) {\n transport.onclose = fn;\n },\n get onerror() {\n return transport.onerror;\n },\n set onerror(fn) {\n transport.onerror = fn;\n },\n get onmessage() {\n return transport.onmessage;\n },\n set onmessage(handler) {\n transport.onmessage = (message: any, extra?: any) => {\n const event = classifier.onIncoming(message);\n if (event) batcher.push(event);\n handler?.(message, extra);\n };\n },\n };\n\n return wrapped as T;\n}\n"],"mappings":"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","names":["pending"]}
|
|
1
|
+
{"version":3,"sources":["../../src/config.ts","../../src/core/classify.ts","../../src/core/batcher.ts","../../src/wrap-transport.ts"],"sourcesContent":["import type { TelemetryOptions } from \"./types.js\";\n\n// Path matches the real route on cloudmcp-nest: POST /v1/telemetry/ingest.\nconst DEFAULT_ENDPOINT = \"https://api.mcpfy.ai/v1/telemetry/ingest\";\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000;\nconst DEFAULT_MAX_BATCH_SIZE = 500;\n\nexport interface ResolvedConfig {\n apiKey: string | undefined;\n endpoint: string;\n flushIntervalMs: number;\n maxBatchSize: number;\n}\n\nexport function resolveConfig(options?: TelemetryOptions): ResolvedConfig {\n return {\n apiKey: options?.apiKey ?? process.env.MCPFY_API_KEY,\n endpoint: options?.endpoint ?? process.env.MCPFY_TELEMETRY_ENDPOINT ?? DEFAULT_ENDPOINT,\n flushIntervalMs: options?.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,\n maxBatchSize: options?.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,\n };\n}\n","import type { DeclaredToolMeta, TelemetryEvent } from \"../types.js\";\n\ninterface PendingEntry {\n method: string;\n startedAt: number;\n argsBytes: number;\n extra: Partial<TelemetryEvent>;\n progressToken?: string;\n progressCount: number;\n}\n\nfunction byteLength(value: unknown): number {\n // `undefined` (no `params` key at all) and `null` (an explicit `\"params\": null`)\n // both mean \"no meaningful params\" — collapsed to the same 0 here so argsBytes\n // doesn't swing on a client's JSON-serialization choice. Matches classify.py's\n // `_byte_length`, which can't tell the two apart once parsed into a dict anyway.\n if (value === undefined || value === null) return 0;\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return 0;\n }\n}\n\n/**\n * Reduces a `tools/list` response's `result.tools` array to non-content metadata —\n * whether/how-long each description is, and how many of its params are individually\n * documented, plus structured-output/annotation presence. Never reads the description\n * text or schema into the event itself.\n */\nfunction extractDeclaredTools(tools: unknown): DeclaredToolMeta[] | undefined {\n if (!Array.isArray(tools)) return undefined;\n const declared: DeclaredToolMeta[] = [];\n for (const tool of tools) {\n if (!tool || typeof tool.name !== \"string\") continue;\n const description = typeof tool.description === \"string\" ? tool.description : \"\";\n const properties = tool.inputSchema?.properties;\n const paramNames = properties && typeof properties === \"object\" ? Object.keys(properties) : [];\n const paramsWithDescriptionCount = paramNames.filter(\n (p) => typeof properties[p]?.description === \"string\" && properties[p].description.trim().length > 0,\n ).length;\n const annotations = tool.annotations && typeof tool.annotations === \"object\" ? tool.annotations : undefined;\n declared.push({\n name: tool.name,\n hasDescription: description.trim().length > 0,\n descriptionLength: description.length,\n paramCount: paramNames.length,\n paramsWithDescriptionCount,\n hasOutputSchema: tool.outputSchema != null && typeof tool.outputSchema === \"object\",\n readOnlyHint: typeof annotations?.readOnlyHint === \"boolean\" ? annotations.readOnlyHint : undefined,\n destructiveHint: typeof annotations?.destructiveHint === \"boolean\" ? annotations.destructiveHint : undefined,\n idempotentHint: typeof annotations?.idempotentHint === \"boolean\" ? annotations.idempotentHint : undefined,\n openWorldHint: typeof annotations?.openWorldHint === \"boolean\" ? annotations.openWorldHint : undefined,\n });\n }\n return declared;\n}\n\n/**\n * Flattens a `ClientCapabilities`/`ServerCapabilities` object to a list of dotted\n * capability paths that are actually declared — group-presence for capabilities with\n * no meaningful sub-flags, `<group>.<subflag>` for the handful that have one worth\n * surfacing (resources/prompts/tools listChanged, resources.subscribe). `experimental`\n * is deliberately skipped (arbitrary custom capability names, out of scope).\n */\nfunction extractCapabilities(capabilities: unknown): string[] | undefined {\n if (!capabilities || typeof capabilities !== \"object\") return undefined;\n const caps = capabilities as Record<string, any>;\n const out: string[] = [];\n const GROUPS = [\"sampling\", \"elicitation\", \"roots\", \"tasks\", \"logging\", \"prompts\", \"resources\", \"tools\", \"completions\"];\n for (const group of GROUPS) {\n const value = caps[group];\n if (value === undefined || value === null) continue;\n out.push(group);\n if (typeof value === \"object\") {\n if (value.listChanged === true) out.push(`${group}.listChanged`);\n if (group === \"resources\" && value.subscribe === true) out.push(`${group}.subscribe`);\n }\n }\n return out.length > 0 ? out : undefined;\n}\n\nfunction extractLabel(method: string, params: any): Partial<TelemetryEvent> {\n switch (method) {\n case \"tools/call\":\n return { toolName: params?.name };\n case \"prompts/get\":\n return { promptName: params?.name };\n case \"resources/read\":\n return { resourceUri: params?.uri };\n case \"initialize\": {\n // Protocol handshake metadata, not user data — same fields the spec itself exchanges in the clear.\n const label: Partial<TelemetryEvent> = {\n clientName: params?.clientInfo?.name,\n clientVersion: params?.clientInfo?.version,\n protocolVersion: params?.protocolVersion,\n };\n const clientCapabilities = extractCapabilities(params?.capabilities);\n if (clientCapabilities) label.clientCapabilities = clientCapabilities;\n return label;\n }\n default:\n return {};\n }\n}\n\n/**\n * Tracks request/response pairs across the two seams every Transport exposes\n * (onmessage = incoming, send = outgoing) and emits one event per completed\n * request. Only method names, byte counts, timing, and outcome are captured —\n * argument values and result content are never read beyond their byte length.\n * The one exception is `tools/list`, where per-tool description/param *presence\n * and length* are captured (never the description text or schema) — see\n * `extractDeclaredTools`.\n *\n * Also tracks a handful of notification types now: `notifications/cancelled` (turns\n * into a completed event with `outcome:\"cancelled\"`, since the original request never\n * gets a response), `notifications/progress` (counted per request, never the progress\n * values themselves), and `notifications/message` plus the three list_changed\n * notifications (emitted as standalone `type:\"notification\"` events). Everything else server-initiated\n * (sampling, elicitation, roots) is still intentionally not tracked — out of scope for\n * this pass.\n */\nexport class MessageClassifier {\n private pending = new Map<string | number, PendingEntry>();\n private pendingByToken = new Map<string, string | number>();\n\n private forgetPending(id: string | number, pending: PendingEntry): void {\n this.pending.delete(id);\n if (pending.progressToken !== undefined) this.pendingByToken.delete(pending.progressToken);\n }\n\n /**\n * Returns an event only for the one incoming case that completes a request without\n * ever seeing a response: a client cancelling its own still-pending request. Every\n * other incoming message (ordinary requests, `notifications/initialized`, and any\n * other notification) returns `undefined`, matching prior behavior exactly.\n */\n onIncoming(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, params } = message;\n\n if (id === undefined && method === \"notifications/cancelled\") {\n // Never read params.reason — free-text, same \"never content\" boundary as everything else.\n const requestId = params?.requestId;\n if (requestId === undefined) return undefined;\n const pending = this.pending.get(requestId);\n if (!pending) return undefined;\n this.forgetPending(requestId, pending);\n return {\n type: \"request\",\n method: pending.method,\n argsBytes: pending.argsBytes,\n durationMs: Date.now() - pending.startedAt,\n outcome: \"cancelled\",\n progressUpdateCount: pending.progressCount,\n timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n }\n\n if (id === undefined || !method) return undefined; // only requests carry both an id and a method\n\n const progressToken = params?._meta?.progressToken;\n const entry: PendingEntry = {\n method,\n startedAt: Date.now(),\n argsBytes: byteLength(params),\n extra: extractLabel(method, params),\n progressCount: 0,\n };\n if (typeof progressToken === \"string\" || typeof progressToken === \"number\") {\n entry.progressToken = String(progressToken);\n this.pendingByToken.set(entry.progressToken, id);\n }\n this.pending.set(id, entry);\n return undefined;\n }\n\n onOutgoing(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, params, result, error } = message;\n\n if (method && id === undefined) {\n // An outgoing notification (server -> client) — not a response.\n if (method === \"notifications/progress\") {\n const pendingId = this.pendingByToken.get(String(params?.progressToken));\n if (pendingId !== undefined) {\n const pending = this.pending.get(pendingId);\n if (pending) pending.progressCount += 1;\n }\n return undefined;\n }\n if (method === \"notifications/message\") {\n return {\n type: \"notification\",\n method,\n logLevel: typeof params?.level === \"string\" ? params.level : undefined,\n timestamp: new Date().toISOString(),\n };\n }\n if (\n method === \"notifications/tools/list_changed\" ||\n method === \"notifications/resources/list_changed\" ||\n method === \"notifications/prompts/list_changed\"\n ) {\n return { type: \"notification\", method, timestamp: new Date().toISOString() };\n }\n return undefined; // other notifications / server-initiated requests — still out of scope\n }\n\n if (method || id === undefined) return undefined;\n\n const pending = this.pending.get(id);\n if (!pending) return undefined;\n this.forgetPending(id, pending);\n\n const event: TelemetryEvent = {\n type: \"request\",\n method: pending.method,\n argsBytes: pending.argsBytes,\n resultBytes: byteLength(error ?? result),\n durationMs: Date.now() - pending.startedAt,\n outcome: error ? \"error\" : \"ok\",\n errorCode: error?.code,\n progressUpdateCount: pending.progressCount,\n timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n\n if (pending.method === \"initialize\" && !error) {\n if (result?.serverInfo) {\n event.serverName = result.serverInfo.name;\n event.serverVersion = result.serverInfo.version;\n }\n const serverCapabilities = extractCapabilities(result?.capabilities);\n if (serverCapabilities) event.serverCapabilities = serverCapabilities;\n }\n\n if (pending.method === \"tools/call\" && !error && result?.isError === true) {\n event.resultIsError = true;\n }\n\n if (pending.method === \"tools/list\" && !error) {\n const declaredTools = extractDeclaredTools(result?.tools);\n if (declaredTools) event.declaredTools = declaredTools;\n }\n\n return event;\n }\n}\n","import type { TelemetryEvent, SdkName, InstallMode } from \"../types.js\";\nimport type { ResolvedConfig } from \"../config.js\";\n\nexport interface BatchMeta {\n serverName?: string;\n serverVersion?: string;\n sdkName: SdkName;\n sdkVersion?: string;\n installMode: InstallMode;\n}\n\n/**\n * Ring-buffer batcher: queues events, flushes on a timer or when full, and never\n * throws or retries indefinitely — a failed or unreachable ingest endpoint is a\n * silent no-op, by design (telemetry must never affect the server's own behavior,\n * and the endpoint may not exist yet — see telemetry-master-plan.md §4/§7).\n */\nexport class TelemetryBatcher {\n private queue: TelemetryEvent[] = [];\n private timer: ReturnType<typeof setInterval>;\n\n constructor(\n private readonly config: ResolvedConfig,\n private readonly meta: BatchMeta\n ) {\n this.timer = setInterval(() => void this.flush(), config.flushIntervalMs);\n this.timer.unref?.(); // telemetry must never be the reason a process stays alive\n }\n\n push(event: TelemetryEvent): void {\n this.queue.push(event);\n if (this.queue.length >= this.config.maxBatchSize) void this.flush();\n }\n\n async flush(): Promise<void> {\n if (this.queue.length === 0) return;\n const events = this.queue.splice(0, this.queue.length);\n try {\n const response = await fetch(this.config.endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.config.apiKey ?? \"\"}`,\n },\n body: JSON.stringify({\n serverName: this.meta.serverName,\n serverVersion: this.meta.serverVersion,\n sdkName: this.meta.sdkName,\n sdkVersion: this.meta.sdkVersion,\n installMode: this.meta.installMode,\n events,\n }),\n });\n // Never throws on a non-2xx (silent drop is still the contract — see class doc),\n // but a warning is the difference between \"found the bug in 30 seconds\" and\n // \"found it three days later\": a 404/401/500 here means events are being\n // discarded even though fetch() itself didn't throw.\n if (!response.ok) {\n console.warn(\n `[mcpfy-pulse] telemetry flush failed: ${response.status} ${response.statusText} (${this.config.endpoint}) — ${events.length} event(s) dropped`\n );\n }\n } catch (err) {\n console.warn(\n `[mcpfy-pulse] telemetry flush failed: ${err instanceof Error ? err.message : String(err)} (${this.config.endpoint}) — ${events.length} event(s) dropped`\n );\n }\n }\n\n async close(): Promise<void> {\n clearInterval(this.timer);\n await this.flush();\n }\n}\n\nconst SHUTDOWN_SIGNALS: NodeJS.Signals[] = [\"SIGTERM\", \"SIGINT\"];\nconst SHUTDOWN_FLUSH_TIMEOUT_MS = 3000;\n\n/**\n * Installs a best-effort \"flush before we die\" hook for a batcher that has no\n * other lifecycle event to hang a flush off of. `withMcpfyTelemetry` wraps a\n * transport inside the *same process* as the server's own code — unlike the stdio\n * proxy, there's no child process it can wait on and forward signals to. Without\n * this, a real MCP session shorter than the flush interval loses every event, via\n * two independent paths, both confirmed against real client/server sessions:\n *\n * - A real MCP client's normal graceful shutdown (close stdin, wait, escalate to\n * SIGTERM if the process is still alive) kills the process via an unhandled\n * signal — instant, no `exit` event, no flush.\n * - Some server setups (e.g. the low-level `Server` class, as opposed to\n * `McpServer`) simply exit *naturally* on stdin EOF — no signal at all, just\n * Node draining an event loop with nothing left to do — which an unhandled\n * signal listener does nothing to catch.\n *\n * Both are covered here: `beforeExit` for the natural-exit case, `SIGTERM`/\n * `SIGINT` for the signal case, sharing one flush bounded by a timeout so a slow\n * or unreachable ingest endpoint can never hang process shutdown either way.\n */\nexport function installShutdownFlush(batcher: TelemetryBatcher): void {\n let flushed = false;\n const flushOnce = (): Promise<void> => {\n if (flushed) return Promise.resolve();\n flushed = true;\n const timeout = new Promise<void>((resolve) => setTimeout(resolve, SHUTDOWN_FLUSH_TIMEOUT_MS).unref());\n return Promise.race([batcher.close(), timeout]).catch(() => {});\n };\n\n // Natural exit: nothing else queued on the event loop, no signal involved.\n // Scheduling async work here (the flush) delays the actual exit until it\n // settles, without ever holding the process open when there's truly nothing\n // to flush — `flush()` itself no-ops instantly on an empty queue.\n process.once(\"beforeExit\", () => {\n void flushOnce();\n });\n\n for (const signal of SHUTDOWN_SIGNALS) {\n const handler = () => {\n // Remove ourselves before re-raising the same signal below, so that re-raise\n // falls through to the default action (or any other listener) instead of\n // looping back into this handler.\n process.removeListener(signal, handler);\n void flushOnce().finally(() => process.kill(process.pid, signal));\n };\n process.on(signal, handler);\n }\n}\n","import { resolveConfig } from \"./config.js\";\nimport { MessageClassifier } from \"./core/classify.js\";\nimport { TelemetryBatcher, installShutdownFlush } from \"./core/batcher.js\";\nimport type { MinimalTransport, TelemetryOptions } from \"./types.js\";\n\n/**\n * Wraps a Transport's `onmessage`/`send` seam to capture telemetry, then delegates\n * everything through to the real transport unchanged. If no API key is configured\n * (via `options.apiKey` or the MCPFY_API_KEY env var), this is a complete no-op —\n * the original transport is returned untouched.\n *\n * Usage (add these lines yourself — nothing here edits your files for you):\n *\n * import { withMcpfyTelemetry } from \"mcpfy-pulse\";\n * const transport = new StdioServerTransport();\n * await server.connect(withMcpfyTelemetry(transport, { apiKey: process.env.MCPFY_API_KEY }));\n */\nexport function withMcpfyTelemetry<T extends MinimalTransport>(transport: T, options?: TelemetryOptions): T {\n const config = resolveConfig(options);\n if (!config.apiKey) return transport;\n\n const classifier = new MessageClassifier();\n const batcher = new TelemetryBatcher(config, {\n serverName: options?.serverName,\n serverVersion: options?.serverVersion,\n sdkName: options?.sdkName ?? \"@modelcontextprotocol/sdk\",\n sdkVersion: options?.sdkVersion,\n installMode: options?.installMode ?? \"sdk-wrapper\",\n });\n installShutdownFlush(batcher);\n\n const wrapped: MinimalTransport = {\n start: () => transport.start(),\n close: () => {\n void batcher.close();\n return transport.close();\n },\n send: (message: any, sendOptions?: any) => {\n const event = classifier.onOutgoing(message);\n if (event) batcher.push(event);\n return transport.send(message, sendOptions);\n },\n get sessionId() {\n return transport.sessionId;\n },\n get onclose() {\n return transport.onclose;\n },\n set onclose(fn) {\n transport.onclose = fn;\n },\n get onerror() {\n return transport.onerror;\n },\n set onerror(fn) {\n transport.onerror = fn;\n },\n get onmessage() {\n return transport.onmessage;\n },\n set onmessage(handler) {\n transport.onmessage = (message: any, extra?: any) => {\n const event = classifier.onIncoming(message);\n if (event) batcher.push(event);\n handler?.(message, extra);\n };\n },\n };\n\n return wrapped as T;\n}\n"],"mappings":"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","names":["pending"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrap-transport.d.ts","sourceRoot":"","sources":["../../src/wrap-transport.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAErE;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,gBAAgB,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"wrap-transport.d.ts","sourceRoot":"","sources":["../../src/wrap-transport.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAErE;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,gBAAgB,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,CAAC,CAqD1G"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcpfy-pulse",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.4",
|
|
5
5
|
"description": "Telemetry for MCP servers: wrap any Transport, or proxy any command over stdio, to capture method/size/duration/outcome metrics for the MCPFY dashboard. Never sends argument values or resource content.",
|
|
6
6
|
"author": "mcpfy",
|
|
7
7
|
"license": "MIT",
|