@martinloop/mcp 0.1.4 → 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 (74) hide show
  1. package/README.md +138 -135
  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 -0
  9. package/dist/prompts.js +455 -0
  10. package/dist/resources.d.ts +29 -0
  11. package/dist/resources.js +575 -0
  12. package/dist/server-validation.d.ts +2 -3
  13. package/dist/server-validation.js +295 -87
  14. package/dist/server.d.ts +76 -7
  15. package/dist/server.js +1135 -247
  16. package/dist/tools/doctor.js +14 -6
  17. package/dist/tools/get-attempt.d.ts +15 -0
  18. package/dist/tools/get-attempt.js +15 -0
  19. package/dist/tools/get-run.d.ts +24 -0
  20. package/dist/tools/get-run.js +23 -0
  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 +14 -0
  24. package/dist/tools/get-verification-results.js +14 -0
  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 +29 -0
  28. package/dist/tools/list-runs.js +24 -0
  29. package/dist/tools/preflight.js +7 -2
  30. package/dist/tools/run-dossier.d.ts +41 -0
  31. package/dist/tools/run-dossier.js +41 -0
  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 -3
  74. package/server.json +2 -2
@@ -1,105 +1,78 @@
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);
18
+ case "martin_list_runs":
19
+ return validateListRunsInput(args);
20
+ case "martin_triage_runs":
21
+ return validateTriageRunsInput(args);
22
+ case "martin_get_run":
23
+ return validateGetRunInput(args);
24
+ case "martin_get_attempt":
25
+ return validateGetAttemptInput(args);
26
+ case "martin_get_verification_results":
27
+ return validateGetVerificationResultsInput(args);
28
+ case "martin_run_dossier":
29
+ return validateRunDossierInput(args);
15
30
  default:
16
- throw new Error(`Unknown tool: ${name}`);
31
+ throw invalidArgumentsError(`Unknown tool: ${name}`, "Refresh the Martin tool manifest and retry.");
17
32
  }
18
33
  }
19
- export function sanitizeToolErrorMessage(error) {
20
- const message = error instanceof Error ? error.message : String(error);
21
- return /([A-Za-z]:\\|\/|policy\.rego|policy\.wasm|\.pem|\.env)/u.test(message)
22
- ? "Tool execution failed."
23
- : message;
24
- }
25
- function validateDoctorInput(args) {
26
- const record = requireObject(args);
27
- assertAllowedKeys(record, ["workingDirectory", "runsDir", "engine"]);
28
- const engine = optionalEnum(record.engine, "engine", ["claude", "codex"]);
29
- return {
30
- ...(record.workingDirectory !== undefined
31
- ? { workingDirectory: resolveSafeRepoRoot(requireString(record.workingDirectory, "workingDirectory")) }
32
- : {}),
33
- ...(record.runsDir !== undefined
34
- ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
35
- : {}),
36
- ...(engine ? { engine } : {})
37
- };
38
- }
39
- function validatePreflightInput(args) {
40
- const record = requireObject(args);
41
- assertAllowedKeys(record, [
42
- "objective",
43
- "workingDirectory",
44
- "engine",
45
- "model",
46
- "maxUsd",
47
- "maxIterations",
48
- "maxTokens",
49
- "verificationPlan",
50
- "allowedPaths",
51
- "deniedPaths",
52
- "workspaceId",
53
- "projectId"
54
- ]);
55
- const engine = optionalEnum(record.engine, "engine", ["claude", "codex"]);
56
- return {
57
- objective: requireString(record.objective, "objective"),
58
- ...(record.workingDirectory !== undefined
59
- ? { workingDirectory: resolveSafeRepoRoot(requireString(record.workingDirectory, "workingDirectory")) }
60
- : {}),
61
- ...(engine ? { engine } : {}),
62
- ...optionalString(record.model, "model"),
63
- ...optionalPositiveNumber(record.maxUsd, "maxUsd"),
64
- ...optionalPositiveInteger(record.maxIterations, "maxIterations"),
65
- ...optionalPositiveInteger(record.maxTokens, "maxTokens"),
66
- ...optionalStringArrayAsObject(record.verificationPlan, "verificationPlan"),
67
- ...optionalPathPatternArrayAsObject(record.allowedPaths, "allowedPaths"),
68
- ...optionalPathPatternArrayAsObject(record.deniedPaths, "deniedPaths"),
69
- ...optionalString(record.workspaceId, "workspaceId"),
70
- ...optionalString(record.projectId, "projectId")
71
- };
72
- }
73
34
  export function resolveSafeRepoRoot(repoRoot, workspaceRoot = process.env.MARTIN_MCP_WORKSPACE_ROOT ?? process.cwd()) {
74
35
  const baseRoot = resolve(workspaceRoot);
75
36
  const candidate = repoRoot ? resolve(baseRoot, repoRoot) : baseRoot;
76
- assertPathWithinRoot(candidate, baseRoot, "workingDirectory");
37
+ assertPathWithinRoot(candidate, baseRoot, "workingDirectory", {
38
+ requireExistingCandidate: true,
39
+ requireExistingRoot: true
40
+ });
77
41
  return candidate;
78
42
  }
79
43
  export function resolveSafeRunsJsonPath(file, runsRoot = resolveRunsRoot(process.env)) {
80
44
  const baseRoot = resolve(runsRoot);
81
45
  const candidate = resolve(baseRoot, file);
82
- assertPathWithinRoot(candidate, baseRoot, "file");
46
+ assertPathWithinRoot(candidate, baseRoot, "file", {
47
+ requireExistingCandidate: true,
48
+ requireExistingRoot: true
49
+ });
83
50
  const extension = extname(candidate).toLowerCase();
84
51
  if (extension !== ".json" && extension !== ".jsonl") {
85
- 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.");
86
53
  }
87
54
  return candidate;
88
55
  }
89
56
  export function resolveSafeRunsPath(file, runsRoot = resolveRunsRoot(process.env)) {
90
57
  const baseRoot = resolve(runsRoot);
91
58
  const candidate = resolve(baseRoot, file);
92
- assertPathWithinRoot(candidate, baseRoot, "file");
59
+ assertPathWithinRoot(candidate, baseRoot, "file", {
60
+ requireExistingCandidate: true,
61
+ requireExistingRoot: true
62
+ });
93
63
  const extension = extname(candidate).toLowerCase();
94
64
  if (extension && extension !== ".json" && extension !== ".jsonl") {
95
- 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.");
96
66
  }
97
67
  return candidate;
98
68
  }
99
69
  export function resolveSafeRunsRootPath(runsRoot, fallbackRunsRoot = resolveRunsRoot(process.env)) {
100
70
  const baseRoot = resolve(fallbackRunsRoot);
101
71
  const candidate = runsRoot ? resolve(baseRoot, runsRoot) : baseRoot;
102
- assertPathWithinRoot(candidate, baseRoot, "runsDir");
72
+ assertPathWithinRoot(candidate, baseRoot, "runsDir", {
73
+ requireExistingCandidate: false,
74
+ requireExistingRoot: false
75
+ });
103
76
  return candidate;
104
77
  }
105
78
  export function resolveSafeLoopRecordPath(loopId, runsRoot = resolveRunsRoot(process.env)) {
@@ -117,7 +90,7 @@ export function normalizeSafePathPatterns(value, name) {
117
90
  normalized.startsWith("/") ||
118
91
  /^[A-Za-z]:\//u.test(normalized) ||
119
92
  normalized.split("/").includes("..")) {
120
- throw new Error(`Invalid ${name}.`);
93
+ throw invalidPathError(`Invalid ${name}.`);
121
94
  }
122
95
  return normalized;
123
96
  });
@@ -159,18 +132,24 @@ function validateRunInput(args) {
159
132
  function validateInspectInput(args) {
160
133
  const record = requireObject(args);
161
134
  assertAllowedKeys(record, ["file", "runsDir"]);
135
+ const resolvedRunsDir = record.runsDir !== undefined
136
+ ? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
137
+ : undefined;
162
138
  return {
163
139
  ...(record.file !== undefined
164
- ? { file: resolveSafeRunsPath(requireString(record.file, "file")) }
140
+ ? {
141
+ file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
142
+ }
165
143
  : {}),
166
- ...(record.runsDir !== undefined
167
- ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
168
- : {})
144
+ ...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {})
169
145
  };
170
146
  }
171
147
  function validateStatusInput(args) {
172
148
  const record = requireObject(args);
173
149
  assertAllowedKeys(record, ["loopJson", "file", "loopId", "runsDir", "latest"]);
150
+ const resolvedRunsDir = record.runsDir !== undefined
151
+ ? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
152
+ : undefined;
174
153
  const selectors = [
175
154
  record.loopJson !== undefined ? "loopJson" : null,
176
155
  record.file !== undefined ? "file" : null,
@@ -178,58 +157,266 @@ function validateStatusInput(args) {
178
157
  record.latest !== undefined ? "latest" : null
179
158
  ].filter((value) => value !== null);
180
159
  if (selectors.length !== 1) {
181
- 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.");
182
161
  }
183
162
  if (record.latest !== undefined && record.latest !== true) {
184
- throw new Error("Invalid latest.");
163
+ throw invalidArgumentsError("Invalid latest.", "latest must be the literal boolean value true.");
185
164
  }
186
165
  return {
187
166
  ...(record.loopJson !== undefined
188
167
  ? { loopJson: requireString(record.loopJson, "loopJson") }
189
168
  : {}),
190
169
  ...(record.file !== undefined
191
- ? { file: resolveSafeRunsPath(requireString(record.file, "file")) }
170
+ ? {
171
+ file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
172
+ }
192
173
  : {}),
193
174
  ...(record.loopId !== undefined
194
- ? { loopId: requireLoopId(record.loopId, "loopId") }
175
+ ? {
176
+ loopId: requireLoopId(record.loopId, "loopId")
177
+ }
178
+ : {}),
179
+ ...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {}),
180
+ ...(record.latest === true ? { latest: true } : {})
181
+ };
182
+ }
183
+ function validateDoctorInput(args) {
184
+ const record = requireObject(args);
185
+ assertAllowedKeys(record, ["workingDirectory", "runsDir", "engine"]);
186
+ return {
187
+ ...(record.workingDirectory !== undefined
188
+ ? { workingDirectory: resolveSafeRepoRoot(requireString(record.workingDirectory, "workingDirectory")) }
195
189
  : {}),
196
190
  ...(record.runsDir !== undefined
197
191
  ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
198
192
  : {}),
193
+ ...optionalEnumAsObject(record.engine, "engine", ["claude", "codex"])
194
+ };
195
+ }
196
+ function validatePreflightInput(args) {
197
+ return validateRunInput(args);
198
+ }
199
+ function validateListRunsInput(args) {
200
+ const record = requireObject(args);
201
+ assertAllowedKeys(record, [
202
+ "runsDir",
203
+ "limit",
204
+ "status",
205
+ "lifecycleState",
206
+ "adapterId",
207
+ "model",
208
+ "updatedAfter"
209
+ ]);
210
+ return {
211
+ ...(record.runsDir !== undefined
212
+ ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
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")
220
+ };
221
+ }
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;
228
+ const selectors = [
229
+ record.file !== undefined ? "file" : null,
230
+ record.loopId !== undefined ? "loopId" : null,
231
+ record.latest !== undefined ? "latest" : null
232
+ ].filter((value) => value !== null);
233
+ if (selectors.length !== 1) {
234
+ throw invalidSelectorError("Provide exactly one of file, loopId, or latest.", "Choose exactly one run selector per call.");
235
+ }
236
+ if (record.latest !== undefined && record.latest !== true) {
237
+ throw invalidArgumentsError("Invalid latest.", "latest must be the literal boolean value true.");
238
+ }
239
+ return {
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 } : {}),
199
249
  ...(record.latest === true ? { latest: true } : {})
200
250
  };
201
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 {
265
+ ...(record.runsDir !== undefined
266
+ ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
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 } : {})
326
+ };
327
+ }
328
+ function validateRunDossierInput(args) {
329
+ return validateGetRunInput(args);
330
+ }
202
331
  function requireObject(value) {
203
332
  if (!value || typeof value !== "object" || Array.isArray(value)) {
204
- throw new Error("Tool arguments must be an object.");
333
+ throw invalidArgumentsError("Tool arguments must be an object.");
205
334
  }
206
335
  return value;
207
336
  }
208
337
  function assertAllowedKeys(record, allowed) {
209
338
  const unknownKeys = Object.keys(record).filter((key) => !allowed.includes(key));
210
339
  if (unknownKeys.length > 0) {
211
- throw new Error(`Unknown arguments: ${unknownKeys.join(", ")}`);
340
+ throw invalidArgumentsError(`Unknown arguments: ${unknownKeys.join(", ")}`);
212
341
  }
213
342
  }
214
- function assertPathWithinRoot(candidatePath, rootPath, name) {
215
- 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);
216
348
  if (relativePath === "" || relativePath === ".") {
217
349
  return;
218
350
  }
219
351
  if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
220
- 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}.`);
221
408
  }
222
409
  }
223
410
  function requireString(value, name) {
224
411
  if (typeof value !== "string" || value.trim().length === 0) {
225
- throw new Error(`Invalid ${name}.`);
412
+ throw invalidArgumentsError(`Invalid ${name}.`);
226
413
  }
227
414
  return value.trim();
228
415
  }
229
416
  function requireLoopId(value, name) {
230
417
  const loopId = requireString(value, name);
231
418
  if (!/^[A-Za-z0-9._-]+$/u.test(loopId)) {
232
- throw new Error(`Invalid ${name}.`);
419
+ throw invalidPathError(`Invalid ${name}.`, "loopId may only include letters, numbers, dots, underscores, and hyphens.");
233
420
  }
234
421
  return loopId;
235
422
  }
@@ -244,7 +431,7 @@ function optionalPositiveNumber(value, name) {
244
431
  return {};
245
432
  }
246
433
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
247
- throw new Error(`Invalid ${name}.`);
434
+ throw invalidArgumentsError(`Invalid ${name}.`);
248
435
  }
249
436
  return { [name]: value };
250
437
  }
@@ -253,7 +440,25 @@ function optionalPositiveInteger(value, name) {
253
440
  return {};
254
441
  }
255
442
  if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
256
- throw new Error(`Invalid ${name}.`);
443
+ throw invalidArgumentsError(`Invalid ${name}.`);
444
+ }
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}.`);
257
462
  }
258
463
  return { [name]: value };
259
464
  }
@@ -262,7 +467,7 @@ function optionalStringArray(value, name) {
262
467
  return undefined;
263
468
  }
264
469
  if (!Array.isArray(value)) {
265
- throw new Error(`Invalid ${name}.`);
470
+ throw invalidArgumentsError(`Invalid ${name}.`);
266
471
  }
267
472
  return value.map((item) => requireString(item, name));
268
473
  }
@@ -279,8 +484,11 @@ function optionalEnum(value, name, allowed) {
279
484
  return undefined;
280
485
  }
281
486
  if (typeof value !== "string" || !allowed.includes(value)) {
282
- throw new Error(`Invalid ${name}.`);
487
+ throw invalidArgumentsError(`Invalid ${name}.`);
283
488
  }
284
489
  return value;
285
490
  }
286
- //# 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 five tools over the Model Context Protocol (stdio transport):
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;