@getmarrow/mcp 3.9.85 → 3.9.87

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
@@ -130,7 +130,13 @@ A direct `marrow_think` can receive a durable pending response before the backen
130
130
 
131
131
  A saved `observed_unverified` outcome is terminal observation evidence, not a committed outcome. Receipt expiry cannot retroactively authorize completed work. Preserve the original decision, receipt, proof and key; an already authorized durable checkpoint may finish through its existing exact recovery path. Do not repeat the action merely to obtain a fresh receipt.
132
132
 
133
- ## What's New in v3.9.85
133
+ ## What's New in v3.9.87
134
+
135
+ v3.9.87 hardens the authenticated control-path canary against single-sample transport blips so monitoring stops flapping on a healthy service. The canary now retries transport-class delivery failures (`request_failed`, `service_unavailable`, `connection_reset`, `dns_unavailable`, `tls_failure`, `edge_access_denied`, `rate_limited`) in-run with the same idempotent operation before declaring a failure, gives the asynchronous `marrow_auto` and `marrow_first_value` calls a separate deadline ceiling (thirty seconds via `MARROW_MCP_CANARY_ASYNC_TOOL_TIMEOUT_MS`), and raises the default total canary budget to forty-five seconds so bounded retries fit. Results recovered by a retry are annotated with `recovered_on_retry: true`. Authentication, authorization, contract, and package-identity failures remain immediate hard failures with no retry. Tool surface, client contracts, and request deadlines are unchanged.
136
+
137
+ v3.9.86 was published with the adapter version constant still at `3.9.85`, so the strict canary identity check rejected it; it is deprecated — use `3.9.87`. SDK `3.7.62` and installer `0.1.56` are unchanged; install MCP `3.9.87`, reload the host, review hook trust, and verify before claiming the updated client is active.
138
+
139
+ ### Previous release: v3.9.85
134
140
 
135
141
  v3.9.85 keeps the existing Marrow Auto request and response deadlines active through response-body consumption and JSON parsing, so a server that sends headers and then stalls ends with the same typed bounded timeout as a stalled header response. The retry owner, four-second write-attempt ceiling, and eight-second Auto response budget are unchanged.
136
142
 
@@ -1,8 +1,8 @@
1
1
  import { resolveMarrowEnv, type ResolvedMarrowEnv } from './env';
2
- export declare const MCP_ADAPTER_VERSION = "3.9.85";
2
+ export declare const MCP_ADAPTER_VERSION = "3.9.87";
3
3
  export declare const NATIVE_HOOK_MATCHER = "Bash|Edit|Write|MultiEdit|mcp__(?!marrow__marrow_).*";
4
4
  export declare const GROK_NATIVE_HOOK_MATCHER = "run_terminal_command|search_replace|write|spawn_subagent|use_tool|workflow|image_gen|image_edit|image_to_video|reference_to_video";
5
- export declare const MCP_PACKAGE_SPEC = "@getmarrow/mcp@3.9.85";
5
+ export declare const MCP_PACKAGE_SPEC = "@getmarrow/mcp@3.9.87";
6
6
  export declare const CONTEXT_HOOK_COMMAND: string;
7
7
  export declare const PRE_ACTION_HOOK_COMMAND: string;
8
8
  export declare const ACTION_RESULT_HOOK_COMMAND: string;
@@ -20,7 +20,7 @@ const node_fs_1 = require("node:fs");
20
20
  const node_os_1 = require("node:os");
21
21
  const node_path_1 = require("node:path");
22
22
  const env_1 = require("./env");
23
- exports.MCP_ADAPTER_VERSION = '3.9.85';
23
+ exports.MCP_ADAPTER_VERSION = '3.9.87';
24
24
  exports.NATIVE_HOOK_MATCHER = 'Bash|Edit|Write|MultiEdit|mcp__(?!marrow__marrow_).*';
25
25
  exports.GROK_NATIVE_HOOK_MATCHER = 'run_terminal_command|search_replace|write|spawn_subagent|use_tool|workflow|image_gen|image_edit|image_to_video|reference_to_video';
26
26
  exports.MCP_PACKAGE_SPEC = `@getmarrow/mcp@${exports.MCP_ADAPTER_VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/mcp",
3
- "version": "3.9.85",
3
+ "version": "3.9.87",
4
4
  "mcpName": "io.github.getmarrow/marrow",
5
5
  "description": "Runtime control, proof, and fleet intelligence for MCP-compatible AI agents.",
6
6
  "bin": {
@@ -16,14 +16,24 @@ try {
16
16
 
17
17
  const CANARY_TOOL_TIMEOUT_MARGIN_MS = 2_000;
18
18
  const CANARY_TOOL_TIMEOUT_MAX_MS = 10_000;
19
+ const CANARY_ASYNC_TOOL_TIMEOUT_MAX_MS = 30_000;
19
20
  const CANARY_DEFAULT_TOOL_TIMEOUT_MS = Math.min(
20
21
  CANARY_TOOL_TIMEOUT_MAX_MS,
21
22
  (MARROW_AUTO_RESPONSE_BUDGET_MAX_MS || 8_000) + CANARY_TOOL_TIMEOUT_MARGIN_MS,
22
23
  );
24
+ const CANARY_DEFAULT_ASYNC_TOOL_TIMEOUT_MS = Math.min(
25
+ CANARY_ASYNC_TOOL_TIMEOUT_MAX_MS,
26
+ (MARROW_AUTO_RESPONSE_BUDGET_MAX_MS || 8_000) + CANARY_TOOL_TIMEOUT_MARGIN_MS,
27
+ );
28
+ const CANARY_DEFAULT_TOTAL_TIMEOUT_MS = 45_000;
29
+ const TRANSPORT_RETRY_WAIT_MS = 1_000;
23
30
 
24
31
  if (CANARY_DEFAULT_TOOL_TIMEOUT_MS < MARROW_AUTO_RESPONSE_BUDGET_MAX_MS + CANARY_TOOL_TIMEOUT_MARGIN_MS) {
25
32
  configurationFailed = true;
26
33
  }
34
+ if (CANARY_DEFAULT_ASYNC_TOOL_TIMEOUT_MS < MARROW_AUTO_RESPONSE_BUDGET_MAX_MS + CANARY_TOOL_TIMEOUT_MARGIN_MS) {
35
+ configurationFailed = true;
36
+ }
27
37
 
28
38
  function canaryCases(operationId) {
29
39
  return [
@@ -56,6 +66,15 @@ const HOT_PATH_TOOLS = new Set([
56
66
  'marrow_agent_runtime',
57
67
  'marrow_auto',
58
68
  ]);
69
+ const ASYNC_PATH_TOOLS = new Set([
70
+ 'marrow_auto',
71
+ 'marrow_first_value',
72
+ ]);
73
+ const TRANSPORT_RETRY_ERROR_CLASSES = new Set(['tool_unavailable', 'unavailable503']);
74
+ const AUTO_TRANSPORT_RETRY_CATEGORIES = new Set([
75
+ 'request_failed', 'service_unavailable', 'connection_reset', 'dns_unavailable',
76
+ 'tls_failure', 'edge_access_denied', 'rate_limited',
77
+ ]);
59
78
  const OUTPUT_LIMIT_BYTES = 256 * 1024;
60
79
  const TOOL_NAMES = new Set(canaryCases('').map(([name]) => name));
61
80
  const errorEvidence = new WeakMap();
@@ -542,7 +561,14 @@ async function executeCanary(env, options, context) {
542
561
  CANARY_TOOL_TIMEOUT_MAX_MS,
543
562
  env,
544
563
  );
545
- const totalTimeoutMs = boundedMs('MARROW_MCP_CANARY_TOTAL_TIMEOUT_MS', 30_000, 2_000, 60_000, env);
564
+ const asyncToolTimeoutMs = boundedMs(
565
+ 'MARROW_MCP_CANARY_ASYNC_TOOL_TIMEOUT_MS',
566
+ CANARY_DEFAULT_ASYNC_TOOL_TIMEOUT_MS,
567
+ 250,
568
+ CANARY_ASYNC_TOOL_TIMEOUT_MAX_MS,
569
+ env,
570
+ );
571
+ const totalTimeoutMs = boundedMs('MARROW_MCP_CANARY_TOTAL_TIMEOUT_MS', CANARY_DEFAULT_TOTAL_TIMEOUT_MS, 2_000, 60_000, env);
546
572
  const expectedVersion = env.MARROW_EXPECTED_MCP_VERSION || packageVersion;
547
573
  const childPath = env.MARROW_MCP_CANARY_CHILD || resolve(__dirname, '../dist/cli.js');
548
574
  const childEnv = {
@@ -598,9 +624,11 @@ async function executeCanary(env, options, context) {
598
624
  const results = context.results;
599
625
  for (const [name, args] of cases) {
600
626
  const callStarted = performance.now();
627
+ const perToolTimeoutMs = ASYNC_PATH_TOOLS.has(name) ? asyncToolTimeoutMs : toolTimeoutMs;
601
628
  let called;
602
629
  let payload;
603
630
  let attempts = 0;
631
+ let recoveredOnRetry = false;
604
632
  context.retryWaitMs = 0;
605
633
  context.autoMetrics = undefined;
606
634
  context.autoPhase = undefined;
@@ -613,13 +641,13 @@ async function executeCanary(env, options, context) {
613
641
  stage('tool_call', name, attempt + 1);
614
642
  attempts += 1;
615
643
  try {
616
- called = await client.request('tools/call', { name, arguments: args });
644
+ called = await client.request('tools/call', { name, arguments: args }, perToolTimeoutMs);
617
645
  if (client.fatalError) throw client.fatalError;
618
646
  stage('tool_validate', name, attempt + 1);
619
647
  payload = toolPayload(called, name);
620
648
  } catch (error) {
649
+ const evidence = errorEvidence.get(error);
621
650
  if (name === 'marrow_auto') {
622
- const evidence = errorEvidence.get(error);
623
651
  autoAttemptRecords.push({
624
652
  attempt: attempt + 1,
625
653
  duration_ms: safeMs(performance.now() - outerAttemptStarted),
@@ -631,6 +659,17 @@ async function executeCanary(env, options, context) {
631
659
  actual_wait_ms: 0,
632
660
  });
633
661
  }
662
+ // One bounded in-run retry for transport-class failures: a single edge
663
+ // reset or 503 must not fail the canary when the very next call succeeds.
664
+ const remaining = totalTimeoutMs - (performance.now() - context.started);
665
+ const mayRetry = name === 'marrow_auto' ? attempt < 2 : attempt < 1;
666
+ if (evidence && TRANSPORT_RETRY_ERROR_CLASSES.has(evidence.error_class)
667
+ && mayRetry && remaining > TRANSPORT_RETRY_WAIT_MS + 250) {
668
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, TRANSPORT_RETRY_WAIT_MS));
669
+ context.retryWaitMs += TRANSPORT_RETRY_WAIT_MS;
670
+ recoveredOnRetry = true;
671
+ continue;
672
+ }
634
673
  throw error;
635
674
  }
636
675
  if (name !== 'marrow_auto') break;
@@ -643,11 +682,17 @@ async function executeCanary(env, options, context) {
643
682
  context.autoMetrics = autoTimings(payload);
644
683
  const waitClass = autoWaitClass(payload);
645
684
  context.autoPhase = waitClass ? payload.phase : undefined;
646
- if (waitClass !== 'completion_pending') break;
685
+ const autoFailureCategory = payload?.live_delivery?.failure?.error?.category;
686
+ const transportRetryable = waitClass == null
687
+ && AUTO_TRANSPORT_RETRY_CATEGORIES.has(autoFailureCategory);
688
+ if (waitClass !== 'completion_pending' && !transportRetryable) break;
647
689
  if (attempt < 2) {
648
690
  const requested = payload.retry_after_ms;
649
- const delayMs = typeof requested === 'number' && Number.isFinite(requested) && requested >= 0
650
- ? Math.ceil(requested) : 250;
691
+ const serverWaitMs = typeof requested === 'number' && Number.isFinite(requested) && requested >= 0
692
+ ? Math.ceil(requested) : null;
693
+ const delayMs = transportRetryable
694
+ ? Math.max(TRANSPORT_RETRY_WAIT_MS, serverWaitMs ?? 0)
695
+ : serverWaitMs ?? 250;
651
696
  const remaining = totalTimeoutMs - (performance.now() - context.started);
652
697
  if (delayMs >= remaining) break;
653
698
  const waitStarted = performance.now();
@@ -655,6 +700,7 @@ async function executeCanary(env, options, context) {
655
700
  const actualWaitMs = Math.round(performance.now() - waitStarted);
656
701
  outerAttemptRecord.actual_wait_ms = actualWaitMs;
657
702
  context.retryWaitMs += actualWaitMs;
703
+ if (transportRetryable) recoveredOnRetry = true;
658
704
  }
659
705
  }
660
706
  const latencyMs = Math.round(performance.now() - callStarted);
@@ -662,6 +708,7 @@ async function executeCanary(env, options, context) {
662
708
  const live = payload.stale !== true && payload.source !== 'last_known' && payload.available !== false;
663
709
  if (!live) throw canaryError('contract', `${name} returned cached or unavailable guidance`);
664
710
  results.push({ tool: name, ok: true, live: true, latency_ms: latencyMs,
711
+ ...(recoveredOnRetry ? { recovered_on_retry: true } : {}),
665
712
  ...(name === 'marrow_auto' ? {
666
713
  ...autoTimings(payload),
667
714
  attempts,
@@ -709,7 +756,10 @@ if (require.main === module) {
709
756
 
710
757
  module.exports = {
711
758
  CANARY_DEFAULT_TOOL_TIMEOUT_MS,
759
+ CANARY_DEFAULT_ASYNC_TOOL_TIMEOUT_MS,
760
+ CANARY_DEFAULT_TOTAL_TIMEOUT_MS,
712
761
  CANARY_TOOL_TIMEOUT_MARGIN_MS,
762
+ CANARY_ASYNC_TOOL_TIMEOUT_MAX_MS,
713
763
  MARROW_AUTO_RESPONSE_BUDGET_MAX_MS,
714
764
  PersistentMcpClient,
715
765
  latencyGroup,
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.85",
9
+ "version": "3.9.87",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@getmarrow/mcp",
14
- "version": "3.9.85",
14
+ "version": "3.9.87",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },