@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.
- package/README.md +131 -158
- 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 -3
- package/dist/prompts.js +445 -74
- package/dist/resources.d.ts +27 -5
- package/dist/resources.js +557 -71
- package/dist/server-validation.d.ts +2 -3
- package/dist/server-validation.js +262 -122
- package/dist/server.d.ts +76 -7
- package/dist/server.js +1126 -400
- package/dist/tools/doctor.js +14 -6
- package/dist/tools/get-attempt.d.ts +13 -6
- package/dist/tools/get-attempt.js +14 -5
- package/dist/tools/get-run.d.ts +19 -12
- package/dist/tools/get-run.js +20 -11
- 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 +10 -7
- package/dist/tools/get-verification-results.js +11 -6
- package/dist/tools/inspect-loop.d.ts +9 -0
- package/dist/tools/inspect-loop.js +11 -2
- package/dist/tools/list-runs.d.ts +25 -5
- package/dist/tools/list-runs.js +21 -4
- package/dist/tools/preflight.js +7 -2
- package/dist/tools/run-dossier.d.ts +37 -4
- package/dist/tools/run-dossier.js +40 -5
- 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 -5
- package/server.json +2 -2
- package/dist/tools/cockpit-support.d.ts +0 -69
- package/dist/tools/cockpit-support.js +0 -108
|
@@ -1,19 +1,24 @@
|
|
|
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);
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
? {
|
|
140
|
+
? {
|
|
141
|
+
file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
|
|
142
|
+
}
|
|
175
143
|
: {}),
|
|
176
|
-
...(
|
|
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
|
|
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
|
|
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
|
-
? {
|
|
170
|
+
? {
|
|
171
|
+
file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
|
|
172
|
+
}
|
|
202
173
|
: {}),
|
|
203
174
|
...(record.loopId !== undefined
|
|
204
|
-
? {
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
183
|
+
function validateDoctorInput(args) {
|
|
213
184
|
const record = requireObject(args);
|
|
214
|
-
assertAllowedKeys(record, ["runsDir", "
|
|
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.
|
|
193
|
+
...optionalEnumAsObject(record.engine, "engine", ["claude", "codex"])
|
|
220
194
|
};
|
|
221
195
|
}
|
|
222
|
-
function
|
|
223
|
-
|
|
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
|
|
199
|
+
function validateListRunsInput(args) {
|
|
238
200
|
const record = requireObject(args);
|
|
239
|
-
assertAllowedKeys(record, [
|
|
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
|
|
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
|
|
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
|
|
237
|
+
throw invalidArgumentsError("Invalid latest.", "latest must be the literal boolean value true.");
|
|
258
238
|
}
|
|
259
239
|
return {
|
|
260
|
-
...(record.
|
|
261
|
-
|
|
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
|
|
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
|
|
340
|
+
throw invalidArgumentsError(`Unknown arguments: ${unknownKeys.join(", ")}`);
|
|
277
341
|
}
|
|
278
342
|
}
|
|
279
|
-
function assertPathWithinRoot(candidatePath, rootPath, name) {
|
|
280
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
487
|
+
throw invalidArgumentsError(`Invalid ${name}.`);
|
|
351
488
|
}
|
|
352
489
|
return value;
|
|
353
490
|
}
|
|
354
|
-
|
|
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;
|