mcpfy-pulse 0.1.0 → 0.1.2
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 +13 -2
- package/dist/bin/mcpfy-proxy.js.map +1 -1
- package/dist/src/core/batcher.d.ts.map +1 -1
- package/dist/src/core/classify.d.ts.map +1 -1
- package/dist/src/index.cjs +13 -2
- package/dist/src/index.cjs.map +1 -1
- package/dist/src/index.js +13 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/types.d.ts +8 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +7 -7
package/dist/bin/mcpfy-proxy.js
CHANGED
|
@@ -78,6 +78,9 @@ var MessageClassifier = class {
|
|
|
78
78
|
event.serverName = result.serverInfo.name;
|
|
79
79
|
event.serverVersion = result.serverInfo.version;
|
|
80
80
|
}
|
|
81
|
+
if (pending.method === "tools/call" && !error && result?.isError === true) {
|
|
82
|
+
event.resultIsError = true;
|
|
83
|
+
}
|
|
81
84
|
return event;
|
|
82
85
|
}
|
|
83
86
|
};
|
|
@@ -102,7 +105,7 @@ var TelemetryBatcher = class {
|
|
|
102
105
|
if (this.queue.length === 0) return;
|
|
103
106
|
const events = this.queue.splice(0, this.queue.length);
|
|
104
107
|
try {
|
|
105
|
-
await fetch(this.config.endpoint, {
|
|
108
|
+
const response = await fetch(this.config.endpoint, {
|
|
106
109
|
method: "POST",
|
|
107
110
|
headers: {
|
|
108
111
|
"content-type": "application/json",
|
|
@@ -117,7 +120,15 @@ var TelemetryBatcher = class {
|
|
|
117
120
|
events
|
|
118
121
|
})
|
|
119
122
|
});
|
|
120
|
-
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
console.warn(
|
|
125
|
+
`[mcpfy-pulse] telemetry flush failed: ${response.status} ${response.statusText} (${this.config.endpoint}) \u2014 ${events.length} event(s) dropped`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.warn(
|
|
130
|
+
`[mcpfy-pulse] telemetry flush failed: ${err instanceof Error ? err.message : String(err)} (${this.config.endpoint}) \u2014 ${events.length} event(s) dropped`
|
|
131
|
+
);
|
|
121
132
|
}
|
|
122
133
|
}
|
|
123
134
|
async close() {
|
|
@@ -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) classifier.onIncoming(message);\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 { TelemetryEvent } from \"../types.js\";\n\ninterface PendingEntry {\n method: string;\n startedAt: number;\n argsBytes: number;\n extra: Partial<TelemetryEvent>;\n}\n\nfunction byteLength(value: unknown): number {\n if (value === undefined) return 0;\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return 0;\n }\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 return {\n clientName: params?.clientInfo?.name,\n clientVersion: params?.clientInfo?.version,\n protocolVersion: params?.protocolVersion,\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 *\n * Server-initiated requests/notifications (sampling, elicitation, logging,\n * progress) are intentionally not tracked yet — out of scope for this pass.\n */\nexport class MessageClassifier {\n private pending = new Map<string | number, PendingEntry>();\n\n onIncoming(message: any): void {\n if (!message || typeof message !== \"object\") return;\n const { id, method, params } = message;\n if (id === undefined || !method) return; // only requests carry both an id and a method\n this.pending.set(id, {\n method,\n startedAt: Date.now(),\n argsBytes: byteLength(params),\n extra: extractLabel(method, params),\n });\n }\n\n onOutgoing(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, result, error } = message;\n if (method || id === undefined) return undefined;\n\n const pending = this.pending.get(id);\n if (!pending) return undefined;\n this.pending.delete(id);\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 timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n\n if (pending.method === \"initialize\" && result?.serverInfo) {\n event.serverName = result.serverInfo.name;\n event.serverVersion = result.serverInfo.version;\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 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 } catch {\n // silent drop\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":[]}
|
|
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) classifier.onIncoming(message);\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 { TelemetryEvent } from \"../types.js\";\n\ninterface PendingEntry {\n method: string;\n startedAt: number;\n argsBytes: number;\n extra: Partial<TelemetryEvent>;\n}\n\nfunction byteLength(value: unknown): number {\n if (value === undefined) return 0;\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return 0;\n }\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 return {\n clientName: params?.clientInfo?.name,\n clientVersion: params?.clientInfo?.version,\n protocolVersion: params?.protocolVersion,\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 *\n * Server-initiated requests/notifications (sampling, elicitation, logging,\n * progress) are intentionally not tracked yet — out of scope for this pass.\n */\nexport class MessageClassifier {\n private pending = new Map<string | number, PendingEntry>();\n\n onIncoming(message: any): void {\n if (!message || typeof message !== \"object\") return;\n const { id, method, params } = message;\n if (id === undefined || !method) return; // only requests carry both an id and a method\n this.pending.set(id, {\n method,\n startedAt: Date.now(),\n argsBytes: byteLength(params),\n extra: extractLabel(method, params),\n });\n }\n\n onOutgoing(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, result, error } = message;\n if (method || id === undefined) return undefined;\n\n const pending = this.pending.get(id);\n if (!pending) return undefined;\n this.pending.delete(id);\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 timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n\n if (pending.method === \"initialize\" && result?.serverInfo) {\n event.serverName = result.serverInfo.name;\n event.serverVersion = result.serverInfo.version;\n }\n\n if (pending.method === \"tools/call\" && !error && result?.isError === true) {\n event.resultIsError = true;\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":[]}
|
|
@@ -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;
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../../src/core/classify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAsClD;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAA4C;IAE3D,UAAU,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI;IAY9B,UAAU,CAAC,OAAO,EAAE,GAAG,GAAG,cAAc,GAAG,SAAS;
|
|
1
|
+
{"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../../src/core/classify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAsClD;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAA4C;IAE3D,UAAU,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI;IAY9B,UAAU,CAAC,OAAO,EAAE,GAAG,GAAG,cAAc,GAAG,SAAS;CAgCrD"}
|
package/dist/src/index.cjs
CHANGED
|
@@ -99,6 +99,9 @@ var MessageClassifier = class {
|
|
|
99
99
|
event.serverName = result.serverInfo.name;
|
|
100
100
|
event.serverVersion = result.serverInfo.version;
|
|
101
101
|
}
|
|
102
|
+
if (pending.method === "tools/call" && !error && result?.isError === true) {
|
|
103
|
+
event.resultIsError = true;
|
|
104
|
+
}
|
|
102
105
|
return event;
|
|
103
106
|
}
|
|
104
107
|
};
|
|
@@ -123,7 +126,7 @@ var TelemetryBatcher = class {
|
|
|
123
126
|
if (this.queue.length === 0) return;
|
|
124
127
|
const events = this.queue.splice(0, this.queue.length);
|
|
125
128
|
try {
|
|
126
|
-
await fetch(this.config.endpoint, {
|
|
129
|
+
const response = await fetch(this.config.endpoint, {
|
|
127
130
|
method: "POST",
|
|
128
131
|
headers: {
|
|
129
132
|
"content-type": "application/json",
|
|
@@ -138,7 +141,15 @@ var TelemetryBatcher = class {
|
|
|
138
141
|
events
|
|
139
142
|
})
|
|
140
143
|
});
|
|
141
|
-
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
console.warn(
|
|
146
|
+
`[mcpfy-pulse] telemetry flush failed: ${response.status} ${response.statusText} (${this.config.endpoint}) \u2014 ${events.length} event(s) dropped`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.warn(
|
|
151
|
+
`[mcpfy-pulse] telemetry flush failed: ${err instanceof Error ? err.message : String(err)} (${this.config.endpoint}) \u2014 ${events.length} event(s) dropped`
|
|
152
|
+
);
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
155
|
async 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 { TelemetryEvent } from \"../types.js\";\n\ninterface PendingEntry {\n method: string;\n startedAt: number;\n argsBytes: number;\n extra: Partial<TelemetryEvent>;\n}\n\nfunction byteLength(value: unknown): number {\n if (value === undefined) return 0;\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return 0;\n }\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 return {\n clientName: params?.clientInfo?.name,\n clientVersion: params?.clientInfo?.version,\n protocolVersion: params?.protocolVersion,\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 *\n * Server-initiated requests/notifications (sampling, elicitation, logging,\n * progress) are intentionally not tracked yet — out of scope for this pass.\n */\nexport class MessageClassifier {\n private pending = new Map<string | number, PendingEntry>();\n\n onIncoming(message: any): void {\n if (!message || typeof message !== \"object\") return;\n const { id, method, params } = message;\n if (id === undefined || !method) return; // only requests carry both an id and a method\n this.pending.set(id, {\n method,\n startedAt: Date.now(),\n argsBytes: byteLength(params),\n extra: extractLabel(method, params),\n });\n }\n\n onOutgoing(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, result, error } = message;\n if (method || id === undefined) return undefined;\n\n const pending = this.pending.get(id);\n if (!pending) return undefined;\n this.pending.delete(id);\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 timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n\n if (pending.method === \"initialize\" && result?.serverInfo) {\n event.serverName = result.serverInfo.name;\n event.serverVersion = result.serverInfo.version;\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 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 } catch {\n // silent drop\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 classifier.onIncoming(message);\n handler?.(message, extra);\n };\n },\n };\n\n return wrapped as T;\n}\n"],"mappings":"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","names":[]}
|
|
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 { TelemetryEvent } from \"../types.js\";\n\ninterface PendingEntry {\n method: string;\n startedAt: number;\n argsBytes: number;\n extra: Partial<TelemetryEvent>;\n}\n\nfunction byteLength(value: unknown): number {\n if (value === undefined) return 0;\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return 0;\n }\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 return {\n clientName: params?.clientInfo?.name,\n clientVersion: params?.clientInfo?.version,\n protocolVersion: params?.protocolVersion,\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 *\n * Server-initiated requests/notifications (sampling, elicitation, logging,\n * progress) are intentionally not tracked yet — out of scope for this pass.\n */\nexport class MessageClassifier {\n private pending = new Map<string | number, PendingEntry>();\n\n onIncoming(message: any): void {\n if (!message || typeof message !== \"object\") return;\n const { id, method, params } = message;\n if (id === undefined || !method) return; // only requests carry both an id and a method\n this.pending.set(id, {\n method,\n startedAt: Date.now(),\n argsBytes: byteLength(params),\n extra: extractLabel(method, params),\n });\n }\n\n onOutgoing(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, result, error } = message;\n if (method || id === undefined) return undefined;\n\n const pending = this.pending.get(id);\n if (!pending) return undefined;\n this.pending.delete(id);\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 timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n\n if (pending.method === \"initialize\" && result?.serverInfo) {\n event.serverName = result.serverInfo.name;\n event.serverVersion = result.serverInfo.version;\n }\n\n if (pending.method === \"tools/call\" && !error && result?.isError === true) {\n event.resultIsError = true;\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 classifier.onIncoming(message);\n handler?.(message, extra);\n };\n },\n };\n\n return wrapped as T;\n}\n"],"mappings":"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","names":[]}
|
package/dist/src/index.js
CHANGED
|
@@ -73,6 +73,9 @@ var MessageClassifier = class {
|
|
|
73
73
|
event.serverName = result.serverInfo.name;
|
|
74
74
|
event.serverVersion = result.serverInfo.version;
|
|
75
75
|
}
|
|
76
|
+
if (pending.method === "tools/call" && !error && result?.isError === true) {
|
|
77
|
+
event.resultIsError = true;
|
|
78
|
+
}
|
|
76
79
|
return event;
|
|
77
80
|
}
|
|
78
81
|
};
|
|
@@ -97,7 +100,7 @@ var TelemetryBatcher = class {
|
|
|
97
100
|
if (this.queue.length === 0) return;
|
|
98
101
|
const events = this.queue.splice(0, this.queue.length);
|
|
99
102
|
try {
|
|
100
|
-
await fetch(this.config.endpoint, {
|
|
103
|
+
const response = await fetch(this.config.endpoint, {
|
|
101
104
|
method: "POST",
|
|
102
105
|
headers: {
|
|
103
106
|
"content-type": "application/json",
|
|
@@ -112,7 +115,15 @@ var TelemetryBatcher = class {
|
|
|
112
115
|
events
|
|
113
116
|
})
|
|
114
117
|
});
|
|
115
|
-
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
console.warn(
|
|
120
|
+
`[mcpfy-pulse] telemetry flush failed: ${response.status} ${response.statusText} (${this.config.endpoint}) \u2014 ${events.length} event(s) dropped`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.warn(
|
|
125
|
+
`[mcpfy-pulse] telemetry flush failed: ${err instanceof Error ? err.message : String(err)} (${this.config.endpoint}) \u2014 ${events.length} event(s) dropped`
|
|
126
|
+
);
|
|
116
127
|
}
|
|
117
128
|
}
|
|
118
129
|
async 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 { TelemetryEvent } from \"../types.js\";\n\ninterface PendingEntry {\n method: string;\n startedAt: number;\n argsBytes: number;\n extra: Partial<TelemetryEvent>;\n}\n\nfunction byteLength(value: unknown): number {\n if (value === undefined) return 0;\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return 0;\n }\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 return {\n clientName: params?.clientInfo?.name,\n clientVersion: params?.clientInfo?.version,\n protocolVersion: params?.protocolVersion,\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 *\n * Server-initiated requests/notifications (sampling, elicitation, logging,\n * progress) are intentionally not tracked yet — out of scope for this pass.\n */\nexport class MessageClassifier {\n private pending = new Map<string | number, PendingEntry>();\n\n onIncoming(message: any): void {\n if (!message || typeof message !== \"object\") return;\n const { id, method, params } = message;\n if (id === undefined || !method) return; // only requests carry both an id and a method\n this.pending.set(id, {\n method,\n startedAt: Date.now(),\n argsBytes: byteLength(params),\n extra: extractLabel(method, params),\n });\n }\n\n onOutgoing(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, result, error } = message;\n if (method || id === undefined) return undefined;\n\n const pending = this.pending.get(id);\n if (!pending) return undefined;\n this.pending.delete(id);\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 timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n\n if (pending.method === \"initialize\" && result?.serverInfo) {\n event.serverName = result.serverInfo.name;\n event.serverVersion = result.serverInfo.version;\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 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 } catch {\n // silent drop\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 classifier.onIncoming(message);\n handler?.(message, extra);\n };\n },\n };\n\n return wrapped as T;\n}\n"],"mappings":"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","names":[]}
|
|
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 { TelemetryEvent } from \"../types.js\";\n\ninterface PendingEntry {\n method: string;\n startedAt: number;\n argsBytes: number;\n extra: Partial<TelemetryEvent>;\n}\n\nfunction byteLength(value: unknown): number {\n if (value === undefined) return 0;\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return 0;\n }\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 return {\n clientName: params?.clientInfo?.name,\n clientVersion: params?.clientInfo?.version,\n protocolVersion: params?.protocolVersion,\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 *\n * Server-initiated requests/notifications (sampling, elicitation, logging,\n * progress) are intentionally not tracked yet — out of scope for this pass.\n */\nexport class MessageClassifier {\n private pending = new Map<string | number, PendingEntry>();\n\n onIncoming(message: any): void {\n if (!message || typeof message !== \"object\") return;\n const { id, method, params } = message;\n if (id === undefined || !method) return; // only requests carry both an id and a method\n this.pending.set(id, {\n method,\n startedAt: Date.now(),\n argsBytes: byteLength(params),\n extra: extractLabel(method, params),\n });\n }\n\n onOutgoing(message: any): TelemetryEvent | undefined {\n if (!message || typeof message !== \"object\") return undefined;\n const { id, method, result, error } = message;\n if (method || id === undefined) return undefined;\n\n const pending = this.pending.get(id);\n if (!pending) return undefined;\n this.pending.delete(id);\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 timestamp: new Date().toISOString(),\n ...pending.extra,\n };\n\n if (pending.method === \"initialize\" && result?.serverInfo) {\n event.serverName = result.serverInfo.name;\n event.serverVersion = result.serverInfo.version;\n }\n\n if (pending.method === \"tools/call\" && !error && result?.isError === true) {\n event.resultIsError = true;\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 classifier.onIncoming(message);\n handler?.(message, extra);\n };\n },\n };\n\n return wrapped as T;\n}\n"],"mappings":"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","names":[]}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -49,5 +49,13 @@ export interface TelemetryEvent {
|
|
|
49
49
|
outcome?: "ok" | "error";
|
|
50
50
|
errorCode?: number;
|
|
51
51
|
timestamp: string;
|
|
52
|
+
/**
|
|
53
|
+
* `tools/call` only: the result's own `isError` flag (MCP's CallToolResult.isError) —
|
|
54
|
+
* a tool that ran fine at the JSON-RPC level (outcome:"ok") but reports its own
|
|
55
|
+
* business-logic failure in-band. One boolean read off the result envelope, same
|
|
56
|
+
* "protocol metadata, not content" treatment as clientInfo on `initialize` — never the
|
|
57
|
+
* result content itself.
|
|
58
|
+
*/
|
|
59
|
+
resultIsError?: boolean;
|
|
52
60
|
}
|
|
53
61
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/src/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;IAChD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG,2BAA2B,GAAG,SAAS,CAAC;AAC5E,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,aAAa,CAAC;AAEpE,MAAM,WAAW,gBAAgB;IAC/B,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;IAChD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG,2BAA2B,GAAG,SAAS,CAAC;AAC5E,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,aAAa,CAAC;AAEpE,MAAM,WAAW,gBAAgB;IAC/B,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB"}
|
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.2",
|
|
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",
|
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "rimraf dist && tsup && tsc --emitDeclarationOnly --declaration",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest"
|
|
53
|
+
},
|
|
49
54
|
"devDependencies": {
|
|
50
55
|
"@types/node": "^22.10.2",
|
|
51
56
|
"rimraf": "^6.0.1",
|
|
@@ -53,10 +58,5 @@
|
|
|
53
58
|
"tsx": "^4.19.2",
|
|
54
59
|
"typescript": "^5.6.3",
|
|
55
60
|
"vitest": "^2.1.8"
|
|
56
|
-
},
|
|
57
|
-
"scripts": {
|
|
58
|
-
"build": "rimraf dist && tsup && tsc --emitDeclarationOnly --declaration",
|
|
59
|
-
"test": "vitest run",
|
|
60
|
-
"test:watch": "vitest"
|
|
61
61
|
}
|
|
62
|
-
}
|
|
62
|
+
}
|