@getmarrow/mcp 3.9.56 → 3.9.58
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 +21 -2
- package/dist/cli.js +244 -109
- package/dist/cli.js.map +1 -1
- package/dist/hook-context.d.ts +2 -1
- package/dist/hook-context.d.ts.map +1 -1
- package/dist/hook-context.js +2 -1
- package/dist/hook-context.js.map +1 -1
- package/dist/hook-contract.d.ts +6 -6
- package/dist/hook-contract.js +1 -1
- package/dist/hook-session.d.ts +1 -1
- package/dist/hook.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -15
- package/dist/index.js.map +1 -1
- package/dist/redact.js +1 -1
- package/dist/redact.js.map +1 -1
- package/dist/request-reliability.d.ts +27 -0
- package/dist/request-reliability.d.ts.map +1 -0
- package/dist/request-reliability.js +264 -0
- package/dist/request-reliability.js.map +1 -0
- package/dist/runtime-contract.d.ts +5 -0
- package/dist/runtime-contract.d.ts.map +1 -0
- package/dist/runtime-contract.js +107 -0
- package/dist/runtime-contract.js.map +1 -0
- package/package.json +7 -4
- package/scripts/control-path-canary.cjs +306 -0
- package/server.json +2 -2
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isValidRuntimeResult = isValidRuntimeResult;
|
|
4
|
+
exports.normalizeRuntimeResult = normalizeRuntimeResult;
|
|
5
|
+
exports.highRiskRuntimeCanClose = highRiskRuntimeCanClose;
|
|
6
|
+
const RUNTIME_GATE_DECISIONS = new Set([
|
|
7
|
+
'allow',
|
|
8
|
+
'proceed',
|
|
9
|
+
'warn',
|
|
10
|
+
'review_required',
|
|
11
|
+
'owner_approval_required',
|
|
12
|
+
'block',
|
|
13
|
+
'deny',
|
|
14
|
+
'denied',
|
|
15
|
+
]);
|
|
16
|
+
const RUNTIME_RISK_LEVELS = new Set(['low', 'medium', 'high', 'critical']);
|
|
17
|
+
function isValidRuntimeResult(value) {
|
|
18
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
19
|
+
return false;
|
|
20
|
+
const riskGate = value.risk_gate;
|
|
21
|
+
if (!riskGate || typeof riskGate !== 'object' || Array.isArray(riskGate))
|
|
22
|
+
return false;
|
|
23
|
+
const gate = riskGate;
|
|
24
|
+
return typeof gate.allow === 'boolean'
|
|
25
|
+
&& typeof gate.decision === 'string'
|
|
26
|
+
&& RUNTIME_GATE_DECISIONS.has(gate.decision);
|
|
27
|
+
}
|
|
28
|
+
function normalizeRuntimeResult(value) {
|
|
29
|
+
if (isValidRuntimeResult(value))
|
|
30
|
+
return value;
|
|
31
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
32
|
+
return null;
|
|
33
|
+
const slim = value;
|
|
34
|
+
if (slim.response_mode !== 'slim'
|
|
35
|
+
|| typeof slim.decision !== 'string'
|
|
36
|
+
|| !RUNTIME_GATE_DECISIONS.has(slim.decision)
|
|
37
|
+
|| typeof slim.risk_level !== 'string'
|
|
38
|
+
|| !RUNTIME_RISK_LEVELS.has(slim.risk_level)
|
|
39
|
+
|| typeof slim.gate_required !== 'boolean'
|
|
40
|
+
|| typeof slim.proof_required !== 'boolean'
|
|
41
|
+
|| typeof slim.proof_complete !== 'boolean') {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const allow = ['allow', 'proceed', 'warn'].includes(slim.decision);
|
|
45
|
+
const gateReceiptId = typeof slim.gate_receipt_id === 'string' && slim.gate_receipt_id
|
|
46
|
+
? slim.gate_receipt_id
|
|
47
|
+
: null;
|
|
48
|
+
return {
|
|
49
|
+
...slim,
|
|
50
|
+
ok: slim.ok !== false,
|
|
51
|
+
action: '',
|
|
52
|
+
agent_id: typeof slim.agent_id === 'string' ? slim.agent_id : null,
|
|
53
|
+
session_id: typeof slim.session_id === 'string' ? slim.session_id : null,
|
|
54
|
+
status: { health: slim.health || null, missed_hooks: slim.missed_hooks || [] },
|
|
55
|
+
decision_brief: {},
|
|
56
|
+
risk_gate: {
|
|
57
|
+
allow,
|
|
58
|
+
decision: slim.decision === 'proceed' ? 'allow' : slim.decision,
|
|
59
|
+
risk_level: slim.risk_level,
|
|
60
|
+
reasons: [],
|
|
61
|
+
gate_receipt_id: gateReceiptId,
|
|
62
|
+
},
|
|
63
|
+
relevant_lessons: [],
|
|
64
|
+
deployment_playbooks: [],
|
|
65
|
+
template_suggestion: {},
|
|
66
|
+
gate_receipt_id: gateReceiptId,
|
|
67
|
+
gate_receipt: gateReceiptId ? {
|
|
68
|
+
id: gateReceiptId,
|
|
69
|
+
required: slim.gate_required,
|
|
70
|
+
decision: slim.decision,
|
|
71
|
+
} : null,
|
|
72
|
+
proof_pack: {
|
|
73
|
+
required: slim.proof_required,
|
|
74
|
+
enforced: slim.proof_required,
|
|
75
|
+
fields: [],
|
|
76
|
+
missing: slim.proof_complete ? [] : ['required_proof'],
|
|
77
|
+
complete: slim.proof_complete,
|
|
78
|
+
commit_endpoint: '/v1/agent/commit',
|
|
79
|
+
rule: slim.proof_required ? 'proof_required_before_complete' : 'outcome_commit_required',
|
|
80
|
+
},
|
|
81
|
+
before_you_act: typeof slim.before_you_act === 'string' ? slim.before_you_act : null,
|
|
82
|
+
exact_next_action: typeof slim.exact_next_action === 'string' ? slim.exact_next_action : null,
|
|
83
|
+
auto_outcome_closure: null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function highRiskRuntimeCanClose(runtime, proof, explicitReceiptId, now = Date.now()) {
|
|
87
|
+
const decision = String(runtime.risk_gate?.decision || '').toLowerCase();
|
|
88
|
+
const receiptDecision = String(runtime.gate_receipt?.decision || decision).toLowerCase();
|
|
89
|
+
const receiptId = typeof explicitReceiptId === 'string' && explicitReceiptId.trim()
|
|
90
|
+
? explicitReceiptId.trim()
|
|
91
|
+
: runtime.gate_receipt?.id || runtime.gate_receipt_id || '';
|
|
92
|
+
const receiptExpired = runtime.gate_receipt?.expires_at
|
|
93
|
+
? Date.parse(runtime.gate_receipt.expires_at) <= now
|
|
94
|
+
: false;
|
|
95
|
+
return Boolean(proof
|
|
96
|
+
&& Object.keys(proof).length > 0
|
|
97
|
+
&& runtime.risk_gate?.allow === true
|
|
98
|
+
&& ['allow', 'proceed', 'warn'].includes(decision)
|
|
99
|
+
&& ['allow', 'proceed', 'warn'].includes(receiptDecision)
|
|
100
|
+
&& runtime.proof_pack?.complete === true
|
|
101
|
+
&& receiptId
|
|
102
|
+
&& !receiptExpired
|
|
103
|
+
&& runtime.gate_receipt?.owner_approval_required !== true
|
|
104
|
+
&& runtime.intervention?.must_stop !== true
|
|
105
|
+
&& runtime.intervention?.allow !== false);
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=runtime-contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-contract.js","sourceRoot":"","sources":["../src/runtime-contract.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmarrow/mcp",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.58",
|
|
4
4
|
"mcpName": "io.github.getmarrow/marrow",
|
|
5
5
|
"description": "Runtime control, proof, and fleet intelligence for MCP-compatible AI agents.",
|
|
6
6
|
"bin": {
|
|
7
|
-
"marrow-mcp": "dist/cli.js"
|
|
7
|
+
"marrow-mcp": "dist/cli.js",
|
|
8
|
+
"marrow-mcp-canary": "scripts/control-path-canary.cjs"
|
|
8
9
|
},
|
|
9
10
|
"main": "dist/index.js",
|
|
10
11
|
"types": "dist/index.d.ts",
|
|
@@ -13,7 +14,8 @@
|
|
|
13
14
|
"dev": "tsc --watch",
|
|
14
15
|
"test": "npm run build && node --test test/*.test.js",
|
|
15
16
|
"registry:check": "node scripts/mcp-registry-check.cjs",
|
|
16
|
-
"registry:verify": "node scripts/mcp-registry-verify.cjs"
|
|
17
|
+
"registry:verify": "node scripts/mcp-registry-verify.cjs",
|
|
18
|
+
"canary:control-path": "node scripts/control-path-canary.cjs"
|
|
17
19
|
},
|
|
18
20
|
"keywords": [
|
|
19
21
|
"mcp",
|
|
@@ -68,7 +70,8 @@
|
|
|
68
70
|
"README.md",
|
|
69
71
|
"server.json",
|
|
70
72
|
"scripts/mcp-registry-check.cjs",
|
|
71
|
-
"scripts/mcp-registry-verify.cjs"
|
|
73
|
+
"scripts/mcp-registry-verify.cjs",
|
|
74
|
+
"scripts/control-path-canary.cjs"
|
|
72
75
|
],
|
|
73
76
|
"dependencies": {
|
|
74
77
|
"@getmarrow/sdk": "^3.7.54"
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawn } = require('node:child_process');
|
|
3
|
+
const { performance } = require('node:perf_hooks');
|
|
4
|
+
const { resolve } = require('node:path');
|
|
5
|
+
const { version: packageVersion } = require('../package.json');
|
|
6
|
+
|
|
7
|
+
const cases = [
|
|
8
|
+
['marrow_status', {}],
|
|
9
|
+
['marrow_runtime_status', { fast: true }],
|
|
10
|
+
['marrow_orient', { autoWarn: true }],
|
|
11
|
+
['marrow_ask', { query: 'What should this agent verify before a safe release?' }],
|
|
12
|
+
['marrow_agent_runtime', { action: 'Review a local documentation note', type: 'general', surfaces: ['workspace'] }],
|
|
13
|
+
['marrow_first_value', { action: 'Verify Marrow control-path availability', type: 'review', surfaces: ['api'] }],
|
|
14
|
+
['marrow_buyer_proof', { periodDays: 7 }],
|
|
15
|
+
['marrow_governance_control_plane', {}],
|
|
16
|
+
['marrow_value_report', { period: '7d' }],
|
|
17
|
+
['marrow_fleet_lessons', { query: 'safe release verification', limit: 3 }],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const HOT_PATH_TOOLS = new Set([
|
|
21
|
+
'marrow_status',
|
|
22
|
+
'marrow_runtime_status',
|
|
23
|
+
'marrow_orient',
|
|
24
|
+
'marrow_ask',
|
|
25
|
+
'marrow_agent_runtime',
|
|
26
|
+
]);
|
|
27
|
+
const OUTPUT_LIMIT_BYTES = 256 * 1024;
|
|
28
|
+
|
|
29
|
+
function boundedMs(name, fallback, minimum = 100, maximum = 10_000, env = process.env) {
|
|
30
|
+
const parsed = Number(env[name] || fallback);
|
|
31
|
+
return Number.isFinite(parsed) ? Math.max(minimum, Math.min(maximum, Math.floor(parsed))) : fallback;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function percentile(values, quantile) {
|
|
35
|
+
if (!values.length) return null;
|
|
36
|
+
const ordered = [...values].sort((a, b) => a - b);
|
|
37
|
+
return ordered[Math.max(0, Math.ceil(quantile * ordered.length) - 1)];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function latencyGroup(rows) {
|
|
41
|
+
const values = rows.map((row) => row.latency_ms);
|
|
42
|
+
return {
|
|
43
|
+
count: values.length,
|
|
44
|
+
p50_ms: percentile(values, 0.5),
|
|
45
|
+
p95_ms: percentile(values, 0.95),
|
|
46
|
+
p99_ms: percentile(values, 0.99),
|
|
47
|
+
max_ms: values.length ? Math.max(...values) : null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toolPayload(message, name) {
|
|
52
|
+
if (!message) throw new Error(`${name} returned no MCP tool response`);
|
|
53
|
+
if (message.error) throw new Error(`${name} returned JSON-RPC error ${message.error.code}`);
|
|
54
|
+
const result = message.result || {};
|
|
55
|
+
const text = result.content?.[0]?.text;
|
|
56
|
+
if (typeof text !== 'string' || !text.trim()) throw new Error(`${name} returned no MCP tool payload`);
|
|
57
|
+
let payload;
|
|
58
|
+
try { payload = JSON.parse(text); } catch { throw new Error(`${name} returned invalid tool JSON`); }
|
|
59
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.keys(payload).length === 0) {
|
|
60
|
+
throw new Error(`${name} returned an empty or invalid tool payload`);
|
|
61
|
+
}
|
|
62
|
+
if (result.isError || payload.ok === false || payload.available === false) {
|
|
63
|
+
const code = payload.error?.code || payload.error_code || 'tool_unavailable';
|
|
64
|
+
throw new Error(`${name} unavailable (${code})`);
|
|
65
|
+
}
|
|
66
|
+
return payload;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validatePayload(name, payload) {
|
|
70
|
+
const requireField = (valid, field) => {
|
|
71
|
+
if (!valid) throw new Error(`${name} returned an invalid ${field} contract`);
|
|
72
|
+
};
|
|
73
|
+
if (name === 'marrow_status' || name === 'marrow_runtime_status') {
|
|
74
|
+
requireField(
|
|
75
|
+
typeof payload.status === 'string'
|
|
76
|
+
|| typeof payload.health === 'string'
|
|
77
|
+
|| typeof payload.active === 'boolean'
|
|
78
|
+
|| (payload.runtime && typeof payload.runtime === 'object'),
|
|
79
|
+
'status',
|
|
80
|
+
);
|
|
81
|
+
} else if (name === 'marrow_orient') {
|
|
82
|
+
requireField(Array.isArray(payload.warnings) && typeof payload.shouldPause === 'boolean', 'orientation');
|
|
83
|
+
} else if (name === 'marrow_ask') {
|
|
84
|
+
requireField(typeof payload.answer === 'string' && payload.answer.trim().length > 0, 'answer');
|
|
85
|
+
} else if (name === 'marrow_agent_runtime') {
|
|
86
|
+
requireField(
|
|
87
|
+
payload.risk_gate
|
|
88
|
+
&& typeof payload.risk_gate === 'object'
|
|
89
|
+
&& typeof payload.risk_gate.allow === 'boolean'
|
|
90
|
+
&& typeof payload.risk_gate.decision === 'string'
|
|
91
|
+
&& payload.proof_pack
|
|
92
|
+
&& typeof payload.proof_pack.complete === 'boolean',
|
|
93
|
+
'runtime gate',
|
|
94
|
+
);
|
|
95
|
+
} else if (name === 'marrow_first_value') {
|
|
96
|
+
requireField(typeof payload.active === 'boolean' && typeof payload.headline === 'string', 'first-value');
|
|
97
|
+
} else if (name === 'marrow_value_report') {
|
|
98
|
+
requireField(
|
|
99
|
+
payload.period
|
|
100
|
+
&& typeof payload.period.days === 'number'
|
|
101
|
+
&& payload.metrics
|
|
102
|
+
&& typeof payload.metrics.decisions?.total === 'number'
|
|
103
|
+
&& payload.fleet
|
|
104
|
+
&& typeof payload.fleet.active_agents === 'number',
|
|
105
|
+
'value report',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class PersistentMcpClient {
|
|
111
|
+
constructor(input) {
|
|
112
|
+
this.command = input.command;
|
|
113
|
+
this.args = input.args;
|
|
114
|
+
this.env = input.env;
|
|
115
|
+
this.timeoutMs = input.timeoutMs;
|
|
116
|
+
this.spawnProcess = input.spawnProcess || spawn;
|
|
117
|
+
this.maxOutputBytes = input.maxOutputBytes || OUTPUT_LIMIT_BYTES;
|
|
118
|
+
this.nextId = 1;
|
|
119
|
+
this.pending = new Map();
|
|
120
|
+
this.stdoutBuffer = '';
|
|
121
|
+
this.stderrBytes = 0;
|
|
122
|
+
this.closed = false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
start() {
|
|
126
|
+
if (this.child) return;
|
|
127
|
+
this.child = this.spawnProcess(this.command, this.args, {
|
|
128
|
+
env: this.env,
|
|
129
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
130
|
+
});
|
|
131
|
+
this.child.stdout.setEncoding('utf8');
|
|
132
|
+
this.child.stdout.on('data', (chunk) => this.onStdout(chunk));
|
|
133
|
+
this.child.stderr.on('data', (chunk) => {
|
|
134
|
+
this.stderrBytes += Buffer.byteLength(chunk);
|
|
135
|
+
if (this.stderrBytes > this.maxOutputBytes) this.abort(new Error('MCP child exceeded stderr limit'));
|
|
136
|
+
});
|
|
137
|
+
this.child.once('error', (error) => this.abort(new Error(`MCP child failed: ${error.code || 'process_error'}`)));
|
|
138
|
+
this.child.once('exit', (code, signal) => {
|
|
139
|
+
this.closed = true;
|
|
140
|
+
if (this.pending.size > 0) this.rejectAll(new Error(`MCP child exited ${code ?? signal ?? 'unknown'}`));
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
onStdout(chunk) {
|
|
145
|
+
this.stdoutBuffer += chunk;
|
|
146
|
+
if (Buffer.byteLength(this.stdoutBuffer) > this.maxOutputBytes) {
|
|
147
|
+
this.abort(new Error('MCP child exceeded stdout limit'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
for (;;) {
|
|
151
|
+
const newline = this.stdoutBuffer.indexOf('\n');
|
|
152
|
+
if (newline < 0) break;
|
|
153
|
+
const line = this.stdoutBuffer.slice(0, newline).trim();
|
|
154
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newline + 1);
|
|
155
|
+
if (!line) continue;
|
|
156
|
+
let message;
|
|
157
|
+
try { message = JSON.parse(line); } catch {
|
|
158
|
+
this.abort(new Error('MCP child emitted malformed JSON'));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!Number.isSafeInteger(message.id)) continue;
|
|
162
|
+
const pending = this.pending.get(message.id);
|
|
163
|
+
if (!pending) {
|
|
164
|
+
this.abort(new Error(`MCP child returned unexpected response id ${message.id}`));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
clearTimeout(pending.timer);
|
|
168
|
+
this.pending.delete(message.id);
|
|
169
|
+
pending.resolve(message);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
request(method, params = {}, timeoutMs = this.timeoutMs) {
|
|
174
|
+
if (!this.child || this.closed || !this.child.stdin.writable) {
|
|
175
|
+
return Promise.reject(new Error('MCP child is not writable'));
|
|
176
|
+
}
|
|
177
|
+
const id = this.nextId++;
|
|
178
|
+
return new Promise((resolveRequest, rejectRequest) => {
|
|
179
|
+
const timer = setTimeout(() => {
|
|
180
|
+
this.pending.delete(id);
|
|
181
|
+
const error = new Error(`${method} timed out`);
|
|
182
|
+
rejectRequest(error);
|
|
183
|
+
this.abort(error);
|
|
184
|
+
}, timeoutMs);
|
|
185
|
+
this.pending.set(id, { resolve: resolveRequest, reject: rejectRequest, timer });
|
|
186
|
+
this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`, (error) => {
|
|
187
|
+
if (!error) return;
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
this.pending.delete(id);
|
|
190
|
+
rejectRequest(new Error(`${method} write failed`));
|
|
191
|
+
this.abort(error);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
rejectAll(error) {
|
|
197
|
+
for (const pending of this.pending.values()) {
|
|
198
|
+
clearTimeout(pending.timer);
|
|
199
|
+
pending.reject(error);
|
|
200
|
+
}
|
|
201
|
+
this.pending.clear();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
abort(error) {
|
|
205
|
+
this.rejectAll(error instanceof Error ? error : new Error('MCP child aborted'));
|
|
206
|
+
if (this.child && !this.closed) this.child.kill('SIGKILL');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async stop() {
|
|
210
|
+
if (!this.child || this.closed) return;
|
|
211
|
+
this.child.stdin.end();
|
|
212
|
+
await new Promise((resolveExit) => {
|
|
213
|
+
const timer = setTimeout(() => {
|
|
214
|
+
if (!this.closed) this.child.kill('SIGKILL');
|
|
215
|
+
resolveExit();
|
|
216
|
+
}, 500);
|
|
217
|
+
this.child.once('exit', () => {
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
resolveExit();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function runCanary(env = process.env, options = {}) {
|
|
226
|
+
const key = env.MARROW_API_KEY || '';
|
|
227
|
+
if (!key) throw new Error('MARROW_API_KEY is required for the authenticated MCP control-path canary');
|
|
228
|
+
const transportTimeoutMs = boundedMs('MARROW_REQUEST_TIMEOUT_MS', 2_500, 250, 10_000, env);
|
|
229
|
+
const totalTimeoutMs = boundedMs('MARROW_MCP_CANARY_TOTAL_TIMEOUT_MS', 30_000, 2_000, 60_000, env);
|
|
230
|
+
const expectedVersion = env.MARROW_EXPECTED_MCP_VERSION || packageVersion;
|
|
231
|
+
const childPath = env.MARROW_MCP_CANARY_CHILD || resolve(__dirname, '../dist/cli.js');
|
|
232
|
+
const childEnv = {
|
|
233
|
+
...env,
|
|
234
|
+
MARROW_API_KEY: key,
|
|
235
|
+
MARROW_BASE_URL: env.MARROW_BASE_URL || 'https://api.getmarrow.ai',
|
|
236
|
+
MARROW_AUTO_ENROLL: 'false',
|
|
237
|
+
MARROW_TOOL_PROFILE: 'full',
|
|
238
|
+
MARROW_CONTROL_PATH_CANARY: '1',
|
|
239
|
+
MARROW_REQUEST_TIMEOUT_MS: String(transportTimeoutMs),
|
|
240
|
+
};
|
|
241
|
+
delete childEnv.NODE_TEST_CONTEXT;
|
|
242
|
+
const client = new PersistentMcpClient({
|
|
243
|
+
command: process.execPath,
|
|
244
|
+
args: [childPath],
|
|
245
|
+
timeoutMs: transportTimeoutMs,
|
|
246
|
+
env: childEnv,
|
|
247
|
+
spawnProcess: options.spawnProcess,
|
|
248
|
+
});
|
|
249
|
+
const totalTimer = setTimeout(() => client.abort(new Error('MCP canary total timeout')), totalTimeoutMs);
|
|
250
|
+
const processStarted = performance.now();
|
|
251
|
+
client.start();
|
|
252
|
+
try {
|
|
253
|
+
const initialized = await client.request('initialize', {}, transportTimeoutMs + 3_000);
|
|
254
|
+
if (initialized.error) throw new Error(`MCP initialize failed ${initialized.error.code}`);
|
|
255
|
+
const version = initialized.result?.serverInfo?.version;
|
|
256
|
+
if (!version) throw new Error('MCP initialize produced no version');
|
|
257
|
+
if (version !== expectedVersion) throw new Error(`MCP canary loaded ${version} instead of expected ${expectedVersion}`);
|
|
258
|
+
const listed = await client.request('tools/list', {});
|
|
259
|
+
if (listed.error) throw new Error(`MCP tools/list failed ${listed.error.code}`);
|
|
260
|
+
const names = new Set((listed.result?.tools || []).map((tool) => tool.name));
|
|
261
|
+
for (const [name] of cases) {
|
|
262
|
+
if (!names.has(name)) throw new Error(`${name} is missing from the full MCP contract`);
|
|
263
|
+
}
|
|
264
|
+
const initializationMs = Math.round(performance.now() - processStarted);
|
|
265
|
+
const results = [];
|
|
266
|
+
for (const [name, args] of cases) {
|
|
267
|
+
const callStarted = performance.now();
|
|
268
|
+
const called = await client.request('tools/call', { name, arguments: args });
|
|
269
|
+
const latencyMs = Math.round(performance.now() - callStarted);
|
|
270
|
+
const payload = toolPayload(called, name);
|
|
271
|
+
validatePayload(name, payload);
|
|
272
|
+
const live = payload.stale !== true && payload.source !== 'last_known' && payload.available !== false;
|
|
273
|
+
if (!live) throw new Error(`${name} returned cached or unavailable guidance`);
|
|
274
|
+
results.push({ tool: name, ok: true, live: true, latency_ms: latencyMs });
|
|
275
|
+
}
|
|
276
|
+
const hotPath = results.filter((row) => HOT_PATH_TOOLS.has(row.tool));
|
|
277
|
+
const reports = results.filter((row) => !HOT_PATH_TOOLS.has(row.tool));
|
|
278
|
+
return {
|
|
279
|
+
ok: true,
|
|
280
|
+
package_version: version,
|
|
281
|
+
process_count: 1,
|
|
282
|
+
initialization_ms: initializationMs,
|
|
283
|
+
per_tool_latency_excludes_initialization: true,
|
|
284
|
+
tools_checked: results.length,
|
|
285
|
+
latency_groups: {
|
|
286
|
+
hot_path: latencyGroup(hotPath),
|
|
287
|
+
reports: latencyGroup(reports),
|
|
288
|
+
},
|
|
289
|
+
results,
|
|
290
|
+
};
|
|
291
|
+
} finally {
|
|
292
|
+
clearTimeout(totalTimer);
|
|
293
|
+
await client.stop();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (require.main === module) {
|
|
298
|
+
runCanary().then((result) => {
|
|
299
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
300
|
+
}).catch((error) => {
|
|
301
|
+
process.stderr.write(`MCP_CONTROL_PATH_CANARY=FAIL ${error instanceof Error ? error.message : 'unknown_error'}\n`);
|
|
302
|
+
process.exitCode = 1;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = { PersistentMcpClient, latencyGroup, percentile, runCanary };
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/getmarrow/marrow-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "3.9.
|
|
9
|
+
"version": "3.9.58",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@getmarrow/mcp",
|
|
14
|
-
"version": "3.9.
|
|
14
|
+
"version": "3.9.58",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|