@mattral/collector 1.0.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/dist/health-checker.d.ts +43 -0
- package/dist/health-checker.d.ts.map +1 -0
- package/dist/health-checker.js +93 -0
- package/dist/health-checker.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/json-rpc-framer.d.ts +51 -0
- package/dist/json-rpc-framer.d.ts.map +1 -0
- package/dist/json-rpc-framer.js +46 -0
- package/dist/json-rpc-framer.js.map +1 -0
- package/dist/proxy-collector.d.ts +52 -0
- package/dist/proxy-collector.d.ts.map +1 -0
- package/dist/proxy-collector.js +159 -0
- package/dist/proxy-collector.js.map +1 -0
- package/dist/transport-factory.d.ts +11 -0
- package/dist/transport-factory.d.ts.map +1 -0
- package/dist/transport-factory.js +34 -0
- package/dist/transport-factory.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type DriftEvent, type Server } from "@mattral/core";
|
|
2
|
+
import type { ReliabilityStore } from "@mattral/storage";
|
|
3
|
+
export interface HealthCheckResult {
|
|
4
|
+
serverId: string;
|
|
5
|
+
reachable: boolean;
|
|
6
|
+
latencyMs: number;
|
|
7
|
+
toolCount: number;
|
|
8
|
+
driftEvents: DriftEvent[];
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface HealthCheckerOptions {
|
|
12
|
+
store: ReliabilityStore;
|
|
13
|
+
/** Client identity advertised to monitored servers during the MCP handshake. */
|
|
14
|
+
clientInfo?: {
|
|
15
|
+
name: string;
|
|
16
|
+
version: string;
|
|
17
|
+
};
|
|
18
|
+
/** Timeout for the initialize handshake and the tools/list round trip. Default 10s. */
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
}
|
|
21
|
+
/** Reserved pseudo tool-name under which health-check round trips are recorded. */
|
|
22
|
+
export declare const HEALTH_CHECK_TOOL_NAME = "__health_check__";
|
|
23
|
+
/**
|
|
24
|
+
* Performs a single, side-effect-free health check against a server:
|
|
25
|
+
* connects, completes the MCP `initialize` handshake, calls `tools/list`,
|
|
26
|
+
* and disconnects.
|
|
27
|
+
*
|
|
28
|
+
* This deliberately never calls a tool. Per the project's operational
|
|
29
|
+
* principles (spec section 11 / docs/architecture.md#security), a
|
|
30
|
+
* monitoring tool must not exercise side effects on a server it doesn't
|
|
31
|
+
* have explicit consent to invoke. Observing *real* tool calls — the ones a
|
|
32
|
+
* live agent chose to make — is the job of {@link StdioProxyCollector}
|
|
33
|
+
* instead, where mcp-reliable never originates the call itself.
|
|
34
|
+
*/
|
|
35
|
+
export declare class HealthChecker {
|
|
36
|
+
private readonly store;
|
|
37
|
+
private readonly clientInfo;
|
|
38
|
+
private readonly timeoutMs;
|
|
39
|
+
constructor(options: HealthCheckerOptions);
|
|
40
|
+
check(server: Server): Promise<HealthCheckResult>;
|
|
41
|
+
private recordHealthCheckExecution;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=health-checker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-checker.d.ts","sourceRoot":"","sources":["../src/health-checker.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,UAAU,EAEf,KAAK,MAAM,EAEZ,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,gBAAgB,CAAC;IACxB,gFAAgF;IAChF,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,mFAAmF;AACnF,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AAEzD;;;;;;;;;;;GAWG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoC;IAC/D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,OAAO,EAAE,oBAAoB;IAMnC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAwDvD,OAAO,CAAC,0BAA0B;CAmBnC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { computeSchemaFingerprint, detectSchemaDrift } from "@mattral/core";
|
|
4
|
+
import { createClientTransport } from "./transport-factory.js";
|
|
5
|
+
/** Reserved pseudo tool-name under which health-check round trips are recorded. */
|
|
6
|
+
export const HEALTH_CHECK_TOOL_NAME = "__health_check__";
|
|
7
|
+
/**
|
|
8
|
+
* Performs a single, side-effect-free health check against a server:
|
|
9
|
+
* connects, completes the MCP `initialize` handshake, calls `tools/list`,
|
|
10
|
+
* and disconnects.
|
|
11
|
+
*
|
|
12
|
+
* This deliberately never calls a tool. Per the project's operational
|
|
13
|
+
* principles (spec section 11 / docs/architecture.md#security), a
|
|
14
|
+
* monitoring tool must not exercise side effects on a server it doesn't
|
|
15
|
+
* have explicit consent to invoke. Observing *real* tool calls — the ones a
|
|
16
|
+
* live agent chose to make — is the job of {@link StdioProxyCollector}
|
|
17
|
+
* instead, where mcp-reliable never originates the call itself.
|
|
18
|
+
*/
|
|
19
|
+
export class HealthChecker {
|
|
20
|
+
store;
|
|
21
|
+
clientInfo;
|
|
22
|
+
timeoutMs;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.store = options.store;
|
|
25
|
+
this.clientInfo = options.clientInfo ?? { name: "mcp-reliable", version: "1.0.0" };
|
|
26
|
+
this.timeoutMs = options.timeoutMs ?? 10_000;
|
|
27
|
+
}
|
|
28
|
+
async check(server) {
|
|
29
|
+
const start = performance.now();
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const client = new Client(this.clientInfo, { capabilities: {} });
|
|
32
|
+
const transport = createClientTransport(server.transport);
|
|
33
|
+
const driftEvents = [];
|
|
34
|
+
try {
|
|
35
|
+
await client.connect(transport, { timeout: this.timeoutMs });
|
|
36
|
+
const { tools } = await client.listTools(undefined, { timeout: this.timeoutMs });
|
|
37
|
+
const latencyMs = performance.now() - start;
|
|
38
|
+
for (const tool of tools) {
|
|
39
|
+
const fingerprint = computeSchemaFingerprint(tool.inputSchema);
|
|
40
|
+
const existing = this.store.getTool(server.id, tool.name);
|
|
41
|
+
const drift = detectSchemaDrift({
|
|
42
|
+
serverId: server.id,
|
|
43
|
+
toolName: tool.name,
|
|
44
|
+
previousFingerprint: existing?.schemaFingerprint,
|
|
45
|
+
currentFingerprint: fingerprint,
|
|
46
|
+
now
|
|
47
|
+
});
|
|
48
|
+
if (drift) {
|
|
49
|
+
this.store.recordDriftEvent(drift);
|
|
50
|
+
driftEvents.push(drift);
|
|
51
|
+
}
|
|
52
|
+
const toolRecord = {
|
|
53
|
+
id: existing?.id ?? randomUUID(),
|
|
54
|
+
serverId: server.id,
|
|
55
|
+
name: tool.name,
|
|
56
|
+
description: tool.description,
|
|
57
|
+
schemaFingerprint: fingerprint,
|
|
58
|
+
inputSchema: tool.inputSchema,
|
|
59
|
+
firstSeenAt: existing?.firstSeenAt ?? now,
|
|
60
|
+
lastSeenAt: now
|
|
61
|
+
};
|
|
62
|
+
this.store.upsertTool(toolRecord);
|
|
63
|
+
}
|
|
64
|
+
this.recordHealthCheckExecution(server.id, now, latencyMs, true);
|
|
65
|
+
return { serverId: server.id, reachable: true, latencyMs, toolCount: tools.length, driftEvents };
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const latencyMs = performance.now() - start;
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
this.recordHealthCheckExecution(server.id, now, latencyMs, false, message);
|
|
71
|
+
return { serverId: server.id, reachable: false, latencyMs, toolCount: 0, driftEvents, error: message };
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
await client.close().catch(() => {
|
|
75
|
+
// Best-effort: the connection may already be dead if the check failed.
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
recordHealthCheckExecution(serverId, startedAt, durationMs, success, errorMessage) {
|
|
80
|
+
const execution = {
|
|
81
|
+
id: randomUUID(),
|
|
82
|
+
serverId,
|
|
83
|
+
toolName: HEALTH_CHECK_TOOL_NAME,
|
|
84
|
+
startedAt,
|
|
85
|
+
durationMs,
|
|
86
|
+
success,
|
|
87
|
+
errorMessage,
|
|
88
|
+
source: "health-check"
|
|
89
|
+
};
|
|
90
|
+
this.store.recordExecution(execution);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=health-checker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-checker.js","sourceRoot":"","sources":["../src/health-checker.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal JSON-RPC 2.0 message shape, loosely typed on purpose: the proxy
|
|
3
|
+
* only needs to inspect `id`, `method`, `result`, and `error` — it forwards
|
|
4
|
+
* every other field verbatim without validating them against the full MCP
|
|
5
|
+
* type definitions (that validation is the target server's job).
|
|
6
|
+
*/
|
|
7
|
+
export interface JsonRpcMessage {
|
|
8
|
+
jsonrpc?: string;
|
|
9
|
+
id?: string | number;
|
|
10
|
+
method?: string;
|
|
11
|
+
params?: {
|
|
12
|
+
name?: string;
|
|
13
|
+
arguments?: unknown;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
result?: {
|
|
17
|
+
isError?: boolean;
|
|
18
|
+
content?: unknown;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
};
|
|
21
|
+
error?: {
|
|
22
|
+
code: number;
|
|
23
|
+
message: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The MCP stdio transport frames JSON-RPC messages as one JSON object per
|
|
30
|
+
* line (newline-delimited), per the MCP specification. This class buffers
|
|
31
|
+
* partial reads from a stream and yields fully-parsed messages as they
|
|
32
|
+
* become available — the same approach the official SDK's internal
|
|
33
|
+
* ReadBuffer uses, reimplemented here so the proxy collector can stay a
|
|
34
|
+
* thin, dependency-light pass-through rather than depending on SDK
|
|
35
|
+
* internals that aren't part of its public API.
|
|
36
|
+
*
|
|
37
|
+
* Malformed lines are surfaced via the `onParseError` callback rather than
|
|
38
|
+
* thrown, since a single bad line should not take down the whole proxy —
|
|
39
|
+
* it is forwarded to the child/downstream as raw bytes regardless (the
|
|
40
|
+
* proxy's job is to observe traffic, not to enforce protocol correctness).
|
|
41
|
+
*/
|
|
42
|
+
export declare class JsonRpcFramer {
|
|
43
|
+
private readonly onMessage;
|
|
44
|
+
private readonly onParseError?;
|
|
45
|
+
private buffer;
|
|
46
|
+
constructor(onMessage: (message: JsonRpcMessage, raw: string) => void, onParseError?: ((raw: string, error: unknown) => void) | undefined);
|
|
47
|
+
push(chunk: Buffer | string): void;
|
|
48
|
+
}
|
|
49
|
+
/** Serializes a JSON-RPC message back to the newline-delimited wire format. */
|
|
50
|
+
export declare function serializeJsonRpc(message: JsonRpcMessage): string;
|
|
51
|
+
//# sourceMappingURL=json-rpc-framer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-rpc-framer.d.ts","sourceRoot":"","sources":["../src/json-rpc-framer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IACxE,MAAM,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAC1E,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAClE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,aAAa;IAItB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;IAJhC,OAAO,CAAC,MAAM,CAAM;gBAGD,SAAS,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,EACzD,YAAY,CAAC,GAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,aAAA;IAGvE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;CAgBnC;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAEhE"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP stdio transport frames JSON-RPC messages as one JSON object per
|
|
3
|
+
* line (newline-delimited), per the MCP specification. This class buffers
|
|
4
|
+
* partial reads from a stream and yields fully-parsed messages as they
|
|
5
|
+
* become available — the same approach the official SDK's internal
|
|
6
|
+
* ReadBuffer uses, reimplemented here so the proxy collector can stay a
|
|
7
|
+
* thin, dependency-light pass-through rather than depending on SDK
|
|
8
|
+
* internals that aren't part of its public API.
|
|
9
|
+
*
|
|
10
|
+
* Malformed lines are surfaced via the `onParseError` callback rather than
|
|
11
|
+
* thrown, since a single bad line should not take down the whole proxy —
|
|
12
|
+
* it is forwarded to the child/downstream as raw bytes regardless (the
|
|
13
|
+
* proxy's job is to observe traffic, not to enforce protocol correctness).
|
|
14
|
+
*/
|
|
15
|
+
export class JsonRpcFramer {
|
|
16
|
+
onMessage;
|
|
17
|
+
onParseError;
|
|
18
|
+
buffer = "";
|
|
19
|
+
constructor(onMessage, onParseError) {
|
|
20
|
+
this.onMessage = onMessage;
|
|
21
|
+
this.onParseError = onParseError;
|
|
22
|
+
}
|
|
23
|
+
push(chunk) {
|
|
24
|
+
this.buffer += chunk.toString("utf8");
|
|
25
|
+
let newlineIndex;
|
|
26
|
+
while ((newlineIndex = this.buffer.indexOf("\n")) !== -1) {
|
|
27
|
+
const line = this.buffer.slice(0, newlineIndex);
|
|
28
|
+
this.buffer = this.buffer.slice(newlineIndex + 1);
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (trimmed.length === 0)
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
const message = JSON.parse(trimmed);
|
|
34
|
+
this.onMessage(message, line);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
this.onParseError?.(line, error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Serializes a JSON-RPC message back to the newline-delimited wire format. */
|
|
43
|
+
export function serializeJsonRpc(message) {
|
|
44
|
+
return JSON.stringify(message) + "\n";
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=json-rpc-framer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-rpc-framer.js","sourceRoot":"","sources":["../src/json-rpc-framer.ts"],"names":[],"mappings":"AAgBA;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,aAAa;IAIL;IACA;IAJX,MAAM,GAAG,EAAE,CAAC;IAEpB,YACmB,SAAyD,EACzD,YAAoD;QADpD,cAAS,GAAT,SAAS,CAAgD;QACzD,iBAAY,GAAZ,YAAY,CAAwC;IACpE,CAAC;IAEJ,IAAI,CAAC,KAAsB;QACzB,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,YAAoB,CAAC;QACzB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACzD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC;YAChD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC;YAClD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YACnC,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAmB,CAAC;gBACtD,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAChC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;IACH,CAAC;CACF;AAED,+EAA+E;AAC/E,MAAM,UAAU,gBAAgB,CAAC,OAAuB;IACtD,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;AACxC,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type DriftEvent, type Execution } from "@mattral/core";
|
|
2
|
+
import type { ReliabilityStore } from "@mattral/storage";
|
|
3
|
+
export interface StdioProxyCollectorOptions {
|
|
4
|
+
serverId: string;
|
|
5
|
+
store: ReliabilityStore;
|
|
6
|
+
command: string;
|
|
7
|
+
args?: string[];
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
/** Called for every recorded execution, in addition to persisting it to the store. */
|
|
11
|
+
onExecution?: (execution: Execution) => void;
|
|
12
|
+
/** Called whenever a drift event is detected and recorded. */
|
|
13
|
+
onDrift?: (event: DriftEvent) => void;
|
|
14
|
+
/** Stream carrying traffic FROM the real client (defaults to this process's stdin). */
|
|
15
|
+
input?: NodeJS.ReadableStream;
|
|
16
|
+
/** Stream carrying traffic TO the real client (defaults to this process's stdout). */
|
|
17
|
+
output?: NodeJS.WritableStream;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A transparent stdio proxy that sits between an MCP client (an agent) and
|
|
21
|
+
* a real MCP server: it spawns the target server as a child process and
|
|
22
|
+
* forwards every JSON-RPC message between the client and the child
|
|
23
|
+
* unmodified, while recording an `Execution` for every `tools/call`
|
|
24
|
+
* request/response pair it observes, and a `Tool` (with schema drift
|
|
25
|
+
* detection) for every `tools/list` response.
|
|
26
|
+
*
|
|
27
|
+
* Because it only ever forwards traffic a real client originated, it
|
|
28
|
+
* requires no special consent model of its own for the calls it observes —
|
|
29
|
+
* see docs/architecture.md#collection-model. From the client's point of
|
|
30
|
+
* view, this proxy *is* the MCP server: point your agent's server command
|
|
31
|
+
* at `mcp-reliable proxy <server-id> -- <original command>` instead of the
|
|
32
|
+
* original command directly.
|
|
33
|
+
*/
|
|
34
|
+
export declare class StdioProxyCollector {
|
|
35
|
+
private readonly options;
|
|
36
|
+
private child;
|
|
37
|
+
private readonly pendingCalls;
|
|
38
|
+
private readonly pendingListRequests;
|
|
39
|
+
constructor(options: StdioProxyCollectorOptions);
|
|
40
|
+
/**
|
|
41
|
+
* Starts the child process and begins forwarding traffic. Resolves with
|
|
42
|
+
* the child's exit code once it terminates, or rejects if it could not
|
|
43
|
+
* be started at all (e.g. command not found).
|
|
44
|
+
*/
|
|
45
|
+
start(): Promise<number | null>;
|
|
46
|
+
stop(): Promise<void>;
|
|
47
|
+
private handleDownstreamMessage;
|
|
48
|
+
private handleUpstreamMessage;
|
|
49
|
+
private recordCallExecution;
|
|
50
|
+
private recordToolsListResult;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=proxy-collector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-collector.d.ts","sourceRoot":"","sources":["../src/proxy-collector.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,UAAU,EACf,KAAK,SAAS,EAEf,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,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;CAChC;AAoBD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA2C;IACxE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA8B;gBAEtD,OAAO,EAAE,0BAA0B;IAI/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,qBAAqB;CAiC9B"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { computeOutputSignature, computeSchemaFingerprint, detectSchemaDrift } from "@mattral/core";
|
|
4
|
+
import { JsonRpcFramer } from "./json-rpc-framer.js";
|
|
5
|
+
function isToolsListResult(value) {
|
|
6
|
+
return (typeof value === "object" &&
|
|
7
|
+
value !== null &&
|
|
8
|
+
Array.isArray(value.tools));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A transparent stdio proxy that sits between an MCP client (an agent) and
|
|
12
|
+
* a real MCP server: it spawns the target server as a child process and
|
|
13
|
+
* forwards every JSON-RPC message between the client and the child
|
|
14
|
+
* unmodified, while recording an `Execution` for every `tools/call`
|
|
15
|
+
* request/response pair it observes, and a `Tool` (with schema drift
|
|
16
|
+
* detection) for every `tools/list` response.
|
|
17
|
+
*
|
|
18
|
+
* Because it only ever forwards traffic a real client originated, it
|
|
19
|
+
* requires no special consent model of its own for the calls it observes —
|
|
20
|
+
* see docs/architecture.md#collection-model. From the client's point of
|
|
21
|
+
* view, this proxy *is* the MCP server: point your agent's server command
|
|
22
|
+
* at `mcp-reliable proxy <server-id> -- <original command>` instead of the
|
|
23
|
+
* original command directly.
|
|
24
|
+
*/
|
|
25
|
+
export class StdioProxyCollector {
|
|
26
|
+
options;
|
|
27
|
+
child;
|
|
28
|
+
pendingCalls = new Map();
|
|
29
|
+
pendingListRequests = new Set();
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Starts the child process and begins forwarding traffic. Resolves with
|
|
35
|
+
* the child's exit code once it terminates, or rejects if it could not
|
|
36
|
+
* be started at all (e.g. command not found).
|
|
37
|
+
*/
|
|
38
|
+
start() {
|
|
39
|
+
const { command, args = [], env, cwd } = this.options;
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const child = spawn(command, args, {
|
|
42
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
43
|
+
cwd,
|
|
44
|
+
stdio: ["pipe", "pipe", "inherit"]
|
|
45
|
+
});
|
|
46
|
+
this.child = child;
|
|
47
|
+
const childStdin = child.stdin;
|
|
48
|
+
const childStdout = child.stdout;
|
|
49
|
+
if (!childStdin || !childStdout) {
|
|
50
|
+
reject(new Error("Failed to open stdio pipes for the child MCP server process"));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const input = this.options.input ?? process.stdin;
|
|
54
|
+
const output = this.options.output ?? process.stdout;
|
|
55
|
+
// Downstream: messages FROM the client TO the child (requests, notifications).
|
|
56
|
+
const downstreamFramer = new JsonRpcFramer((message, raw) => {
|
|
57
|
+
this.handleDownstreamMessage(message);
|
|
58
|
+
childStdin.write(raw + "\n");
|
|
59
|
+
}, (raw) => childStdin.write(raw + "\n") // forward unparseable lines verbatim; never drop traffic
|
|
60
|
+
);
|
|
61
|
+
input.on("data", (chunk) => downstreamFramer.push(chunk));
|
|
62
|
+
input.on("end", () => child.kill());
|
|
63
|
+
// Upstream: messages FROM the child TO the client (responses, notifications).
|
|
64
|
+
const upstreamFramer = new JsonRpcFramer((message, raw) => {
|
|
65
|
+
this.handleUpstreamMessage(message);
|
|
66
|
+
output.write(raw + "\n");
|
|
67
|
+
}, (raw) => output.write(raw + "\n"));
|
|
68
|
+
childStdout.on("data", (chunk) => upstreamFramer.push(chunk));
|
|
69
|
+
child.on("error", (error) => reject(error));
|
|
70
|
+
child.on("exit", (code) => resolve(code));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async stop() {
|
|
74
|
+
this.child?.kill();
|
|
75
|
+
}
|
|
76
|
+
handleDownstreamMessage(message) {
|
|
77
|
+
if (message.id === undefined)
|
|
78
|
+
return;
|
|
79
|
+
if (message.method === "tools/call") {
|
|
80
|
+
const toolName = message.params?.name ?? "unknown";
|
|
81
|
+
const inputBytes = Buffer.byteLength(JSON.stringify(message.params?.arguments ?? {}), "utf8");
|
|
82
|
+
this.pendingCalls.set(message.id, { toolName, startedAt: Date.now(), inputBytes });
|
|
83
|
+
}
|
|
84
|
+
else if (message.method === "tools/list") {
|
|
85
|
+
this.pendingListRequests.add(message.id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
handleUpstreamMessage(message) {
|
|
89
|
+
if (message.id === undefined)
|
|
90
|
+
return;
|
|
91
|
+
const pendingCall = this.pendingCalls.get(message.id);
|
|
92
|
+
if (pendingCall) {
|
|
93
|
+
this.pendingCalls.delete(message.id);
|
|
94
|
+
this.recordCallExecution(pendingCall, message);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (this.pendingListRequests.has(message.id)) {
|
|
98
|
+
this.pendingListRequests.delete(message.id);
|
|
99
|
+
if (isToolsListResult(message.result)) {
|
|
100
|
+
this.recordToolsListResult(message.result);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
recordCallExecution(pending, message) {
|
|
105
|
+
const durationMs = Date.now() - pending.startedAt;
|
|
106
|
+
const isError = Boolean(message.error) || Boolean(message.result?.isError);
|
|
107
|
+
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;
|
|
111
|
+
const execution = {
|
|
112
|
+
id: randomUUID(),
|
|
113
|
+
serverId: this.options.serverId,
|
|
114
|
+
toolName: pending.toolName,
|
|
115
|
+
startedAt: pending.startedAt,
|
|
116
|
+
durationMs,
|
|
117
|
+
success: !isError,
|
|
118
|
+
errorMessage: isError ? message.error?.message ?? "Tool call returned isError: true" : undefined,
|
|
119
|
+
outputSignature,
|
|
120
|
+
inputBytes: pending.inputBytes,
|
|
121
|
+
outputBytes,
|
|
122
|
+
source: "proxy"
|
|
123
|
+
};
|
|
124
|
+
this.options.store.recordExecution(execution);
|
|
125
|
+
this.options.onExecution?.(execution);
|
|
126
|
+
}
|
|
127
|
+
recordToolsListResult(result) {
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
for (const tool of result.tools) {
|
|
130
|
+
if (typeof tool.name !== "string")
|
|
131
|
+
continue;
|
|
132
|
+
const fingerprint = computeSchemaFingerprint(tool.inputSchema);
|
|
133
|
+
const existing = this.options.store.getTool(this.options.serverId, tool.name);
|
|
134
|
+
const drift = detectSchemaDrift({
|
|
135
|
+
serverId: this.options.serverId,
|
|
136
|
+
toolName: tool.name,
|
|
137
|
+
previousFingerprint: existing?.schemaFingerprint,
|
|
138
|
+
currentFingerprint: fingerprint,
|
|
139
|
+
now
|
|
140
|
+
});
|
|
141
|
+
if (drift) {
|
|
142
|
+
this.options.store.recordDriftEvent(drift);
|
|
143
|
+
this.options.onDrift?.(drift);
|
|
144
|
+
}
|
|
145
|
+
const toolRecord = {
|
|
146
|
+
id: existing?.id ?? randomUUID(),
|
|
147
|
+
serverId: this.options.serverId,
|
|
148
|
+
name: tool.name,
|
|
149
|
+
description: tool.description,
|
|
150
|
+
schemaFingerprint: fingerprint,
|
|
151
|
+
inputSchema: tool.inputSchema,
|
|
152
|
+
firstSeenAt: existing?.firstSeenAt ?? now,
|
|
153
|
+
lastSeenAt: now
|
|
154
|
+
};
|
|
155
|
+
this.options.store.upsertTool(toolRecord);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=proxy-collector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-collector.js","sourceRoot":"","sources":["../src/proxy-collector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
2
|
+
import type { TransportConfig } from "@mattral/core";
|
|
3
|
+
/**
|
|
4
|
+
* Builds the appropriate official SDK client transport for a server's
|
|
5
|
+
* configured transport, used by {@link HealthChecker}. The stdio proxy
|
|
6
|
+
* collector does *not* use this — it talks to the child process directly
|
|
7
|
+
* (see docs/architecture.md#transport-adapters for why the two collection
|
|
8
|
+
* modes have different transport-handling needs).
|
|
9
|
+
*/
|
|
10
|
+
export declare function createClientTransport(config: TransportConfig): Transport;
|
|
11
|
+
//# sourceMappingURL=transport-factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport-factory.d.ts","sourceRoot":"","sources":["../src/transport-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAI/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAsBxE"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
/**
|
|
5
|
+
* Builds the appropriate official SDK client transport for a server's
|
|
6
|
+
* configured transport, used by {@link HealthChecker}. The stdio proxy
|
|
7
|
+
* collector does *not* use this — it talks to the child process directly
|
|
8
|
+
* (see docs/architecture.md#transport-adapters for why the two collection
|
|
9
|
+
* modes have different transport-handling needs).
|
|
10
|
+
*/
|
|
11
|
+
export function createClientTransport(config) {
|
|
12
|
+
switch (config.type) {
|
|
13
|
+
case "stdio":
|
|
14
|
+
return new StdioClientTransport({
|
|
15
|
+
command: config.command,
|
|
16
|
+
args: config.args,
|
|
17
|
+
env: config.env,
|
|
18
|
+
cwd: config.cwd
|
|
19
|
+
});
|
|
20
|
+
case "sse":
|
|
21
|
+
return new SSEClientTransport(new URL(config.url), {
|
|
22
|
+
requestInit: config.headers ? { headers: config.headers } : undefined
|
|
23
|
+
});
|
|
24
|
+
case "http":
|
|
25
|
+
return new StreamableHTTPClientTransport(new URL(config.url), {
|
|
26
|
+
requestInit: config.headers ? { headers: config.headers } : undefined
|
|
27
|
+
});
|
|
28
|
+
default: {
|
|
29
|
+
const exhaustiveCheck = config;
|
|
30
|
+
throw new Error(`Unsupported transport config: ${JSON.stringify(exhaustiveCheck)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=transport-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport-factory.js","sourceRoot":"","sources":["../src/transport-factory.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AAGnG;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAuB;IAC3D,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,OAAO;YACV,OAAO,IAAI,oBAAoB,CAAC;gBAC9B,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,GAAG,EAAE,MAAM,CAAC,GAAG;gBACf,GAAG,EAAE,MAAM,CAAC,GAAG;aAChB,CAAC,CAAC;QACL,KAAK,KAAK;YACR,OAAO,IAAI,kBAAkB,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;gBACjD,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS;aACtE,CAAC,CAAC;QACL,KAAK,MAAM;YACT,OAAO,IAAI,6BAA6B,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;gBAC5D,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS;aACtE,CAAC,CAAC;QACL,OAAO,CAAC,CAAC,CAAC;YACR,MAAM,eAAe,GAAU,MAAM,CAAC;YACtC,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mattral/collector",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Health checking and traffic-observing proxy collectors for MCP servers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Mattral/mcp-reliable.git",
|
|
10
|
+
"directory": "packages/collector"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/Mattral/mcp-reliable#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Mattral/mcp-reliable/issues"
|
|
15
|
+
},
|
|
16
|
+
"author": "Min Htet Myet (Mattral)",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"reliability",
|
|
21
|
+
"observability",
|
|
22
|
+
"proxy",
|
|
23
|
+
"health-check"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20.0.0"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -b",
|
|
44
|
+
"clean": "rimraf dist *.tsbuildinfo"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@mattral/core": "^1.0.0",
|
|
48
|
+
"@mattral/storage": "^1.0.0",
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"typescript": "^5.7.3"
|
|
53
|
+
}
|
|
54
|
+
}
|