@getmarrow/mcp 3.9.57 → 3.9.59

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 CHANGED
@@ -57,7 +57,7 @@ Verify current claims through the [public evidence manifest](https://getmarrow.a
57
57
  ## Install
58
58
 
59
59
  ```bash
60
- npx @getmarrow/mcp setup
60
+ npx -y --package=@getmarrow/mcp@latest marrow-mcp setup
61
61
  ```
62
62
 
63
63
  Set the key through trusted secret storage:
@@ -75,7 +75,7 @@ Then configure the MCP server:
75
75
  "mcpServers": {
76
76
  "marrow": {
77
77
  "command": "npx",
78
- "args": ["-y", "@getmarrow/mcp"]
78
+ "args": ["-y", "--package=@getmarrow/mcp@latest", "marrow-mcp"]
79
79
  }
80
80
  }
81
81
  }
@@ -96,17 +96,39 @@ npx -y @getmarrow/install@latest activate
96
96
  npx -y @getmarrow/install@latest doctor
97
97
 
98
98
  # Manual MCP-only setup
99
- npx -y @getmarrow/mcp@latest setup
99
+ npx -y --package=@getmarrow/mcp@latest marrow-mcp setup
100
100
 
101
101
  # Verify live read latency, last success, and local backlog
102
- npx -y @getmarrow/mcp@latest ping
102
+ npx -y --package=@getmarrow/mcp@latest marrow-mcp ping
103
103
  ```
104
104
 
105
105
  Detection and notification are automatic. After explicit installer activation, the local controller may restore only Marrow-managed hooks/configuration. Package upgrades, owner policy, credentials, and unrelated configuration remain explicit and subject to the operator's normal change policy.
106
106
 
107
- ## What's New in v3.9.57
107
+ ## What's New in v3.9.59
108
108
 
109
- v3.9.57 makes the agent control path reliable and deliberately small by default:
109
+ v3.9.59 makes the six-tool control path reliable and honest across ordinary edge and geographic latency:
110
+
111
+ - the default health deadline is 2.5 seconds instead of an unrealistically narrow 400 ms;
112
+ - `MARROW_PING_TIMEOUT_MS` can tune the probe between 500 ms and 5 seconds;
113
+ - authenticated control reads tolerate cold network/TLS paths while cached reads still return quickly;
114
+ - status, ask, runtime, and handoff responses report measured current/p50/p99 latency plus owner-only queue health;
115
+ - a first-session outage returns a clearly labeled local safety brief, and infrastructure failures are never mislabeled as policy denials;
116
+ - timeout errors return a concrete retry delay instead of an unresolved placeholder;
117
+ - Cloudflare edge denials are separated from key-scope or Marrow policy rejections;
118
+ - MCP initialization carries the always-on control/proof instructions even when a client does not request the optional prompt template;
119
+ - update, launch, setup, spool, and ping commands use the unambiguous `npx --package ... marrow-mcp` form;
120
+ - explicit spool drains tolerate slow edge delivery without extending passive hook latency.
121
+
122
+ ## Previous: v3.9.58
123
+
124
+ v3.9.58 makes latency evidence accurate by reusing one initialized MCP process for the complete control-path canary:
125
+
126
+ - startup, initialization, and tool discovery are measured separately from authenticated tool calls;
127
+ - hot-path steering and report latency are reported as separate p50, p95, p99, and maximum groups;
128
+ - response IDs, malformed output, RPC failures, timeouts, and incomplete tool contracts fail closed;
129
+ - one bounded process prevents package and process startup time from being mistaken for API latency.
130
+
131
+ The compact agent control path introduced in v3.9.57 remains the default:
110
132
 
111
133
  - `marrow_status`, `marrow_ask`, and `marrow_agent_runtime` use authenticated routes with bounded retries and typed failures;
112
134
  - transient read failures return an owner-only last-known brief when available, while cached guidance can never authorize high-risk work;
@@ -126,7 +148,7 @@ v3.9.56 adds tenant-scoped coordination and evidence-only replay to the existing
126
148
  - transient failures can use an owner-only, account/key/agent-scoped last-known brief for at most one hour, clearly labeled with its age;
127
149
  - 401 and 403 responses never use cached guidance, and cached runtime guidance cannot authorize high-risk work;
128
150
  - `marrow_ask` now maps to the canonical decision brief contract instead of a separate route;
129
- - `npx -y @getmarrow/mcp@latest ping` reports current latency, rolling measured p50/p99, last success, and lifecycle backlog health;
151
+ - `npx -y --package=@getmarrow/mcp@latest marrow-mcp ping` reports current latency, rolling measured p50/p99, last success, and lifecycle backlog health;
130
152
  - `marrow_coordinate` acquires/releases tenant-scoped resource leases and carries compact child proof packets without sharing transcripts;
131
153
  - `marrow_replay_compare` compares already-recorded baseline and candidate outcomes with durable proof and never executes either model;
132
154
  - both new tools preserve agent-bound key scope, reject unsafe path identifiers, and return unavailable or incomplete evidence rather than manufacturing a winner.
@@ -309,15 +331,15 @@ it does not run models, retain prompts, or infer a winner from labels.
309
331
 
310
332
  ## Passive Use
311
333
 
312
- `npx @getmarrow/mcp setup` installs supported prompt, tool-result, and session-stop hooks so the agent can receive before-action context, record meaningful tool outcomes, and keep unfinished closure visible without the owner repeatedly prompting it to use Marrow.
334
+ `npx -y --package=@getmarrow/mcp@latest marrow-mcp setup` installs supported prompt, tool-result, and session-stop hooks so the agent can receive before-action context, record meaningful tool outcomes, and keep unfinished closure visible without the owner repeatedly prompting it to use Marrow.
313
335
 
314
336
  The hooks send compact classifications and lifecycle receipts. They do not need raw prompts, completions, command output, tool output, or credentials. A completed tool or session does not automatically become a successful business outcome; explicit success/failure closure is required.
315
337
 
316
338
  Transient lifecycle receipts use a bounded owner-only spool and are retried with stable event IDs. Operators can inspect and drain it without exposing event content:
317
339
 
318
340
  ```bash
