@martinloop/mcp 0.3.9 → 0.5.1
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 +82 -141
- package/dist/discovery-metadata.d.ts +1 -1
- package/dist/discovery-metadata.js +1 -1
- package/dist/resources.d.ts +1 -2
- package/dist/resources.js +62 -98
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +16 -11
- package/dist/server.d.ts +23 -0
- package/dist/server.js +192 -15
- package/dist/tools/doctor.js +5 -4
- package/dist/tools/get-status.d.ts +2 -0
- package/dist/tools/get-status.js +1 -0
- package/dist/tools/preflight.d.ts +2 -0
- package/dist/tools/preflight.js +43 -3
- package/dist/tools/run-dossier.d.ts +3 -1
- package/dist/tools/run-dossier.js +40 -2
- package/dist/tools/run-loop.d.ts +5 -3
- package/dist/tools/run-loop.js +15 -9
- package/dist/tools/tool-errors.js +1 -1
- package/dist/tools/tool-response.js +9 -2
- package/dist/tools/tool-support.d.ts +4 -2
- package/dist/tools/tool-support.js +6 -15
- package/dist/vendor/adapters/claude-cli.d.ts +2 -2
- package/dist/vendor/adapters/claude-cli.js +97 -69
- package/dist/vendor/adapters/cli-bridge.d.ts +9 -1
- package/dist/vendor/adapters/cli-bridge.js +54 -13
- package/dist/vendor/adapters/codex-launcher.d.ts +61 -1
- package/dist/vendor/adapters/codex-launcher.js +118 -19
- package/dist/vendor/adapters/index.d.ts +2 -4
- package/dist/vendor/adapters/index.js +2 -4
- package/dist/vendor/adapters/openai-compatible.d.ts +2 -3
- package/dist/vendor/adapters/openai-compatible.js +80 -15
- package/dist/vendor/adapters/verifier-only.js +9 -4
- package/dist/vendor/contracts/adapter-capabilities.d.ts +1 -1
- package/dist/vendor/contracts/context-handoff.d.ts +100 -0
- package/dist/vendor/contracts/context-handoff.js +11 -0
- package/dist/vendor/contracts/context-manifest.d.ts +195 -0
- package/dist/vendor/contracts/context-manifest.js +17 -0
- package/dist/vendor/contracts/context-shadow.d.ts +75 -0
- package/dist/vendor/contracts/context-shadow.js +8 -0
- package/dist/vendor/contracts/delivery.d.ts +47 -0
- package/dist/vendor/contracts/delivery.js +18 -0
- package/dist/vendor/contracts/exits.d.ts +126 -0
- package/dist/vendor/contracts/exits.js +32 -0
- package/dist/vendor/contracts/index.d.ts +55 -6
- package/dist/vendor/contracts/index.js +24 -0
- package/dist/vendor/contracts/mission.d.ts +102 -0
- package/dist/vendor/contracts/mission.js +64 -0
- package/dist/vendor/contracts/operator.d.ts +3 -1
- package/dist/vendor/contracts/operator.js +2 -1
- package/dist/vendor/contracts/verified-handoff.d.ts +106 -0
- package/dist/vendor/contracts/verified-handoff.js +40 -0
- package/dist/vendor/core/context-chain-gate.d.ts +89 -0
- package/dist/vendor/core/context-chain-gate.js +173 -0
- package/dist/vendor/core/context-compiler.d.ts +53 -0
- package/dist/vendor/core/context-compiler.js +261 -0
- package/dist/vendor/core/context-handoff.d.ts +42 -0
- package/dist/vendor/core/context-handoff.js +167 -0
- package/dist/vendor/core/context-shadow.d.ts +39 -0
- package/dist/vendor/core/context-shadow.js +200 -0
- package/dist/vendor/core/delivery/index.d.ts +6 -0
- package/dist/vendor/core/delivery/index.js +4 -0
- package/dist/vendor/core/delivery/message-client.d.ts +18 -0
- package/dist/vendor/core/delivery/message-client.js +38 -0
- package/dist/vendor/core/delivery/message-ledger.d.ts +35 -0
- package/dist/vendor/core/delivery/message-ledger.js +97 -0
- package/dist/vendor/core/delivery/message-schema.d.ts +16 -0
- package/dist/vendor/core/delivery/message-schema.js +111 -0
- package/dist/vendor/core/delivery/update-check.d.ts +19 -0
- package/dist/vendor/core/delivery/update-check.js +107 -0
- package/dist/vendor/core/exit-signal.d.ts +88 -0
- package/dist/vendor/core/exit-signal.js +339 -0
- package/dist/vendor/core/exits.d.ts +41 -0
- package/dist/vendor/core/exits.js +258 -0
- package/dist/vendor/core/grounding.d.ts +2 -1
- package/dist/vendor/core/grounding.js +40 -5
- package/dist/vendor/core/index.d.ts +47 -6
- package/dist/vendor/core/index.js +1102 -670
- package/dist/vendor/core/mission/aggregate.d.ts +27 -0
- package/dist/vendor/core/mission/aggregate.js +42 -0
- package/dist/vendor/core/mission/index.d.ts +2 -0
- package/dist/vendor/core/mission/index.js +1 -0
- package/dist/vendor/core/persistence/compiler.d.ts +8 -0
- package/dist/vendor/core/persistence/compiler.js +156 -0
- package/dist/vendor/core/persistence/context-handoff-store.d.ts +68 -0
- package/dist/vendor/core/persistence/context-handoff-store.js +112 -0
- package/dist/vendor/core/persistence/index.d.ts +4 -0
- package/dist/vendor/core/persistence/index.js +2 -0
- package/dist/vendor/core/persistence/ledger.d.ts +1 -1
- package/dist/vendor/core/persistence/mission-store.d.ts +56 -0
- package/dist/vendor/core/persistence/mission-store.js +281 -0
- package/dist/vendor/core/persistence/runs-reader.d.ts +2 -0
- package/dist/vendor/core/policy.d.ts +2 -0
- package/dist/vendor/core/policy.js +11 -8
- package/dist/vendor/core/red-blue/red-phase.d.ts +2 -1
- package/dist/vendor/core/red-blue/red-phase.js +3 -3
- package/dist/vendor/core/red-blue/risk-tiers.d.ts +2 -4
- package/dist/vendor/core/red-blue/risk-tiers.js +1 -3
- package/dist/vendor/core/rollback.js +15 -1
- package/dist/vendor/core/routing.d.ts +0 -42
- package/dist/vendor/core/routing.js +4 -154
- package/dist/vendor/core/savings.d.ts +13 -0
- package/dist/vendor/core/savings.js +22 -0
- package/dist/vendor/core/termination-store.d.ts +15 -0
- package/dist/vendor/core/termination-store.js +44 -0
- package/dist/vendor/core/verified-handoff.d.ts +72 -0
- package/dist/vendor/core/verified-handoff.js +238 -0
- package/package.json +4 -10
- package/server.json +3 -3
- package/dist/vendor/adapters/stub-agent-cli.d.ts +0 -8
- package/dist/vendor/adapters/stub-agent-cli.js +0 -40
- package/dist/vendor/adapters/stub-direct-provider.d.ts +0 -8
- package/dist/vendor/adapters/stub-direct-provider.js +0 -12
|
@@ -84,144 +84,6 @@ function calculateDirectConfidence(signals, input) {
|
|
|
84
84
|
// ---------------------------------------------------------------------------
|
|
85
85
|
// Public API
|
|
86
86
|
// ---------------------------------------------------------------------------
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// Cost-aware model tier routing
|
|
89
|
-
//
|
|
90
|
-
// Engine-agnostic tier system — each engine maps tiers to real model IDs.
|
|
91
|
-
// Never pay for reasoning/orchestration on simple tasks.
|
|
92
|
-
//
|
|
93
|
-
// Tier → concrete model per engine:
|
|
94
|
-
// haiku → claude-haiku-4-5, gpt-4o-mini, gemini-2.5-flash, deepseek-chat, qwen3-8b, llama-3.1-8b
|
|
95
|
-
// sonnet → claude-sonnet-4-6, gpt-4o, gemini-2.5-pro, deepseek-r1, qwen3-32b, llama-3.3-70b
|
|
96
|
-
// opus → claude-opus-4-6, o3, gemini-2.5-ultra, deepseek-r1-671b, qwen3-235b, llama-3.1-405b
|
|
97
|
-
//
|
|
98
|
-
// Pricing reference (USD per 1M tokens, approx 2026):
|
|
99
|
-
// haiku: $0.80 input / $4.00 output (~1x baseline)
|
|
100
|
-
// sonnet: $3.00 input / $15.00 output (~4x haiku)
|
|
101
|
-
// opus: $15.00 input / $75.00 output (~19x haiku)
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
const SONNET_AVG_COST_PER_USD_BUDGET = 0.35; // sonnet uses ~35% of budget on average
|
|
104
|
-
const HAIKU_AVG_COST_PER_USD_BUDGET = 0.10; // haiku uses ~10% — ~3.5x cheaper
|
|
105
|
-
function selectModelTier(mode) {
|
|
106
|
-
// Route already encodes complexity. Direct = passed the simplicity check → haiku.
|
|
107
|
-
if (mode === "direct")
|
|
108
|
-
return "haiku";
|
|
109
|
-
if (mode === "manager")
|
|
110
|
-
return "sonnet";
|
|
111
|
-
return "opus";
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Resolve a tier to a concrete model ID for a specific engine.
|
|
115
|
-
* Allows downstream adapters to switch models automatically.
|
|
116
|
-
*/
|
|
117
|
-
export function resolveModelForTier(tier, engine) {
|
|
118
|
-
const matrix = {
|
|
119
|
-
claude: {
|
|
120
|
-
haiku: "claude-haiku-4-5-20251001",
|
|
121
|
-
sonnet: "claude-sonnet-4-6",
|
|
122
|
-
opus: "claude-opus-4-6"
|
|
123
|
-
},
|
|
124
|
-
codex: {
|
|
125
|
-
haiku: "gpt-4o-mini",
|
|
126
|
-
sonnet: "gpt-4.1",
|
|
127
|
-
opus: "o3"
|
|
128
|
-
},
|
|
129
|
-
gemini: {
|
|
130
|
-
haiku: "gemini-2.5-flash",
|
|
131
|
-
sonnet: "gemini-2.5-pro",
|
|
132
|
-
opus: "gemini-2.5-ultra"
|
|
133
|
-
},
|
|
134
|
-
openai: {
|
|
135
|
-
haiku: "gpt-4o-mini",
|
|
136
|
-
sonnet: "gpt-4o",
|
|
137
|
-
opus: "o3"
|
|
138
|
-
},
|
|
139
|
-
deepseek: {
|
|
140
|
-
haiku: "deepseek-chat",
|
|
141
|
-
sonnet: "deepseek-r1",
|
|
142
|
-
opus: "deepseek-r1"
|
|
143
|
-
},
|
|
144
|
-
qwen: {
|
|
145
|
-
haiku: "qwen/qwen3-8b",
|
|
146
|
-
sonnet: "qwen/qwen3-32b",
|
|
147
|
-
opus: "qwen/qwen3-235b-a22b"
|
|
148
|
-
},
|
|
149
|
-
llama: {
|
|
150
|
-
haiku: "meta-llama/llama-3.1-8b-instruct",
|
|
151
|
-
sonnet: "meta-llama/llama-3.3-70b-instruct",
|
|
152
|
-
opus: "meta-llama/llama-3.1-405b-instruct"
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
const engineKey = engine.toLowerCase().split("-")[0] ?? engine;
|
|
156
|
-
const claudeMatrix = matrix.claude;
|
|
157
|
-
return matrix[engineKey]?.[tier] ?? claudeMatrix?.[tier] ?? "claude-sonnet-4-6";
|
|
158
|
-
}
|
|
159
|
-
function estimateSavingVsSonnet(tier, budgetUsd) {
|
|
160
|
-
if (tier === "sonnet")
|
|
161
|
-
return 0;
|
|
162
|
-
if (tier === "haiku")
|
|
163
|
-
return Math.round((SONNET_AVG_COST_PER_USD_BUDGET - HAIKU_AVG_COST_PER_USD_BUDGET) * budgetUsd * 100) / 100;
|
|
164
|
-
// opus costs more than sonnet — negative saving
|
|
165
|
-
return Math.round(-1 * SONNET_AVG_COST_PER_USD_BUDGET * budgetUsd * 0.5 * 100) / 100;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Select the best available engine for a given tier requirement.
|
|
169
|
-
*
|
|
170
|
-
* When multiple engines are available, picks the cheapest that meets
|
|
171
|
-
* the capability requirement. Falls back gracefully:
|
|
172
|
-
* - If required tier unavailable, upgrades to next tier of same engine
|
|
173
|
-
* - If only one engine family available, uses its best equivalent
|
|
174
|
-
*
|
|
175
|
-
* @param requiredTier - Minimum capability tier needed
|
|
176
|
-
* @param availableEngines - What's installed and authenticated
|
|
177
|
-
* @returns Best engine ID and concrete model to use
|
|
178
|
-
*/
|
|
179
|
-
export function selectBestEngine(requiredTier, availableEngines) {
|
|
180
|
-
const available = availableEngines.filter((e) => e.available);
|
|
181
|
-
if (available.length === 0) {
|
|
182
|
-
// Nothing detected — default to claude (user must install)
|
|
183
|
-
return {
|
|
184
|
-
engineId: "claude",
|
|
185
|
-
model: resolveModelForTier(requiredTier, "claude"),
|
|
186
|
-
reasoning: "No engines detected. Defaulting to claude. Run martin doctor for setup instructions."
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
const tierRank = {
|
|
190
|
-
haiku: 0, sonnet: 1, opus: 2
|
|
191
|
-
};
|
|
192
|
-
const requiredRank = tierRank[requiredTier];
|
|
193
|
-
// Find the cheapest engine that meets or exceeds the required tier
|
|
194
|
-
const eligible = available
|
|
195
|
-
.filter((e) => tierRank[e.capabilityTier] >= requiredRank)
|
|
196
|
-
.sort((a, b) => {
|
|
197
|
-
// Sort by cost (cheap first), then by exact tier match
|
|
198
|
-
const costOrder = { cheap: 0, mid: 1, expensive: 2 };
|
|
199
|
-
if (costOrder[a.costTier] !== costOrder[b.costTier]) {
|
|
200
|
-
return costOrder[a.costTier] - costOrder[b.costTier];
|
|
201
|
-
}
|
|
202
|
-
// Prefer exact tier match over overqualified
|
|
203
|
-
const aExact = a.capabilityTier === requiredTier ? 0 : 1;
|
|
204
|
-
const bExact = b.capabilityTier === requiredTier ? 0 : 1;
|
|
205
|
-
return aExact - bExact;
|
|
206
|
-
});
|
|
207
|
-
if (eligible.length > 0) {
|
|
208
|
-
const best = eligible[0];
|
|
209
|
-
const model = resolveModelForTier(requiredTier, best.id);
|
|
210
|
-
return {
|
|
211
|
-
engineId: best.id,
|
|
212
|
-
model,
|
|
213
|
-
reasoning: `Selected ${best.id} (${model}) as cheapest available engine for ${requiredTier}-tier task.`
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
// All available engines are below required tier — upgrade to best available
|
|
217
|
-
const best = available.sort((a, b) => tierRank[b.capabilityTier] - tierRank[a.capabilityTier])[0];
|
|
218
|
-
const model = resolveModelForTier(best.capabilityTier, best.id);
|
|
219
|
-
return {
|
|
220
|
-
engineId: best.id,
|
|
221
|
-
model,
|
|
222
|
-
reasoning: `No ${requiredTier}-tier engine available. Upgrading to ${best.id} (${model}) — best available.`
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
87
|
export function classifyRoute(input) {
|
|
226
88
|
const policy = { ...DEFAULT_ROUTING_POLICY, ...input.policy };
|
|
227
89
|
const signals = extractComplexitySignals(input);
|
|
@@ -230,7 +92,6 @@ export function classifyRoute(input) {
|
|
|
230
92
|
const blockedSteps = [];
|
|
231
93
|
// Policy override: forced mode
|
|
232
94
|
if (policy.mode === "direct") {
|
|
233
|
-
const tier = selectModelTier("direct");
|
|
234
95
|
return {
|
|
235
96
|
selectedMode: "direct",
|
|
236
97
|
confidence: 1,
|
|
@@ -239,13 +100,10 @@ export function classifyRoute(input) {
|
|
|
239
100
|
reason: ["Policy forces direct execution mode."],
|
|
240
101
|
blockedSteps: ["manager", "planner", "router", "consensus"],
|
|
241
102
|
compressed: true,
|
|
242
|
-
compressionSummary: "Policy: direct mode — skipping all orchestration."
|
|
243
|
-
recommendedModelTier: tier,
|
|
244
|
-
estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
|
|
103
|
+
compressionSummary: "Policy: direct mode — skipping all orchestration."
|
|
245
104
|
};
|
|
246
105
|
}
|
|
247
106
|
if (policy.mode === "consensus") {
|
|
248
|
-
const tier = selectModelTier("consensus");
|
|
249
107
|
return {
|
|
250
108
|
selectedMode: "consensus",
|
|
251
109
|
confidence: 0.5,
|
|
@@ -253,9 +111,7 @@ export function classifyRoute(input) {
|
|
|
253
111
|
expectedPreworkBurnPct: 35,
|
|
254
112
|
reason: ["Policy forces consensus mode."],
|
|
255
113
|
blockedSteps: [],
|
|
256
|
-
compressed: false
|
|
257
|
-
recommendedModelTier: tier,
|
|
258
|
-
estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
|
|
114
|
+
compressed: false
|
|
259
115
|
};
|
|
260
116
|
}
|
|
261
117
|
// Adaptive routing
|
|
@@ -268,7 +124,6 @@ export function classifyRoute(input) {
|
|
|
268
124
|
if (signals.scopeRestricted)
|
|
269
125
|
reason.push("Path scope is restricted.");
|
|
270
126
|
blockedSteps.push("manager", "consensus");
|
|
271
|
-
const tier = selectModelTier("direct");
|
|
272
127
|
return {
|
|
273
128
|
selectedMode: "direct",
|
|
274
129
|
confidence: directConfidence,
|
|
@@ -277,9 +132,7 @@ export function classifyRoute(input) {
|
|
|
277
132
|
reason,
|
|
278
133
|
blockedSteps,
|
|
279
134
|
compressed: true,
|
|
280
|
-
compressionSummary: `Compressed from manager→planner→worker to direct worker. Estimated savings: ${String(Math.round(input.budgetUsd * 0.3 * 100) / 100)} USD
|
|
281
|
-
recommendedModelTier: tier,
|
|
282
|
-
estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
|
|
135
|
+
compressionSummary: `Compressed from manager→planner→worker to direct worker. Estimated savings: ${String(Math.round(input.budgetUsd * 0.3 * 100) / 100)} USD.`
|
|
283
136
|
};
|
|
284
137
|
}
|
|
285
138
|
// Complex task — needs orchestration
|
|
@@ -293,7 +146,6 @@ export function classifyRoute(input) {
|
|
|
293
146
|
reason.push(`Touches ${String(signals.fileCount)}+ files.`);
|
|
294
147
|
const needsConsensus = signals.hasSecurityKeywords && signals.hasMigrationKeywords;
|
|
295
148
|
const mode = needsConsensus ? "consensus" : "manager";
|
|
296
|
-
const tier = selectModelTier(mode);
|
|
297
149
|
return {
|
|
298
150
|
selectedMode: mode,
|
|
299
151
|
confidence: directConfidence,
|
|
@@ -301,9 +153,7 @@ export function classifyRoute(input) {
|
|
|
301
153
|
expectedPreworkBurnPct: needsConsensus ? 40 : 25,
|
|
302
154
|
reason,
|
|
303
155
|
blockedSteps: needsConsensus ? [] : ["consensus"],
|
|
304
|
-
compressed: false
|
|
305
|
-
recommendedModelTier: tier,
|
|
306
|
-
estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
|
|
156
|
+
compressed: false
|
|
307
157
|
};
|
|
308
158
|
}
|
|
309
159
|
function estimateDirectCost(budgetUsd) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LoopRecord } from "../contracts/index.js";
|
|
2
|
+
import type { ExitDecision } from "./policy.js";
|
|
3
|
+
export interface AvoidedUsdInput {
|
|
4
|
+
lifecycleState: LoopRecord["lifecycleState"];
|
|
5
|
+
actualUsd: number;
|
|
6
|
+
uncontrolledBaselineUsd?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function calculateAvoidedUsd(input: AvoidedUsdInput): number;
|
|
9
|
+
export declare function calculateLoopAvoidedUsd(input: {
|
|
10
|
+
loop: LoopRecord;
|
|
11
|
+
decision: ExitDecision;
|
|
12
|
+
uncontrolledBaselineUsd?: number;
|
|
13
|
+
}): number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function calculateAvoidedUsd(input) {
|
|
2
|
+
if (input.lifecycleState !== "completed")
|
|
3
|
+
return 0;
|
|
4
|
+
if (!Number.isFinite(input.actualUsd) || input.actualUsd < 0)
|
|
5
|
+
return 0;
|
|
6
|
+
if (input.uncontrolledBaselineUsd === undefined ||
|
|
7
|
+
!Number.isFinite(input.uncontrolledBaselineUsd) ||
|
|
8
|
+
input.uncontrolledBaselineUsd <= input.actualUsd) {
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
return roundUsd(input.uncontrolledBaselineUsd - input.actualUsd);
|
|
12
|
+
}
|
|
13
|
+
export function calculateLoopAvoidedUsd(input) {
|
|
14
|
+
return calculateAvoidedUsd({
|
|
15
|
+
lifecycleState: input.decision.lifecycleState,
|
|
16
|
+
actualUsd: input.loop.cost.actualUsd,
|
|
17
|
+
uncontrolledBaselineUsd: input.uncontrolledBaselineUsd
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function roundUsd(value) {
|
|
21
|
+
return Math.round(value * 100) / 100;
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TerminationEnvelopeV1 } from "../contracts/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Atomically persist the termination envelope for a governed run.
|
|
4
|
+
*
|
|
5
|
+
* Crash-safe write sequence (mirrors exit-signal.ts atomic publication):
|
|
6
|
+
* 1. Write complete JSON to a unique tmp file (wx, 0o600) in runDirectory
|
|
7
|
+
* 2. Sync and close the tmp file
|
|
8
|
+
* 3. Hard-link tmp → termination.json (EEXIST → slot already occupied)
|
|
9
|
+
* 4. On EEXIST, read and return the existing envelope
|
|
10
|
+
* 5. Unlink the tmp file in finally (best-effort)
|
|
11
|
+
*
|
|
12
|
+
* First terminal exit path wins — competing callers that hit EEXIST get back
|
|
13
|
+
* the envelope that won the race. This is the A1 idempotency guarantee.
|
|
14
|
+
*/
|
|
15
|
+
export declare function persistTerminationEnvelope(runDirectory: string, envelope: TerminationEnvelopeV1): Promise<TerminationEnvelopeV1>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { link, open, readFile, unlink } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
/**
|
|
5
|
+
* Atomically persist the termination envelope for a governed run.
|
|
6
|
+
*
|
|
7
|
+
* Crash-safe write sequence (mirrors exit-signal.ts atomic publication):
|
|
8
|
+
* 1. Write complete JSON to a unique tmp file (wx, 0o600) in runDirectory
|
|
9
|
+
* 2. Sync and close the tmp file
|
|
10
|
+
* 3. Hard-link tmp → termination.json (EEXIST → slot already occupied)
|
|
11
|
+
* 4. On EEXIST, read and return the existing envelope
|
|
12
|
+
* 5. Unlink the tmp file in finally (best-effort)
|
|
13
|
+
*
|
|
14
|
+
* First terminal exit path wins — competing callers that hit EEXIST get back
|
|
15
|
+
* the envelope that won the race. This is the A1 idempotency guarantee.
|
|
16
|
+
*/
|
|
17
|
+
export async function persistTerminationEnvelope(runDirectory, envelope) {
|
|
18
|
+
const finalPath = join(runDirectory, "termination.json");
|
|
19
|
+
const tmpPath = join(runDirectory, `.termination-tmp-${randomUUID()}.json`);
|
|
20
|
+
const json = `${JSON.stringify(envelope, null, 2)}\n`;
|
|
21
|
+
let handle;
|
|
22
|
+
try {
|
|
23
|
+
handle = await open(tmpPath, "wx", 0o600);
|
|
24
|
+
await handle.writeFile(json, "utf8");
|
|
25
|
+
await handle.sync();
|
|
26
|
+
await handle.close();
|
|
27
|
+
handle = undefined;
|
|
28
|
+
try {
|
|
29
|
+
await link(tmpPath, finalPath);
|
|
30
|
+
return envelope;
|
|
31
|
+
}
|
|
32
|
+
catch (linkErr) {
|
|
33
|
+
if (linkErr.code !== "EEXIST")
|
|
34
|
+
throw linkErr;
|
|
35
|
+
// Another path won the race — read what's on disk and return it
|
|
36
|
+
const existing = await readFile(finalPath, "utf8");
|
|
37
|
+
return JSON.parse(existing);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await handle?.close().catch(() => undefined);
|
|
42
|
+
await unlink(tmpPath).catch(() => undefined);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ExecutionMode, LoopRecord, ReceiptIntegritySummary, TestIntegrityStatus, TestIntegrityVerdict, VerifiedHandoffOutcome, VerifiedHandoffRecoveryV1, VerifiedHandoffRequirementV1, VerifiedHandoffScopeV1, VerifiedHandoffTestIntegrityV1, VerifiedHandoffV1 } from "../contracts/index.js";
|
|
2
|
+
export declare function toTestIntegrityVerdict(status: TestIntegrityStatus): TestIntegrityVerdict;
|
|
3
|
+
export interface VerifierExecutionBinding {
|
|
4
|
+
runId: string;
|
|
5
|
+
workspaceId: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
commands: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface BoundVerifierEvidence {
|
|
10
|
+
passed: boolean;
|
|
11
|
+
binding?: VerifierExecutionBinding;
|
|
12
|
+
steps?: Array<{
|
|
13
|
+
command: string;
|
|
14
|
+
launched: boolean;
|
|
15
|
+
completed?: boolean;
|
|
16
|
+
crashed?: boolean;
|
|
17
|
+
exitCode?: number;
|
|
18
|
+
timedOut?: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export declare function verifierActuallyPassed(evidence: BoundVerifierEvidence | null | undefined, expected: VerifierExecutionBinding): boolean;
|
|
22
|
+
export declare function resolveVerifiedHandoffOutcome(input: {
|
|
23
|
+
lifecycleState: LoopRecord["lifecycleState"];
|
|
24
|
+
executionStatus?: "completed" | "write_blocked" | "sandbox_blocked" | "policy_rejected" | "failed";
|
|
25
|
+
verificationStatus: BuildVerifiedHandoffInput["verification"]["status"];
|
|
26
|
+
receiptIntegrity: ReceiptIntegritySummary["state"];
|
|
27
|
+
scopeStatus?: VerifiedHandoffScopeV1["status"];
|
|
28
|
+
testIntegrityStatus?: TestIntegrityStatus;
|
|
29
|
+
mutationRequired?: boolean;
|
|
30
|
+
changedFileCount?: number;
|
|
31
|
+
definitionOfDonePreSatisfied?: boolean;
|
|
32
|
+
evidenceContradicted?: boolean;
|
|
33
|
+
governanceClaimEligible?: boolean;
|
|
34
|
+
unresolvedWorkCount: number;
|
|
35
|
+
}): VerifiedHandoffOutcome;
|
|
36
|
+
export declare function deriveVerifiedHandoffExecutionBoundary(loop: LoopRecord): {
|
|
37
|
+
executionMode: ExecutionMode;
|
|
38
|
+
governanceClaimEligible: boolean;
|
|
39
|
+
};
|
|
40
|
+
export interface BuildVerifiedHandoffInput {
|
|
41
|
+
loop: LoopRecord;
|
|
42
|
+
generatedAt?: string;
|
|
43
|
+
receiptIntegrity: ReceiptIntegritySummary;
|
|
44
|
+
verification: {
|
|
45
|
+
status: "passed" | "failed" | "contradicted" | "not_run";
|
|
46
|
+
summary: string;
|
|
47
|
+
steps: Array<{
|
|
48
|
+
command: string;
|
|
49
|
+
launched: boolean;
|
|
50
|
+
completed?: boolean;
|
|
51
|
+
crashed?: boolean;
|
|
52
|
+
exitCode?: number;
|
|
53
|
+
timedOut?: boolean;
|
|
54
|
+
detail?: string;
|
|
55
|
+
}>;
|
|
56
|
+
warnings: string[];
|
|
57
|
+
binding?: VerifierExecutionBinding;
|
|
58
|
+
};
|
|
59
|
+
executionStatus?: "completed" | "write_blocked" | "sandbox_blocked" | "policy_rejected" | "failed";
|
|
60
|
+
mutationRequired?: boolean;
|
|
61
|
+
definitionOfDonePreSatisfied?: boolean;
|
|
62
|
+
evidenceContradicted?: boolean;
|
|
63
|
+
changedFiles?: string[];
|
|
64
|
+
scope?: Partial<VerifiedHandoffScopeV1>;
|
|
65
|
+
testIntegrity?: Partial<VerifiedHandoffTestIntegrityV1>;
|
|
66
|
+
requirements?: VerifiedHandoffRequirementV1[];
|
|
67
|
+
unresolvedWork?: string[];
|
|
68
|
+
stopReason?: string;
|
|
69
|
+
recovery?: Partial<VerifiedHandoffRecoveryV1>;
|
|
70
|
+
nextAction: string;
|
|
71
|
+
}
|
|
72
|
+
export declare function buildVerifiedHandoff(input: BuildVerifiedHandoffInput): VerifiedHandoffV1;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: MartinLoop contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Mapping: granular TestIntegrityStatus → public TestIntegrityVerdict
|
|
6
|
+
//
|
|
7
|
+
// A new enum value MUST appear here or TypeScript compilation will fail,
|
|
8
|
+
// ensuring the mapping is always exhaustive.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export function toTestIntegrityVerdict(status) {
|
|
11
|
+
switch (status) {
|
|
12
|
+
case "UNCHANGED":
|
|
13
|
+
case "AUTHORIZED_CHANGE":
|
|
14
|
+
return "VERIFIED";
|
|
15
|
+
case "PREVENTED":
|
|
16
|
+
case "DETECTED_AND_ROLLED_BACK":
|
|
17
|
+
case "DETECTED_NEEDS_REVIEW":
|
|
18
|
+
return "TAMPERING_DETECTED";
|
|
19
|
+
case "NOT_EVALUATED":
|
|
20
|
+
return "NOT_EVALUATED";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Outcome resolution — deterministic, no hidden defaults
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
const STOPPED_LIFECYCLE_STATES = new Set([
|
|
27
|
+
"budget_exit",
|
|
28
|
+
"diminishing_returns",
|
|
29
|
+
"stuck_exit",
|
|
30
|
+
"human_escalation",
|
|
31
|
+
]);
|
|
32
|
+
export function verifierActuallyPassed(evidence, expected) {
|
|
33
|
+
if (!evidence?.passed || !evidence.binding || !evidence.steps) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (evidence.binding.runId !== expected.runId ||
|
|
37
|
+
evidence.binding.workspaceId !== expected.workspaceId ||
|
|
38
|
+
evidence.binding.cwd !== expected.cwd ||
|
|
39
|
+
JSON.stringify(evidence.binding.commands) !== JSON.stringify(expected.commands)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (evidence.steps.length !== expected.commands.length) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return evidence.steps.every((step, index) => step.command === expected.commands[index] &&
|
|
46
|
+
step.launched === true &&
|
|
47
|
+
step.completed === true &&
|
|
48
|
+
step.crashed === false &&
|
|
49
|
+
step.timedOut !== true &&
|
|
50
|
+
step.exitCode === 0);
|
|
51
|
+
}
|
|
52
|
+
export function resolveVerifiedHandoffOutcome(input) {
|
|
53
|
+
if (STOPPED_LIFECYCLE_STATES.has(input.lifecycleState) ||
|
|
54
|
+
input.executionStatus === "write_blocked" ||
|
|
55
|
+
input.executionStatus === "sandbox_blocked" ||
|
|
56
|
+
input.executionStatus === "policy_rejected") {
|
|
57
|
+
return "STOPPED";
|
|
58
|
+
}
|
|
59
|
+
if (input.executionStatus === "failed" ||
|
|
60
|
+
input.governanceClaimEligible === false ||
|
|
61
|
+
input.evidenceContradicted === true ||
|
|
62
|
+
(input.mutationRequired === true &&
|
|
63
|
+
(input.changedFileCount ?? 0) === 0 &&
|
|
64
|
+
input.definitionOfDonePreSatisfied !== true)) {
|
|
65
|
+
return "NEEDS_REVIEW";
|
|
66
|
+
}
|
|
67
|
+
const evidenceTrustworthy = input.receiptIntegrity === "verified";
|
|
68
|
+
const scopeAcceptable = input.scopeStatus === undefined ||
|
|
69
|
+
input.scopeStatus === "WITHIN_SCOPE" ||
|
|
70
|
+
input.scopeStatus === "NOT_EVALUATED";
|
|
71
|
+
const testIntegrityAcceptable = input.testIntegrityStatus === undefined ||
|
|
72
|
+
input.testIntegrityStatus === "UNCHANGED" ||
|
|
73
|
+
input.testIntegrityStatus === "AUTHORIZED_CHANGE" ||
|
|
74
|
+
input.testIntegrityStatus === "NOT_EVALUATED";
|
|
75
|
+
if (input.verificationStatus === "passed" &&
|
|
76
|
+
evidenceTrustworthy &&
|
|
77
|
+
scopeAcceptable &&
|
|
78
|
+
testIntegrityAcceptable &&
|
|
79
|
+
input.unresolvedWorkCount === 0) {
|
|
80
|
+
return "VERIFIED";
|
|
81
|
+
}
|
|
82
|
+
return "NEEDS_REVIEW";
|
|
83
|
+
}
|
|
84
|
+
const SIMULATED_ADAPTER_PATTERN = /(?:^|:)(?:stub|fixture|synthetic)(?::|$)/u;
|
|
85
|
+
function isExecutionMode(value) {
|
|
86
|
+
return value === "governed" || value === "verification_only" || value === "simulated";
|
|
87
|
+
}
|
|
88
|
+
export function deriveVerifiedHandoffExecutionBoundary(loop) {
|
|
89
|
+
const adapterIds = loop.attempts.map((attempt) => attempt.adapterId);
|
|
90
|
+
const hasSimulatedAdapter = adapterIds.some((adapterId) => adapterId === "direct:proof:no-mutation" ||
|
|
91
|
+
SIMULATED_ADAPTER_PATTERN.test(adapterId));
|
|
92
|
+
const hasVerifierOnlyAdapter = adapterIds.some((adapterId) => adapterId === "direct:verifier:verify-only");
|
|
93
|
+
const persistedMode = isExecutionMode(loop.metadata["executionMode"])
|
|
94
|
+
? loop.metadata["executionMode"]
|
|
95
|
+
: undefined;
|
|
96
|
+
const groundingEvidenceAvailable = loop.metadata["groundingEvidenceStatus"] !== "unavailable";
|
|
97
|
+
const executionMode = hasSimulatedAdapter
|
|
98
|
+
? "simulated"
|
|
99
|
+
: hasVerifierOnlyAdapter
|
|
100
|
+
? "verification_only"
|
|
101
|
+
: persistedMode ?? (adapterIds.length > 0 ? "governed" : "simulated");
|
|
102
|
+
return {
|
|
103
|
+
executionMode,
|
|
104
|
+
governanceClaimEligible: executionMode === "governed" && adapterIds.length > 0 && groundingEvidenceAvailable,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function toEvidenceStatus(status) {
|
|
108
|
+
switch (status) {
|
|
109
|
+
case "passed":
|
|
110
|
+
return "PASSED";
|
|
111
|
+
case "failed":
|
|
112
|
+
return "FAILED";
|
|
113
|
+
case "contradicted":
|
|
114
|
+
return "CONTRADICTED";
|
|
115
|
+
case "not_run":
|
|
116
|
+
return "NOT_RUN";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function toCheck(step) {
|
|
120
|
+
const status = !step.launched
|
|
121
|
+
? "NOT_RUN"
|
|
122
|
+
: step.timedOut
|
|
123
|
+
? "FAILED"
|
|
124
|
+
: step.exitCode === 0
|
|
125
|
+
? "PASSED"
|
|
126
|
+
: "FAILED";
|
|
127
|
+
return {
|
|
128
|
+
command: step.command,
|
|
129
|
+
status,
|
|
130
|
+
...(step.exitCode !== undefined ? { exitCode: step.exitCode } : {}),
|
|
131
|
+
...(step.timedOut !== undefined ? { timedOut: step.timedOut } : {}),
|
|
132
|
+
...(step.detail ? { detail: step.detail } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Main builder
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
export function buildVerifiedHandoff(input) {
|
|
139
|
+
const generatedAt = input.generatedAt ?? new Date().toISOString();
|
|
140
|
+
const changedFiles = input.changedFiles ?? [];
|
|
141
|
+
const unresolvedWork = input.unresolvedWork ?? [];
|
|
142
|
+
const scope = {
|
|
143
|
+
status: input.scope?.status ?? "NOT_EVALUATED",
|
|
144
|
+
allowedPaths: input.scope?.allowedPaths ?? input.loop.task.allowedPaths ?? [],
|
|
145
|
+
deniedPaths: input.scope?.deniedPaths ?? input.loop.task.deniedPaths ?? [],
|
|
146
|
+
changedFiles: input.scope?.changedFiles ?? changedFiles,
|
|
147
|
+
violations: input.scope?.violations ?? [],
|
|
148
|
+
};
|
|
149
|
+
const testIntegrityStatus = input.testIntegrity?.status ?? "NOT_EVALUATED";
|
|
150
|
+
const testIntegrity = {
|
|
151
|
+
verdict: toTestIntegrityVerdict(testIntegrityStatus),
|
|
152
|
+
status: testIntegrityStatus,
|
|
153
|
+
protectedPaths: input.testIntegrity?.protectedPaths ?? [],
|
|
154
|
+
changedProtectedPaths: input.testIntegrity?.changedProtectedPaths ?? [],
|
|
155
|
+
findings: input.testIntegrity?.findings ?? [],
|
|
156
|
+
summary: input.testIntegrity?.summary ??
|
|
157
|
+
"Test integrity was not evaluated for this run.",
|
|
158
|
+
};
|
|
159
|
+
const executionBoundary = deriveVerifiedHandoffExecutionBoundary(input.loop);
|
|
160
|
+
const outcome = resolveVerifiedHandoffOutcome({
|
|
161
|
+
lifecycleState: input.loop.lifecycleState,
|
|
162
|
+
executionStatus: input.executionStatus,
|
|
163
|
+
verificationStatus: input.verification.status,
|
|
164
|
+
receiptIntegrity: input.receiptIntegrity.state,
|
|
165
|
+
scopeStatus: scope.status,
|
|
166
|
+
testIntegrityStatus: testIntegrity.status,
|
|
167
|
+
mutationRequired: input.mutationRequired ?? input.loop.task.mutationMode === "edit",
|
|
168
|
+
changedFileCount: scope.changedFiles.length,
|
|
169
|
+
definitionOfDonePreSatisfied: input.definitionOfDonePreSatisfied,
|
|
170
|
+
evidenceContradicted: input.evidenceContradicted ?? input.verification.status === "contradicted",
|
|
171
|
+
governanceClaimEligible: executionBoundary.governanceClaimEligible,
|
|
172
|
+
unresolvedWorkCount: unresolvedWork.length,
|
|
173
|
+
});
|
|
174
|
+
const handoffId = `vh_${createHash("sha256")
|
|
175
|
+
.update(`${input.loop.loopId}:${generatedAt}`)
|
|
176
|
+
.digest("hex")
|
|
177
|
+
.slice(0, 16)}`;
|
|
178
|
+
return {
|
|
179
|
+
schemaVersion: "1.0.0",
|
|
180
|
+
handoffId,
|
|
181
|
+
loopId: input.loop.loopId,
|
|
182
|
+
generatedAt,
|
|
183
|
+
task: {
|
|
184
|
+
title: input.loop.task.title,
|
|
185
|
+
objective: input.loop.task.objective,
|
|
186
|
+
},
|
|
187
|
+
definitionOfDone: {
|
|
188
|
+
acceptanceCriteria: input.loop.task.acceptanceCriteria ?? [],
|
|
189
|
+
verificationPlan: input.loop.task.verificationPlan,
|
|
190
|
+
},
|
|
191
|
+
outcome,
|
|
192
|
+
executionMode: executionBoundary.executionMode,
|
|
193
|
+
governanceClaimEligible: executionBoundary.governanceClaimEligible,
|
|
194
|
+
sourceStatus: {
|
|
195
|
+
status: input.loop.status,
|
|
196
|
+
lifecycleState: input.loop.lifecycleState,
|
|
197
|
+
},
|
|
198
|
+
verification: {
|
|
199
|
+
status: toEvidenceStatus(input.verification.status),
|
|
200
|
+
summary: input.verification.summary,
|
|
201
|
+
checks: input.verification.steps.map(toCheck),
|
|
202
|
+
warnings: input.verification.warnings,
|
|
203
|
+
},
|
|
204
|
+
requirements: input.requirements ?? [],
|
|
205
|
+
scope,
|
|
206
|
+
testIntegrity,
|
|
207
|
+
unresolvedWork,
|
|
208
|
+
...(input.stopReason ? { stopReason: input.stopReason } : {}),
|
|
209
|
+
recovery: {
|
|
210
|
+
rollbackBoundaryAvailable: input.recovery?.rollbackBoundaryAvailable ?? false,
|
|
211
|
+
rollbackAttempted: input.recovery?.rollbackAttempted ?? false,
|
|
212
|
+
...(input.recovery?.rollbackSucceeded !== undefined
|
|
213
|
+
? { rollbackSucceeded: input.recovery.rollbackSucceeded }
|
|
214
|
+
: {}),
|
|
215
|
+
...(input.recovery?.isolatedRef
|
|
216
|
+
? { isolatedRef: input.recovery.isolatedRef }
|
|
217
|
+
: {}),
|
|
218
|
+
...(input.recovery?.nextCommand
|
|
219
|
+
? { nextCommand: input.recovery.nextCommand }
|
|
220
|
+
: {}),
|
|
221
|
+
summary: input.recovery?.summary ??
|
|
222
|
+
"No explicit recovery evidence was supplied to the handoff builder.",
|
|
223
|
+
},
|
|
224
|
+
usage: {
|
|
225
|
+
attempts: input.loop.attempts.length,
|
|
226
|
+
actualUsd: input.loop.cost.actualUsd,
|
|
227
|
+
...(input.loop.cost.estimatedUsd !== undefined
|
|
228
|
+
? { estimatedUsd: input.loop.cost.estimatedUsd }
|
|
229
|
+
: {}),
|
|
230
|
+
tokensIn: input.loop.cost.tokensIn,
|
|
231
|
+
tokensOut: input.loop.cost.tokensOut,
|
|
232
|
+
costProvenance: input.loop.cost.provenance ??
|
|
233
|
+
"unavailable",
|
|
234
|
+
},
|
|
235
|
+
receiptIntegrity: input.receiptIntegrity,
|
|
236
|
+
nextAction: input.nextAction,
|
|
237
|
+
};
|
|
238
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@martinloop/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"mcpName": "io.github.Keesan12/martin-loop",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
|
-
"description": "
|
|
7
|
+
"description": "MCP server for AI coding agents — verifier gates, stop limits, and Verified Handoffs.",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "MartinLoop contributors",
|
|
10
10
|
"homepage": "https://martinloop.com/",
|
|
@@ -18,18 +18,11 @@
|
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"mcp",
|
|
21
|
-
"mcp-server",
|
|
22
21
|
"model-context-protocol",
|
|
23
22
|
"martin-loop",
|
|
24
23
|
"ai-agent",
|
|
25
|
-
"ai-governance",
|
|
26
|
-
"agent-governance",
|
|
27
24
|
"claude",
|
|
28
|
-
"claude-code",
|
|
29
25
|
"codex",
|
|
30
|
-
"cursor",
|
|
31
|
-
"budget-guardrails",
|
|
32
|
-
"verifier-gates",
|
|
33
26
|
"martin_doctor",
|
|
34
27
|
"martin_triage_runs",
|
|
35
28
|
"martin_run_dossier",
|
|
@@ -68,7 +61,8 @@
|
|
|
68
61
|
"start": "node dist/server.js",
|
|
69
62
|
"inspect:live": "node ./scripts/inspect-live.mjs",
|
|
70
63
|
"mcpb:build": "node ./mcpb/build-mcpb.mjs",
|
|
71
|
-
"mcpb:validate": "mcpb validate ./dist-mcpb/martinloop"
|
|
64
|
+
"mcpb:validate": "mcpb validate ./dist-mcpb/martinloop",
|
|
65
|
+
"mcpb:smoke": "node ./mcpb/smoke-mcpb.mjs"
|
|
72
66
|
},
|
|
73
67
|
"dependencies": {
|
|
74
68
|
"@modelcontextprotocol/sdk": "^1.29.0",
|