mcpfy-pulse 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -1,25 +1,62 @@
1
- # mcpfy-pulse
1
+ <div align="center">
2
2
 
3
- Telemetry for MCP servers. Captures method names, payload sizes, durations, and outcomes
4
- for every JSON-RPC request an MCP server handles — never argument values, never resource
5
- content. See [`telemetry-master-plan.md`](../../../../telemetry-master-plan.md) for the
6
- full design.
3
+ <pre>
4
+ ███╗ ███╗ ██████╗██████╗ ███████╗██╗ ██╗ ██████╗ ██╗ ██╗██╗ ███████╗███████╗
5
+ ████╗ ████║██╔════╝██╔══██╗██╔════╝╚██╗ ██╔╝ ██╔══██╗██║ ██║██║ ██╔════╝██╔════╝
6
+ ██╔████╔██║██║ ██████╔╝█████╗ ╚████╔╝ ██████╔╝██║ ██║██║ ███████╗█████╗
7
+ ██║╚██╔╝██║██║ ██╔═══╝ ██╔══╝ ╚██╔╝ ██╔═══╝ ██║ ██║██║ ╚════██║██╔══╝
8
+ ██║ ╚═╝ ██║╚██████╗██║ ██║ ██║ ██║ ╚██████╔╝███████╗███████║███████╗
9
+ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝
10
+ </pre>
7
11
 
8
- There are exactly three ways to use this. Pick the one that matches your situation none
12
+ 📊 Tool-level telemetry | 🩺 Server health score | 🧠 Tool intelligence | 🎯 Result quality scoring | 🔌 Works with any MCP server
13
+
14
+ A drop-in telemetry SDK that captures tool-level usage and performance to give you a real-time health signal for your MCP server - works with any MCP server.
15
+
16
+ ```bash
17
+ npm install mcpfy-pulse
18
+ ```
19
+
20
+ </div>
21
+
22
+ There are exactly three ways to use this. Pick the one that matches your situation - none
9
23
  of them edit your files for you.
10
24
 
25
+ Every mode needs an API key. Create one from your [MCPFY dashboard](https://mcpfy.ai/dashboard/telemetry)
26
+ (**Dashboard → Telemetry**), then set it as `MCPFY_API_KEY` as shown below.
27
+
11
28
  ## 1. You're using `mcpfy-sdk`
12
29
 
13
- Nothing to install or import. Set one environment variable:
30
+ No install step - `mcpfy-pulse` ships bundled with `mcpfy-sdk`. Just set one environment
31
+ variable:
14
32
 
15
33
  ```bash
16
34
  MCPFY_API_KEY=mk_live_xxx node dist/server.js
17
35
  ```
18
36
 
19
37
  `mcpfy-sdk` checks for `MCPFY_API_KEY` internally and wraps its own transport
20
- automatically. Unset the variable and nothing changes no code path is even touched.
38
+ automatically. Unset the variable and nothing changes - no code path is even touched.
39
+
40
+ ## 2. You're not using `mcpfy-sdk` - you built your own server
41
+
42
+ For anyone who wrote their own server on the raw `@modelcontextprotocol/sdk` (or
43
+ anything else that exposes a `Transport`) and has the source in front of them. Three
44
+ steps:
45
+
46
+ **1. Install the package:**
47
+
48
+ ```bash
49
+ npm install mcpfy-pulse
50
+ ```
51
+
52
+ **2. Set your API key** (e.g. in your `.env` file):
53
+
54
+ ```bash
55
+ MCPFY_API_KEY=mk_live_xxx
56
+ ```
21
57
 
22
- ## 2. You have a raw `@modelcontextprotocol/sdk` server (source available)
58
+ **3. Wrap your transport**, right before you connect it - this works for any transport
59
+ (stdio, HTTP, SSE, ...), not just the stdio example below:
23
60
 
24
61
  ```ts
25
62
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -31,14 +68,16 @@ await server.connect(
31
68
  );
32
69
  ```
33
70
 
34
- `withMcpfyTelemetry` wraps the `Transport`'s `onmessage`/`send` seam the two points
71
+ `withMcpfyTelemetry` wraps the `Transport`'s `onmessage`/`send` seam - the two points
35
72
  every JSON-RPC message passes through regardless of which SDK built the server. If
36
- `apiKey` is unset, it returns the original transport unchanged.
73
+ `apiKey` is unset, it returns the original transport unchanged, so it's safe to leave
74
+ this in place across environments.
37
75
 
38
76
  ## 3. You're running someone else's server locally (no source access)
39
77
 
40
- Edit your MCP client's config (`claude_desktop_config.json`, Cursor's `mcp.json`, etc.)
41
- to route the command through the proxy:
78
+ No install step - `npx` fetches `mcpfy-proxy` automatically the first time it runs. Edit
79
+ your MCP client's config (`claude_desktop_config.json`, Cursor's `mcp.json`, etc.) to
80
+ route the command through the proxy:
42
81
 
