@getmarrow/mcp 3.9.84 → 3.9.86

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/mcp",
3
- "version": "3.9.84",
3
+ "version": "3.9.86",
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();
@@ -112,6 +131,103 @@ function autoTimings(payload) {
112
131
  return result;
113
132
  }
114
133
 
134
+ const AUTO_HTTP_ERROR_CATEGORIES = new Set([
135
+ 'authentication_required', 'permission_denied', 'proof_required', 'rate_limited',
136
+ 'request_timeout', 'dns_unavailable', 'connection_reset', 'tls_failure',
137
+ 'edge_access_denied', 'service_unavailable', 'invalid_response', 'request_failed',
138
+ ]);
139
+ const AUTO_TRACE_CODES = new Set([
140
+ 'runtime_pending', 'think_pending', 'commit_pending', 'pending', 'created',
141
+ 'committed', 'closed', 'replayed', 'idempotent_replay', 'lost_ack_recovered',
142
+ 'duplicate', 'original', 'new',
143
+ ]);
144
+
145
+ function safeAutoHttpTrace(payload) {
146
+ const source = payload?.http_attempt_trace;
147
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
148
+ return { attempts: [], dropped_count: 0 };
149
+ }
150
+ const attempts = [];
151
+ let rejected = Array.isArray(source.attempts) ? Math.max(0, source.attempts.length - 12) : 0;
152
+ for (const value of Array.isArray(source.attempts) ? source.attempts.slice(0, 12) : []) {
153
+ if (!value || typeof value !== 'object' || Array.isArray(value)
154
+ || !['think', 'commit'].includes(value.route_phase)) {
155
+ rejected += 1;
156
+ continue;
157
+ }
158
+ const serverTimings = {};
159
+ const spans = value.server_timings_ms;
160
+ if (spans && typeof spans === 'object' && !Array.isArray(spans)) {
161
+ for (const name of ['auth_ms', 'parse_ms']) {
162
+ const duration = spans[name];
163
+ if (typeof duration === 'number' && Number.isFinite(duration) && duration >= 0 && duration <= 60_000) {
164
+ serverTimings[name] = Math.round(duration);
165
+ }
166
+ }
167
+ }
168
+ const status = Number.isInteger(value.status) && value.status >= 100 && value.status <= 599
169
+ ? value.status : null;
170
+ const errorCategory = AUTO_HTTP_ERROR_CATEGORIES.has(value.error_category)
171
+ ? value.error_category : null;
172
+ attempts.push({
173
+ route_phase: value.route_phase,
174
+ duration_ms: safeMs(value.duration_ms),
175
+ status,
176
+ error_category: errorCategory,
177
+ typed_timeout: errorCategory === 'request_timeout' && value.typed_timeout === true,
178
+ pending_code: AUTO_TRACE_CODES.has(value.pending_code) ? value.pending_code : null,
179
+ replay_code: AUTO_TRACE_CODES.has(value.replay_code) ? value.replay_code : null,
180
+ server_timings_ms: serverTimings,
181
+ server_timing_coverage: Object.keys(serverTimings).length ? 'partial' : 'unavailable',
182
+ requested_wait_ms: value.requested_wait_ms == null ? null : safeMs(value.requested_wait_ms),
183
+ actual_wait_ms: safeMs(value.actual_wait_ms),
184
+ });
185
+ }
186
+ const reportedDropped = Number.isInteger(source.dropped_count) && source.dropped_count >= 0
187
+ ? Math.min(999, source.dropped_count) : 0;
188
+ return { attempts, dropped_count: Math.min(999, reportedDropped + rejected) };
189
+ }
190
+
191
+ function autoOuterAttempt(payload, attempt, durationMs) {
192
+ const phase = ['runtime_pending', 'think_pending', 'decision_created', 'proof_required',
193
+ 'review_required', 'owner_approval_required', 'commit_pending', 'closed'].includes(payload?.phase)
194
+ ? payload.phase : null;
195
+ const deliveryFailure = payload?.live_delivery?.failure?.error;
196
+ const errorCategory = AUTO_HTTP_ERROR_CATEGORIES.has(deliveryFailure?.category)
197
+ ? deliveryFailure.category : null;
198
+ return {
199
+ attempt,
200
+ duration_ms: safeMs(durationMs),
201
+ phase,
202
+ status: errorCategory ? 'failed' : payload?.live_delivery?.committed === true
203
+ ? 'committed' : payload?.live_delivery?.accepted === true ? 'pending' : 'unconfirmed',
204
+ error_category: errorCategory,
205
+ ...autoTimings(payload),
206
+ http_attempt_trace: safeAutoHttpTrace(payload),
207
+ requested_wait_ms: payload?.retry_after_ms == null ? null : safeMs(payload.retry_after_ms),
208
+ actual_wait_ms: 0,
209
+ };
210
+ }
211
+
212
+ function safeAutoOuterAttempts(source) {
213
+ if (!Array.isArray(source)) return [];
214
+ return source.slice(0, 3).map((record, index) => ({
215
+ attempt: index + 1,
216
+ duration_ms: safeMs(record?.duration_ms),
217
+ phase: ['runtime_pending', 'think_pending', 'decision_created', 'proof_required',
218
+ 'review_required', 'owner_approval_required', 'commit_pending', 'closed'].includes(record?.phase)
219
+ ? record.phase : null,
220
+ status: ['committed', 'pending', 'failed', 'unconfirmed'].includes(record?.status)
221
+ ? record.status : 'unconfirmed',
222
+ error_category: AUTO_HTTP_ERROR_CATEGORIES.has(record?.error_category)
223
+ ? record.error_category : null,
224
+ ...autoTimings(record),
225
+ http_attempt_trace: safeAutoHttpTrace({ http_attempt_trace: record?.http_attempt_trace }),
226
+ requested_wait_ms: record?.requested_wait_ms == null ? null : safeMs(record.requested_wait_ms),
227
+ actual_wait_ms: safeMs(record?.actual_wait_ms),
228
+ }));
229
+ }
230
+
115
231
  function autoWaitClass(payload) {
116
232
  if (payload?.live_delivery?.committed !== false
117
233
  || typeof payload.live_delivery.accepted !== 'boolean'
@@ -131,6 +247,7 @@ function failureRecord(error, context = {}) {
131
247
  tool: row.tool, ok: true, live: true, latency_ms: safeMs(row.latency_ms),
132
248
  ...(row.tool === 'marrow_auto' ? {
133
249
  ...autoTimings(row), attempts: row.attempts, retry_wait_ms: safeMs(row.retry_wait_ms),
250
+ auto_attempt_records: safeAutoOuterAttempts(row.auto_attempt_records),
134
251
  } : {}),
135
252
  }));
