@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.
- package/README.md +138 -135
- package/dist/discovery-metadata.d.ts +16 -0
- package/dist/discovery-metadata.js +62 -0
- package/dist/discovery-support.d.ts +62 -0
- package/dist/discovery-support.js +224 -0
- package/dist/package-version.d.ts +1 -0
- package/dist/package-version.js +3 -0
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.js +455 -0
- package/dist/resources.d.ts +29 -0
- package/dist/resources.js +575 -0
- package/dist/server-validation.d.ts +2 -3
- package/dist/server-validation.js +295 -87
- package/dist/server.d.ts +76 -7
- package/dist/server.js +1135 -247
- package/dist/tools/doctor.js +14 -6
- package/dist/tools/get-attempt.d.ts +15 -0
- package/dist/tools/get-attempt.js +15 -0
- package/dist/tools/get-run.d.ts +24 -0
- package/dist/tools/get-run.js +23 -0
- package/dist/tools/get-status.d.ts +11 -0
- package/dist/tools/get-status.js +12 -2
- package/dist/tools/get-verification-results.d.ts +14 -0
- package/dist/tools/get-verification-results.js +14 -0
- package/dist/tools/inspect-loop.d.ts +9 -0
- package/dist/tools/inspect-loop.js +11 -2
- package/dist/tools/list-runs.d.ts +29 -0
- package/dist/tools/list-runs.js +24 -0
- package/dist/tools/preflight.js +7 -2
- package/dist/tools/run-dossier.d.ts +41 -0
- package/dist/tools/run-dossier.js +41 -0
- package/dist/tools/run-loop.d.ts +19 -0
- package/dist/tools/run-loop.js +41 -3
- package/dist/tools/run-store.d.ts +57 -3
- package/dist/tools/run-store.js +404 -53
- package/dist/tools/tool-errors.d.ts +37 -0
- package/dist/tools/tool-errors.js +170 -0
- package/dist/tools/tool-response.d.ts +16 -0
- package/dist/tools/tool-response.js +34 -0
- package/dist/tools/tool-support.d.ts +92 -2
- package/dist/tools/tool-support.js +358 -63
- package/dist/tools/triage-runs.d.ts +33 -0
- package/dist/tools/triage-runs.js +138 -0
- package/dist/vendor/adapters/claude-cli.js +0 -1
- package/dist/vendor/adapters/cli-bridge.js +0 -1
- package/dist/vendor/adapters/direct-provider.js +0 -1
- package/dist/vendor/adapters/index.js +0 -1
- package/dist/vendor/adapters/runtime-support.js +0 -1
- package/dist/vendor/adapters/stub-agent-cli.js +0 -1
- package/dist/vendor/adapters/stub-direct-provider.js +0 -1
- package/dist/vendor/adapters/verifier-only.js +0 -1
- package/dist/vendor/contracts/governance.js +0 -1
- package/dist/vendor/contracts/index.d.ts +2 -0
- package/dist/vendor/contracts/index.js +1 -1
- package/dist/vendor/contracts/operator.d.ts +19 -0
- package/dist/vendor/contracts/operator.js +11 -0
- package/dist/vendor/core/compiler.js +0 -1
- package/dist/vendor/core/context-integrity.js +0 -1
- package/dist/vendor/core/grounding.js +0 -1
- package/dist/vendor/core/index.js +1 -2
- package/dist/vendor/core/leash.js +19 -12
- package/dist/vendor/core/persistence/compiler.js +0 -1
- package/dist/vendor/core/persistence/index.js +0 -1
- package/dist/vendor/core/persistence/ledger.js +0 -1
- package/dist/vendor/core/persistence/runs-reader.js +0 -1
- package/dist/vendor/core/persistence/store.js +0 -1
- package/dist/vendor/core/policy.js +0 -1
- package/dist/vendor/core/red-blue/red-phase.d.ts +64 -0
- package/dist/vendor/core/red-blue/red-phase.js +135 -0
- package/dist/vendor/core/red-blue/risk-tiers.d.ts +22 -0
- package/dist/vendor/core/red-blue/risk-tiers.js +32 -0
- package/dist/vendor/core/rollback.js +2 -3
- package/package.json +10 -3
- package/server.json +2 -2
|
@@ -1,105 +1,78 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
? {
|
|
140
|
+
? {
|
|
141
|
+
file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
|
|
142
|
+
}
|
|
165
143
|
: {}),
|
|
166
|
-
...(
|
|
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
|
|
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
|
|
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
|
-
? {
|
|
170
|
+
? {
|
|
171
|
+
file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
|
|
172
|
+
}
|
|
192
173
|
: {}),
|
|
193
174
|
...(record.loopId !== undefined
|
|
194
|
-
? {
|
|
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
|
|
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
|
|
340
|
+
throw invalidArgumentsError(`Unknown arguments: ${unknownKeys.join(", ")}`);
|
|
212
341
|
}
|
|
213
342
|
}
|
|
214
|
-
function assertPathWithinRoot(candidatePath, rootPath, name) {
|
|
215
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
487
|
+
throw invalidArgumentsError(`Invalid ${name}.`);
|
|
283
488
|
}
|
|
284
489
|
return value;
|
|
285
490
|
}
|
|
286
|
-
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
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;
|