@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.
@@ -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.56",
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.56",
9
+ "version": "3.9.58",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@getmarrow/mcp",
14
- "version": "3.9.56",
14
+ "version": "3.9.58",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },