@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 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 three stdio tools:
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 "npx -y @martinloop/mcp"
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.keesan12/martin-loop`
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, `server.json`, and the MCP release notes under `docs/release/`.
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;
@@ -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 { [name]: value };
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 three tools over the Model Context Protocol (stdio transport):
6
- * martin_run execute a full Martin loop on a coding task
7
- * martin_inspectsummarise a saved loop record file
8
- * martin_status return cost and pressure state from a loop record
5
+ * Exposes a governed local MCP cockpit over stdio:
6
+ * martin_doctor inspect local readiness and run-store health
7
+ * martin_preflightnormalize 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