@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/README.md +7 -1
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/hook-contract.d.ts +2 -2
- package/dist/hook-contract.js +1 -1
- package/dist/index.d.ts +33 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +202 -23
- package/dist/index.js.map +1 -1
- package/dist/request-reliability.d.ts +4 -2
- package/dist/request-reliability.d.ts.map +1 -1
- package/dist/request-reliability.js +40 -8
- package/dist/request-reliability.js.map +1 -1
- package/package.json +1 -1
- package/scripts/control-path-canary.cjs +193 -10
- package/server.json +2 -2
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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
|
|
524
|
-
? Math.ceil(requested) :
|
|
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
|
-
|
|
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
|
-
...(
|
|
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.
|
|
9
|
+
"version": "3.9.85",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@getmarrow/mcp",
|
|
14
|
-
"version": "3.9.
|
|
14
|
+
"version": "3.9.85",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|