@mattral/collector 1.0.0 → 1.1.0
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 +32 -0
- package/dist/error-message.d.ts +28 -0
- package/dist/error-message.d.ts.map +1 -0
- package/dist/error-message.js +42 -0
- package/dist/error-message.js.map +1 -0
- package/dist/output-payload.d.ts +32 -0
- package/dist/output-payload.d.ts.map +1 -0
- package/dist/output-payload.js +54 -0
- package/dist/output-payload.js.map +1 -0
- package/dist/proxy-collector.d.ts +31 -0
- package/dist/proxy-collector.d.ts.map +1 -1
- package/dist/proxy-collector.js +63 -8
- package/dist/proxy-collector.js.map +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @mattral/collector
|
|
2
|
+
|
|
3
|
+
Health checking and traffic-observing proxy collectors for MCP servers —
|
|
4
|
+
the data-collection layer of [mcp-reliable](https://www.npmjs.com/package/mcp-reliable).
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @mattral/collector
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## What's in it
|
|
11
|
+
|
|
12
|
+
- **`HealthChecker`** — periodic reachability + latency checks against a
|
|
13
|
+
registered server (`initialize` handshake + `tools/list`), with a
|
|
14
|
+
configurable connection timeout.
|
|
15
|
+
- **`StdioProxyCollector`** — sits transparently between a real MCP client
|
|
16
|
+
and a real stdio server, forwarding every message unmodified while
|
|
17
|
+
recording each execution and running drift detection (schema,
|
|
18
|
+
output-shape, statistical/latency) against real traffic.
|
|
19
|
+
- **`extractOutputPayload`** / **`extractErrorMessage`** — correctly
|
|
20
|
+
unwrap MCP's `content` block convention to get at a tool's actual output
|
|
21
|
+
or error text, instead of the transport-level envelope around it.
|
|
22
|
+
|
|
23
|
+
## Learn more
|
|
24
|
+
|
|
25
|
+
See the [mcp-reliable GitHub repo](https://github.com/Mattral/mcp-reliable),
|
|
26
|
+
particularly `docs/architecture.md` for how health checks and the proxy
|
|
27
|
+
each observe a server differently, and `docs/decisions/` for the
|
|
28
|
+
reasoning behind the extraction helpers above.
|
|
29
|
+
|
|
30
|
+
## License
|
|
31
|
+
|
|
32
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { JsonRpcMessage } from "./json-rpc-framer.js";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts a human-readable error message from a raw MCP `tools/call`
|
|
4
|
+
* response, for both kinds of failure the protocol allows:
|
|
5
|
+
*
|
|
6
|
+
* - **Protocol-level failures**: the JSON-RPC response has an `error`
|
|
7
|
+
* object (e.g. a malformed request, a transport-level failure). Its
|
|
8
|
+
* `.message` is already the right text — nothing to unwrap.
|
|
9
|
+
* - **Tool-level failures**: the JSON-RPC response is a normal `result`
|
|
10
|
+
* with `isError: true`, and the actual, tool-specific error description
|
|
11
|
+
* lives in `result.content` as ordinary text blocks — this is the MCP
|
|
12
|
+
* spec's convention for a tool reporting its *own* failure without it
|
|
13
|
+
* being treated as a transport-level protocol error (this project's own
|
|
14
|
+
* reference server's `flaky` tool does exactly this).
|
|
15
|
+
*
|
|
16
|
+
* Before this function existed, every tool-level failure's real error
|
|
17
|
+
* text was discarded in favor of one generic placeholder string,
|
|
18
|
+
* regardless of what the tool actually said — the same underlying mistake
|
|
19
|
+
* as the pre-`extractOutputPayload` output-shape bug (see
|
|
20
|
+
* docs/decisions/0015-statistical-drift-detection.md): treating MCP's
|
|
21
|
+
* `content` array as opaque instead of reading what's actually inside it.
|
|
22
|
+
* See docs/decisions/0016-error-classification.md.
|
|
23
|
+
*
|
|
24
|
+
* Falls back to a generic placeholder only when there's truly no error
|
|
25
|
+
* object and no text content to describe the failure at all.
|
|
26
|
+
*/
|
|
27
|
+
export declare function extractErrorMessage(message: JsonRpcMessage): string;
|
|
28
|
+
//# sourceMappingURL=error-message.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-message.d.ts","sourceRoot":"","sources":["../src/error-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAkBnE"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts a human-readable error message from a raw MCP `tools/call`
|
|
3
|
+
* response, for both kinds of failure the protocol allows:
|
|
4
|
+
*
|
|
5
|
+
* - **Protocol-level failures**: the JSON-RPC response has an `error`
|
|
6
|
+
* object (e.g. a malformed request, a transport-level failure). Its
|
|
7
|
+
* `.message` is already the right text — nothing to unwrap.
|
|
8
|
+
* - **Tool-level failures**: the JSON-RPC response is a normal `result`
|
|
9
|
+
* with `isError: true`, and the actual, tool-specific error description
|
|
10
|
+
* lives in `result.content` as ordinary text blocks — this is the MCP
|
|
11
|
+
* spec's convention for a tool reporting its *own* failure without it
|
|
12
|
+
* being treated as a transport-level protocol error (this project's own
|
|
13
|
+
* reference server's `flaky` tool does exactly this).
|
|
14
|
+
*
|
|
15
|
+
* Before this function existed, every tool-level failure's real error
|
|
16
|
+
* text was discarded in favor of one generic placeholder string,
|
|
17
|
+
* regardless of what the tool actually said — the same underlying mistake
|
|
18
|
+
* as the pre-`extractOutputPayload` output-shape bug (see
|
|
19
|
+
* docs/decisions/0015-statistical-drift-detection.md): treating MCP's
|
|
20
|
+
* `content` array as opaque instead of reading what's actually inside it.
|
|
21
|
+
* See docs/decisions/0016-error-classification.md.
|
|
22
|
+
*
|
|
23
|
+
* Falls back to a generic placeholder only when there's truly no error
|
|
24
|
+
* object and no text content to describe the failure at all.
|
|
25
|
+
*/
|
|
26
|
+
export function extractErrorMessage(message) {
|
|
27
|
+
if (message.error?.message)
|
|
28
|
+
return message.error.message;
|
|
29
|
+
const content = message.result?.content;
|
|
30
|
+
if (Array.isArray(content)) {
|
|
31
|
+
const texts = content
|
|
32
|
+
.filter((block) => typeof block === "object" &&
|
|
33
|
+
block !== null &&
|
|
34
|
+
block.type === "text" &&
|
|
35
|
+
typeof block.text === "string")
|
|
36
|
+
.map((block) => block.text);
|
|
37
|
+
if (texts.length > 0)
|
|
38
|
+
return texts.join(" ");
|
|
39
|
+
}
|
|
40
|
+
return "Tool call returned isError: true, with no error text or content to describe why.";
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=error-message.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-message.js","sourceRoot":"","sources":["../src/error-message.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAuB;IACzD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO;QAAE,OAAO,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;IAEzD,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC;IACxC,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,OAAO;aAClB,MAAM,CACL,CAAC,KAAK,EAA2C,EAAE,CACjD,OAAO,KAAK,KAAK,QAAQ;YACzB,KAAK,KAAK,IAAI;YACb,KAA4B,CAAC,IAAI,KAAK,MAAM;YAC7C,OAAQ,KAA4B,CAAC,IAAI,KAAK,QAAQ,CACzD;aACA,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,kFAAkF,CAAC;AAC5F,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts the "meaningful" structured payload from a raw MCP
|
|
3
|
+
* `tools/call` result, for output-shape drift detection to actually be
|
|
4
|
+
* useful against it.
|
|
5
|
+
*
|
|
6
|
+
* Naively hashing `result.content` itself is close to useless for the
|
|
7
|
+
* extremely common case where a tool returns its real structured data as
|
|
8
|
+
* JSON text inside a single content block — `content: [{ type: "text",
|
|
9
|
+
* text: "{...}" }]` has the exact same `[{type, text}]` shape whether the
|
|
10
|
+
* embedded JSON is `{"message": "..."}` or `{"echoed": "...", "respondedAt":
|
|
11
|
+
* "..."}`. A structural hash of the wrapper alone would never notice the
|
|
12
|
+
* tool's actual output changed underneath it — this is exactly what this
|
|
13
|
+
* project's own reference server does (see
|
|
14
|
+
* `examples/reference-mcp-server/src/index.ts`), and what a great many
|
|
15
|
+
* real-world MCP tools do too, for any tool that doesn't declare a formal
|
|
16
|
+
* `outputSchema`. This function unwraps that, so what gets fingerprinted
|
|
17
|
+
* is the real payload, not MCP's transport-level envelope around it.
|
|
18
|
+
*
|
|
19
|
+
* Precedence:
|
|
20
|
+
* 1. `structuredContent`, if the tool declared an output schema and the
|
|
21
|
+
* SDK populated it — already a real object, nothing to unwrap.
|
|
22
|
+
* 2. A single text content block that parses as JSON — the common
|
|
23
|
+
* "structured data serialized as text" convention.
|
|
24
|
+
* 3. Otherwise, the raw `content` array as-is — still useful: catches a
|
|
25
|
+
* tool switching between text/image/resource blocks, or adding/
|
|
26
|
+
* removing blocks, even without parseable JSON inside them.
|
|
27
|
+
* 4. If there's no `content` or `structuredContent` at all, the raw
|
|
28
|
+
* result object, matching how this was computed before this function
|
|
29
|
+
* existed (see docs/decisions/0015-statistical-drift-detection.md).
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractOutputPayload(result: unknown): unknown;
|
|
32
|
+
//# sourceMappingURL=output-payload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-payload.d.ts","sourceRoot":"","sources":["../src/output-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAsB7D"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts the "meaningful" structured payload from a raw MCP
|
|
3
|
+
* `tools/call` result, for output-shape drift detection to actually be
|
|
4
|
+
* useful against it.
|
|
5
|
+
*
|
|
6
|
+
* Naively hashing `result.content` itself is close to useless for the
|
|
7
|
+
* extremely common case where a tool returns its real structured data as
|
|
8
|
+
* JSON text inside a single content block — `content: [{ type: "text",
|
|
9
|
+
* text: "{...}" }]` has the exact same `[{type, text}]` shape whether the
|
|
10
|
+
* embedded JSON is `{"message": "..."}` or `{"echoed": "...", "respondedAt":
|
|
11
|
+
* "..."}`. A structural hash of the wrapper alone would never notice the
|
|
12
|
+
* tool's actual output changed underneath it — this is exactly what this
|
|
13
|
+
* project's own reference server does (see
|
|
14
|
+
* `examples/reference-mcp-server/src/index.ts`), and what a great many
|
|
15
|
+
* real-world MCP tools do too, for any tool that doesn't declare a formal
|
|
16
|
+
* `outputSchema`. This function unwraps that, so what gets fingerprinted
|
|
17
|
+
* is the real payload, not MCP's transport-level envelope around it.
|
|
18
|
+
*
|
|
19
|
+
* Precedence:
|
|
20
|
+
* 1. `structuredContent`, if the tool declared an output schema and the
|
|
21
|
+
* SDK populated it — already a real object, nothing to unwrap.
|
|
22
|
+
* 2. A single text content block that parses as JSON — the common
|
|
23
|
+
* "structured data serialized as text" convention.
|
|
24
|
+
* 3. Otherwise, the raw `content` array as-is — still useful: catches a
|
|
25
|
+
* tool switching between text/image/resource blocks, or adding/
|
|
26
|
+
* removing blocks, even without parseable JSON inside them.
|
|
27
|
+
* 4. If there's no `content` or `structuredContent` at all, the raw
|
|
28
|
+
* result object, matching how this was computed before this function
|
|
29
|
+
* existed (see docs/decisions/0015-statistical-drift-detection.md).
|
|
30
|
+
*/
|
|
31
|
+
export function extractOutputPayload(result) {
|
|
32
|
+
if (result === null || typeof result !== "object")
|
|
33
|
+
return result;
|
|
34
|
+
const r = result;
|
|
35
|
+
if (r.structuredContent !== undefined)
|
|
36
|
+
return r.structuredContent;
|
|
37
|
+
if (Array.isArray(r.content)) {
|
|
38
|
+
if (r.content.length === 1) {
|
|
39
|
+
const block = r.content[0];
|
|
40
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(block.text);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Not JSON - a genuine plain-text result. Fall through to
|
|
46
|
+
// hashing the content array itself.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return r.content;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=output-payload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-payload.js","sourceRoot":"","sources":["../src/output-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAe;IAClD,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IACjE,MAAM,CAAC,GAAG,MAA4D,CAAC;IAEvE,IAAI,CAAC,CAAC,iBAAiB,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC,iBAAiB,CAAC;IAElE,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7B,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAqC,CAAC;YAC/D,IAAI,KAAK,EAAE,IAAI,KAAK,MAAM,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC7D,IAAI,CAAC;oBACH,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAChC,CAAC;gBAAC,MAAM,CAAC;oBACP,0DAA0D;oBAC1D,oCAAoC;gBACtC,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,CAAC,CAAC,OAAO,CAAC;IACnB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -15,6 +15,24 @@ export interface StdioProxyCollectorOptions {
|
|
|
15
15
|
input?: NodeJS.ReadableStream;
|
|
16
16
|
/** Stream carrying traffic TO the real client (defaults to this process's stdout). */
|
|
17
17
|
output?: NodeJS.WritableStream;
|
|
18
|
+
/**
|
|
19
|
+
* How many executions of the *same tool* to accumulate between
|
|
20
|
+
* window-based drift checks (output-shape and statistical/latency).
|
|
21
|
+
* Unlike schema drift (an O(1) fingerprint compare, checked on every
|
|
22
|
+
* `tools/list`), these compare whole windows of stored executions —
|
|
23
|
+
* running that on literally every single call would mean a store query
|
|
24
|
+
* plus a KS test per call on hot paths. Default 10: frequent enough that
|
|
25
|
+
* drift is caught within a handful of seconds of typical traffic, cheap
|
|
26
|
+
* enough not to matter. See docs/decisions/0015-statistical-drift-detection.md.
|
|
27
|
+
*/
|
|
28
|
+
driftCheckIntervalCalls?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Size of each of the two (baseline, recent) windows compared for
|
|
31
|
+
* window-based drift. Default 20, matching `detectLatencyDrift`'s own
|
|
32
|
+
* default `minSampleSize` — there's no point fetching a bigger window
|
|
33
|
+
* than the detector needs.
|
|
34
|
+
*/
|
|
35
|
+
driftWindowSize?: number;
|
|
18
36
|
}
|
|
19
37
|
/**
|
|
20
38
|
* A transparent stdio proxy that sits between an MCP client (an agent) and
|
|
@@ -33,9 +51,12 @@ export interface StdioProxyCollectorOptions {
|
|
|
33
51
|
*/
|
|
34
52
|
export declare class StdioProxyCollector {
|
|
35
53
|
private readonly options;
|
|
54
|
+
private readonly driftCheckIntervalCalls;
|
|
55
|
+
private readonly driftWindowSize;
|
|
36
56
|
private child;
|
|
37
57
|
private readonly pendingCalls;
|
|
38
58
|
private readonly pendingListRequests;
|
|
59
|
+
private readonly callCountByTool;
|
|
39
60
|
constructor(options: StdioProxyCollectorOptions);
|
|
40
61
|
/**
|
|
41
62
|
* Starts the child process and begins forwarding traffic. Resolves with
|
|
@@ -47,6 +68,16 @@ export declare class StdioProxyCollector {
|
|
|
47
68
|
private handleDownstreamMessage;
|
|
48
69
|
private handleUpstreamMessage;
|
|
49
70
|
private recordCallExecution;
|
|
71
|
+
private recordDrift;
|
|
72
|
+
/**
|
|
73
|
+
* Every `driftCheckIntervalCalls` calls to a given tool, pulls its last
|
|
74
|
+
* two `driftWindowSize`-execution windows and runs the window-based
|
|
75
|
+
* drift detectors (output-shape, latency) against them. Schema drift is
|
|
76
|
+
* handled separately in `recordToolsListResult` — it's a per-fingerprint
|
|
77
|
+
* compare, not a window comparison, so it runs on every `tools/list`
|
|
78
|
+
* instead of being throttled here.
|
|
79
|
+
*/
|
|
80
|
+
private maybeCheckWindowDrift;
|
|
50
81
|
private recordToolsListResult;
|
|
51
82
|
}
|
|
52
83
|
//# sourceMappingURL=proxy-collector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy-collector.d.ts","sourceRoot":"","sources":["../src/proxy-collector.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"proxy-collector.d.ts","sourceRoot":"","sources":["../src/proxy-collector.ts"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,UAAU,EACf,KAAK,SAAS,EAEf,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAKzD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,gBAAgB,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sFAAsF;IACtF,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,IAAI,CAAC;IAC7C,8DAA8D;IAC9D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B;;;;;;;;;OASG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAoBD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAS;IACjD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA2C;IACxE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA8B;IAClE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA6B;gBAEjD,OAAO,EAAE,0BAA0B;IAM/C;;;;OAIG;IACH,KAAK,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA+CzB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,qBAAqB;IAkB7B,OAAO,CAAC,mBAAmB;IA4B3B,OAAO,CAAC,WAAW;IAKnB;;;;;;;OAOG;IACH,OAAO,CAAC,qBAAqB;IAsC7B,OAAO,CAAC,qBAAqB;CAgC9B"}
|
package/dist/proxy-collector.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { computeOutputSignature, computeSchemaFingerprint, detectSchemaDrift } from "@mattral/core";
|
|
3
|
+
import { computeOutputSignature, computeSchemaFingerprint, detectLatencyDrift, detectOutputShapeDrift, detectSchemaDrift } from "@mattral/core";
|
|
4
4
|
import { JsonRpcFramer } from "./json-rpc-framer.js";
|
|
5
|
+
import { extractErrorMessage } from "./error-message.js";
|
|
6
|
+
import { extractOutputPayload } from "./output-payload.js";
|
|
5
7
|
function isToolsListResult(value) {
|
|
6
8
|
return (typeof value === "object" &&
|
|
7
9
|
value !== null &&
|
|
@@ -24,11 +26,16 @@ function isToolsListResult(value) {
|
|
|
24
26
|
*/
|
|
25
27
|
export class StdioProxyCollector {
|
|
26
28
|
options;
|
|
29
|
+
driftCheckIntervalCalls;
|
|
30
|
+
driftWindowSize;
|
|
27
31
|
child;
|
|
28
32
|
pendingCalls = new Map();
|
|
29
33
|
pendingListRequests = new Set();
|
|
34
|
+
callCountByTool = new Map();
|
|
30
35
|
constructor(options) {
|
|
31
36
|
this.options = options;
|
|
37
|
+
this.driftCheckIntervalCalls = options.driftCheckIntervalCalls ?? 10;
|
|
38
|
+
this.driftWindowSize = options.driftWindowSize ?? 20;
|
|
32
39
|
}
|
|
33
40
|
/**
|
|
34
41
|
* Starts the child process and begins forwarding traffic. Resolves with
|
|
@@ -102,12 +109,11 @@ export class StdioProxyCollector {
|
|
|
102
109
|
}
|
|
103
110
|
}
|
|
104
111
|
recordCallExecution(pending, message) {
|
|
105
|
-
const
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
const durationMs = now - pending.startedAt;
|
|
106
114
|
const isError = Boolean(message.error) || Boolean(message.result?.isError);
|
|
107
115
|
const outputBytes = message.result !== undefined ? Buffer.byteLength(JSON.stringify(message.result), "utf8") : undefined;
|
|
108
|
-
const outputSignature = !isError && message.result !== undefined
|
|
109
|
-
? computeOutputSignature(message.result.content ?? message.result)
|
|
110
|
-
: undefined;
|
|
116
|
+
const outputSignature = !isError && message.result !== undefined ? computeOutputSignature(extractOutputPayload(message.result)) : undefined;
|
|
111
117
|
const execution = {
|
|
112
118
|
id: randomUUID(),
|
|
113
119
|
serverId: this.options.serverId,
|
|
@@ -115,7 +121,7 @@ export class StdioProxyCollector {
|
|
|
115
121
|
startedAt: pending.startedAt,
|
|
116
122
|
durationMs,
|
|
117
123
|
success: !isError,
|
|
118
|
-
errorMessage: isError ? message
|
|
124
|
+
errorMessage: isError ? extractErrorMessage(message) : undefined,
|
|
119
125
|
outputSignature,
|
|
120
126
|
inputBytes: pending.inputBytes,
|
|
121
127
|
outputBytes,
|
|
@@ -123,6 +129,56 @@ export class StdioProxyCollector {
|
|
|
123
129
|
};
|
|
124
130
|
this.options.store.recordExecution(execution);
|
|
125
131
|
this.options.onExecution?.(execution);
|
|
132
|
+
this.maybeCheckWindowDrift(pending.toolName, now);
|
|
133
|
+
}
|
|
134
|
+
recordDrift(drift) {
|
|
135
|
+
this.options.store.recordDriftEvent(drift);
|
|
136
|
+
this.options.onDrift?.(drift);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Every `driftCheckIntervalCalls` calls to a given tool, pulls its last
|
|
140
|
+
* two `driftWindowSize`-execution windows and runs the window-based
|
|
141
|
+
* drift detectors (output-shape, latency) against them. Schema drift is
|
|
142
|
+
* handled separately in `recordToolsListResult` — it's a per-fingerprint
|
|
143
|
+
* compare, not a window comparison, so it runs on every `tools/list`
|
|
144
|
+
* instead of being throttled here.
|
|
145
|
+
*/
|
|
146
|
+
maybeCheckWindowDrift(toolName, now) {
|
|
147
|
+
const count = (this.callCountByTool.get(toolName) ?? 0) + 1;
|
|
148
|
+
this.callCountByTool.set(toolName, count);
|
|
149
|
+
if (count % this.driftCheckIntervalCalls !== 0)
|
|
150
|
+
return;
|
|
151
|
+
const windowSize = this.driftWindowSize;
|
|
152
|
+
// Most-recent-first (see SqliteStore#listExecutions): the first half is
|
|
153
|
+
// the "recent" window, the second half the "baseline" it's compared
|
|
154
|
+
// against.
|
|
155
|
+
const executions = this.options.store.listExecutions({
|
|
156
|
+
serverId: this.options.serverId,
|
|
157
|
+
toolName,
|
|
158
|
+
limit: windowSize * 2
|
|
159
|
+
});
|
|
160
|
+
if (executions.length < windowSize * 2)
|
|
161
|
+
return;
|
|
162
|
+
const recent = executions.slice(0, windowSize);
|
|
163
|
+
const baseline = executions.slice(windowSize, windowSize * 2);
|
|
164
|
+
const shapeDrift = detectOutputShapeDrift({
|
|
165
|
+
serverId: this.options.serverId,
|
|
166
|
+
toolName,
|
|
167
|
+
baseline,
|
|
168
|
+
recent,
|
|
169
|
+
now
|
|
170
|
+
});
|
|
171
|
+
if (shapeDrift)
|
|
172
|
+
this.recordDrift(shapeDrift);
|
|
173
|
+
const latencyDrift = detectLatencyDrift({
|
|
174
|
+
serverId: this.options.serverId,
|
|
175
|
+
toolName,
|
|
176
|
+
baseline,
|
|
177
|
+
recent,
|
|
178
|
+
now
|
|
179
|
+
});
|
|
180
|
+
if (latencyDrift)
|
|
181
|
+
this.recordDrift(latencyDrift);
|
|
126
182
|
}
|
|
127
183
|
recordToolsListResult(result) {
|
|
128
184
|
const now = Date.now();
|
|
@@ -139,8 +195,7 @@ export class StdioProxyCollector {
|
|
|
139
195
|
now
|
|
140
196
|
});
|
|
141
197
|
if (drift) {
|
|
142
|
-
this.
|
|
143
|
-
this.options.onDrift?.(drift);
|
|
198
|
+
this.recordDrift(drift);
|
|
144
199
|
}
|
|
145
200
|
const toolRecord = {
|
|
146
201
|
id: existing?.id ?? randomUUID(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy-collector.js","sourceRoot":"","sources":["../src/proxy-collector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,iBAAiB,EAIlB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,aAAa,EAAuB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"proxy-collector.js","sourceRoot":"","sources":["../src/proxy-collector.ts"],"names":[],"mappings":"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"}
|