43
82
  ```jsonc
44
83
  // before:
@@ -54,35 +93,5 @@ to route the command through the proxy:
54
93
 
55
94
  `mcpfy-proxy` becomes the process your client spawns. It spawns the real command as its
56
95
  own child, sits in that child's stdin/stdout, and forwards every byte unchanged while
57
- classifying JSON-RPC messages on the side. Works for any language Python, Go, Rust,
58
- anything since it only ever reads newline-delimited JSON off a pipe.
59
-
60
- ## What gets sent
61
-
62
- One event per completed request:
63
-
64
- ```json
65
- {
66
- "method": "tools/call",
67
- "toolName": "create_pull_request",
68
- "argsBytes": 312,
69
- "resultBytes": 1024,
70
- "durationMs": 1840,
71
- "outcome": "ok",
72
- "timestamp": "2026-08-06T10:23:15.000Z"
73
- }
74
- ```
75
-
76
- Batched and POSTed every 5 seconds (or every 500 events, whichever comes first) to
77
- `MCPFY_TELEMETRY_ENDPOINT` (defaults to the MCPFY ingest URL). If the request fails —
78
- including "the endpoint doesn't exist yet," which is currently true, see the master
79
- plan §4/§7 — the batch is silently dropped. Telemetry never throws, never retries
80
- indefinitely, and never delays or blocks the actual MCP traffic it's observing.
81
-
82
- ## Environment variables
83
-
84
- | Variable | Purpose |
85
- |---|---|
86
- | `MCPFY_API_KEY` | Required for any telemetry to be sent. Unset = no-op everywhere. |
87
- | `MCPFY_TELEMETRY_ENDPOINT` | Override the ingest URL (e.g. for local testing). |
88
- | `MCPFY_GATEWAY` | Set internally by MCP-backend for gateway-routed servers, which already log through `McpGatewayLogger` — `mcpfy-sdk` skips wrapping when this is set, to avoid double-counting. Not something you set yourself. |
96
+ classifying JSON-RPC messages on the side. Works for any language - Python, Go, Rust,
97
+ anything - since it only ever reads newline-delimited JSON off a pipe.
@@ -18,13 +18,56 @@ function resolveConfig(options) {
18
18
 
19
19
  // src/core/classify.ts
20
20
  function byteLength(value) {
21
- if (value === void 0) return 0;
21
+ if (value === void 0 || value === null) return 0;
22
22
  try {
23
23
  return Buffer.byteLength(JSON.stringify(value));
24
24
  } catch {
25
25
  return 0;
26
26
  }
27
27
  }
28
+ function extractDeclaredTools(tools) {
29
+ if (!Array.isArray(tools)) return void 0;
30
+ const declared = [];
31
+ for (const tool of tools) {
32
+ if (!tool || typeof tool.name !== "string") continue;
33
+ const description = typeof tool.description === "string" ? tool.description : "";
34
+ const properties = tool.inputSchema?.properties;
35
+ const paramNames = properties && typeof properties === "object" ? Object.keys(properties) : [];
36
+ const paramsWithDescriptionCount = paramNames.filter(
37
+ (p) => typeof properties[p]?.description === "string" && properties[p].description.trim().length > 0
38
+ ).length;
39
+ const annotations = tool.annotations && typeof tool.annotations === "object" ? tool.annotations : void 0;
40
+ declared.push({
41
+ name: tool.name,
42
+ hasDescription: description.trim().length > 0,
43
+ descriptionLength: description.length,
44
+ paramCount: paramNames.length,
45
+ paramsWithDescriptionCount,
46
+ hasOutputSchema: tool.outputSchema != null && typeof tool.outputSchema === "object",
47
+ readOnlyHint: typeof annotations?.readOnlyHint === "boolean" ? annotations.readOnlyHint : void 0,
48
+ destructiveHint: typeof annotations?.destructiveHint === "boolean" ? annotations.destructiveHint : void 0,
49
+ idempotentHint: typeof annotations?.idempotentHint === "boolean" ? annotations.idempotentHint : void 0,
50
+ openWorldHint: typeof annotations?.openWorldHint === "boolean" ? annotations.openWorldHint : void 0
51
+ });
52
+ }
53
+ return declared;
54
+ }
55
+ function extractCapabilities(capabilities) {
56
+ if (!capabilities || typeof capabilities !== "object") return void 0;
57
+ const caps = capabilities;
58
+ const out = [];
59
+ const GROUPS = ["sampling", "elicitation", "roots", "tasks", "logging", "prompts", "resources", "tools", "completions"];
60
+ for (const group of GROUPS) {
61
+ const value = caps[group];
62
+ if (value === void 0 || value === null) continue;
63
+ out.push(group);
64
+ if (typeof value === "object") {
65
+ if (value.listChanged === true) out.push(`${group}.listChanged`);
66
+ if (group === "resources" && value.subscribe === true) out.push(`${group}.subscribe`);
67
+ }
68
+ }
69
+ return out.length > 0 ? out : void 0;
70
+ }
28
71
  function extractLabel(method, params) {
29
72
  switch (method) {
30
73
  case "tools/call":
@@ -33,36 +76,98 @@ function extractLabel(method, params) {
33
76
  return { promptName: params?.name };
34
77
  case "resources/read":
35
78
  return { resourceUri: params?.uri };
36
- case "initialize":
37
- return {
79
+ case "initialize": {
80
+ const label = {
38
81
  clientName: params?.clientInfo?.name,
39
82
  clientVersion: params?.clientInfo?.version,
40
83
  protocolVersion: params?.protocolVersion
41
84
  };
85
+ const clientCapabilities = extractCapabilities(params?.capabilities);
86
+ if (clientCapabilities) label.clientCapabilities = clientCapabilities;
87
+ return label;
88
+ }
42
89
  default:
43
90
  return {};
44
91
  }
45
92
  }
46
93
  var MessageClassifier = class {
47
94
  pending = /* @__PURE__ */ new Map();
95
+ pendingByToken = /* @__PURE__ */ new Map();
96
+ forgetPending(id, pending) {
97
+ this.pending.delete(id);
98
+ if (pending.progressToken !== void 0) this.pendingByToken.delete(pending.progressToken);
99
+ }
100
+ /**
101
+ * Returns an event only for the one incoming case that completes a request without
102
+ * ever seeing a response: a client cancelling its own still-pending request. Every
103
+ * other incoming message (ordinary requests, `notifications/initialized`, and any
104
+ * other notification) returns `undefined`, matching prior behavior exactly.
105
+ */
48
106
  onIncoming(message) {
49
- if (!message || typeof message !== "object") return;
107
+ if (!message || typeof message !== "object") return void 0;
50
108
  const { id, method, params } = message;
51
- if (id === void 0 || !method) return;
52
- this.pending.set(id, {
109
+ if (id === void 0 && method === "notifications/cancelled") {
110
+ const requestId = params?.requestId;
111
+ if (requestId === void 0) return void 0;
112
+ const pending = this.pending.get(requestId);
113
+ if (!pending) return void 0;
114
+ this.forgetPending(requestId, pending);
115
+ return {
116
+ type: "request",
117
+ method: pending.method,
118
+ argsBytes: pending.argsBytes,
119
+ durationMs: Date.now() - pending.startedAt,
120
+ outcome: "cancelled",
121
+ progressUpdateCount: pending.progressCount,
122
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
123
+ ...pending.extra
124
+ };
125
+ }
126
+ if (id === void 0 || !method) return void 0;
127
+ const progressToken = params?._meta?.progressToken;
128
+ const entry = {
53
129
  method,
54
130
  startedAt: Date.now(),
55
131
  argsBytes: byteLength(params),
56
- extra: extractLabel(method, params)
57
- });
132
+ extra: extractLabel(method, params),
133
+ progressCount: 0
134
+ };
135
+ if (typeof progressToken === "string" || typeof progressToken === "number") {
136
+ entry.progressToken = String(progressToken);
137
+ this.pendingByToken.set(entry.progressToken, id);
138
+ }
139
+ this.pending.set(id, entry);
140
+ return void 0;
58
141
  }
59
142
  onOutgoing(message) {
60
143
  if (!message || typeof message !== "object") return void 0;
61
- const { id, method, result, error } = message;
144
+ const { id, method, params, result, error } = message;
145
+ if (method && id === void 0) {
146
+ if (method === "notifications/progress") {
147
+ const pendingId = this.pendingByToken.get(String(params?.progressToken));
148
+ if (pendingId !== void 0) {
149
+ const pending2 = this.pending.get(pendingId);
150
+ if (pending2) pending2.progressCount += 1;
151
+ }
152
+ return void 0;
153
+ }
154
+ if (method === "notifications/message") {
155
+ return {
156
+ type: "notification",
157
+ method,
158
+ logLevel: typeof params?.level === "string" ? params.level : void 0,
159
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
160
+ };
161
+ }
162
+ if (method === "notifications/tools/list_changed" || method === "notifications/resources/list_changed" || method === "notifications/prompts/list_changed") {
163
+ return { type: "notification", method, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
164
+ }
165
+ return void 0;
166
+ }
62
167
  if (method || id === void 0) return void 0;
63
168
  const pending = this.pending.get(id);
64
169
  if (!pending) return void 0;
65
- this.pending.delete(id);
170
+ this.forgetPending(id, pending);
66
171
  const event = {
67
172
  type: "request",
68
173
  method: pending.method,
@@ -71,16 +176,25 @@ var MessageClassifier = class {
71
176
  durationMs: Date.now() - pending.startedAt,
72
177
  outcome: error ? "error" : "ok",
73
178
  errorCode: error?.code,
179
+ progressUpdateCount: pending.progressCount,
74
180
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
75
181
  ...pending.extra
76
182
  };
77
- if (pending.method === "initialize" && result?.serverInfo) {
78
- event.serverName = result.serverInfo.name;
79
- event.serverVersion = result.serverInfo.version;
183
+ if (pending.method === "initialize" && !error) {
184
+ if (result?.serverInfo) {
185
+ event.serverName = result.serverInfo.name;
186
+ event.serverVersion = result.serverInfo.version;
187
+ }
188
+ const serverCapabilities = extractCapabilities(result?.capabilities);
189
+ if (serverCapabilities) event.serverCapabilities = serverCapabilities;
80
190
  }
81
191
  if (pending.method === "tools/call" && !error && result?.isError === true) {
82
192
  event.resultIsError = true;
83
193
  }
194
+ if (pending.method === "tools/list" && !error) {
195
+ const declaredTools = extractDeclaredTools(result?.tools);
196
+ if (declaredTools) event.declaredTools = declaredTools;
197
+ }
84
198
  return event;
85
199
  }
86
200
  };
@@ -184,7 +298,9 @@ async function runProxy(argv) {
184
298
  });
185
299
  pipeLines(process.stdin, child.stdin, (line) => {
186
300
  const message = tryParse(line);
187
- if (message) classifier.onIncoming(message);
301
+ if (!message) return;
302
+ const event = classifier.onIncoming(message);
303
+ if (event && batcher) batcher.push(event);
188
304
  });
189
305
  pipeLines(child.stdout, process.stdout, (line) => {
190
306
  const message = tryParse(line);
@@ -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 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
+ {"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"]}
@@ -4,13 +4,29 @@ import type { TelemetryEvent } from "../types.js";
4
4
  * (onmessage = incoming, send = outgoing) and emits one event per completed
5
5
  * request. Only method names, byte counts, timing, and outcome are captured —
6
6
  * argument values and result content are never read beyond their byte length.
7
+ * The one exception is `tools/list`, where per-tool description/param *presence
8
+ * and length* are captured (never the description text or schema) — see
9
+ * `extractDeclaredTools`.
7
10
  *
8
- * Server-initiated requests/notifications (sampling, elicitation, logging,
9
- * progress) are intentionally not tracked yet out of scope for this pass.
11
+ * Also tracks a handful of notification types now: `notifications/cancelled` (turns
12
+ * into a completed event with `outcome:"cancelled"`, since the original request never
13
+ * gets a response), `notifications/progress` (counted per request, never the progress
14
+ * values themselves), and `notifications/message` plus the three list_changed
15
+ * notifications (emitted as standalone `type:"notification"` events). Everything else server-initiated
16
+ * (sampling, elicitation, roots) is still intentionally not tracked — out of scope for
17
+ * this pass.
10
18
  */
11
19
  export declare class MessageClassifier {
12
20
  private pending;
13
- onIncoming(message: any): void;
21
+ private pendingByToken;
22
+ private forgetPending;
23
+ /**
24
+ * Returns an event only for the one incoming case that completes a request without
25
+ * ever seeing a response: a client cancelling its own still-pending request. Every
26
+ * other incoming message (ordinary requests, `notifications/initialized`, and any
27
+ * other notification) returns `undefined`, matching prior behavior exactly.
28
+ */
29
+ onIncoming(message: any): TelemetryEvent | undefined;
14
30
  onOutgoing(message: any): TelemetryEvent | undefined;
15
31
  }
16
32
  //# sourceMappingURL=classify.d.ts.map
@@ -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;CAgCrD"}
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../../src/core/classify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,cAAc,EAAE,MAAM,aAAa,CAAC;AA0GpE;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAA4C;IAC3D,OAAO,CAAC,cAAc,CAAsC;IAE5D,OAAO,CAAC,aAAa;IAKrB;;;;;OAKG;IACH,UAAU,CAAC,OAAO,EAAE,GAAG,GAAG,cAAc,GAAG,SAAS;IAyCpD,UAAU,CAAC,OAAO,EAAE,GAAG,GAAG,cAAc,GAAG,SAAS;CAuErD"}
@@ -39,13 +39,56 @@ function resolveConfig(options) {
39
39
 
40
40
  // src/core/classify.ts
41
41
  function byteLength(value) {
42
- if (value === void 0) return 0;
42
+ if (value === void 0 || value === null) return 0;
43
43
  try {
44
44
  return Buffer.byteLength(JSON.stringify(value));
45
45
  } catch {
46
46
  return 0;
47
47
  }
48
48
  }
49
+ function extractDeclaredTools(tools) {
50
+ if (!Array.isArray(tools)) return void 0;
51
+ const declared = [];
52
+ for (const tool of tools) {
53
+ if (!tool || typeof tool.name !== "string") continue;
54
+ const description = typeof tool.description === "string" ? tool.description : "";
55
+ const properties = tool.inputSchema?.properties;
56
+ const paramNames = properties && typeof properties === "object" ? Object.keys(properties) : [];
57
+ const paramsWithDescriptionCount = paramNames.filter(
58
+ (p) => typeof properties[p]?.description === "string" && properties[p].description.trim().length > 0
59
+ ).length;
60
+ const annotations = tool.annotations && typeof tool.annotations === "object" ? tool.annotations : void 0;
61
+ declared.push({
62
+ name: tool.name,
63
+ hasDescription: description.trim().length > 0,
64
+ descriptionLength: description.length,
65
+ paramCount: paramNames.length,
66
+ paramsWithDescriptionCount,
67
+ hasOutputSchema: tool.outputSchema != null && typeof tool.outputSchema === "object",
68
+ readOnlyHint: typeof annotations?.readOnlyHint === "boolean" ? annotations.readOnlyHint : void 0,
69
+ destructiveHint: typeof annotations?.destructiveHint === "boolean" ? annotations.destructiveHint : void 0,
70
+ idempotentHint: typeof annotations?.idempotentHint === "boolean" ? annotations.idempotentHint : void 0,
71
+ openWorldHint: typeof annotations?.openWorldHint === "boolean" ? annotations.openWorldHint : void 0
72
+ });
73
+ }
74
+ return declared;
75
+ }
76
+ function extractCapabilities(capabilities) {
77
+ if (!capabilities || typeof capabilities !== "object") return void 0;
78
+ const caps = capabilities;
79
+ const out = [];
80
+ const GROUPS = ["sampling", "elicitation", "roots", "tasks", "logging", "prompts", "resources", "tools", "completions"];
81
+ for (const group of GROUPS) {
82
+ const value = caps[group];
83
+ if (value === void 0 || value === null) continue;
84
+ out.push(group);
85
+ if (typeof value === "object") {
86
+ if (value.listChanged === true) out.push(`${group}.listChanged`);
87
+ if (group === "resources" && value.subscribe === true) out.push(`${group}.subscribe`);
88
+ }
89
+ }
90
+ return out.length > 0 ? out : void 0;
91
+ }
49
92
  function extractLabel(method, params) {
50
93
  switch (method) {
51
94
  case "tools/call":
@@ -54,36 +97,98 @@ function extractLabel(method, params) {
54
97
  return { promptName: params?.name };
55
98
  case "resources/read":
56
99
  return { resourceUri: params?.uri };
57
- case "initialize":
58
- return {
100
+ case "initialize": {
101
+ const label = {
59
102
  clientName: params?.clientInfo?.name,
60
103
  clientVersion: params?.clientInfo?.version,
61
104
  protocolVersion: params?.protocolVersion
62
105
  };
106
+ const clientCapabilities = extractCapabilities(params?.capabilities);
107
+ if (clientCapabilities) label.clientCapabilities = clientCapabilities;
108
+ return label;
109
+ }
63
110
  default:
64
111
  return {};
65
112
  }
66
113
  }
67
114
  var MessageClassifier = class {
68
115
  pending = /* @__PURE__ */ new Map();
116
+ pendingByToken = /* @__PURE__ */ new Map();
117
+ forgetPending(id, pending) {
118
+ this.pending.delete(id);
119
+ if (pending.progressToken !== void 0) this.pendingByToken.delete(pending.progressToken);
120
+ }
121
+ /**
122
+ * Returns an event only for the one incoming case that completes a request without
123
+ * ever seeing a response: a client cancelling its own still-pending request. Every
124
+ * other incoming message (ordinary requests, `notifications/initialized`, and any
125
+ * other notification) returns `undefined`, matching prior behavior exactly.
126
+ */
69
127
  onIncoming(message) {
70
- if (!message || typeof message !== "object") return;
128
+ if (!message || typeof message !== "object") return void 0;
71
129
  const { id, method, params } = message;
72
- if (id === void 0 || !method) return;
73
- this.pending.set(id, {
130
+ if (id === void 0 && method === "notifications/cancelled") {
131
+ const requestId = params?.requestId;
132
+ if (requestId === void 0) return void 0;
133
+ const pending = this.pending.get(requestId);
134
+ if (!pending) return void 0;
135
+ this.forgetPending(requestId, pending);
136
+ return {
137
+ type: "request",
138
+ method: pending.method,
139
+ argsBytes: pending.argsBytes,
140
+ durationMs: Date.now() - pending.startedAt,
141
+ outcome: "cancelled",
142
+ progressUpdateCount: pending.progressCount,
143
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
144
+ ...pending.extra
145
+ };
146
+ }
147
+ if (id === void 0 || !method) return void 0;
148
+ const progressToken = params?._meta?.progressToken;
149
+ const entry = {
74
150
  method,
75
151
  startedAt: Date.now(),
76
152
  argsBytes: byteLength(params),
77
- extra: extractLabel(method, params)
78
- });
153
+ extra: extractLabel(method, params),
154
+ progressCount: 0
155
+ };
156
+ if (typeof progressToken === "string" || typeof progressToken === "number") {
157
+ entry.progressToken = String(progressToken);
158
+ this.pendingByToken.set(entry.progressToken, id);
159
+ }
160
+ this.pending.set(id, entry);
161
+ return void 0;
79
162
  }
80
163
  onOutgoing(message) {
81
164
  if (!message || typeof message !== "object") return void 0;
82
- const { id, method, result, error } = message;
165
+ const { id, method, params, result, error } = message;
166
+ if (method && id === void 0) {
167
+ if (method === "notifications/progress") {
168
+ const pendingId = this.pendingByToken.get(String(params?.progressToken));
169
+ if (pendingId !== void 0) {
170
+ const pending2 = this.pending.get(pendingId);
171
+ if (pending2) pending2.progressCount += 1;
172
+ }
173
+ return void 0;
174
+ }
175
+ if (method === "notifications/message") {
176
+ return {
177
+ type: "notification",
178
+ method,
179
+ logLevel: typeof params?.level === "string" ? params.level : void 0,
180
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
181
+ };
182
+ }
183
+ if (method === "notifications/tools/list_changed" || method === "notifications/resources/list_changed" || method === "notifications/prompts/list_changed") {
184
+ return { type: "notification", method, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
185
+ }
186
+ return void 0;
187
+ }
83
188
  if (method || id === void 0) return void 0;
84
189
  const pending = this.pending.get(id);
85
190
  if (!pending) return void 0;
86
- this.pending.delete(id);
191
+ this.forgetPending(id, pending);
87
192
  const event = {
88
193
  type: "request",
89
194
  method: pending.method,
@@ -92,16 +197,25 @@ var MessageClassifier = class {
92
197
  durationMs: Date.now() - pending.startedAt,
93
198
  outcome: error ? "error" : "ok",
94
199
  errorCode: error?.code,
200
+ progressUpdateCount: pending.progressCount,
95
201
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
96
202
  ...pending.extra
97
203
  };
98
- if (pending.method === "initialize" && result?.serverInfo) {
99
- event.serverName = result.serverInfo.name;
100
- event.serverVersion = result.serverInfo.version;
204
+ if (pending.method === "initialize" && !error) {
205
+ if (result?.serverInfo) {
206
+ event.serverName = result.serverInfo.name;
207
+ event.serverVersion = result.serverInfo.version;
208
+ }
209
+ const serverCapabilities = extractCapabilities(result?.capabilities);
210
+ if (serverCapabilities) event.serverCapabilities = serverCapabilities;
101
211
  }
102
212
  if (pending.method === "tools/call" && !error && result?.isError === true) {
103
213
  event.resultIsError = true;
104
214
  }
215
+ if (pending.method === "tools/list" && !error) {
216
+ const declaredTools = extractDeclaredTools(result?.tools);
217
+ if (declaredTools) event.declaredTools = declaredTools;
218
+ }
105
219
  return event;
106
220
  }
107
221
  };
@@ -201,7 +315,8 @@ function withMcpfyTelemetry(transport, options) {
201
315
  },
202
316
  set onmessage(handler) {
203
317
  transport.onmessage = (message, extra) => {
204
- classifier.onIncoming(message);
318
+ const event = classifier.onIncoming(message);
319
+ if (event) batcher.push(event);
205
320
  handler?.(message, extra);
206
321
  };
207
322
  }
@@ -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 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":[]}
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"]}
package/dist/src/index.js CHANGED
@@ -13,13 +13,56 @@ function resolveConfig(options) {
13
13
 
14
14
  // src/core/classify.ts
15
15
  function byteLength(value) {
16
- if (value === void 0) return 0;
16
+ if (value === void 0 || value === null) return 0;
17
17
  try {
18
18
  return Buffer.byteLength(JSON.stringify(value));
19
19
  } catch {
20
20
  return 0;
21
21
  }
22
22
  }
23
+ function extractDeclaredTools(tools) {
24
+ if (!Array.isArray(tools)) return void 0;
25
+ const declared = [];
26
+ for (const tool of tools) {
27
+ if (!tool || typeof tool.name !== "string") continue;
28
+ const description = typeof tool.description === "string" ? tool.description : "";
29
+ const properties = tool.inputSchema?.properties;
30
+ const paramNames = properties && typeof properties === "object" ? Object.keys(properties) : [];
31
+ const paramsWithDescriptionCount = paramNames.filter(
32
+ (p) => typeof properties[p]?.description === "string" && properties[p].description.trim().length > 0
33
+ ).length;
34
+ const annotations = tool.annotations && typeof tool.annotations === "object" ? tool.annotations : void 0;
35
+ declared.push({
36
+ name: tool.name,
37
+ hasDescription: description.trim().length > 0,
38
+ descriptionLength: description.length,
39
+ paramCount: paramNames.length,
40
+ paramsWithDescriptionCount,
41
+ hasOutputSchema: tool.outputSchema != null && typeof tool.outputSchema === "object",
42
+ readOnlyHint: typeof annotations?.readOnlyHint === "boolean" ? annotations.readOnlyHint : void 0,
43
+ destructiveHint: typeof annotations?.destructiveHint === "boolean" ? annotations.destructiveHint : void 0,
44
+ idempotentHint: typeof annotations?.idempotentHint === "boolean" ? annotations.idempotentHint : void 0,
45
+ openWorldHint: typeof annotations?.openWorldHint === "boolean" ? annotations.openWorldHint : void 0
46
+ });
47
+ }
48
+ return declared;
49
+ }
50
+ function extractCapabilities(capabilities) {
51
+ if (!capabilities || typeof capabilities !== "object") return void 0;
52
+ const caps = capabilities;
53
+ const out = [];
54
+ const GROUPS = ["sampling", "elicitation", "roots", "tasks", "logging", "prompts", "resources", "tools", "completions"];
55
+ for (const group of GROUPS) {
56
+ const value = caps[group];
57
+ if (value === void 0 || value === null) continue;
58
+ out.push(group);
59
+ if (typeof value === "object") {
60
+ if (value.listChanged === true) out.push(`${group}.listChanged`);
61
+ if (group === "resources" && value.subscribe === true) out.push(`${group}.subscribe`);
62
+ }
63
+ }
64
+ return out.length > 0 ? out : void 0;
65
+ }
23
66
  function extractLabel(method, params) {
24
67
  switch (method) {
25
68
  case "tools/call":
@@ -28,36 +71,98 @@ function extractLabel(method, params) {
28
71
  return { promptName: params?.name };
29
72
  case "resources/read":
30
73
  return { resourceUri: params?.uri };
31
- case "initialize":
32
- return {
74
+ case "initialize": {
75
+ const label = {
33
76
  clientName: params?.clientInfo?.name,
34
77
  clientVersion: params?.clientInfo?.version,
35
78
  protocolVersion: params?.protocolVersion
36
79
  };
80
+ const clientCapabilities = extractCapabilities(params?.capabilities);
81
+ if (clientCapabilities) label.clientCapabilities = clientCapabilities;
82
+ return label;
83
+ }
37
84
  default:
38
85
  return {};
39
86
  }
40
87
  }
41
88
  var MessageClassifier = class {
42
89
  pending = /* @__PURE__ */ new Map();
90
+ pendingByToken = /* @__PURE__ */ new Map();
91
+ forgetPending(id, pending) {
92
+ this.pending.delete(id);
93
+ if (pending.progressToken !== void 0) this.pendingByToken.delete(pending.progressToken);
94
+ }
95
+ /**
96
+ * Returns an event only for the one incoming case that completes a request without
97
+ * ever seeing a response: a client cancelling its own still-pending request. Every
98
+ * other incoming message (ordinary requests, `notifications/initialized`, and any
99
+ * other notification) returns `undefined`, matching prior behavior exactly.
100
+ */
43
101
  onIncoming(message) {
44
- if (!message || typeof message !== "object") return;
102
+ if (!message || typeof message !== "object") return void 0;
45
103
  const { id, method, params } = message;
46
- if (id === void 0 || !method) return;
47
- this.pending.set(id, {
104
+ if (id === void 0 && method === "notifications/cancelled") {
105
+ const requestId = params?.requestId;
106
+ if (requestId === void 0) return void 0;
107
+ const pending = this.pending.get(requestId);
108
+ if (!pending) return void 0;
109
+ this.forgetPending(requestId, pending);
110
+ return {
111
+ type: "request",
112
+ method: pending.method,
113
+ argsBytes: pending.argsBytes,
114
+ durationMs: Date.now() - pending.startedAt,
115
+ outcome: "cancelled",
116
+ progressUpdateCount: pending.progressCount,
117
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
118
+ ...pending.extra
119
+ };
120
+ }
121
+ if (id === void 0 || !method) return void 0;
122
+ const progressToken = params?._meta?.progressToken;
123
+ const entry = {
48
124
  method,
49
125
  startedAt: Date.now(),
50
126
  argsBytes: byteLength(params),
51
- extra: extractLabel(method, params)
52
- });
127
+ extra: extractLabel(method, params),
128
+ progressCount: 0
129
+ };
130
+ if (typeof progressToken === "string" || typeof progressToken === "number") {
131
+ entry.progressToken = String(progressToken);
132
+ this.pendingByToken.set(entry.progressToken, id);
133
+ }
134
+ this.pending.set(id, entry);
135
+ return void 0;
53
136
  }
54
137
  onOutgoing(message) {
55
138
  if (!message || typeof message !== "object") return void 0;
56
- const { id, method, result, error } = message;
139
+ const { id, method, params, result, error } = message;
140
+ if (method && id === void 0) {
141
+ if (method === "notifications/progress") {
142
+ const pendingId = this.pendingByToken.get(String(params?.progressToken));
143
+ if (pendingId !== void 0) {
144
+ const pending2 = this.pending.get(pendingId);
145
+ if (pending2) pending2.progressCount += 1;
146
+ }
147
+ return void 0;
148
+ }
149
+ if (method === "notifications/message") {
150
+ return {
151
+ type: "notification",
152
+ method,
153
+ logLevel: typeof params?.level === "string" ? params.level : void 0,
154
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
155
+ };
156
+ }
157
+ if (method === "notifications/tools/list_changed" || method === "notifications/resources/list_changed" || method === "notifications/prompts/list_changed") {
158
+ return { type: "notification", method, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
159
+ }
160
+ return void 0;
161
+ }
57
162
  if (method || id === void 0) return void 0;
58
163
  const pending = this.pending.get(id);
59
164
  if (!pending) return void 0;
60
- this.pending.delete(id);
165
+ this.forgetPending(id, pending);
61
166
  const event = {
62
167
  type: "request",
63
168
  method: pending.method,
@@ -66,16 +171,25 @@ var MessageClassifier = class {
66
171
  durationMs: Date.now() - pending.startedAt,
67
172
  outcome: error ? "error" : "ok",
68
173
  errorCode: error?.code,
174
+ progressUpdateCount: pending.progressCount,
69
175
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
70
176
  ...pending.extra
71
177
  };
72
- if (pending.method === "initialize" && result?.serverInfo) {
73
- event.serverName = result.serverInfo.name;
74
- event.serverVersion = result.serverInfo.version;
178
+ if (pending.method === "initialize" && !error) {
179
+ if (result?.serverInfo) {
180
+ event.serverName = result.serverInfo.name;
181
+ event.serverVersion = result.serverInfo.version;
182
+ }
183
+ const serverCapabilities = extractCapabilities(result?.capabilities);
184
+ if (serverCapabilities) event.serverCapabilities = serverCapabilities;
75
185
  }
76
186
  if (pending.method === "tools/call" && !error && result?.isError === true) {
77
187
  event.resultIsError = true;
78
188
  }
189
+ if (pending.method === "tools/list" && !error) {
190
+ const declaredTools = extractDeclaredTools(result?.tools);
191
+ if (declaredTools) event.declaredTools = declaredTools;
192
+ }
79
193
  return event;
80
194
  }
81
195
  };
@@ -175,7 +289,8 @@ function withMcpfyTelemetry(transport, options) {
175
289
  },
176
290
  set onmessage(handler) {
177
291
  transport.onmessage = (message, extra) => {
178
- classifier.onIncoming(message);
292
+ const event = classifier.onIncoming(message);
293
+ if (event) batcher.push(event);
179
294
  handler?.(message, extra);
180
295
  };
181
296
  }
@@ -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 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":[]}
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 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/proxy/run.ts"],"names":[],"mappings":"AAmCA;;;;;;;;GAQG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5D"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/proxy/run.ts"],"names":[],"mappings":"AAmCA;;;;;;;;GAQG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAyD5D"}
@@ -15,6 +15,31 @@ export interface MinimalTransport {
15
15
  }
16
16
  export type SdkName = "mcpfy-sdk" | "@modelcontextprotocol/sdk" | "unknown";
17
17
  export type InstallMode = "sdk-env" | "sdk-wrapper" | "stdio-proxy";
18
+ /**
19
+ * One declared tool from a `tools/list` response, reduced to lightweight metadata —
20
+ * never the description text or schema itself, same "never content" treatment as
21
+ * everything else this package captures. Powers capability-gap / orphan-tool /
22
+ * description-quality scoring server-side without ever shipping the actual text.
23
+ */
24
+ export interface DeclaredToolMeta {
25
+ name: string;
26
+ hasDescription: boolean;
27
+ descriptionLength: number;
28
+ paramCount: number;
29
+ paramsWithDescriptionCount: number;
30
+ /** Whether the tool declares `outputSchema` (structured-output support) — presence only. */
31
+ hasOutputSchema: boolean;
32
+ /**
33
+ * `Tool.annotations` hints, read as-is — `undefined` means "not declared," never
34
+ * coerced to the MCP spec's nominal defaults (e.g. `destructiveHint` defaults to
35
+ * `true` when absent per spec). Applying that default would mislabel every tool
36
+ * that simply didn't declare annotations as destructive.
37
+ */
38
+ readOnlyHint?: boolean;
39
+ destructiveHint?: boolean;
40
+ idempotentHint?: boolean;
41
+ openWorldHint?: boolean;
42
+ }
18
43
  export interface TelemetryOptions {
19
44
  /** Defaults to the MCPFY_API_KEY env var. Unset (in either form) = no-op, nothing is sent. */
20
45
  apiKey?: string;
@@ -33,7 +58,12 @@ export interface TelemetryOptions {
33
58
  * never argument values or result content. See telemetry-master-plan.md §2-3.
34
59
  */
35
60
  export interface TelemetryEvent {
36
- type: "request";
61
+ /**
62
+ * "request" = a completed (or cancelled) request/response pair. "notification" = a
63
+ * standalone, fire-and-forget message with no matching request (log messages,
64
+ * list_changed) — see `logLevel` below for the one field notification events carry.
65
+ */
66
+ type: "request" | "notification";
37
67
  method: string;
38
68
  toolName?: string;
39
69
  promptName?: string;
@@ -46,9 +76,33 @@ export interface TelemetryEvent {
46
76
  argsBytes?: number;
47
77
  resultBytes?: number;
48
78
  durationMs?: number;
49
- outcome?: "ok" | "error";
79
+ /**
80
+ * "cancelled" means the client sent `notifications/cancelled` for this request before
81
+ * a response arrived — `durationMs` then measures time-to-cancel, not completion time.
82
+ */
83
+ outcome?: "ok" | "error" | "cancelled";
50
84
  errorCode?: number;
51
85
  timestamp: string;
86
+ /**
87
+ * Count of `notifications/progress` messages correlated to this request via its
88
+ * `_meta.progressToken`, before it completed. Always present (default 0) on completed
89
+ * request events — a tool-hygiene signal ("does this tool report progress"), not the
90
+ * progress values themselves (never captured).
91
+ */
92
+ progressUpdateCount?: number;
93
+ /**
94
+ * `type:"notification"`, `method:"notifications/message"` only — the log level
95
+ * (MCP's `LoggingLevel`). Never `params.data` (arbitrary log payload) or the logger name.
96
+ */
97
+ logLevel?: string;
98
+ /**
99
+ * `initialize` only: which optional capabilities each side declared, as a flat list of
100
+ * dotted paths (e.g. `["resources.subscribe", "tools.listChanged", "logging"]`) —
101
+ * group-presence for simple capabilities, a dotted sub-flag for the handful with
102
+ * meaningful booleans. `experimental` capabilities are never included.
103
+ */
104
+ clientCapabilities?: string[];
105
+ serverCapabilities?: string[];
52
106
  /**
53
107
  * `tools/call` only: the result's own `isError` flag (MCP's CallToolResult.isError) —
54
108
  * a tool that ran fine at the JSON-RPC level (outcome:"ok") but reports its own
@@ -57,5 +111,10 @@ export interface TelemetryEvent {
57
111
  * result content itself.
58
112
  */
59
113
  resultIsError?: boolean;
114
+ /**
115
+ * `tools/list` only: the declared tool catalog, reduced to non-content metadata.
116
+ * See `DeclaredToolMeta`.
117
+ */
118
+ declaredTools?: DeclaredToolMeta[];
60
119
  }
61
120
  //# sourceMappingURL=types.d.ts.map
@@ -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;IAClB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
@@ -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,CAmD1G"}
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,CAoD1G"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcpfy-pulse",
3
3
  "type": "module",
4
- "version": "0.1.2",
4
+ "version": "0.1.3",
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",