@martinloop/mcp 0.6.0 → 0.6.2

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.
@@ -1 +1 @@
1
- export declare const MARTIN_MCP_PACKAGE_VERSION = "0.6.0";
1
+ export declare const MARTIN_MCP_PACKAGE_VERSION = "0.6.2";
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by build — do not edit by hand.
2
2
  // Source of truth: packages/mcp/package.json#version
3
- export const MARTIN_MCP_PACKAGE_VERSION = "0.6.0";
3
+ export const MARTIN_MCP_PACKAGE_VERSION = "0.6.2";
@@ -55,6 +55,8 @@ export interface CodexExecArgsOptions {
55
55
  autonomyResolution?: CodexAutonomyResolution;
56
56
  }
57
57
  type SpawnSyncLike = typeof spawnSync;
58
+ export declare function markCodexAutonomyResolutionVerifiedByLaunchProbe(resolution: CodexAutonomyResolution): CodexAutonomyResolution;
59
+ export declare function isCodexAutonomyResolutionVerifiedByLaunchProbe(resolution: CodexAutonomyResolution): boolean;
58
60
  export declare function clearCodexCapabilityCacheForTests(): void;
59
61
  export declare function cacheCodexCapabilityProfile(profile: CodexCapabilityProfile, platform?: NodeJS.Platform): CodexCapabilityProfile;
60
62
  export declare function probeCodexCapabilities(binaryPath: string, options?: {
@@ -3,6 +3,14 @@ import { extname } from "node:path";
3
3
  import { createSpawnPlan, resolveNpmShimScript } from "./cli-bridge.js";
4
4
  import { DEFAULT_AGENT_EXECUTION_INTENT } from "../contracts/index.js";
5
5
  const capabilityCache = new Map();
6
+ const launchProbeVerifiedAutonomyResolutions = new WeakSet();
7
+ export function markCodexAutonomyResolutionVerifiedByLaunchProbe(resolution) {
8
+ launchProbeVerifiedAutonomyResolutions.add(resolution);
9
+ return resolution;
10
+ }
11
+ export function isCodexAutonomyResolutionVerifiedByLaunchProbe(resolution) {
12
+ return launchProbeVerifiedAutonomyResolutions.has(resolution);
13
+ }
6
14
  function escapeRegex(value) {
7
15
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
8
16
  }
@@ -197,6 +205,20 @@ export function codexWriteStrategies(profile) {
197
205
  }
198
206
  export function resolveCodexAutonomyCandidates(profile, intent = DEFAULT_AGENT_EXECUTION_INTENT) {
199
207
  const candidates = [];
208
+ // Prefer the least-privileged advertised workspace-write mode. The launch
209
+ // probe still has final authority: it must prove an in-workspace write and
210
+ // prove the paired outside-workspace marker was denied before this
211
+ // resolution can be selected for a governed run.
212
+ if (profile.sandbox?.values.includes("workspace-write")) {
213
+ candidates.push({
214
+ binaryPath: profile.binaryPath,
215
+ intent,
216
+ strategy: "sandbox+approval",
217
+ sandboxValue: "workspace-write"
218
+ });
219
+ }
220
+ // Keep provider automation as a compatibility fallback. It is never trusted
221
+ // merely because it is advertised; the same boundary probe must prove it.
200
222
  if (profile.automation) {
201
223
  candidates.push({
202
224
  binaryPath: profile.binaryPath,
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createAgentCliAdapter } from "./claude-cli.js";
3
- import { buildCodexExecArgs, buildCodexStdin, probeCodexCapabilities } from "./codex-launcher.js";
3
+ import { buildCodexExecArgs, buildCodexStdin, isCodexAutonomyResolutionVerifiedByLaunchProbe, probeCodexCapabilities } from "./codex-launcher.js";
4
4
  import { createSpawnPlan } from "./cli-bridge.js";
5
5
  function createCodexSpawnRouter(input) {
6
6
  if (input.injectedSpawn) {
@@ -33,6 +33,9 @@ export function createCodexCliAdapter(options = {}) {
33
33
  if (autonomyResolution && autonomyResolution.binaryPath !== selectedBinary) {
34
34
  throw new Error("Codex exact binary autonomy resolution mismatch.");
35
35
  }
36
+ if (autonomyResolution && !isCodexAutonomyResolutionVerifiedByLaunchProbe(autonomyResolution)) {
37
+ throw new Error("Codex autonomy resolution was not verified by the launch probe.");
38
+ }
36
39
  const sandbox = options.sandbox ?? "workspace-write";
37
40
  const extraArgs = options.extraArgs ?? [];
38
41
  const launchModel = options.model;
@@ -1,4 +1,5 @@
1
1
  import { type CodexExecArgsOptions } from "./codex-capabilities.js";
2
+ import { probeCodexLaunch as probeCodexLaunchHost, type CodexLaunchProbeResult } from "./codex-host.js";
2
3
  /**
3
4
  * Compatibility facade for callers that still populate the original
4
5
  * `approval` profile field. Internally automation modes and approval policies
@@ -6,5 +7,11 @@ import { type CodexExecArgsOptions } from "./codex-capabilities.js";
6
7
  * builder without restoring any hard-coded flag assumption.
7
8
  */
8
9
  export declare function buildCodexExecArgs(options: CodexExecArgsOptions): string[];
9
- export { buildCodexStdin, cacheCodexCapabilityProfile, clearCodexCapabilityCacheForTests, codexWriteStrategies, probeCodexCapabilities, resolveCodexAutonomyCandidates, type CodexAutonomyResolution, type CodexApprovalCapability, type CodexCapabilityFlag, type CodexCapabilityProfile, type CodexExecArgsOptions, type CodexFlagScope, type CodexPromptTransport, type CodexSandboxCapability, type CodexWriteStrategy } from "./codex-capabilities.js";
10
- export { checkCodexSandboxPreflight, detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, probeFilesystemWriteCapability, resolveCliCommandAvailability, type CliCommandAvailability, type CodexHostDiagnosis, type CodexHostPlatform, type CodexInstallKind, type CodexInvocationMode, type CodexLaunchProbeResult, type CodexProbeCandidateResult, type CodexSandboxPreflightOk, type CodexSandboxPreflightOutcome, type CodexSandboxPreflightReadOnly } from "./codex-host.js";
10
+ /**
11
+ * Public launch-probe facade. Only a successful probe may brand an autonomy
12
+ * resolution as safe for adapter execution; structurally similar objects
13
+ * supplied directly by callers remain untrusted.
14
+ */
15
+ export declare function probeCodexLaunch(input: Parameters<typeof probeCodexLaunchHost>[0]): CodexLaunchProbeResult;
16
+ export { buildCodexStdin, cacheCodexCapabilityProfile, clearCodexCapabilityCacheForTests, codexWriteStrategies, isCodexAutonomyResolutionVerifiedByLaunchProbe, probeCodexCapabilities, resolveCodexAutonomyCandidates, type CodexAutonomyResolution, type CodexApprovalCapability, type CodexCapabilityFlag, type CodexCapabilityProfile, type CodexExecArgsOptions, type CodexFlagScope, type CodexPromptTransport, type CodexSandboxCapability, type CodexWriteStrategy } from "./codex-capabilities.js";
17
+ export { checkCodexSandboxPreflight, detectCodexHostPlatform, diagnoseCodexHost, probeFilesystemWriteCapability, resolveCliCommandAvailability, type CliCommandAvailability, type CodexHostDiagnosis, type CodexHostPlatform, type CodexInstallKind, type CodexInvocationMode, type CodexLaunchProbeResult, type CodexProbeCandidateResult, type CodexSandboxPreflightOk, type CodexSandboxPreflightOutcome, type CodexSandboxPreflightReadOnly } from "./codex-host.js";
@@ -1,4 +1,5 @@
1
- import { buildCodexExecArgs as buildCapabilityDrivenCodexExecArgs } from "./codex-capabilities.js";
1
+ import { buildCodexExecArgs as buildCapabilityDrivenCodexExecArgs, markCodexAutonomyResolutionVerifiedByLaunchProbe } from "./codex-capabilities.js";
2
+ import { probeCodexLaunch as probeCodexLaunchHost } from "./codex-host.js";
2
3
  /**
3
4
  * Compatibility facade for callers that still populate the original
4
5
  * `approval` profile field. Internally automation modes and approval policies
@@ -18,5 +19,17 @@ export function buildCodexExecArgs(options) {
18
19
  capabilityProfile: normalizedProfile
19
20
  });
20
21
  }
21
- export { buildCodexStdin, cacheCodexCapabilityProfile, clearCodexCapabilityCacheForTests, codexWriteStrategies, probeCodexCapabilities, resolveCodexAutonomyCandidates } from "./codex-capabilities.js";
22
- export { checkCodexSandboxPreflight, detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, probeFilesystemWriteCapability, resolveCliCommandAvailability } from "./codex-host.js";
22
+ /**
23
+ * Public launch-probe facade. Only a successful probe may brand an autonomy
24
+ * resolution as safe for adapter execution; structurally similar objects
25
+ * supplied directly by callers remain untrusted.
26
+ */
27
+ export function probeCodexLaunch(input) {
28
+ const result = probeCodexLaunchHost(input);
29
+ if (result.ok && result.autonomyResolution) {
30
+ markCodexAutonomyResolutionVerifiedByLaunchProbe(result.autonomyResolution);
31
+ }
32
+ return result;
33
+ }
34
+ export { buildCodexStdin, cacheCodexCapabilityProfile, clearCodexCapabilityCacheForTests, codexWriteStrategies, isCodexAutonomyResolutionVerifiedByLaunchProbe, probeCodexCapabilities, resolveCodexAutonomyCandidates } from "./codex-capabilities.js";
35
+ export { checkCodexSandboxPreflight, detectCodexHostPlatform, diagnoseCodexHost, probeFilesystemWriteCapability, resolveCliCommandAvailability } from "./codex-host.js";
@@ -30,6 +30,7 @@
30
30
  */
31
31
  import { readGitChangedFiles, runVerification } from "./cli-bridge.js";
32
32
  import { createAdapterCapabilities, normalizeUsage } from "./runtime-support.js";
33
+ import { applyWorkspaceEdits, buildWorkspaceSnapshot } from "./workspace-edit-protocol.js";
33
34
  // ---------------------------------------------------------------------------
34
35
  // OpenRouter/OpenAI-compatible model pricing ($/1K tokens)
35
36
  // Automatically used when baseUrl contains openrouter.ai or known providers.
@@ -105,11 +106,12 @@ function normalizeOpenAiCompatibleUsage(input) {
105
106
  // ---------------------------------------------------------------------------
106
107
  const DEFAULT_SYSTEM_PROMPT = `You are an expert software engineer executing a governed coding task.
107
108
  Follow these rules exactly:
108
- - Read the task description carefully and implement only what is asked.
109
- - Do not add features, refactors, or improvements beyond the stated task.
110
- - If the task asks you to write or modify code, output the complete file content with changes applied.
111
- - Be precise, minimal, and test-backed in all changes.
112
- - State what you changed and why at the end of your response.`;
109
+ - Read the task description and repository snapshot carefully and implement only what is asked.
110
+ - Do not add unrelated features, refactors, or improvements.
111
+ - Respect every allowed-path and denied-path boundary in the task contract.
112
+ - For governed coding runs, return ONLY the structured JSON edit plan requested in the user message.
113
+ - Every edit must contain the complete replacement content for one repository-relative text file.
114
+ - Do not claim success unless your proposed edits satisfy the acceptance criteria and verifier.`;
113
115
  export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
114
116
  export function resolveOpenAiCompatibleRuntimeConfig(env = process.env) {
115
117
  const apiKey = env["MARTIN_OPENAI_API_KEY"] ?? "";
@@ -121,7 +123,7 @@ export function resolveOpenAiCompatibleRuntimeConfig(env = process.env) {
121
123
  authPosture: apiKey.length > 0 ? "api_key" : "anonymous_or_local"
122
124
  };
123
125
  }
124
- function buildPrompt(request) {
126
+ function buildPrompt(request, workspaceSnapshot = "") {
125
127
  const lines = [
126
128
  `TASK: ${request.context.taskTitle}`,
127
129
  ``,
@@ -132,6 +134,15 @@ function buildPrompt(request) {
132
134
  if (request.context.focus) {
133
135
  lines.push(`FOCUS: ${request.context.focus}`, ``);
134
136
  }
137
+ if ((request.context.acceptanceCriteria?.length ?? 0) > 0) {
138
+ lines.push(`ACCEPTANCE CRITERIA:`, ...(request.context.acceptanceCriteria ?? []).map((criterion) => ` - ${criterion}`), ``);
139
+ }
140
+ if ((request.context.allowedPaths?.length ?? 0) > 0) {
141
+ lines.push(`ALLOWED EDIT PATHS:`, ...(request.context.allowedPaths ?? []).map((path) => ` - ${path}`), ``);
142
+ }
143
+ if ((request.context.deniedPaths?.length ?? 0) > 0) {
144
+ lines.push(`DENIED EDIT PATHS:`, ...(request.context.deniedPaths ?? []).map((path) => ` - ${path}`), ``);
145
+ }
135
146
  if (request.context.verificationPlan.length > 0) {
136
147
  lines.push(`VERIFICATION COMMANDS (must pass after your changes):`, ...request.context.verificationPlan.map((cmd) => ` ${cmd}`), ``);
137
148
  }
@@ -142,6 +153,9 @@ function buildPrompt(request) {
142
153
  }
143
154
  }
144
155
  lines.push(`BUDGET REMAINING: $${request.context.remainingBudgetUsd.toFixed(4)} | Iterations left: ${request.context.remainingIterations}`);
156
+ if (workspaceSnapshot) {
157
+ lines.push(``, `WORKSPACE SNAPSHOT (read-only context; paths are repository-relative):`, workspaceSnapshot, ``, `RESPONSE CONTRACT:`, `Return ONLY JSON with this shape:`, `{"summary":"short description","edits":[{"path":"src/file.ts","content":"complete replacement file content"}],"deletions":[]}`, `Use only repository-relative paths. Every proposed path is validated by MartinLoop before any file is written.`, `If a file should not change, omit it. Do not wrap the JSON in explanatory prose.`);
158
+ }
145
159
  return lines.join("\n");
146
160
  }
147
161
  // ---------------------------------------------------------------------------
@@ -181,10 +195,21 @@ export function createOpenAiCompatibleAdapter(options) {
181
195
  })
182
196
  },
183
197
  async execute(request) {
184
- const prompt = buildPrompt(request);
185
- const estimated = estimateCost(model, prompt.length, 2000);
186
198
  const hasVerificationSteps = request.context.verificationPlan.length > 0 ||
187
199
  (request.context.verificationStack?.length ?? 0) > 0;
200
+ const governedCodingRun = request.context.mutationMode === "edit" ||
201
+ hasVerificationSteps ||
202
+ (request.context.allowedPaths?.length ?? 0) > 0 ||
203
+ (request.context.deniedPaths?.length ?? 0) > 0;
204
+ const workspaceSnapshot = governedCodingRun
205
+ ? await buildWorkspaceSnapshot({
206
+ workingDirectory,
207
+ allowedPaths: request.context.allowedPaths,
208
+ deniedPaths: request.context.deniedPaths
209
+ })
210
+ : "";
211
+ const prompt = buildPrompt(request, workspaceSnapshot);
212
+ const estimated = estimateCost(model, prompt.length, 2000);
188
213
  const baselineChangedFiles = hasVerificationSteps
189
214
  ? new Set(await readGitChangedFiles(workingDirectory, 5_000))
190
215
  : new Set();
@@ -336,6 +361,33 @@ export function createOpenAiCompatibleAdapter(options) {
336
361
  failure: { message: "empty_response" }
337
362
  };
338
363
  }
364
+ if (governedCodingRun) {
365
+ try {
366
+ await applyWorkspaceEdits({
367
+ workingDirectory,
368
+ responseText,
369
+ allowedPaths: request.context.allowedPaths,
370
+ deniedPaths: request.context.deniedPaths
371
+ });
372
+ }
373
+ catch (error) {
374
+ const message = error instanceof Error ? error.message : String(error);
375
+ return {
376
+ status: "failed",
377
+ summary: `${model} did not produce an admissible governed workspace edit: ${message}`,
378
+ usage: normalizeOpenAiCompatibleUsage({
379
+ model,
380
+ tokensIn,
381
+ tokensOut,
382
+ usageWasFullyProviderReported,
383
+ modelSource,
384
+ billingMode
385
+ }),
386
+ verification: { passed: false, summary: "Workspace edit protocol failed before verifier execution." },
387
+ failure: { message }
388
+ };
389
+ }
390
+ }
339
391
  // Run verification
340
392
  const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack, undefined, {
341
393
  runId: request.loopId,
@@ -0,0 +1,34 @@
1
+ export interface WorkspaceEdit {
2
+ path: string;
3
+ content: string;
4
+ }
5
+ export interface WorkspaceEditPlan {
6
+ summary?: string;
7
+ edits: WorkspaceEdit[];
8
+ deletions: string[];
9
+ }
10
+ export interface WorkspaceSnapshotOptions {
11
+ workingDirectory: string;
12
+ allowedPaths?: string[];
13
+ deniedPaths?: string[];
14
+ maxFiles?: number;
15
+ maxBytes?: number;
16
+ maxFileBytes?: number;
17
+ }
18
+ export interface ApplyWorkspaceEditsOptions {
19
+ workingDirectory: string;
20
+ responseText: string;
21
+ allowedPaths?: string[];
22
+ deniedPaths?: string[];
23
+ }
24
+ export declare function pathAllowedByGovernance(path: string, allowedPaths: string[] | undefined, deniedPaths: string[] | undefined): boolean;
25
+ export declare function parseWorkspaceEditPlan(responseText: string): WorkspaceEditPlan;
26
+ export declare function applyWorkspaceEditPlan(plan: WorkspaceEditPlan, options: Omit<ApplyWorkspaceEditsOptions, "responseText">): Promise<{
27
+ changedFiles: string[];
28
+ summary?: string;
29
+ }>;
30
+ export declare function applyWorkspaceEdits(options: ApplyWorkspaceEditsOptions): Promise<{
31
+ changedFiles: string[];
32
+ summary?: string;
33
+ }>;
34
+ export declare function buildWorkspaceSnapshot(options: WorkspaceSnapshotOptions): Promise<string>;
@@ -0,0 +1,271 @@
1
+ import { lstat, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ const DEFAULT_MAX_FILES = 48;
4
+ const DEFAULT_MAX_BYTES = 120_000;
5
+ const DEFAULT_MAX_FILE_BYTES = 24_000;
6
+ const SKIP_DIRECTORIES = new Set([
7
+ ".git",
8
+ ".martin",
9
+ "node_modules",
10
+ "dist",
11
+ "build",
12
+ "coverage",
13
+ ".next",
14
+ ".turbo",
15
+ ]);
16
+ function normalizePath(value) {
17
+ return value.replace(/\\/gu, "/").replace(/^\.\//u, "");
18
+ }
19
+ function globToRegExp(pattern) {
20
+ const normalized = normalizePath(pattern.trim());
21
+ let source = "^";
22
+ for (let i = 0; i < normalized.length; i += 1) {
23
+ const ch = normalized[i] ?? "";
24
+ if (ch === "*") {
25
+ if (normalized[i + 1] === "*") {
26
+ if (normalized[i + 2] === "/") {
27
+ source += "(?:.*/)?";
28
+ i += 2;
29
+ }
30
+ else {
31
+ source += ".*";
32
+ i += 1;
33
+ }
34
+ }
35
+ else {
36
+ source += "[^/]*";
37
+ }
38
+ continue;
39
+ }
40
+ if (ch === "?") {
41
+ source += "[^/]";
42
+ continue;
43
+ }
44
+ source += ch.replace(/[|\\{}()[\]^$+?.]/gu, "\\$&");
45
+ }
46
+ source += "$";
47
+ return new RegExp(source, "u");
48
+ }
49
+ function matchesAny(path, patterns) {
50
+ if (!patterns || patterns.length === 0) {
51
+ return false;
52
+ }
53
+ const normalized = normalizePath(path);
54
+ return patterns.some((pattern) => globToRegExp(pattern).test(normalized));
55
+ }
56
+ export function pathAllowedByGovernance(path, allowedPaths, deniedPaths) {
57
+ const normalized = normalizePath(path);
58
+ if (matchesAny(normalized, deniedPaths)) {
59
+ return false;
60
+ }
61
+ if (!allowedPaths || allowedPaths.length === 0) {
62
+ return true;
63
+ }
64
+ return matchesAny(normalized, allowedPaths);
65
+ }
66
+ function assertSafeRelativePath(path, workingDirectory) {
67
+ const normalized = normalizePath(path.trim());
68
+ if (!normalized || isAbsolute(path) || normalized.startsWith("../") || normalized === "..") {
69
+ throw new Error(`WORKSPACE_EDIT_INVALID_PATH: ${path}`);
70
+ }
71
+ const target = resolve(workingDirectory, normalized);
72
+ const rel = relative(resolve(workingDirectory), target);
73
+ if (!rel || rel.startsWith(`..${sep}`) || rel === ".." || isAbsolute(rel)) {
74
+ throw new Error(`WORKSPACE_EDIT_PATH_ESCAPE: ${path}`);
75
+ }
76
+ return normalized;
77
+ }
78
+ async function assertNoSymlinkTraversal(path, workingDirectory) {
79
+ const root = resolve(workingDirectory);
80
+ const segments = normalizePath(path).split("/").filter(Boolean);
81
+ let cursor = root;
82
+ for (const segment of segments) {
83
+ cursor = resolve(cursor, segment);
84
+ try {
85
+ const info = await lstat(cursor);
86
+ if (info.isSymbolicLink()) {
87
+ throw new Error(`WORKSPACE_EDIT_SYMLINK_REJECTED: ${path}`);
88
+ }
89
+ }
90
+ catch (error) {
91
+ const code = error.code;
92
+ if (code === "ENOENT") {
93
+ return;
94
+ }
95
+ throw error;
96
+ }
97
+ }
98
+ }
99
+ function extractJsonPayload(responseText) {
100
+ const trimmed = responseText.trim();
101
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/iu)?.[1]?.trim();
102
+ if (fenced) {
103
+ return fenced;
104
+ }
105
+ const first = trimmed.indexOf("{");
106
+ const last = trimmed.lastIndexOf("}");
107
+ if (first >= 0 && last > first) {
108
+ return trimmed.slice(first, last + 1);
109
+ }
110
+ return trimmed;
111
+ }
112
+ export function parseWorkspaceEditPlan(responseText) {
113
+ let parsed;
114
+ try {
115
+ parsed = JSON.parse(extractJsonPayload(responseText));
116
+ }
117
+ catch (error) {
118
+ throw new Error(`WORKSPACE_EDIT_PROTOCOL_INVALID_JSON: ${error instanceof Error ? error.message : String(error)}`);
119
+ }
120
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
121
+ throw new Error("WORKSPACE_EDIT_PROTOCOL_INVALID_SHAPE: expected JSON object");
122
+ }
123
+ const record = parsed;
124
+ const rawEdits = record.edits ?? [];
125
+ const rawDeletions = record.deletions ?? [];
126
+ if (!Array.isArray(rawEdits) || !Array.isArray(rawDeletions)) {
127
+ throw new Error("WORKSPACE_EDIT_PROTOCOL_INVALID_SHAPE: edits/deletions must be arrays");
128
+ }
129
+ const edits = rawEdits.map((entry, index) => {
130
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
131
+ throw new Error(`WORKSPACE_EDIT_PROTOCOL_INVALID_EDIT: edits[${index}]`);
132
+ }
133
+ const edit = entry;
134
+ if (typeof edit.path !== "string" || typeof edit.content !== "string") {
135
+ throw new Error(`WORKSPACE_EDIT_PROTOCOL_INVALID_EDIT: edits[${index}] requires path/content strings`);
136
+ }
137
+ return { path: edit.path, content: edit.content };
138
+ });
139
+ const deletions = rawDeletions.map((entry, index) => {
140
+ if (typeof entry !== "string") {
141
+ throw new Error(`WORKSPACE_EDIT_PROTOCOL_INVALID_DELETION: deletions[${index}]`);
142
+ }
143
+ return entry;
144
+ });
145
+ if (edits.length === 0 && deletions.length === 0) {
146
+ throw new Error("WORKSPACE_EDIT_PROTOCOL_NO_CHANGES: model returned no workspace edits");
147
+ }
148
+ return {
149
+ ...(typeof record.summary === "string" ? { summary: record.summary } : {}),
150
+ edits,
151
+ deletions,
152
+ };
153
+ }
154
+ export async function applyWorkspaceEditPlan(plan, options) {
155
+ const normalizedEdits = plan.edits.map((edit) => ({
156
+ ...edit,
157
+ path: assertSafeRelativePath(edit.path, options.workingDirectory),
158
+ }));
159
+ const normalizedDeletions = plan.deletions.map((path) => assertSafeRelativePath(path, options.workingDirectory));
160
+ const proposedPaths = [
161
+ ...normalizedEdits.map((edit) => edit.path),
162
+ ...normalizedDeletions,
163
+ ];
164
+ const duplicate = proposedPaths.find((path, index) => proposedPaths.indexOf(path) !== index);
165
+ if (duplicate) {
166
+ throw new Error(`WORKSPACE_EDIT_PROTOCOL_DUPLICATE_PATH: ${duplicate}`);
167
+ }
168
+ for (const path of proposedPaths) {
169
+ if (!pathAllowedByGovernance(path, options.allowedPaths, options.deniedPaths)) {
170
+ throw new Error(`WORKSPACE_EDIT_GOVERNANCE_REJECTED: ${path}`);
171
+ }
172
+ await assertNoSymlinkTraversal(path, options.workingDirectory);
173
+ }
174
+ for (const edit of normalizedEdits) {
175
+ const target = resolve(options.workingDirectory, edit.path);
176
+ await mkdir(dirname(target), { recursive: true });
177
+ await writeFile(target, edit.content, "utf8");
178
+ }
179
+ for (const path of normalizedDeletions) {
180
+ await rm(resolve(options.workingDirectory, path), { force: true });
181
+ }
182
+ return {
183
+ changedFiles: proposedPaths,
184
+ ...(plan.summary ? { summary: plan.summary } : {}),
185
+ };
186
+ }
187
+ export async function applyWorkspaceEdits(options) {
188
+ return applyWorkspaceEditPlan(parseWorkspaceEditPlan(options.responseText), options);
189
+ }
190
+ async function walkFiles(root, current = root) {
191
+ const entries = await readdir(current, { withFileTypes: true });
192
+ const files = [];
193
+ for (const entry of entries) {
194
+ if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name)) {
195
+ continue;
196
+ }
197
+ const absolute = resolve(current, entry.name);
198
+ if (entry.isDirectory()) {
199
+ files.push(...(await walkFiles(root, absolute)));
200
+ continue;
201
+ }
202
+ if (entry.isFile()) {
203
+ files.push(normalizePath(relative(root, absolute)));
204
+ }
205
+ }
206
+ return files;
207
+ }
208
+ function isProbablyBinary(buffer) {
209
+ const sample = buffer.subarray(0, Math.min(buffer.length, 8_192));
210
+ return sample.includes(0);
211
+ }
212
+ function scoreSnapshotPath(path, allowedPaths, deniedPaths) {
213
+ let score = 0;
214
+ if (matchesAny(path, allowedPaths))
215
+ score += 100;
216
+ if (matchesAny(path, deniedPaths))
217
+ score += 50;
218
+ if (/^(?:src|app|lib|test|tests|config)\//u.test(path))
219
+ score += 25;
220
+ if (/^(?:package\.json|pyproject\.toml|Cargo\.toml|go\.mod|README\.md)$/u.test(path))
221
+ score += 20;
222
+ if (/\.(?:ts|tsx|js|jsx|py|go|rs|java|kt|json|yaml|yml|toml|md)$/u.test(path))
223
+ score += 10;
224
+ return score;
225
+ }
226
+ export async function buildWorkspaceSnapshot(options) {
227
+ const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
228
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
229
+ const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
230
+ const root = resolve(options.workingDirectory);
231
+ const candidates = (await walkFiles(root)).sort((a, b) => {
232
+ const scoreDelta = scoreSnapshotPath(b, options.allowedPaths, options.deniedPaths) -
233
+ scoreSnapshotPath(a, options.allowedPaths, options.deniedPaths);
234
+ return scoreDelta || a.localeCompare(b);
235
+ });
236
+ const sections = [];
237
+ let totalBytes = 0;
238
+ let included = 0;
239
+ for (const path of candidates) {
240
+ if (included >= maxFiles || totalBytes >= maxBytes)
241
+ break;
242
+ const absolute = resolve(root, path);
243
+ let info;
244
+ try {
245
+ info = await stat(absolute);
246
+ }
247
+ catch {
248
+ continue;
249
+ }
250
+ if (!info.isFile() || info.size > maxFileBytes)
251
+ continue;
252
+ let buffer;
253
+ try {
254
+ buffer = await readFile(absolute);
255
+ }
256
+ catch {
257
+ continue;
258
+ }
259
+ if (isProbablyBinary(buffer))
260
+ continue;
261
+ const content = buffer.toString("utf8");
262
+ const section = `--- FILE: ${path} ---\n${content}\n--- END FILE ---`;
263
+ const sectionBytes = Buffer.byteLength(section, "utf8");
264
+ if (totalBytes + sectionBytes > maxBytes)
265
+ continue;
266
+ sections.push(section);
267
+ totalBytes += sectionBytes;
268
+ included += 1;
269
+ }
270
+ return sections.join("\n\n");
271
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@martinloop/mcp",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "mcpName": "io.github.Keesan12/martin-loop",
5
5
  "private": false,
6
6
  "type": "module",
7
- "description": "MCP server for AI coding agents — verifier gates, stop limits, and Verified Handoffs.",
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/",
@@ -41,11 +41,6 @@
41
41
  "martin-loop-mcp": "./dist/server.js"
42
42
  },
43
43
  "exports": {
44
- ".": {
45
- "types": "./dist/server.d.ts",
46
- "import": "./dist/server.js",
47
- "default": "./dist/server.js"
48
- },
49
44
  "./server.json": "./server.json",
50
45
  "./package.json": "./package.json"
51
46
  },
@@ -79,12 +74,12 @@
79
74
  "mcpb:smoke": "node ./mcpb/smoke-mcpb.mjs"
80
75
  },
81
76
  "dependencies": {
82
- "@modelcontextprotocol/sdk": "1.29.0",
77
+ "@modelcontextprotocol/sdk": "^1.29.0",
83
78
  "@open-policy-agent/opa-wasm": "^1.10.0",
84
- "@opentelemetry/api-logs": "^0.221.0",
85
- "@opentelemetry/exporter-logs-otlp-http": "^0.221.0",
86
- "@opentelemetry/resources": "^2.10.0",
87
- "@opentelemetry/sdk-logs": "^0.221.0",
79
+ "@opentelemetry/api-logs": "^0.214.0",
80
+ "@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
81
+ "@opentelemetry/resources": "^2.6.1",
82
+ "@opentelemetry/sdk-logs": "^0.214.0",
88
83
  "ts-morph": "^21.0.0"
89
84
  },
90
85
  "devDependencies": {
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/Keesan12/martin-loop",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.6.0",
10
+ "version": "0.6.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@martinloop/mcp",
15
- "version": "0.6.0",
15
+ "version": "0.6.2",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }