@martinloop/mcp 0.2.0 → 0.2.5

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.
Files changed (76) hide show
  1. package/README.md +131 -158
  2. package/dist/discovery-metadata.d.ts +16 -0
  3. package/dist/discovery-metadata.js +62 -0
  4. package/dist/discovery-support.d.ts +62 -0
  5. package/dist/discovery-support.js +224 -0
  6. package/dist/package-version.d.ts +1 -0
  7. package/dist/package-version.js +3 -0
  8. package/dist/prompts.d.ts +13 -3
  9. package/dist/prompts.js +445 -74
  10. package/dist/resources.d.ts +27 -5
  11. package/dist/resources.js +557 -71
  12. package/dist/server-validation.d.ts +2 -3
  13. package/dist/server-validation.js +262 -122
  14. package/dist/server.d.ts +76 -7
  15. package/dist/server.js +1126 -400
  16. package/dist/tools/doctor.js +14 -6
  17. package/dist/tools/get-attempt.d.ts +13 -6
  18. package/dist/tools/get-attempt.js +14 -5
  19. package/dist/tools/get-run.d.ts +19 -12
  20. package/dist/tools/get-run.js +20 -11
  21. package/dist/tools/get-status.d.ts +11 -0
  22. package/dist/tools/get-status.js +12 -2
  23. package/dist/tools/get-verification-results.d.ts +10 -7
  24. package/dist/tools/get-verification-results.js +11 -6
  25. package/dist/tools/inspect-loop.d.ts +9 -0
  26. package/dist/tools/inspect-loop.js +11 -2
  27. package/dist/tools/list-runs.d.ts +25 -5
  28. package/dist/tools/list-runs.js +21 -4
  29. package/dist/tools/preflight.js +7 -2
  30. package/dist/tools/run-dossier.d.ts +37 -4
  31. package/dist/tools/run-dossier.js +40 -5
  32. package/dist/tools/run-loop.d.ts +19 -0
  33. package/dist/tools/run-loop.js +41 -3
  34. package/dist/tools/run-store.d.ts +57 -3
  35. package/dist/tools/run-store.js +404 -53
  36. package/dist/tools/tool-errors.d.ts +37 -0
  37. package/dist/tools/tool-errors.js +170 -0
  38. package/dist/tools/tool-response.d.ts +16 -0
  39. package/dist/tools/tool-response.js +34 -0
  40. package/dist/tools/tool-support.d.ts +92 -2
  41. package/dist/tools/tool-support.js +358 -63
  42. package/dist/tools/triage-runs.d.ts +33 -0
  43. package/dist/tools/triage-runs.js +138 -0
  44. package/dist/vendor/adapters/claude-cli.js +0 -1
  45. package/dist/vendor/adapters/cli-bridge.js +0 -1
  46. package/dist/vendor/adapters/direct-provider.js +0 -1
  47. package/dist/vendor/adapters/index.js +0 -1
  48. package/dist/vendor/adapters/runtime-support.js +0 -1
  49. package/dist/vendor/adapters/stub-agent-cli.js +0 -1
  50. package/dist/vendor/adapters/stub-direct-provider.js +0 -1
  51. package/dist/vendor/adapters/verifier-only.js +0 -1
  52. package/dist/vendor/contracts/governance.js +0 -1
  53. package/dist/vendor/contracts/index.d.ts +2 -0
  54. package/dist/vendor/contracts/index.js +1 -1
  55. package/dist/vendor/contracts/operator.d.ts +19 -0
  56. package/dist/vendor/contracts/operator.js +11 -0
  57. package/dist/vendor/core/compiler.js +0 -1
  58. package/dist/vendor/core/context-integrity.js +0 -1
  59. package/dist/vendor/core/grounding.js +0 -1
  60. package/dist/vendor/core/index.js +1 -2
  61. package/dist/vendor/core/leash.js +19 -12
  62. package/dist/vendor/core/persistence/compiler.js +0 -1
  63. package/dist/vendor/core/persistence/index.js +0 -1
  64. package/dist/vendor/core/persistence/ledger.js +0 -1
  65. package/dist/vendor/core/persistence/runs-reader.js +0 -1
  66. package/dist/vendor/core/persistence/store.js +0 -1
  67. package/dist/vendor/core/policy.js +0 -1
  68. package/dist/vendor/core/red-blue/red-phase.d.ts +64 -0
  69. package/dist/vendor/core/red-blue/red-phase.js +135 -0
  70. package/dist/vendor/core/red-blue/risk-tiers.d.ts +22 -0
  71. package/dist/vendor/core/red-blue/risk-tiers.js +32 -0
  72. package/dist/vendor/core/rollback.js +2 -3
  73. package/package.json +10 -5
  74. package/server.json +2 -2
  75. package/dist/tools/cockpit-support.d.ts +0 -69
  76. package/dist/tools/cockpit-support.js +0 -108
@@ -1,19 +1,24 @@
1
- import { extname, isAbsolute, relative, resolve } from "node:path";
1
+ import { existsSync, lstatSync, realpathSync } from "node:fs";
2
+ import { dirname, extname, isAbsolute, relative, resolve } from "node:path";
2
3
  import { resolveRunsRoot } from "./vendor/core/index.js";
4
+ import { invalidArgumentsError, invalidPathError, invalidSelectorError } from "./tools/tool-errors.js";
5
+ export { sanitizeToolErrorMessage } from "./tools/tool-errors.js";
3
6
  export function validateToolInput(name, args) {
4
7
  switch (name) {
5
- case "martin_doctor":
6
- return validateDoctorInput(args);
7
- case "martin_preflight":
8
- return validatePreflightInput(args);
9
8
  case "martin_run":
10
9
  return validateRunInput(args);
11
10
  case "martin_inspect":
12
11
  return validateInspectInput(args);
13
12
  case "martin_status":
14
13
  return validateStatusInput(args);
14
+ case "martin_doctor":
15
+ return validateDoctorInput(args);
16
+ case "martin_preflight":
17
+ return validatePreflightInput(args);
15
18
  case "martin_list_runs":
16
19
  return validateListRunsInput(args);
20
+ case "martin_triage_runs":
21
+ return validateTriageRunsInput(args);
17
22
  case "martin_get_run":
18
23
  return validateGetRunInput(args);
19
24
  case "martin_get_attempt":
@@ -23,93 +28,51 @@ export function validateToolInput(name, args) {
23
28
  case "martin_run_dossier":
24
29
  return validateRunDossierInput(args);
25
30
  default:
26
- throw new Error(`Unknown tool: ${name}`);
31
+ throw invalidArgumentsError(`Unknown tool: ${name}`, "Refresh the Martin tool manifest and retry.");
27
32
  }
28
33
  }
29
- export function sanitizeToolErrorMessage(error) {
30
- const message = error instanceof Error ? error.message : String(error);
31
- return /([A-Za-z]:\\|\/|policy\.rego|policy\.wasm|\.pem|\.env)/u.test(message)
32
- ? "Tool execution failed."
33
- : message;
34
- }
35
- function validateDoctorInput(args) {
36
- const record = requireObject(args);
37
- assertAllowedKeys(record, ["workingDirectory", "runsDir", "engine"]);
38
- const engine = optionalEnum(record.engine, "engine", ["claude", "codex"]);
39
- return {
40
- ...(record.workingDirectory !== undefined
41
- ? { workingDirectory: resolveSafeRepoRoot(requireString(record.workingDirectory, "workingDirectory")) }
42
- : {}),
43
- ...(record.runsDir !== undefined
44
- ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
45
- : {}),
46
- ...(engine ? { engine } : {})
47
- };
48
- }
49
- function validatePreflightInput(args) {
50
- const record = requireObject(args);
51
- assertAllowedKeys(record, [
52
- "objective",
53
- "workingDirectory",
54
- "engine",
55
- "model",
56
- "maxUsd",
57
- "maxIterations",
58
- "maxTokens",
59
- "verificationPlan",
60
- "allowedPaths",
61
- "deniedPaths",
62
- "workspaceId",
63
- "projectId"
64
- ]);
65
- const engine = optionalEnum(record.engine, "engine", ["claude", "codex"]);
66
- return {
67
- objective: requireString(record.objective, "objective"),
68
- ...(record.workingDirectory !== undefined
69
- ? { workingDirectory: resolveSafeRepoRoot(requireString(record.workingDirectory, "workingDirectory")) }
70
- : {}),
71
- ...(engine ? { engine } : {}),
72
- ...optionalString(record.model, "model"),
73
- ...optionalPositiveNumber(record.maxUsd, "maxUsd"),
74
- ...optionalPositiveInteger(record.maxIterations, "maxIterations"),
75
- ...optionalPositiveInteger(record.maxTokens, "maxTokens"),
76
- ...optionalStringArrayAsObject(record.verificationPlan, "verificationPlan"),
77
- ...optionalPathPatternArrayAsObject(record.allowedPaths, "allowedPaths"),
78
- ...optionalPathPatternArrayAsObject(record.deniedPaths, "deniedPaths"),
79
- ...optionalString(record.workspaceId, "workspaceId"),
80
- ...optionalString(record.projectId, "projectId")
81
- };
82
- }
83
34
  export function resolveSafeRepoRoot(repoRoot, workspaceRoot = process.env.MARTIN_MCP_WORKSPACE_ROOT ?? process.cwd()) {
84
35
  const baseRoot = resolve(workspaceRoot);
85
36
  const candidate = repoRoot ? resolve(baseRoot, repoRoot) : baseRoot;
86
- assertPathWithinRoot(candidate, baseRoot, "workingDirectory");
37
+ assertPathWithinRoot(candidate, baseRoot, "workingDirectory", {
38
+ requireExistingCandidate: true,
39
+ requireExistingRoot: true
40
+ });
87
41
  return candidate;
88
42
  }
89
43
  export function resolveSafeRunsJsonPath(file, runsRoot = resolveRunsRoot(process.env)) {
90
44
  const baseRoot = resolve(runsRoot);
91
45
  const candidate = resolve(baseRoot, file);
92
- assertPathWithinRoot(candidate, baseRoot, "file");
46
+ assertPathWithinRoot(candidate, baseRoot, "file", {
47
+ requireExistingCandidate: true,
48
+ requireExistingRoot: true
49
+ });
93
50
  const extension = extname(candidate).toLowerCase();
94
51
  if (extension !== ".json" && extension !== ".jsonl") {
95
- throw new Error("Invalid file.");
52
+ throw invalidPathError("Invalid file.", "Point file at a loop-record.json, a legacy .jsonl file, or a run directory under the runs root.");
96
53
  }
97
54
  return candidate;
98
55
  }
99
56
  export function resolveSafeRunsPath(file, runsRoot = resolveRunsRoot(process.env)) {
100
57
  const baseRoot = resolve(runsRoot);
101
58
  const candidate = resolve(baseRoot, file);
102
- assertPathWithinRoot(candidate, baseRoot, "file");
59
+ assertPathWithinRoot(candidate, baseRoot, "file", {
60
+ requireExistingCandidate: true,
61
+ requireExistingRoot: true
62
+ });
103
63
  const extension = extname(candidate).toLowerCase();
104
64
  if (extension && extension !== ".json" && extension !== ".jsonl") {
105
- throw new Error("Invalid file.");
65
+ throw invalidPathError("Invalid file.", "Point file at a loop-record.json, a legacy .jsonl file, or a run directory under the runs root.");
106
66
  }
107
67
  return candidate;
108
68
  }
109
69
  export function resolveSafeRunsRootPath(runsRoot, fallbackRunsRoot = resolveRunsRoot(process.env)) {
110
70
  const baseRoot = resolve(fallbackRunsRoot);
111
71
  const candidate = runsRoot ? resolve(baseRoot, runsRoot) : baseRoot;
112
- assertPathWithinRoot(candidate, baseRoot, "runsDir");
72
+ assertPathWithinRoot(candidate, baseRoot, "runsDir", {
73
+ requireExistingCandidate: false,
74
+ requireExistingRoot: false
75
+ });
113
76
  return candidate;
114
77
  }
115
78
  export function resolveSafeLoopRecordPath(loopId, runsRoot = resolveRunsRoot(process.env)) {
@@ -127,7 +90,7 @@ export function normalizeSafePathPatterns(value, name) {
127
90
  normalized.startsWith("/") ||
128
91
  /^[A-Za-z]:\//u.test(normalized) ||
129
92
  normalized.split("/").includes("..")) {
130
- throw new Error(`Invalid ${name}.`);
93
+ throw invalidPathError(`Invalid ${name}.`);
131
94
  }
132
95
  return normalized;
133
96
  });
@@ -169,18 +132,24 @@ function validateRunInput(args) {
169
132
  function validateInspectInput(args) {
170
133
  const record = requireObject(args);
171
134
  assertAllowedKeys(record, ["file", "runsDir"]);
135
+ const resolvedRunsDir = record.runsDir !== undefined
136
+ ? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
137
+ : undefined;
172
138
  return {
173
139
  ...(record.file !== undefined
174
- ? { file: resolveSafeRunsPath(requireString(record.file, "file")) }
140
+ ? {
141
+ file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
142
+ }
175
143
  : {}),
176
- ...(record.runsDir !== undefined
177
- ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
178
- : {})
144
+ ...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {})
179
145
  };
180
146
  }
181
147
  function validateStatusInput(args) {
182
148
  const record = requireObject(args);
183
149
  assertAllowedKeys(record, ["loopJson", "file", "loopId", "runsDir", "latest"]);
150
+ const resolvedRunsDir = record.runsDir !== undefined
151
+ ? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
152
+ : undefined;
184
153
  const selectors = [
185
154
  record.loopJson !== undefined ? "loopJson" : null,
186
155
  record.file !== undefined ? "file" : null,
@@ -188,113 +157,266 @@ function validateStatusInput(args) {
188
157
  record.latest !== undefined ? "latest" : null
189
158
  ].filter((value) => value !== null);
190
159
  if (selectors.length !== 1) {
191
- throw new Error("Provide exactly one of loopJson, file, loopId, or latest.");
160
+ throw invalidSelectorError("Provide exactly one of loopJson, file, loopId, or latest.", "Choose exactly one status selector per call.");
192
161
  }
193
162
  if (record.latest !== undefined && record.latest !== true) {
194
- throw new Error("Invalid latest.");
163
+ throw invalidArgumentsError("Invalid latest.", "latest must be the literal boolean value true.");
195
164
  }
196
165
  return {
197
166
  ...(record.loopJson !== undefined
198
167
  ? { loopJson: requireString(record.loopJson, "loopJson") }
199
168
  : {}),
200
169
  ...(record.file !== undefined
201
- ? { file: resolveSafeRunsPath(requireString(record.file, "file")) }
170
+ ? {
171
+ file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
172
+ }
202
173
  : {}),
203
174
  ...(record.loopId !== undefined
204
- ? { loopId: requireLoopId(record.loopId, "loopId") }
205
- : {}),
206
- ...(record.runsDir !== undefined
207
- ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
175
+ ? {
176
+ loopId: requireLoopId(record.loopId, "loopId")
177
+ }
208
178
  : {}),
179
+ ...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {}),
209
180
  ...(record.latest === true ? { latest: true } : {})
210
181
  };
211
182
  }
212
- function validateListRunsInput(args) {
183
+ function validateDoctorInput(args) {
213
184
  const record = requireObject(args);
214
- assertAllowedKeys(record, ["runsDir", "limit"]);
185
+ assertAllowedKeys(record, ["workingDirectory", "runsDir", "engine"]);
215
186
  return {
187
+ ...(record.workingDirectory !== undefined
188
+ ? { workingDirectory: resolveSafeRepoRoot(requireString(record.workingDirectory, "workingDirectory")) }
189
+ : {}),
216
190
  ...(record.runsDir !== undefined
217
191
  ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
218
192
  : {}),
219
- ...(record.limit !== undefined ? { limit: requirePositiveInteger(record.limit, "limit") } : {})
193
+ ...optionalEnumAsObject(record.engine, "engine", ["claude", "codex"])
220
194
  };
221
195
  }
222
- function validateGetRunInput(args) {
223
- const record = requireObject(args);
224
- assertAllowedKeys(record, ["loopId", "runsDir", "latest"]);
225
- return validateRunSelector(record);
226
- }
227
- function validateGetVerificationResultsInput(args) {
228
- const record = requireObject(args);
229
- assertAllowedKeys(record, ["loopId", "runsDir", "latest"]);
230
- return validateRunSelector(record);
231
- }
232
- function validateRunDossierInput(args) {
233
- const record = requireObject(args);
234
- assertAllowedKeys(record, ["loopId", "runsDir", "latest"]);
235
- return validateRunSelector(record);
196
+ function validatePreflightInput(args) {
197
+ return validateRunInput(args);
236
198
  }
237
- function validateGetAttemptInput(args) {
199
+ function validateListRunsInput(args) {
238
200
  const record = requireObject(args);
239
- assertAllowedKeys(record, ["loopId", "attemptIndex", "runsDir"]);
201
+ assertAllowedKeys(record, [
202
+ "runsDir",
203
+ "limit",
204
+ "status",
205
+ "lifecycleState",
206
+ "adapterId",
207
+ "model",
208
+ "updatedAfter"
209
+ ]);
240
210
  return {
241
- loopId: requireLoopId(record.loopId, "loopId"),
242
- attemptIndex: requirePositiveInteger(record.attemptIndex, "attemptIndex"),
243
211
  ...(record.runsDir !== undefined
244
212
  ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
245
- : {})
213
+ : {}),
214
+ ...optionalPositiveInteger(record.limit, "limit"),
215
+ ...optionalString(record.status, "status"),
216
+ ...optionalString(record.lifecycleState, "lifecycleState"),
217
+ ...optionalString(record.adapterId, "adapterId"),
218
+ ...optionalString(record.model, "model"),
219
+ ...optionalString(record.updatedAfter, "updatedAfter")
246
220
  };
247
221
  }
248
- function validateRunSelector(record) {
222
+ function validateGetRunInput(args) {
223
+ const record = requireObject(args);
224
+ assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest"]);
225
+ const resolvedRunsDir = record.runsDir !== undefined
226
+ ? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
227
+ : undefined;
249
228
  const selectors = [
229
+ record.file !== undefined ? "file" : null,
250
230
  record.loopId !== undefined ? "loopId" : null,
251
231
  record.latest !== undefined ? "latest" : null
252
232
  ].filter((value) => value !== null);
253
233
  if (selectors.length !== 1) {
254
- throw new Error("Provide exactly one of loopId or latest.");
234
+ throw invalidSelectorError("Provide exactly one of file, loopId, or latest.", "Choose exactly one run selector per call.");
255
235
  }
256
236
  if (record.latest !== undefined && record.latest !== true) {
257
- throw new Error("Invalid latest.");
237
+ throw invalidArgumentsError("Invalid latest.", "latest must be the literal boolean value true.");
258
238
  }
259
239
  return {
260
- ...(record.loopId !== undefined ? { loopId: requireLoopId(record.loopId, "loopId") } : {}),
261
- ...(record.latest === true ? { latest: true } : {}),
240
+ ...(record.file !== undefined
241
+ ? {
242
+ file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
243
+ }
244
+ : {}),
245
+ ...(record.loopId !== undefined
246
+ ? { loopId: requireLoopId(record.loopId, "loopId") }
247
+ : {}),
248
+ ...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {}),
249
+ ...(record.latest === true ? { latest: true } : {})
250
+ };
251
+ }
252
+ function validateTriageRunsInput(args) {
253
+ const record = requireObject(args);
254
+ assertAllowedKeys(record, [
255
+ "runsDir",
256
+ "limit",
257
+ "status",
258
+ "lifecycleState",
259
+ "adapterId",
260
+ "model",
261
+ "updatedAfter",
262
+ "includeHealthy"
263
+ ]);
264
+ return {
262
265
  ...(record.runsDir !== undefined
263
266
  ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
264
- : {})
267
+ : {}),
268
+ ...optionalPositiveInteger(record.limit, "limit"),
269
+ ...optionalString(record.status, "status"),
270
+ ...optionalString(record.lifecycleState, "lifecycleState"),
271
+ ...optionalString(record.adapterId, "adapterId"),
272
+ ...optionalString(record.model, "model"),
273
+ ...optionalString(record.updatedAfter, "updatedAfter"),
274
+ ...optionalBoolean(record.includeHealthy, "includeHealthy")
275
+ };
276
+ }
277
+ function validateGetAttemptInput(args) {
278
+ const record = requireObject(args);
279
+ assertAllowedKeys(record, ["file", "loopId", "runsDir", "attemptIndex"]);
280
+ const resolvedRunsDir = record.runsDir !== undefined
281
+ ? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
282
+ : undefined;
283
+ const selectors = [
284
+ record.file !== undefined ? "file" : null,
285
+ record.loopId !== undefined ? "loopId" : null
286
+ ].filter((value) => value !== null);
287
+ if (selectors.length !== 1) {
288
+ throw invalidSelectorError("Provide exactly one of file or loopId.", "Choose exactly one run selector per call.");
289
+ }
290
+ return {
291
+ ...(record.file !== undefined
292
+ ? {
293
+ file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
294
+ }
295
+ : {}),
296
+ ...(record.loopId !== undefined
297
+ ? { loopId: requireLoopId(record.loopId, "loopId") }
298
+ : {}),
299
+ ...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {}),
300
+ ...optionalPositiveInteger(record.attemptIndex, "attemptIndex")
301
+ };
302
+ }
303
+ function validateGetVerificationResultsInput(args) {
304
+ const record = requireObject(args);
305
+ assertAllowedKeys(record, ["file", "loopId", "runsDir"]);
306
+ const resolvedRunsDir = record.runsDir !== undefined
307
+ ? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
308
+ : undefined;
309
+ const selectors = [
310
+ record.file !== undefined ? "file" : null,
311
+ record.loopId !== undefined ? "loopId" : null
312
+ ].filter((value) => value !== null);
313
+ if (selectors.length !== 1) {
314
+ throw invalidSelectorError("Provide exactly one of file or loopId.", "Choose exactly one run selector per call.");
315
+ }
316
+ return {
317
+ ...(record.file !== undefined
318
+ ? {
319
+ file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
320
+ }
321
+ : {}),
322
+ ...(record.loopId !== undefined
323
+ ? { loopId: requireLoopId(record.loopId, "loopId") }
324
+ : {}),
325
+ ...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {})
265
326
  };
266
327
  }
328
+ function validateRunDossierInput(args) {
329
+ return validateGetRunInput(args);
330
+ }
267
331
  function requireObject(value) {
268
332
  if (!value || typeof value !== "object" || Array.isArray(value)) {
269
- throw new Error("Tool arguments must be an object.");
333
+ throw invalidArgumentsError("Tool arguments must be an object.");
270
334
  }
271
335
  return value;
272
336
  }
273
337
  function assertAllowedKeys(record, allowed) {
274
338
  const unknownKeys = Object.keys(record).filter((key) => !allowed.includes(key));
275
339
  if (unknownKeys.length > 0) {
276
- throw new Error(`Unknown arguments: ${unknownKeys.join(", ")}`);
340
+ throw invalidArgumentsError(`Unknown arguments: ${unknownKeys.join(", ")}`);
277
341
  }
278
342
  }
279
- function assertPathWithinRoot(candidatePath, rootPath, name) {
280
- const relativePath = relative(rootPath, candidatePath);
343
+ function assertPathWithinRoot(candidatePath, rootPath, name, options = {}) {
344
+ assertNoSymbolicLinkSegments(candidatePath, name, rootPath);
345
+ const canonicalRoot = canonicalizePath(rootPath, name, options.requireExistingRoot ?? false);
346
+ const canonicalCandidate = canonicalizePath(candidatePath, name, options.requireExistingCandidate ?? false);
347
+ const relativePath = relative(canonicalRoot, canonicalCandidate);
281
348
  if (relativePath === "" || relativePath === ".") {
282
349
  return;
283
350
  }
284
351
  if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
285
- throw new Error(`Invalid ${name}.`);
352
+ throw invalidPathError(`Invalid ${name}.`);
353
+ }
354
+ }
355
+ function assertNoSymbolicLinkSegments(pathValue, name, stopAtPath) {
356
+ const stopAt = stopAtPath ? resolve(stopAtPath) : undefined;
357
+ let current = resolve(pathValue);
358
+ while (true) {
359
+ if (existsSync(current)) {
360
+ try {
361
+ const stats = lstatSync(current);
362
+ if (stats.isSymbolicLink()) {
363
+ throw invalidPathError(`Invalid ${name}.`);
364
+ }
365
+ }
366
+ catch (error) {
367
+ if (error instanceof Error) {
368
+ throw error;
369
+ }
370
+ throw invalidPathError(`Invalid ${name}.`);
371
+ }
372
+ }
373
+ if (stopAt && relative(stopAt, current) === "") {
374
+ break;
375
+ }
376
+ const parent = dirname(current);
377
+ if (parent === current) {
378
+ break;
379
+ }
380
+ current = parent;
381
+ }
382
+ }
383
+ function canonicalizePath(pathValue, name, requireExisting) {
384
+ const resolvedPath = resolve(pathValue);
385
+ if (!existsSync(resolvedPath)) {
386
+ if (requireExisting) {
387
+ throw invalidPathError(`Invalid ${name}.`);
388
+ }
389
+ return resolvedPath;
390
+ }
391
+ try {
392
+ const stats = lstatSync(resolvedPath);
393
+ if (stats.isSymbolicLink()) {
394
+ throw invalidPathError(`Invalid ${name}.`);
395
+ }
396
+ }
397
+ catch (error) {
398
+ if (error instanceof Error) {
399
+ throw error;
400
+ }
401
+ throw invalidPathError(`Invalid ${name}.`);
402
+ }
403
+ try {
404
+ return realpathSync.native(resolvedPath);
405
+ }
406
+ catch {
407
+ throw invalidPathError(`Invalid ${name}.`);
286
408
  }
287
409
  }
288
410
  function requireString(value, name) {
289
411
  if (typeof value !== "string" || value.trim().length === 0) {
290
- throw new Error(`Invalid ${name}.`);
412
+ throw invalidArgumentsError(`Invalid ${name}.`);
291
413
  }
292
414
  return value.trim();
293
415
  }
294
416
  function requireLoopId(value, name) {
295
417
  const loopId = requireString(value, name);
296
418
  if (!/^[A-Za-z0-9._-]+$/u.test(loopId)) {
297
- throw new Error(`Invalid ${name}.`);
419
+ throw invalidPathError(`Invalid ${name}.`, "loopId may only include letters, numbers, dots, underscores, and hyphens.");
298
420
  }
299
421
  return loopId;
300
422
  }
@@ -309,7 +431,7 @@ function optionalPositiveNumber(value, name) {
309
431
  return {};
310
432
  }
311
433
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
312
- throw new Error(`Invalid ${name}.`);
434
+ throw invalidArgumentsError(`Invalid ${name}.`);
313
435
  }
314
436
  return { [name]: value };
315
437
  }
@@ -317,20 +439,35 @@ function optionalPositiveInteger(value, name) {
317
439
  if (value === undefined) {
318
440
  return {};
319
441
  }
320
- return { [name]: requirePositiveInteger(value, name) };
321
- }
322
- function requirePositiveInteger(value, name) {
323
442
  if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
324
- throw new Error(`Invalid ${name}.`);
443
+ throw invalidArgumentsError(`Invalid ${name}.`);
325
444
  }
326
- return value;
445
+ return { [name]: value };
446
+ }
447
+ function optionalNonNegativeInteger(value, name) {
448
+ if (value === undefined) {
449
+ return {};
450
+ }
451
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
452
+ throw invalidArgumentsError(`Invalid ${name}.`);
453
+ }
454
+ return { [name]: value };
455
+ }
456
+ function optionalBoolean(value, name) {
457
+ if (value === undefined) {
458
+ return {};
459
+ }
460
+ if (typeof value !== "boolean") {
461
+ throw invalidArgumentsError(`Invalid ${name}.`);
462
+ }
463
+ return { [name]: value };
327
464
  }
328
465
  function optionalStringArray(value, name) {
329
466
  if (value === undefined) {
330
467
  return undefined;
331
468
  }
332
469
  if (!Array.isArray(value)) {
333
- throw new Error(`Invalid ${name}.`);
470
+ throw invalidArgumentsError(`Invalid ${name}.`);
334
471
  }
335
472
  return value.map((item) => requireString(item, name));
336
473
  }
@@ -347,8 +484,11 @@ function optionalEnum(value, name, allowed) {
347
484
  return undefined;
348
485
  }
349
486
  if (typeof value !== "string" || !allowed.includes(value)) {
350
- throw new Error(`Invalid ${name}.`);
487
+ throw invalidArgumentsError(`Invalid ${name}.`);
351
488
  }
352
489
  return value;
353
490
  }
354
- //# sourceMappingURL=server-validation.js.map
491
+ function optionalEnumAsObject(value, name, allowed) {
492
+ const enumValue = optionalEnum(value, name, allowed);
493
+ return enumValue ? { [name]: enumValue } : {};
494
+ }
package/dist/server.d.ts CHANGED
@@ -2,12 +2,9 @@
2
2
  /**
3
3
  * Martin Loop MCP Server
4
4
  *
5
- * Exposes a governed local MCP cockpit over stdio:
6
- * martin_doctor — inspect local readiness and run-store health
7
- * martin_preflight — normalize a proposed run contract before execution
8
- * martin_run — execute a full Martin loop on a coding task
9
- * martin_inspect — summarise a saved loop record file
10
- * martin_status — return cost and pressure state from a loop record
5
+ * Martin Loop MCP is a governed execution cockpit for AI coding agents.
6
+ * It exposes execution, diagnostics, run inspection, resources, and prompts
7
+ * over the Model Context Protocol (stdio transport).
11
8
  *
12
9
  * Setup (Claude Code):
13
10
  * macOS/Linux: claude mcp add --scope user martin-loop -- npx @martinloop/mcp
@@ -19,4 +16,76 @@
19
16
  * Manual start:
20
17
  * node dist/server.js
21
18
  */
22
- export {};
19
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
20
+ export declare function createMartinMcpServer(serverInfo?: {
21
+ name?: string;
22
+ version?: string;
23
+ }): Server<{
24
+ method: string;
25
+ params?: {
26
+ [x: string]: unknown;
27
+ _meta?: {
28
+ [x: string]: unknown;
29
+ progressToken?: string | number | undefined;
30
+ "io.modelcontextprotocol/related-task"?: {
31
+ taskId: string;
32
+ } | undefined;
33
+ } | undefined;
34
+ } | undefined;
35
+ }, {
36
+ method: string;
37
+ params?: {
38
+ [x: string]: unknown;
39
+ _meta?: {
40
+ [x: string]: unknown;
41
+ progressToken?: string | number | undefined;
42
+ "io.modelcontextprotocol/related-task"?: {
43
+ taskId: string;
44
+ } | undefined;
45
+ } | undefined;
46
+ } | undefined;
47
+ }, {
48
+ [x: string]: unknown;
49
+ _meta?: {
50
+ [x: string]: unknown;
51
+ progressToken?: string | number | undefined;
52
+ "io.modelcontextprotocol/related-task"?: {
53
+ taskId: string;
54
+ } | undefined;
55
+ } | undefined;
56
+ }>;
57
+ export declare function connectMartinMcpStdioServer(): Promise<Server<{
58
+ method: string;
59
+ params?: {
60
+ [x: string]: unknown;
61
+ _meta?: {
62
+ [x: string]: unknown;
63
+ progressToken?: string | number | undefined;
64
+ "io.modelcontextprotocol/related-task"?: {
65
+ taskId: string;
66
+ } | undefined;
67
+ } | undefined;
68
+ } | undefined;
69
+ }, {
70
+ method: string;
71
+ params?: {
72
+ [x: string]: unknown;
73
+ _meta?: {
74
+ [x: string]: unknown;
75
+ progressToken?: string | number | undefined;
76
+ "io.modelcontextprotocol/related-task"?: {
77
+ taskId: string;
78
+ } | undefined;
79
+ } | undefined;
80
+ } | undefined;
81
+ }, {
82
+ [x: string]: unknown;
83
+ _meta?: {
84
+ [x: string]: unknown;
85
+ progressToken?: string | number | undefined;
86
+ "io.modelcontextprotocol/related-task"?: {
87
+ taskId: string;
88
+ } | undefined;
89
+ } | undefined;
90
+ }>>;
91
+ export declare function isDirectExecutionEntry(entryPath: string | undefined, moduleUrl?: string): boolean;