136
253
  return Object.freeze({
@@ -150,6 +267,7 @@ function failureRecord(error, context = {}) {
150
267
  results: Object.freeze(results),
151
268
  ...(context.tool === 'marrow_auto' ? {
152
269
  ...autoTimings(context.autoMetrics), retry_wait_ms: safeMs(context.retryWaitMs),
270
+ auto_attempt_records: safeAutoOuterAttempts(context.autoAttempts),
153
271
  ...(context.autoPhase ? { auto_phase: context.autoPhase } : {}),
154
272
  } : {}),
155
273
  ...(evidence.http_status === undefined ? {} : { http_status: evidence.http_status }),
@@ -443,7 +561,14 @@ async function executeCanary(env, options, context) {
443
561
  CANARY_TOOL_TIMEOUT_MAX_MS,
444
562
  env,
445
563
  );
446
- 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);
447
572
  const expectedVersion = env.MARROW_EXPECTED_MCP_VERSION || packageVersion;
448
573
  const childPath = env.MARROW_MCP_CANARY_CHILD || resolve(__dirname, '../dist/cli.js');
449
574
  const childEnv = {
@@ -499,34 +624,83 @@ async function executeCanary(env, options, context) {
499
624
  const results = context.results;
500
625
  for (const [name, args] of cases) {
501
626
  const callStarted = performance.now();
627
+ const perToolTimeoutMs = ASYNC_PATH_TOOLS.has(name) ? asyncToolTimeoutMs : toolTimeoutMs;
502
628
  let called;
503
629
  let payload;
504
630
  let attempts = 0;
631
+ let recoveredOnRetry = false;
505
632
  context.retryWaitMs = 0;
506
633
  context.autoMetrics = undefined;
507
634
  context.autoPhase = undefined;
635
+ context.autoAttempts = undefined;
636
+ const autoAttemptRecords = [];
637
+ if (name === 'marrow_auto') context.autoAttempts = autoAttemptRecords;
508
638
  for (let attempt = 0; attempt < 3; attempt += 1) {
639
+ const outerAttemptStarted = performance.now();
509
640
  if (client.fatalError) throw client.fatalError;
510
641
  stage('tool_call', name, attempt + 1);
511
642
  attempts += 1;
512
- called = await client.request('tools/call', { name, arguments: args });
513
- if (client.fatalError) throw client.fatalError;
514
- stage('tool_validate', name, attempt + 1);
515
- payload = toolPayload(called, name);
643
+ try {
644
+ called = await client.request('tools/call', { name, arguments: args }, perToolTimeoutMs);
645
+ if (client.fatalError) throw client.fatalError;
646
+ stage('tool_validate', name, attempt + 1);
647
+ payload = toolPayload(called, name);
648
+ } catch (error) {
649
+ const evidence = errorEvidence.get(error);
650
+ if (name === 'marrow_auto') {
651
+ autoAttemptRecords.push({
652
+ attempt: attempt + 1,
653
+ duration_ms: safeMs(performance.now() - outerAttemptStarted),
654
+ phase: null,
655
+ status: 'failed',
656
+ error_category: evidence?.error_class === 'request_timeout' ? 'request_timeout' : null,
657
+ http_attempt_trace: { attempts: [], dropped_count: 0 },
658
+ requested_wait_ms: null,
659
+ actual_wait_ms: 0,
660
+ });
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
+ }
673
+ throw error;
674
+ }
516
675
  if (name !== 'marrow_auto') break;
676
+ const outerAttemptRecord = autoOuterAttempt(
677
+ payload,
678
+ attempt + 1,
679
+ performance.now() - outerAttemptStarted,
680
+ );
681
+ autoAttemptRecords.push(outerAttemptRecord);
517
682
  context.autoMetrics = autoTimings(payload);
518
683
  const waitClass = autoWaitClass(payload);
519
684
  context.autoPhase = waitClass ? payload.phase : undefined;
520
- 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;
521
689
  if (attempt < 2) {
522
690
  const requested = payload.retry_after_ms;
523
- const delayMs = typeof requested === 'number' && Number.isFinite(requested) && requested >= 0
524
- ? 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;
525
696
  const remaining = totalTimeoutMs - (performance.now() - context.started);
526
697
  if (delayMs >= remaining) break;
527
698
  const waitStarted = performance.now();
528
699
  await new Promise((resolveDelay) => setTimeout(resolveDelay, delayMs));
529
- context.retryWaitMs += Math.round(performance.now() - waitStarted);
700
+ const actualWaitMs = Math.round(performance.now() - waitStarted);
701
+ outerAttemptRecord.actual_wait_ms = actualWaitMs;
702
+ context.retryWaitMs += actualWaitMs;
703
+ if (transportRetryable) recoveredOnRetry = true;
530
704
  }
531
705
  }
532
706
  const latencyMs = Math.round(performance.now() - callStarted);
@@ -534,7 +708,13 @@ async function executeCanary(env, options, context) {
534
708
  const live = payload.stale !== true && payload.source !== 'last_known' && payload.available !== false;
535
709
  if (!live) throw canaryError('contract', `${name} returned cached or unavailable guidance`);
536
710
  results.push({ tool: name, ok: true, live: true, latency_ms: latencyMs,
537
- ...(name === 'marrow_auto' ? { ...autoTimings(payload), attempts, retry_wait_ms: context.retryWaitMs } : {}),
711
+ ...(recoveredOnRetry ? { recovered_on_retry: true } : {}),
712
+ ...(name === 'marrow_auto' ? {
713
+ ...autoTimings(payload),
714
+ attempts,
715
+ retry_wait_ms: context.retryWaitMs,
716
+ auto_attempt_records: safeAutoOuterAttempts(autoAttemptRecords),
717
+ } : {}),
538
718
  });
539
719
  }
540
720
  const hotPath = results.filter((row) => HOT_PATH_TOOLS.has(row.tool));
@@ -576,7 +756,10 @@ if (require.main === module) {
576
756
 
577
757
  module.exports = {
578
758
  CANARY_DEFAULT_TOOL_TIMEOUT_MS,
759
+ CANARY_DEFAULT_ASYNC_TOOL_TIMEOUT_MS,
760
+ CANARY_DEFAULT_TOTAL_TIMEOUT_MS,
579
761
  CANARY_TOOL_TIMEOUT_MARGIN_MS,
762
+ CANARY_ASYNC_TOOL_TIMEOUT_MAX_MS,
580
763
  MARROW_AUTO_RESPONSE_BUDGET_MAX_MS,
581
764
  PersistentMcpClient,
582
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.84",
9
+ "version": "3.9.85",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@getmarrow/mcp",
14
- "version": "3.9.84",
14
+ "version": "3.9.85",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },