mcpfy-pulse 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -45
- package/dist/bin/mcpfy-proxy.js +130 -14
- package/dist/bin/mcpfy-proxy.js.map +1 -1
- package/dist/src/core/batcher.d.ts +21 -0
- package/dist/src/core/batcher.d.ts.map +1 -1
- package/dist/src/core/classify.d.ts +19 -3
- package/dist/src/core/classify.d.ts.map +1 -1
- package/dist/src/index.cjs +152 -14
- package/dist/src/index.cjs.map +1 -1
- package/dist/src/index.js +152 -14
- package/dist/src/index.js.map +1 -1
- package/dist/src/proxy/run.d.ts.map +1 -1
- package/dist/src/types.d.ts +61 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/wrap-transport.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,25 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
<pre>
|
|
4
|
+
███╗ ███╗ ██████╗██████╗ ███████╗██╗ ██╗ ██████╗ ██╗ ██╗██╗ ███████╗███████╗
|
|
5
|
+
████╗ ████║██╔════╝██╔══██╗██╔════╝╚██╗ ██╔╝ ██╔══██╗██║ ██║██║ ██╔════╝██╔════╝
|
|
6
|
+
██╔████╔██║██║ ██████╔╝█████╗ ╚████╔╝ ██████╔╝██║ ██║██║ ███████╗█████╗
|
|
7
|
+
██║╚██╔╝██║██║ ██╔═══╝ ██╔══╝ ╚██╔╝ ██╔═══╝ ██║ ██║██║ ╚════██║██╔══╝
|
|
8
|
+
██║ ╚═╝ ██║╚██████╗██║ ██║ ██║ ██║ ╚██████╔╝███████╗███████║███████╗
|
|
9
|
+
╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝
|
|
10
|
+
</pre>
|
|
7
11
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
58
|
-
anything
|
|
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.
|
package/dist/bin/mcpfy-proxy.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
52
|
-
|
|
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.
|
|
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" &&
|
|
78
|
-
|
|
79
|
-
|
|
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)
|
|
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\nconst SHUTDOWN_SIGNALS: NodeJS.Signals[] = [\"SIGTERM\", \"SIGINT\"];\nconst SHUTDOWN_FLUSH_TIMEOUT_MS = 3000;\n\n/**\n * Installs a best-effort \"flush before we die\" hook for a batcher that has no\n * other lifecycle event to hang a flush off of. `withMcpfyTelemetry` wraps a\n * transport inside the *same process* as the server's own code — unlike the stdio\n * proxy, there's no child process it can wait on and forward signals to. Without\n * this, a real MCP session shorter than the flush interval loses every event, via\n * two independent paths, both confirmed against real client/server sessions:\n *\n * - A real MCP client's normal graceful shutdown (close stdin, wait, escalate to\n * SIGTERM if the process is still alive) kills the process via an unhandled\n * signal — instant, no `exit` event, no flush.\n * - Some server setups (e.g. the low-level `Server` class, as opposed to\n * `McpServer`) simply exit *naturally* on stdin EOF — no signal at all, just\n * Node draining an event loop with nothing left to do — which an unhandled\n * signal listener does nothing to catch.\n *\n * Both are covered here: `beforeExit` for the natural-exit case, `SIGTERM`/\n * `SIGINT` for the signal case, sharing one flush bounded by a timeout so a slow\n * or unreachable ingest endpoint can never hang process shutdown either way.\n */\nexport function installShutdownFlush(batcher: TelemetryBatcher): void {\n let flushed = false;\n const flushOnce = (): Promise<void> => {\n if (flushed) return Promise.resolve();\n flushed = true;\n const timeout = new Promise<void>((resolve) => setTimeout(resolve, SHUTDOWN_FLUSH_TIMEOUT_MS).unref());\n return Promise.race([batcher.close(), timeout]).catch(() => {});\n };\n\n // Natural exit: nothing else queued on the event loop, no signal involved.\n // Scheduling async work here (the flush) delays the actual exit until it\n // settles, without ever holding the process open when there's truly nothing\n // to flush — `flush()` itself no-ops instantly on an empty queue.\n process.once(\"beforeExit\", () => {\n void flushOnce();\n });\n\n for (const signal of SHUTDOWN_SIGNALS) {\n const handler = () => {\n // Remove ourselves before re-raising the same signal below, so that re-raise\n // falls through to the default action (or any other listener) instead of\n // looping back into this handler.\n process.removeListener(signal, handler);\n void flushOnce().finally(() => process.kill(process.pid, signal));\n };\n process.on(signal, handler);\n }\n}\n","#!/usr/bin/env node\nimport { runProxy } from \"../proxy/run.js\";\n\nrunProxy(process.argv.slice(2)).catch((err) => {\n process.stderr.write(`mcpfy-proxy: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exitCode = 1;\n});\n"],"mappings":"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","names":["pending"]}
|
|
@@ -23,4 +23,25 @@ export declare class TelemetryBatcher {
|
|
|
23
23
|
flush(): Promise<void>;
|
|
24
24
|
close(): Promise<void>;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Installs a best-effort "flush before we die" hook for a batcher that has no
|
|
28
|
+
* other lifecycle event to hang a flush off of. `withMcpfyTelemetry` wraps a
|
|
29
|
+
* transport inside the *same process* as the server's own code — unlike the stdio
|
|
30
|
+
* proxy, there's no child process it can wait on and forward signals to. Without
|
|
31
|
+
* this, a real MCP session shorter than the flush interval loses every event, via
|
|
32
|
+
* two independent paths, both confirmed against real client/server sessions:
|
|
33
|
+
*
|
|
34
|
+
* - A real MCP client's normal graceful shutdown (close stdin, wait, escalate to
|
|
35
|
+
* SIGTERM if the process is still alive) kills the process via an unhandled
|
|
36
|
+
* signal — instant, no `exit` event, no flush.
|
|
37
|
+
* - Some server setups (e.g. the low-level `Server` class, as opposed to
|
|
38
|
+
* `McpServer`) simply exit *naturally* on stdin EOF — no signal at all, just
|
|
39
|
+
* Node draining an event loop with nothing left to do — which an unhandled
|
|
40
|
+
* signal listener does nothing to catch.
|
|
41
|
+
*
|
|
42
|
+
* Both are covered here: `beforeExit` for the natural-exit case, `SIGTERM`/
|
|
43
|
+
* `SIGINT` for the signal case, sharing one flush bounded by a timeout so a slow
|
|
44
|
+
* or unreachable ingest endpoint can never hang process shutdown either way.
|
|
45
|
+
*/
|
|
46
|
+
export declare function installShutdownFlush(batcher: TelemetryBatcher): void;
|
|
26
47
|
//# sourceMappingURL=batcher.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batcher.d.ts","sourceRoot":"","sources":["../../../src/core/batcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED;;;;;GAKG;AACH,qBAAa,gBAAgB;IAKzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI;IALvB,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,KAAK,CAAiC;gBAG3B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,SAAS;IAMlC,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAK3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}
|
|
1
|
+
{"version":3,"file":"batcher.d.ts","sourceRoot":"","sources":["../../../src/core/batcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED;;;;;GAKG;AACH,qBAAa,gBAAgB;IAKzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI;IALvB,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,KAAK,CAAiC;gBAG3B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,SAAS;IAMlC,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAK3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B;AAKD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CA2BpE"}
|
|
@@ -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
|
-
*
|
|
9
|
-
*
|
|
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
|
-
|
|
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,
|
|
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"}
|