@martinloop/mcp 0.1.3 → 0.2.0
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 +56 -5
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.js +84 -0
- package/dist/resources.d.ts +7 -0
- package/dist/resources.js +89 -0
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +121 -1
- package/dist/server.d.ts +6 -4
- package/dist/server.js +271 -6
- package/dist/tools/cockpit-support.d.ts +69 -0
- package/dist/tools/cockpit-support.js +108 -0
- package/dist/tools/doctor.d.ts +35 -0
- package/dist/tools/doctor.js +56 -0
- package/dist/tools/get-attempt.d.ts +8 -0
- package/dist/tools/get-attempt.js +6 -0
- package/dist/tools/get-run.d.ts +17 -0
- package/dist/tools/get-run.js +14 -0
- package/dist/tools/get-verification-results.d.ts +11 -0
- package/dist/tools/get-verification-results.js +9 -0
- package/dist/tools/list-runs.d.ts +9 -0
- package/dist/tools/list-runs.js +7 -0
- package/dist/tools/preflight.d.ts +62 -0
- package/dist/tools/preflight.js +76 -0
- package/dist/tools/run-dossier.d.ts +8 -0
- package/dist/tools/run-dossier.js +6 -0
- package/dist/tools/tool-support.d.ts +37 -0
- package/dist/tools/tool-support.js +110 -0
- package/package.json +10 -5
- package/server.json +4 -4
package/README.md
CHANGED
|
@@ -2,11 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
Governed MCP server for AI coding agents that need hard spend limits, verifier gates, scoped file edits, and inspectable run records.
|
|
4
4
|
|
|
5
|
-
`@martinloop/mcp` exposes
|
|
5
|
+
`@martinloop/mcp@0.2.0` exposes ten stdio tools plus read-only MCP resources, resource templates, and prompts:
|
|
6
6
|
|
|
7
|
+
- `martin_doctor`
|
|
8
|
+
- `martin_preflight`
|
|
7
9
|
- `martin_run`
|
|
8
10
|
- `martin_inspect`
|
|
9
11
|
- `martin_status`
|
|
12
|
+
- `martin_list_runs`
|
|
13
|
+
- `martin_get_run`
|
|
14
|
+
- `martin_get_attempt`
|
|
15
|
+
- `martin_get_verification_results`
|
|
16
|
+
- `martin_run_dossier`
|
|
17
|
+
|
|
18
|
+
Recommended flow:
|
|
19
|
+
|
|
20
|
+
1. `martin_doctor`
|
|
21
|
+
2. `martin_preflight`
|
|
22
|
+
3. `martin_run`
|
|
23
|
+
4. `martin_list_runs`, `martin_run_dossier`, `martin_inspect`, or `martin_status`
|
|
10
24
|
|
|
11
25
|
## What This Server Is For
|
|
12
26
|
|
|
@@ -31,14 +45,20 @@ Run the packaged server directly:
|
|
|
31
45
|
npx -y @martinloop/mcp
|
|
32
46
|
```
|
|
33
47
|
|
|
48
|
+
Add it to Codex:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
codex mcp add martin-loop -- npx -y @martinloop/mcp
|
|
52
|
+
```
|
|
53
|
+
|
|
34
54
|
Add it to Claude Code:
|
|
35
55
|
|
|
36
56
|
```sh
|
|
37
57
|
# macOS/Linux
|
|
38
|
-
claude mcp add --scope user martin-loop -- npx -y @martinloop/mcp
|
|
58
|
+
claude mcp add --transport stdio --scope user martin-loop -- npx -y @martinloop/mcp
|
|
39
59
|
|
|
40
60
|
# Windows PowerShell/cmd
|
|
41
|
-
claude mcp add --scope user martin-loop cmd /c
|
|
61
|
+
claude mcp add --transport stdio --scope user martin-loop -- cmd /c npx -y @martinloop/mcp
|
|
42
62
|
```
|
|
43
63
|
|
|
44
64
|
Generic stdio configuration:
|
|
@@ -75,9 +95,36 @@ MARTIN_LIVE=false npx -y @martinloop/mcp
|
|
|
75
95
|
|
|
76
96
|
| Tool | Purpose | Required input | Important optional input | Notes |
|
|
77
97
|
| --- | --- | --- | --- | --- |
|
|
98
|
+
| `martin_doctor` | Inspect local readiness and run-store health | none | `workingDirectory`, `runsDir`, `engine` | Read-only setup lane before execution. |
|
|
99
|
+
| `martin_preflight` | Normalize and validate a proposed run contract | `objective` | `workingDirectory`, `engine`, `model`, `maxUsd`, `maxIterations`, `maxTokens`, `verificationPlan`, `allowedPaths`, `deniedPaths`, `workspaceId`, `projectId` | Read-only contract check; does not execute work. |
|
|
78
100
|
| `martin_run` | Run a governed coding loop | `objective` | `workingDirectory`, `engine`, `model`, `maxUsd`, `maxIterations`, `maxTokens`, `verificationPlan`, `allowedPaths`, `deniedPaths`, `workspaceId`, `projectId` | Unknown arguments are rejected. |
|
|
79
101
|
| `martin_inspect` | Read a saved run record or run folder | none | `file`, `runsDir` | `file` may point to a `loop-record.json`, legacy `.jsonl`, or a run directory under the runs root. |
|
|
80
102
|
| `martin_status` | Report budget pressure and stop conditions | exactly one of `loopJson`, `file`, `loopId`, or `latest` | `runsDir` | `latest` must be `true` when used. |
|
|
103
|
+
| `martin_list_runs` | List recent run summaries | none | `runsDir`, `limit` | Read-only cockpit view over local run records. |
|
|
104
|
+
| `martin_get_run` | Load a run dossier | exactly one of `loopId` or `latest` | `runsDir` | Read-only task, budget, cost, and attempt details. |
|
|
105
|
+
| `martin_get_attempt` | Load one attempt | `loopId`, `attemptIndex` | `runsDir` | Read-only attempt evidence. |
|
|
106
|
+
| `martin_get_verification_results` | Extract verifier events | exactly one of `loopId` or `latest` | `runsDir` | Read-only verifier completion summaries. |
|
|
107
|
+
| `martin_run_dossier` | Build a compact review dossier | exactly one of `loopId` or `latest` | `runsDir` | Summary, budget, attempts, and verification evidence. |
|
|
108
|
+
|
|
109
|
+
## Discovery Surface
|
|
110
|
+
|
|
111
|
+
`0.2.0` adds read-only cockpit discovery for MCP hosts that support resources and prompts.
|
|
112
|
+
|
|
113
|
+
Resources:
|
|
114
|
+
|
|
115
|
+
- `martin://runs/summary`
|
|
116
|
+
- `martin://runs/latest`
|
|
117
|
+
|
|
118
|
+
Resource templates:
|
|
119
|
+
|
|
120
|
+
- `martin://runs/{loopId}`
|
|
121
|
+
- `martin://runs/{loopId}/attempts/{attemptIndex}`
|
|
122
|
+
- `martin://runs/{loopId}/verification`
|
|
123
|
+
|
|
124
|
+
Prompts:
|
|
125
|
+
|
|
126
|
+
- `martin_review_run`
|
|
127
|
+
- `martin_triage_failures`
|
|
81
128
|
|
|
82
129
|
## Safe-Root Path Model
|
|
83
130
|
|
|
@@ -169,7 +216,7 @@ The registry manifest artifact for this package is `server.json`. In this reposi
|
|
|
169
216
|
Current metadata:
|
|
170
217
|
|
|
171
218
|
- npm package: `@martinloop/mcp`
|
|
172
|
-
- registry server name: `io.github.
|
|
219
|
+
- registry server name: `io.github.Keesan12/martin-loop`
|
|
173
220
|
- manifest artifact name: `server.json`
|
|
174
221
|
|
|
175
222
|
Official MCP Registry publication is separate from npm publication. After publishing the package to npm, run the publisher from `packages/mcp`:
|
|
@@ -188,12 +235,16 @@ pnpm --filter @martinloop/mcp lint
|
|
|
188
235
|
pnpm --filter @martinloop/mcp test
|
|
189
236
|
pnpm --filter @martinloop/mcp build
|
|
190
237
|
pnpm --filter @martinloop/mcp smoke:pack
|
|
238
|
+
pnpm --filter @martinloop/mcp smoke:published:pack
|
|
239
|
+
pnpm --filter @martinloop/mcp verify:release
|
|
191
240
|
pnpm --filter @martinloop/mcp smoke:published
|
|
192
241
|
```
|
|
193
242
|
|
|
194
243
|
- `smoke:pack` verifies the packed tarball shape and a stdio MCP launch
|
|
244
|
+
- `smoke:published:pack` verifies install-and-run behavior from a freshly packed local tarball before npm publish
|
|
245
|
+
- `verify:release` checks metadata parity, release-note presence, and public MCP doc accuracy for the current package version
|
|
195
246
|
- `smoke:published` verifies the npm-installed artifact through `npm install` plus live MCP tool calls
|
|
196
247
|
|
|
197
248
|
## Version Notes
|
|
198
249
|
|
|
199
|
-
The root `CHANGELOG.md` is repo-wide and includes non-MCP changes. For the `@martinloop/mcp` surface, prefer this README
|
|
250
|
+
The root `CHANGELOG.md` is repo-wide and includes non-MCP changes. For the `@martinloop/mcp` surface, prefer this README and `server.json`.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { GetPromptResult, ListPromptsResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
export declare function listMartinPrompts(): ListPromptsResult["prompts"];
|
|
3
|
+
export declare function getMartinPrompt(name: string, args?: Record<string, unknown>): GetPromptResult;
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export function listMartinPrompts() {
|
|
2
|
+
return [
|
|
3
|
+
{
|
|
4
|
+
name: "martin_review_run",
|
|
5
|
+
description: "Review a Martin Loop run for governance, verification, and release-readiness evidence.",
|
|
6
|
+
arguments: [
|
|
7
|
+
{
|
|
8
|
+
name: "loopId",
|
|
9
|
+
description: "Run loopId to review.",
|
|
10
|
+
required: true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: "objective",
|
|
14
|
+
description: "Review objective or release question.",
|
|
15
|
+
required: false
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "martin_triage_failures",
|
|
21
|
+
description: "Triage failed Martin runs and propose the next safest bounded action.",
|
|
22
|
+
arguments: [
|
|
23
|
+
{
|
|
24
|
+
name: "loopId",
|
|
25
|
+
description: "Optional run loopId to triage. If omitted, use latest run resources.",
|
|
26
|
+
required: false
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
export function getMartinPrompt(name, args = {}) {
|
|
33
|
+
if (name === "martin_review_run") {
|
|
34
|
+
const loopId = requireOptionalText(args.loopId, "loopId") ?? "latest";
|
|
35
|
+
const objective = requireOptionalText(args.objective, "objective") ?? "Assess whether the run is safe to ship.";
|
|
36
|
+
return {
|
|
37
|
+
description: "Review Martin Loop run evidence.",
|
|
38
|
+
messages: [
|
|
39
|
+
{
|
|
40
|
+
role: "user",
|
|
41
|
+
content: {
|
|
42
|
+
type: "text",
|
|
43
|
+
text: [
|
|
44
|
+
`Review Martin Loop run ${loopId}.`,
|
|
45
|
+
`Objective: ${objective}`,
|
|
46
|
+
"Use martin://runs/{loopId}, martin://runs/{loopId}/verification, and the read-only tools before making a shipping recommendation.",
|
|
47
|
+
"Call out missing verifier proof, budget pressure, safety-leash violations, and whether human review is required."
|
|
48
|
+
].join("\n")
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (name === "martin_triage_failures") {
|
|
55
|
+
const loopId = requireOptionalText(args.loopId, "loopId") ?? "latest";
|
|
56
|
+
return {
|
|
57
|
+
description: "Triage failed Martin Loop evidence.",
|
|
58
|
+
messages: [
|
|
59
|
+
{
|
|
60
|
+
role: "user",
|
|
61
|
+
content: {
|
|
62
|
+
type: "text",
|
|
63
|
+
text: [
|
|
64
|
+
`Triage Martin Loop run ${loopId}.`,
|
|
65
|
+
"Use the run dossier, attempt evidence, and verification results.",
|
|
66
|
+
"Return the smallest safe next action and do not suggest re-running until the failure class and verifier evidence are clear."
|
|
67
|
+
].join("\n")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
throw new Error("Unknown prompt.");
|
|
74
|
+
}
|
|
75
|
+
function requireOptionalText(value, name) {
|
|
76
|
+
if (value === undefined) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
80
|
+
throw new Error(`Invalid ${name}.`);
|
|
81
|
+
}
|
|
82
|
+
return value.trim();
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ListResourceTemplatesResult, ListResourcesResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
export interface ResourceReadOptions {
|
|
3
|
+
runsDir?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function listMartinResources(): ListResourcesResult["resources"];
|
|
6
|
+
export declare function listMartinResourceTemplates(): ListResourceTemplatesResult["resourceTemplates"];
|
|
7
|
+
export declare function readMartinResource(uri: string, options?: ResourceReadOptions): Promise<ReadResourceResult>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { buildRunDossier, getAttempt, listRunSummaries, loadSelectedRun } from "./tools/cockpit-support.js";
|
|
2
|
+
import { getVerificationResultsTool } from "./tools/get-verification-results.js";
|
|
3
|
+
export function listMartinResources() {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
uri: "martin://runs/summary",
|
|
7
|
+
name: "Martin run summary",
|
|
8
|
+
description: "Read-only summary of recent governed Martin Loop runs.",
|
|
9
|
+
mimeType: "application/json"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
uri: "martin://runs/latest",
|
|
13
|
+
name: "Latest Martin run",
|
|
14
|
+
description: "Read-only dossier for the newest run in the local run store.",
|
|
15
|
+
mimeType: "application/json"
|
|
16
|
+
}
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
export function listMartinResourceTemplates() {
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
uriTemplate: "martin://runs/{loopId}",
|
|
23
|
+
name: "Martin run dossier",
|
|
24
|
+
description: "Read-only run dossier by loopId.",
|
|
25
|
+
mimeType: "application/json"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
uriTemplate: "martin://runs/{loopId}/attempts/{attemptIndex}",
|
|
29
|
+
name: "Martin run attempt",
|
|
30
|
+
description: "Read-only attempt evidence for a run.",
|
|
31
|
+
mimeType: "application/json"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
uriTemplate: "martin://runs/{loopId}/verification",
|
|
35
|
+
name: "Martin verification results",
|
|
36
|
+
description: "Verifier results extracted from a run ledger.",
|
|
37
|
+
mimeType: "application/json"
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
export async function readMartinResource(uri, options = {}) {
|
|
42
|
+
const parsed = new URL(uri);
|
|
43
|
+
if (parsed.protocol !== "martin:") {
|
|
44
|
+
throw new Error("Unsupported resource URI.");
|
|
45
|
+
}
|
|
46
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
47
|
+
if (parsed.hostname === "runs" && segments.length === 1 && segments[0] === "summary") {
|
|
48
|
+
return asJsonResource(uri, await listRunSummaries({ runsDir: options.runsDir }));
|
|
49
|
+
}
|
|
50
|
+
if (parsed.hostname === "runs" && segments.length === 1 && segments[0] === "latest") {
|
|
51
|
+
const loop = await loadSelectedRun({ latest: true, runsDir: options.runsDir });
|
|
52
|
+
return asJsonResource(uri, buildRunDossier(loop));
|
|
53
|
+
}
|
|
54
|
+
if (parsed.hostname !== "runs" || segments.length < 1) {
|
|
55
|
+
throw new Error("Unknown resource URI.");
|
|
56
|
+
}
|
|
57
|
+
const [loopId, child, attemptIndex] = segments;
|
|
58
|
+
if (!loopId || !/^[A-Za-z0-9._-]+$/u.test(loopId)) {
|
|
59
|
+
throw new Error("Invalid loopId.");
|
|
60
|
+
}
|
|
61
|
+
if (!child) {
|
|
62
|
+
const loop = await loadSelectedRun({ loopId, runsDir: options.runsDir });
|
|
63
|
+
return asJsonResource(uri, buildRunDossier(loop));
|
|
64
|
+
}
|
|
65
|
+
if (child === "attempts") {
|
|
66
|
+
const parsedAttempt = Number(attemptIndex);
|
|
67
|
+
if (!Number.isInteger(parsedAttempt) || parsedAttempt <= 0) {
|
|
68
|
+
throw new Error("Invalid attemptIndex.");
|
|
69
|
+
}
|
|
70
|
+
const loop = await loadSelectedRun({ loopId, runsDir: options.runsDir });
|
|
71
|
+
return asJsonResource(uri, getAttempt(loop, parsedAttempt));
|
|
72
|
+
}
|
|
73
|
+
if (child === "verification") {
|
|
74
|
+
return asJsonResource(uri, await getVerificationResultsTool({ loopId, runsDir: options.runsDir }));
|
|
75
|
+
}
|
|
76
|
+
throw new Error("Unknown resource URI.");
|
|
77
|
+
}
|
|
78
|
+
function asJsonResource(uri, value) {
|
|
79
|
+
return {
|
|
80
|
+
contents: [
|
|
81
|
+
{
|
|
82
|
+
uri,
|
|
83
|
+
mimeType: "application/json",
|
|
84
|
+
text: JSON.stringify(value, null, 2)
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=resources.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type ToolName = "martin_run" | "martin_inspect" | "martin_status";
|
|
1
|
+
type ToolName = "martin_doctor" | "martin_preflight" | "martin_run" | "martin_inspect" | "martin_status" | "martin_list_runs" | "martin_get_run" | "martin_get_attempt" | "martin_get_verification_results" | "martin_run_dossier";
|
|
2
2
|
export declare function validateToolInput(name: ToolName, args: unknown): unknown;
|
|
3
3
|
export declare function sanitizeToolErrorMessage(error: unknown): string;
|
|
4
4
|
export declare function resolveSafeRepoRoot(repoRoot?: string, workspaceRoot?: string): string;
|
|
@@ -2,12 +2,26 @@ import { extname, isAbsolute, relative, resolve } from "node:path";
|
|
|
2
2
|
import { resolveRunsRoot } from "./vendor/core/index.js";
|
|
3
3
|
export function validateToolInput(name, args) {
|
|
4
4
|
switch (name) {
|
|
5
|
+
case "martin_doctor":
|
|
6
|
+
return validateDoctorInput(args);
|
|
7
|
+
case "martin_preflight":
|
|
8
|
+
return validatePreflightInput(args);
|
|
5
9
|
case "martin_run":
|
|
6
10
|
return validateRunInput(args);
|
|
7
11
|
case "martin_inspect":
|
|
8
12
|
return validateInspectInput(args);
|
|
9
13
|
case "martin_status":
|
|
10
14
|
return validateStatusInput(args);
|
|
15
|
+
case "martin_list_runs":
|
|
16
|
+
return validateListRunsInput(args);
|
|
17
|
+
case "martin_get_run":
|
|
18
|
+
return validateGetRunInput(args);
|
|
19
|
+
case "martin_get_attempt":
|
|
20
|
+
return validateGetAttemptInput(args);
|
|
21
|
+
case "martin_get_verification_results":
|
|
22
|
+
return validateGetVerificationResultsInput(args);
|
|
23
|
+
case "martin_run_dossier":
|
|
24
|
+
return validateRunDossierInput(args);
|
|
11
25
|
default:
|
|
12
26
|
throw new Error(`Unknown tool: ${name}`);
|
|
13
27
|
}
|
|
@@ -18,6 +32,54 @@ export function sanitizeToolErrorMessage(error) {
|
|
|
18
32
|
? "Tool execution failed."
|
|
19
33
|
: message;
|
|
20
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
|
+
}
|
|
21
83
|
export function resolveSafeRepoRoot(repoRoot, workspaceRoot = process.env.MARTIN_MCP_WORKSPACE_ROOT ?? process.cwd()) {
|
|
22
84
|
const baseRoot = resolve(workspaceRoot);
|
|
23
85
|
const candidate = repoRoot ? resolve(baseRoot, repoRoot) : baseRoot;
|
|
@@ -147,6 +209,61 @@ function validateStatusInput(args) {
|
|
|
147
209
|
...(record.latest === true ? { latest: true } : {})
|
|
148
210
|
};
|
|
149
211
|
}
|
|
212
|
+
function validateListRunsInput(args) {
|
|
213
|
+
const record = requireObject(args);
|
|
214
|
+
assertAllowedKeys(record, ["runsDir", "limit"]);
|
|
215
|
+
return {
|
|
216
|
+
...(record.runsDir !== undefined
|
|
217
|
+
? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
|
|
218
|
+
: {}),
|
|
219
|
+
...(record.limit !== undefined ? { limit: requirePositiveInteger(record.limit, "limit") } : {})
|
|
220
|
+
};
|
|
221
|
+
}
|
|
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);
|
|
236
|
+
}
|
|
237
|
+
function validateGetAttemptInput(args) {
|
|
238
|
+
const record = requireObject(args);
|
|
239
|
+
assertAllowedKeys(record, ["loopId", "attemptIndex", "runsDir"]);
|
|
240
|
+
return {
|
|
241
|
+
loopId: requireLoopId(record.loopId, "loopId"),
|
|
242
|
+
attemptIndex: requirePositiveInteger(record.attemptIndex, "attemptIndex"),
|
|
243
|
+
...(record.runsDir !== undefined
|
|
244
|
+
? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
|
|
245
|
+
: {})
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function validateRunSelector(record) {
|
|
249
|
+
const selectors = [
|
|
250
|
+
record.loopId !== undefined ? "loopId" : null,
|
|
251
|
+
record.latest !== undefined ? "latest" : null
|
|
252
|
+
].filter((value) => value !== null);
|
|
253
|
+
if (selectors.length !== 1) {
|
|
254
|
+
throw new Error("Provide exactly one of loopId or latest.");
|
|
255
|
+
}
|
|
256
|
+
if (record.latest !== undefined && record.latest !== true) {
|
|
257
|
+
throw new Error("Invalid latest.");
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
...(record.loopId !== undefined ? { loopId: requireLoopId(record.loopId, "loopId") } : {}),
|
|
261
|
+
...(record.latest === true ? { latest: true } : {}),
|
|
262
|
+
...(record.runsDir !== undefined
|
|
263
|
+
? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
|
|
264
|
+
: {})
|
|
265
|
+
};
|
|
266
|
+
}
|
|
150
267
|
function requireObject(value) {
|
|
151
268
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
152
269
|
throw new Error("Tool arguments must be an object.");
|
|
@@ -200,10 +317,13 @@ function optionalPositiveInteger(value, name) {
|
|
|
200
317
|
if (value === undefined) {
|
|
201
318
|
return {};
|
|
202
319
|
}
|
|
320
|
+
return { [name]: requirePositiveInteger(value, name) };
|
|
321
|
+
}
|
|
322
|
+
function requirePositiveInteger(value, name) {
|
|
203
323
|
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
204
324
|
throw new Error(`Invalid ${name}.`);
|
|
205
325
|
}
|
|
206
|
-
return
|
|
326
|
+
return value;
|
|
207
327
|
}
|
|
208
328
|
function optionalStringArray(value, name) {
|
|
209
329
|
if (value === undefined) {
|
package/dist/server.d.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Martin Loop MCP Server
|
|
4
4
|
*
|
|
5
|
-
* Exposes
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
9
11
|
*
|
|
10
12
|
* Setup (Claude Code):
|
|
11
13
|
* macOS/Linux: claude mcp add --scope user martin-loop -- npx @martinloop/mcp
|