319
- npx @getmarrow/mcp spool-status
320
- npx @getmarrow/mcp drain-spool
341
+ npx -y --package=@getmarrow/mcp@latest marrow-mcp spool-status
342
+ npx -y --package=@getmarrow/mcp@latest marrow-mcp drain-spool
321
343
  ```
322
344
 
323
345
  The output contains only state, exact pending/failed counts, oldest receipt timestamps, capacity, and an exact fix. Terminal validation/authentication failures and exhausted retries remain explicit durable failures rather than cycling indefinitely.
package/dist/cli.js CHANGED
@@ -21,6 +21,7 @@ const guidance_cache_1 = require("./guidance-cache");
21
21
  const request_reliability_1 = require("./request-reliability");
22
22
  const hook_contract_1 = require("./hook-contract");
23
23
  const ping_state_1 = require("./ping-state");
24
+ const control_path_state_1 = require("./control-path-state");
24
25
  const redact_1 = require("./redact");
25
26
  const runtime_contract_1 = require("./runtime-contract");
26
27
  // Parse CLI args
@@ -73,7 +74,7 @@ async function runPingCommand() {
73
74
  }
74
75
  const baseUrl = (0, index_1.validateBaseUrl)(resolved.baseUrl || 'https://api.getmarrow.ai');
75
76
  const controller = new AbortController();
76
- const timer = setTimeout(() => controller.abort(), 400);
77
+ const timer = setTimeout(() => controller.abort(), (0, ping_state_1.resolvePingTimeoutMs)(process.env.MARROW_PING_TIMEOUT_MS));
77
78
  timer.unref?.();
78
79
  const started = Date.now();
79
80
  try {
@@ -127,11 +128,13 @@ async function runSpoolCommand(drain) {
127
128
  ? await (0, lifecycle_spool_1.drainLifecycleSpool)({ apiKey, baseUrl, agentId: resolved.agentId || undefined })
128
129
  : (0, lifecycle_spool_1.lifecycleSpoolStatus)({ apiKey, agentId: resolved.agentId || undefined });
129
130
  process.stdout.write(`${JSON.stringify({
130
- ok: status.state !== 'attention_required',
131
+ ok: drain ? status.state === 'clear' : status.state !== 'attention_required',
131
132
  lifecycle_spool: status,
132
133
  }, null, 2)}\n`);
133
134
  if (status.state === 'attention_required')
134
135
  process.exitCode = 2;
136
+ else if (drain && status.state !== 'clear')
137
+ process.exitCode = 1;
135
138
  }
136
139
  // ─── Setup command: inject Marrow instructions into CLAUDE.md ───
137
140
  function runSetup() {
@@ -441,27 +444,56 @@ if (process.argv[2] !== 'keys') {
441
444
  }
442
445
  function toolFailure(toolName, failure) {
443
446
  const result = (0, request_reliability_1.structuredRequestFailure)(failure);
444
- const transient = failure.retryable && !['authentication_required', 'permission_denied'].includes(failure.code);
445
- const supportsStale = ['marrow_agent_runtime', 'marrow_orient', 'marrow_ask', 'marrow_runtime_status', 'marrow_status'].includes(toolName || '');
446
- if (transient && supportsStale) {
447
+ const infrastructureFailure = !['authentication_required', 'permission_denied'].includes(failure.code);
448
+ const supportsStale = ['marrow_agent_runtime', 'marrow_orient', 'marrow_ask', 'marrow_handoff_status', 'marrow_runtime_status', 'marrow_status'].includes(toolName || '');
449
+ const spool = (0, lifecycle_spool_1.lifecycleSpoolStatus)({ apiKey: API_KEY, agentId: FLEET_AGENT_ID });
450
+ result.failure_kind = infrastructureFailure ? 'infrastructure' : 'authorization';
451
+ result.control_path = (0, control_path_state_1.controlPathStats)(toolName || 'marrow_control');
452
+ result.lifecycle_spool = {
453
+ ...spool,
454
+ drain_command: 'npx -y --package=@getmarrow/mcp@latest marrow-mcp drain-spool',
455
+ };
456
+ if (infrastructureFailure && supportsStale) {
447
457
  let cached = null;
448
458
  try {
449
459
  cached = (0, guidance_cache_1.readGuidanceCache)({ apiKey: API_KEY, baseUrl: BASE_URL, agentId: FLEET_AGENT_ID });
450
460
  }
451
461
  catch { /* owner-only cache is best effort */ }
452
- if (cached) {
453
- result.stale_brief = cached.context;
454
- result.stale_ms = cached.stale_ms;
455
- }
456
- result.disposition = 'review_required';
457
- result.allow = false;
462
+ result.stale_brief = cached?.context || [
463
+ '## Marrow control-path outage brief',
464
+ '- This is an infrastructure failure; Marrow returned no policy decision and did not deny the action.',
465
+ '- Preserve local evidence and continue only low-risk, reversible work under the owner\'s existing safeguards.',
466
+ '- Do not perform high-risk work until a fresh Marrow runtime gate returns allow, warn, review_required, or block.',
467
+ ].join('\n');
468
+ result.stale_source = cached ? 'last_known_guidance' : 'local_outage_safety';
469
+ result.stale_ms = cached?.stale_ms ?? null;
470
+ result.authorization_state = 'unavailable';
471
+ result.gate_obtained = false;
458
472
  result.stale_can_authorize_high_risk = false;
473
+ const retryAfterMs = failure.retryAfterMs;
459
474
  result.exact_next_action = cached
460
475
  ? 'Use this last-known brief for low-risk context only. Obtain a fresh Marrow runtime gate before high-risk work.'
461
- : 'Retry once after retry_after_ms. Do not perform high-risk work without a fresh Marrow runtime gate.';
476
+ : retryAfterMs != null
477
+ ? `Wait ${retryAfterMs} ms, then retry once. Do not perform high-risk work without a fresh Marrow runtime gate.`
478
+ : failure.exactFix;
462
479
  }
463
480
  return result;
464
481
  }
482
+ function clientOperationalPayload(toolName, value) {
483
+ const payload = value && typeof value === 'object' && !Array.isArray(value)
484
+ ? value
485
+ : { data: value };
486
+ const spool = (0, lifecycle_spool_1.lifecycleSpoolStatus)({ apiKey: API_KEY, agentId: FLEET_AGENT_ID });
487
+ return {
488
+ ...payload,
489
+ client_update: payload.client_update || (0, request_reliability_1.localClientUpdate)(),
490
+ control_path: (0, control_path_state_1.controlPathStats)(toolName),
491
+ lifecycle_spool: {
492
+ ...spool,
493
+ drain_command: 'npx -y --package=@getmarrow/mcp@latest marrow-mcp drain-spool',
494
+ },
495
+ };
496
+ }
465
497
  // [FIX #9] Runtime validation helper for required string params
466
498
  function requireString(args, name) {
467
499
  const val = args[name];
@@ -489,16 +521,24 @@ if (process.argv[2] !== 'keys') {
489
521
  }
490
522
  }
491
523
  async function withControlDeadline(operation, options = {}) {
524
+ const toolName = options.toolName || 'marrow_control';
492
525
  const hasCache = options.cacheAware !== false && Boolean(cachedGuidance());
493
- const configuredCanaryMs = Number(process.env.MARROW_REQUEST_TIMEOUT_MS);
494
- const timeoutMs = process.env.MARROW_CONTROL_PATH_CANARY === '1' && Number.isFinite(configuredCanaryMs)
495
- ? Math.min(5_000, Math.max(500, Math.floor(configuredCanaryMs)))
496
- : options.highRisk ? 2_000 : hasCache ? 400 : 1_200;
526
+ const configuredTimeoutMs = Number(process.env.MARROW_REQUEST_TIMEOUT_MS);
527
+ const timeoutMs = Number.isFinite(configuredTimeoutMs)
528
+ ? Math.min(10_000, Math.max(150, Math.floor(configuredTimeoutMs)))
529
+ : options.highRisk ? 4_500 : hasCache ? 500 : 4_000;
497
530
  const controller = new AbortController();
498
531
  const timer = setTimeout(() => controller.abort(), timeoutMs);
499
532
  timer.unref?.();
533
+ const startedAt = Date.now();
500
534
  try {
501
- return await operation(controller.signal);
535
+ const result = await operation(controller.signal);
536
+ (0, control_path_state_1.recordControlPathSample)(toolName, Date.now() - startedAt, true);
537
+ return result;
538
+ }
539
+ catch (error) {
540
+ (0, control_path_state_1.recordControlPathSample)(toolName, Date.now() - startedAt, false);
541
+ throw error;
502
542
  }
503
543
  finally {
504
544
  clearTimeout(timer);
@@ -1631,6 +1671,9 @@ if (process.argv[2] !== 'keys') {
1631
1671
  protocolVersion: '2024-11-05',
1632
1672
  capabilities: { tools: {}, prompts: {} },
1633
1673
  serverInfo: { name: 'marrow', version: hook_contract_1.MCP_ADAPTER_VERSION },
1674
+ ...(AUTO_ENROLL ? {
1675
+ instructions: 'Use marrow_agent_runtime before consequential actions, obey fresh allow/warn/review_required/block results, use marrow_ask for relevant prior lessons, and close meaningful outcomes with marrow_commit. Infrastructure failures are not policy denials; continue only low-risk reversible work from the returned outage-safe brief, and require a fresh gate for high-risk work.',
1676
+ } : {}),
1634
1677
  });
1635
1678
  // Auto-enroll: emit enrollment notification on connection
1636
1679
  if (AUTO_ENROLL) {
@@ -1731,7 +1774,7 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
1731
1774
  orientCallCount++;
1732
1775
  const wantAutoWarn = args.autoWarn ?? true;
1733
1776
  const taskType = args.taskType;
1734
- const result = await withControlDeadline((signal) => (0, index_1.marrowOrient)(API_KEY, BASE_URL, { taskType, autoWarn: wantAutoWarn }, SESSION_ID, FLEET_AGENT_ID, signal), { highRisk: isHighRiskAction(`Orient before ${taskType || 'general'} work`, taskType) });
1777
+ const result = await withControlDeadline((signal) => (0, index_1.marrowOrient)(API_KEY, BASE_URL, { taskType, autoWarn: wantAutoWarn }, SESSION_ID, FLEET_AGENT_ID, signal), { highRisk: isHighRiskAction(`Orient before ${taskType || 'general'} work`, taskType), toolName: 'marrow_orient' });
1735
1778
  if (AUTO_ENROLL && orientCallCount === 1) {
1736
1779
  const enrollmentText = `\n\n**Marrow control and proof active**\n\n` +
1737
1780
  `Marrow applies relevant policy and prior lessons before consequential actions, then records evidence and the real outcome afterward.\n\n` +
@@ -1905,7 +1948,7 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
1905
1948
  session_id: SESSION_ID,
1906
1949
  proof: suppliedProof,
1907
1950
  context: { source: 'mcp_auto_risk_upgrade' },
1908
- }, SESSION_ID, FLEET_AGENT_ID, signal), { highRisk: true });
1951
+ }, SESSION_ID, FLEET_AGENT_ID, signal), { highRisk: true, toolName: 'marrow_auto.runtime' });
1909
1952
  storeRuntimeGuidance(runtimeGate);
1910
1953
  }
1911
1954
  const proofCanClose = !highRisk || (0, runtime_contract_1.highRiskRuntimeCanClose)(runtimeGate, suppliedProof, args.gate_receipt_id);
@@ -1979,7 +2022,7 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
1979
2022
  }
1980
2023
  if (toolName === 'marrow_ask') {
1981
2024
  const query = requireString(args, 'query');
1982
- const result = await withControlDeadline((signal) => (0, index_1.marrowAsk)(API_KEY, BASE_URL, { query }, SESSION_ID, FLEET_AGENT_ID, signal));
2025
+ const result = await withControlDeadline((signal) => (0, index_1.marrowAsk)(API_KEY, BASE_URL, { query }, SESSION_ID, FLEET_AGENT_ID, signal), { toolName: 'marrow_ask' });
1983
2026
  try {
1984
2027
  (0, guidance_cache_1.writeGuidanceCache)({
1985
2028
  apiKey: API_KEY,
@@ -1989,15 +2032,12 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
1989
2032
  });
1990
2033
  }
1991
2034
  catch { /* owner-only cache is best effort */ }
1992
- success(id, {
1993
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1994
- });
2035
+ toolSuccess(id, clientOperationalPayload('marrow_ask', result));
1995
2036
  return;
1996
2037
  }
1997
2038
  if (toolName === 'marrow_status') {
1998
- const result = await withControlDeadline((signal) => (0, index_1.marrowStatus)(API_KEY, BASE_URL, SESSION_ID, FLEET_AGENT_ID, signal), { cacheAware: false });
1999
- const statusRecord = result;
2000
- toolSuccess(id, { ...statusRecord, client_update: statusRecord.client_update || (0, request_reliability_1.localClientUpdate)() });
2039
+ const result = await withControlDeadline((signal) => (0, index_1.marrowStatus)(API_KEY, BASE_URL, SESSION_ID, FLEET_AGENT_ID, signal), { cacheAware: false, toolName: 'marrow_status' });
2040
+ toolSuccess(id, clientOperationalPayload('marrow_status', result));
2001
2041
  return;
2002
2042
  }
2003
2043
  if (toolName === 'marrow_create_key') {
@@ -2135,7 +2175,7 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
2135
2175
  return;
2136
2176
  }
2137
2177
  if (toolName === 'marrow_runtime_status') {
2138
- const result = await withControlDeadline((signal) => (0, index_1.marrowRuntimeStatus)(API_KEY, BASE_URL, args.fast !== false, SESSION_ID, FLEET_AGENT_ID, signal), { cacheAware: false });
2178
+ const result = await withControlDeadline((signal) => (0, index_1.marrowRuntimeStatus)(API_KEY, BASE_URL, args.fast !== false, SESSION_ID, FLEET_AGENT_ID, signal), { cacheAware: false, toolName: 'marrow_runtime_status' });
2139
2179
  success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
2140
2180
  return;
2141
2181
  }
@@ -2191,9 +2231,9 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
2191
2231
  : undefined,
2192
2232
  period: args.period,
2193
2233
  };
2194
- const result = await withControlDeadline((signal) => (0, index_1.marrowAgentRuntime)(API_KEY, BASE_URL, runtimeInput, SESSION_ID, FLEET_AGENT_ID, signal), { highRisk: isHighRiskAction(runtimeInput.action, runtimeInput.type) });
2234
+ const result = await withControlDeadline((signal) => (0, index_1.marrowAgentRuntime)(API_KEY, BASE_URL, runtimeInput, SESSION_ID, FLEET_AGENT_ID, signal), { highRisk: isHighRiskAction(runtimeInput.action, runtimeInput.type), toolName: 'marrow_agent_runtime' });
2195
2235
  storeRuntimeGuidance(result);
2196
- success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
2236
+ toolSuccess(id, clientOperationalPayload('marrow_agent_runtime', result));
2197
2237
  return;
2198
2238
  }
2199
2239
  if (toolName === 'marrow_arbitrate') {
@@ -2391,12 +2431,12 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
2391
2431
  return;
2392
2432
  }
2393
2433
  if (toolName === 'marrow_handoff_status') {
2394
- const result = await (0, index_1.marrowHandoffStatus)(API_KEY, BASE_URL, {
2434
+ const result = await withControlDeadline((signal) => (0, index_1.marrowHandoffStatus)(API_KEY, BASE_URL, {
2395
2435
  status: args.status,
2396
2436
  agentId: args.agentId || FLEET_AGENT_ID,
2397
2437
  limit: args.limit,
2398
- }, SESSION_ID, FLEET_AGENT_ID);
2399
- success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
2438
+ }, SESSION_ID, FLEET_AGENT_ID, signal), { cacheAware: false, toolName: 'marrow_handoff_status' });
2439
+ toolSuccess(id, clientOperationalPayload('marrow_handoff_status', result));
2400
2440
  return;
2401
2441
  }
2402
2442
  if (toolName === 'marrow_session_end') {