@martinloop/mcp 0.1.3 → 0.1.4

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,12 +2,21 @@
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.1.4` exposes five stdio tools:
6
6
 
7
+ - `martin_doctor`
8
+ - `martin_preflight`
7
9
  - `martin_run`
8
10
  - `martin_inspect`
9
11
  - `martin_status`
10
12
 
13
+ Recommended flow:
14
+
15
+ 1. `martin_doctor`
16
+ 2. `martin_preflight`
17
+ 3. `martin_run`
18
+ 4. `martin_inspect` or `martin_status`
19
+
11
20
  ## What This Server Is For
12
21
 
13
22
  Use this MCP when a host already knows how to delegate coding work, but you want Martin Loop to bound that work with:
@@ -31,14 +40,20 @@ Run the packaged server directly:
31
40
  npx -y @martinloop/mcp
32
41
  ```
33
42
 
43
+ Add it to Codex:
44
+
45
+ ```sh
46
+ codex mcp add martin-loop -- npx -y @martinloop/mcp
47
+ ```
48
+
34
49
  Add it to Claude Code:
35
50
 
36
51
  ```sh
37
52
  # macOS/Linux
38
- claude mcp add --scope user martin-loop -- npx -y @martinloop/mcp
53
+ claude mcp add --transport stdio --scope user martin-loop -- npx -y @martinloop/mcp
39
54
 
40
55
  # Windows PowerShell/cmd
41
- claude mcp add --scope user martin-loop cmd /c "npx -y @martinloop/mcp"
56
+ claude mcp add --transport stdio --scope user martin-loop -- cmd /c npx -y @martinloop/mcp
42
57
  ```
43
58
 
44
59
  Generic stdio configuration:
@@ -75,6 +90,8 @@ MARTIN_LIVE=false npx -y @martinloop/mcp
75
90
 
76
91
  | Tool | Purpose | Required input | Important optional input | Notes |
77
92
  | --- | --- | --- | --- | --- |
93
+ | `martin_doctor` | Inspect local readiness and run-store health | none | `workingDirectory`, `runsDir`, `engine` | Read-only setup lane before execution. |
94
+ | `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
95
  | `martin_run` | Run a governed coding loop | `objective` | `workingDirectory`, `engine`, `model`, `maxUsd`, `maxIterations`, `maxTokens`, `verificationPlan`, `allowedPaths`, `deniedPaths`, `workspaceId`, `projectId` | Unknown arguments are rejected. |
79
96
  | `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
97
  | `martin_status` | Report budget pressure and stop conditions | exactly one of `loopJson`, `file`, `loopId`, or `latest` | `runsDir` | `latest` must be `true` when used. |
@@ -169,7 +186,7 @@ The registry manifest artifact for this package is `server.json`. In this reposi
169
186
  Current metadata:
170
187
 
171
188
  - npm package: `@martinloop/mcp`
172
- - registry server name: `io.github.keesan12/martin-loop`
189
+ - registry server name: `io.github.Keesan12/martin-loop`
173
190
  - manifest artifact name: `server.json`
174
191
 
175
192
  Official MCP Registry publication is separate from npm publication. After publishing the package to npm, run the publisher from `packages/mcp`:
@@ -188,12 +205,16 @@ pnpm --filter @martinloop/mcp lint
188
205
  pnpm --filter @martinloop/mcp test
189
206
  pnpm --filter @martinloop/mcp build
190
207
  pnpm --filter @martinloop/mcp smoke:pack
208
+ pnpm --filter @martinloop/mcp smoke:published:pack
209
+ pnpm --filter @martinloop/mcp verify:release
191
210
  pnpm --filter @martinloop/mcp smoke:published
192
211
  ```
193
212
 
194
213
  - `smoke:pack` verifies the packed tarball shape and a stdio MCP launch
214
+ - `smoke:published:pack` verifies install-and-run behavior from a freshly packed local tarball before npm publish
215
+ - `verify:release` checks metadata parity, release-note presence, and public MCP doc accuracy for the current package version
195
216
  - `smoke:published` verifies the npm-installed artifact through `npm install` plus live MCP tool calls
196
217
 
197
218
  ## Version Notes
198
219
 
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/`.
220
+ The root `CHANGELOG.md` is repo-wide and includes non-MCP changes. For the `@martinloop/mcp` surface, prefer this README and `server.json`.
@@ -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";
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,6 +2,10 @@ 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":
@@ -18,6 +22,54 @@ export function sanitizeToolErrorMessage(error) {
18
22
  ? "Tool execution failed."
19
23
  : message;
20
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
+ }
21
73
  export function resolveSafeRepoRoot(repoRoot, workspaceRoot = process.env.MARTIN_MCP_WORKSPACE_ROOT ?? process.cwd()) {
22
74
  const baseRoot = resolve(workspaceRoot);
23
75
  const candidate = repoRoot ? resolve(baseRoot, repoRoot) : baseRoot;
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 five tools over the Model Context Protocol (stdio transport):
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
package/dist/server.js 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 five tools over the Model Context Protocol (stdio transport):
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
@@ -21,8 +23,10 @@ import { createRequire } from "node:module";
21
23
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
24
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
25
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
26
+ import { martinDoctorTool } from "./tools/doctor.js";
24
27
  import { getStatusTool } from "./tools/get-status.js";
25
28
  import { inspectLoopTool } from "./tools/inspect-loop.js";
29
+ import { martinPreflightTool } from "./tools/preflight.js";
26
30
  import { runLoopTool } from "./tools/run-loop.js";
27
31
  import { sanitizeToolErrorMessage, validateToolInput } from "./server-validation.js";
28
32
  const require = createRequire(import.meta.url);
@@ -33,6 +37,95 @@ const server = new Server({ name: "martin-loop", version: packageJson.version },
33
37
  // ---------------------------------------------------------------------------
34
38
  server.setRequestHandler(ListToolsRequestSchema, () => ({
35
39
  tools: [
40
+ {
41
+ name: "martin_doctor",
42
+ description: "Inspect Martin MCP readiness without changing code. Reports workspace roots, run-store visibility, execution mode, and whether claude or codex is available on PATH.",
43
+ inputSchema: {
44
+ type: "object",
45
+ additionalProperties: false,
46
+ properties: {
47
+ workingDirectory: {
48
+ type: "string",
49
+ description: "Optional repo-root override resolved under the MCP workspace root (or current working directory). Must stay within that safe root."
50
+ },
51
+ runsDir: {
52
+ type: "string",
53
+ description: "Optional runs-root override resolved under the default Martin runs root. Defaults to MARTIN_RUNS_DIR or ~/.martin/runs."
54
+ },
55
+ engine: {
56
+ type: "string",
57
+ enum: ["claude", "codex"],
58
+ description: "Optional engine to emphasize in the readiness report."
59
+ }
60
+ }
61
+ }
62
+ },
63
+ {
64
+ name: "martin_preflight",
65
+ description: "Validate and normalize a proposed martin_run contract before execution. Reports the effective budget, path scope, engine readiness, and expected run-store layout.",
66
+ inputSchema: {
67
+ type: "object",
68
+ additionalProperties: false,
69
+ properties: {
70
+ objective: {
71
+ type: "string",
72
+ description: "The coding task to complete. Be specific about what needs to change."
73
+ },
74
+ workingDirectory: {
75
+ type: "string",
76
+ description: "Optional repo-root override resolved under the MCP workspace root (or current working directory). Must stay within that safe root."
77
+ },
78
+ engine: {
79
+ type: "string",
80
+ enum: ["claude", "codex"],
81
+ description: "Which agent CLI to use. Defaults to 'claude'."
82
+ },
83
+ model: {
84
+ type: "string",
85
+ description: "Model override passed to the CLI (e.g. 'claude-opus-4-6', 'o3')."
86
+ },
87
+ maxUsd: {
88
+ type: "number",
89
+ exclusiveMinimum: 0,
90
+ description: "Hard budget ceiling in USD. Defaults to 25."
91
+ },
92
+ maxIterations: {
93
+ type: "integer",
94
+ exclusiveMinimum: 0,
95
+ description: "Maximum number of loop attempts. Defaults to 8."
96
+ },
97
+ maxTokens: {
98
+ type: "integer",
99
+ exclusiveMinimum: 0,
100
+ description: "Maximum total tokens across all attempts. Defaults to 80000."
101
+ },
102
+ verificationPlan: {
103
+ type: "array",
104
+ items: { type: "string" },
105
+ description: "Shell commands that must all exit 0 for the task to be considered complete (e.g. ['pnpm test', 'pnpm build'])."
106
+ },
107
+ allowedPaths: {
108
+ type: "array",
109
+ items: { type: "string" },
110
+ description: "Repo-relative path globs Martin may modify, such as ['src/**', 'tests/**']. Absolute paths and '..' traversal are rejected."
111
+ },
112
+ deniedPaths: {
113
+ type: "array",
114
+ items: { type: "string" },
115
+ description: "Repo-relative path globs Martin must never modify, such as ['.env', 'docs/security/**']. Absolute paths and '..' traversal are rejected."
116
+ },
117
+ workspaceId: {
118
+ type: "string",
119
+ description: "Workspace identifier for telemetry. Defaults to 'ws_mcp'."
120
+ },
121
+ projectId: {
122
+ type: "string",
123
+ description: "Project identifier for telemetry. Defaults to 'proj_mcp'."
124
+ }
125
+ },
126
+ required: ["objective"]
127
+ }
128
+ },
36
129
  {
37
130
  name: "martin_run",
38
131
  description: "Execute a full Martin Loop on a coding task. Martin spawns the selected agent CLI (claude or codex), runs the task, classifies failures, and retries within the specified budget. Returns the loop outcome including lifecycle state, attempt count, and spend.",
@@ -161,6 +254,16 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
161
254
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
162
255
  const { name, arguments: args } = request.params;
163
256
  try {
257
+ if (name === "martin_doctor") {
258
+ const input = validateToolInput("martin_doctor", args);
259
+ const output = await martinDoctorTool(input);
260
+ return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
261
+ }
262
+ if (name === "martin_preflight") {
263
+ const input = validateToolInput("martin_preflight", args);
264
+ const output = await martinPreflightTool(input);
265
+ return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
266
+ }
164
267
  if (name === "martin_run") {
165
268
  const input = validateToolInput("martin_run", args);
166
269
  const output = await runLoopTool(input);
@@ -0,0 +1,35 @@
1
+ import { type LoopPreview, type MartinEngine } from "./tool-support.js";
2
+ export interface MartinDoctorInput {
3
+ workingDirectory?: string;
4
+ runsDir?: string;
5
+ engine?: MartinEngine;
6
+ }
7
+ export interface MartinDoctorOutput {
8
+ status: "ok" | "degraded";
9
+ summary: string;
10
+ server: {
11
+ name: "martin-loop";
12
+ nodeVersion: string;
13
+ platform: NodeJS.Platform;
14
+ };
15
+ environment: {
16
+ workspaceRoot: string;
17
+ workingDirectory: string;
18
+ runsRoot: string;
19
+ mode: "live" | "stub";
20
+ liveMode: boolean;
21
+ };
22
+ engines: Record<MartinEngine, {
23
+ available: boolean;
24
+ detail: string;
25
+ resolvedPath?: string;
26
+ }>;
27
+ requestedEngine?: MartinEngine;
28
+ runStore: {
29
+ exists: boolean;
30
+ loopCount: number;
31
+ latestRun?: LoopPreview;
32
+ };
33
+ warnings: string[];
34
+ }
35
+ export declare function martinDoctorTool(input: MartinDoctorInput): Promise<MartinDoctorOutput>;
@@ -0,0 +1,56 @@
1
+ import { resolveRunsRoot } from "../vendor/core/index.js";
2
+ import { resolveSafeRepoRoot, resolveSafeRunsRootPath } from "../server-validation.js";
3
+ import { getEngineAvailability, inspectRunsRoot, resolveExecutionMode } from "./tool-support.js";
4
+ export async function martinDoctorTool(input) {
5
+ const workingDirectory = resolveSafeRepoRoot(input.workingDirectory);
6
+ const runsRoot = resolveSafeRunsRootPath(input.runsDir, resolveRunsRoot(process.env));
7
+ const executionMode = resolveExecutionMode();
8
+ const claude = getEngineAvailability("claude");
9
+ const codex = getEngineAvailability("codex");
10
+ const runStore = await inspectRunsRoot(runsRoot);
11
+ const warnings = [];
12
+ if (!runStore.exists) {
13
+ warnings.push("Configured Martin runs root does not exist yet.");
14
+ }
15
+ if (executionMode.liveMode && !claude.available && !codex.available) {
16
+ warnings.push("Neither claude nor codex is currently available on PATH for live runs.");
17
+ }
18
+ if (input.engine && executionMode.liveMode) {
19
+ const selected = input.engine === "claude" ? claude : codex;
20
+ if (!selected.available) {
21
+ warnings.push(`Requested engine '${input.engine}' is not available on PATH.`);
22
+ }
23
+ }
24
+ warnings.push(...runStore.warnings);
25
+ const status = warnings.length === 0 ? "ok" : "degraded";
26
+ return {
27
+ status,
28
+ summary: status === "ok"
29
+ ? `Doctor passed: ${runStore.loopCount} run(s) visible in ${runsRoot}.`
30
+ : `Doctor found ${warnings.length} issue(s); review warnings before live execution.`,
31
+ server: {
32
+ name: "martin-loop",
33
+ nodeVersion: process.version,
34
+ platform: process.platform
35
+ },
36
+ environment: {
37
+ workspaceRoot: resolveSafeRepoRoot(),
38
+ workingDirectory,
39
+ runsRoot,
40
+ mode: executionMode.mode,
41
+ liveMode: executionMode.liveMode
42
+ },
43
+ engines: {
44
+ claude,
45
+ codex
46
+ },
47
+ ...(input.engine ? { requestedEngine: input.engine } : {}),
48
+ runStore: {
49
+ exists: runStore.exists,
50
+ loopCount: runStore.loopCount,
51
+ ...(runStore.latestRun ? { latestRun: runStore.latestRun } : {})
52
+ },
53
+ warnings
54
+ };
55
+ }
56
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1,62 @@
1
+ import { type MartinEngine } from "./tool-support.js";
2
+ export interface MartinPreflightInput {
3
+ objective: string;
4
+ workingDirectory?: string;
5
+ engine?: MartinEngine;
6
+ model?: string;
7
+ maxUsd?: number;
8
+ maxIterations?: number;
9
+ maxTokens?: number;
10
+ verificationPlan?: string[];
11
+ allowedPaths?: string[];
12
+ deniedPaths?: string[];
13
+ workspaceId?: string;
14
+ projectId?: string;
15
+ }
16
+ export interface MartinPreflightOutput {
17
+ ok: boolean;
18
+ summary: string;
19
+ warnings: string[];
20
+ readiness: {
21
+ mode: "live" | "stub";
22
+ liveMode: boolean;
23
+ engineReady: boolean;
24
+ };
25
+ normalized: {
26
+ objective: string;
27
+ workingDirectory: string;
28
+ engine: MartinEngine;
29
+ model?: string;
30
+ budget: {
31
+ maxUsd: number;
32
+ softLimitUsd: number;
33
+ maxIterations: number;
34
+ maxTokens: number;
35
+ };
36
+ verificationPlan: string[];
37
+ allowedPaths?: string[];
38
+ deniedPaths?: string[];
39
+ workspaceId: string;
40
+ projectId: string;
41
+ };
42
+ execution: {
43
+ requestedEngine: MartinEngine;
44
+ engineAvailability: {
45
+ available: boolean;
46
+ detail: string;
47
+ resolvedPath?: string;
48
+ };
49
+ runsRoot: string;
50
+ pathScope: {
51
+ repoRoot: string;
52
+ allowedPathsCount: number;
53
+ deniedPathsCount: number;
54
+ hasScopeConflicts: boolean;
55
+ };
56
+ expectedRunLayout: {
57
+ runDirectoryPattern: string;
58
+ loopRecordPathPattern: string;
59
+ };
60
+ };
61
+ }
62
+ export declare function martinPreflightTool(input: MartinPreflightInput): Promise<MartinPreflightOutput>;
@@ -0,0 +1,76 @@
1
+ import { DEFAULT_BUDGET } from "../vendor/contracts/index.js";
2
+ import { resolveRunsRoot } from "../vendor/core/index.js";
3
+ import { resolveSafeRepoRoot } from "../server-validation.js";
4
+ import { formatUsd, getEngineAvailability, resolveExecutionMode } from "./tool-support.js";
5
+ export async function martinPreflightTool(input) {
6
+ const executionMode = resolveExecutionMode();
7
+ const workingDirectory = resolveSafeRepoRoot(input.workingDirectory);
8
+ const engine = input.engine ?? "claude";
9
+ const engineAvailability = getEngineAvailability(engine);
10
+ const warnings = [];
11
+ const allowedPaths = input.allowedPaths ?? [];
12
+ const deniedPaths = input.deniedPaths ?? [];
13
+ const overlappingScopes = allowedPaths.filter((candidate) => deniedPaths.includes(candidate));
14
+ const budget = {
15
+ ...DEFAULT_BUDGET,
16
+ ...(input.maxUsd !== undefined ? { maxUsd: input.maxUsd } : {}),
17
+ ...(input.maxIterations !== undefined ? { maxIterations: input.maxIterations } : {}),
18
+ ...(input.maxTokens !== undefined ? { maxTokens: input.maxTokens } : {})
19
+ };
20
+ if (!executionMode.liveMode) {
21
+ warnings.push("Stub mode is active; preflight only proves configuration shape, not live CLI readiness.");
22
+ }
23
+ else if (!engineAvailability.available) {
24
+ warnings.push(`Requested engine '${engine}' is not available on PATH.`);
25
+ }
26
+ if ((input.verificationPlan?.length ?? 0) === 0) {
27
+ warnings.push("No verificationPlan was provided; Martin can run, but completion confidence will be lower.");
28
+ }
29
+ if ((input.allowedPaths?.length ?? 0) === 0) {
30
+ warnings.push("No allowedPaths were provided; Martin will rely on the broader repo root scope.");
31
+ }
32
+ if (overlappingScopes.length > 0) {
33
+ warnings.push(`Some path patterns appear in both allowedPaths and deniedPaths: ${overlappingScopes.join(", ")}.`);
34
+ }
35
+ const ok = !executionMode.liveMode || engineAvailability.available;
36
+ return {
37
+ ok,
38
+ summary: ok
39
+ ? `Preflight ready for ${engine} in ${workingDirectory} with a ${formatUsd(budget.maxUsd)} budget cap.`
40
+ : `Preflight blocked: ${engine} is not available for live execution.`,
41
+ warnings,
42
+ readiness: {
43
+ mode: executionMode.mode,
44
+ liveMode: executionMode.liveMode,
45
+ engineReady: !executionMode.liveMode || engineAvailability.available
46
+ },
47
+ normalized: {
48
+ objective: input.objective,
49
+ workingDirectory,
50
+ engine,
51
+ ...(input.model ? { model: input.model } : {}),
52
+ budget,
53
+ verificationPlan: input.verificationPlan ?? [],
54
+ ...(input.allowedPaths ? { allowedPaths: input.allowedPaths } : {}),
55
+ ...(input.deniedPaths ? { deniedPaths: input.deniedPaths } : {}),
56
+ workspaceId: input.workspaceId ?? "ws_mcp",
57
+ projectId: input.projectId ?? "proj_mcp"
58
+ },
59
+ execution: {
60
+ requestedEngine: engine,
61
+ engineAvailability,
62
+ runsRoot: resolveRunsRoot(process.env),
63
+ pathScope: {
64
+ repoRoot: workingDirectory,
65
+ allowedPathsCount: allowedPaths.length,
66
+ deniedPathsCount: deniedPaths.length,
67
+ hasScopeConflicts: overlappingScopes.length > 0
68
+ },
69
+ expectedRunLayout: {
70
+ runDirectoryPattern: "<runsRoot>/<loopId>/",
71
+ loopRecordPathPattern: "<runsRoot>/<loopId>/loop-record.json"
72
+ }
73
+ }
74
+ };
75
+ }
76
+ //# sourceMappingURL=preflight.js.map
@@ -0,0 +1,37 @@
1
+ export type MartinEngine = "claude" | "codex";
2
+ export interface LoopPreview {
3
+ loopId: string;
4
+ title: string;
5
+ objective: string;
6
+ status: string;
7
+ lifecycleState: string;
8
+ createdAt?: string;
9
+ updatedAt?: string;
10
+ attempts: number;
11
+ costUsd: number;
12
+ avoidedUsd: number;
13
+ pressure: string;
14
+ shouldStop: boolean;
15
+ remainingBudgetUsd: number;
16
+ remainingIterations: number;
17
+ remainingTokens: number;
18
+ }
19
+ export interface CliAvailability {
20
+ available: boolean;
21
+ detail: string;
22
+ resolvedPath?: string;
23
+ }
24
+ export interface ExecutionMode {
25
+ liveMode: boolean;
26
+ mode: "live" | "stub";
27
+ }
28
+ export interface RunStoreInspection {
29
+ exists: boolean;
30
+ loopCount: number;
31
+ latestRun?: LoopPreview;
32
+ warnings: string[];
33
+ }
34
+ export declare function resolveExecutionMode(): ExecutionMode;
35
+ export declare function getEngineAvailability(engine: MartinEngine): CliAvailability;
36
+ export declare function formatUsd(value: number): string;
37
+ export declare function inspectRunsRoot(runsRoot?: string): Promise<RunStoreInspection>;
@@ -0,0 +1,110 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readdir, stat } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { evaluateCostGovernor, readLatestLoopRecordFromFile, resolveRunsRoot } from "../vendor/core/index.js";
5
+ export function resolveExecutionMode() {
6
+ const liveMode = process.env.MARTIN_LIVE !== "false";
7
+ return {
8
+ liveMode,
9
+ mode: liveMode ? "live" : "stub"
10
+ };
11
+ }
12
+ export function getEngineAvailability(engine) {
13
+ const locator = process.platform === "win32" ? "where.exe" : "which";
14
+ const result = spawnSync(locator, [engine], {
15
+ encoding: "utf8",
16
+ stdio: ["ignore", "pipe", "pipe"]
17
+ });
18
+ const resolvedPath = result.status === 0
19
+ ? (result.stdout ?? "")
20
+ .split(/\r?\n/u)
21
+ .map((line) => line.trim())
22
+ .find(Boolean)
23
+ : undefined;
24
+ return result.status === 0
25
+ ? {
26
+ available: true,
27
+ detail: `${engine} is available on PATH.`,
28
+ ...(resolvedPath ? { resolvedPath } : {})
29
+ }
30
+ : {
31
+ available: false,
32
+ detail: `${engine} is not available on PATH.`
33
+ };
34
+ }
35
+ export function formatUsd(value) {
36
+ return `$${value.toFixed(2)}`;
37
+ }
38
+ export async function inspectRunsRoot(runsRoot = resolveRunsRoot(process.env)) {
39
+ let exists = false;
40
+ try {
41
+ exists = (await stat(runsRoot)).isDirectory();
42
+ }
43
+ catch {
44
+ exists = false;
45
+ }
46
+ if (!exists) {
47
+ return {
48
+ exists: false,
49
+ loopCount: 0,
50
+ warnings: []
51
+ };
52
+ }
53
+ const warnings = [];
54
+ const loops = [];
55
+ const entries = await readdir(runsRoot, { withFileTypes: true });
56
+ for (const entry of entries) {
57
+ if (!entry.isDirectory()) {
58
+ continue;
59
+ }
60
+ const loopRecordPath = join(runsRoot, entry.name, "loop-record.json");
61
+ try {
62
+ const loop = await readLatestLoopRecordFromFile(loopRecordPath);
63
+ if (!loop) {
64
+ continue;
65
+ }
66
+ const costState = evaluateCostGovernor({
67
+ budget: loop.budget,
68
+ cost: {
69
+ actualUsd: loop.cost.actualUsd,
70
+ avoidedUsd: loop.cost.avoidedUsd ?? 0,
71
+ tokensIn: loop.cost.tokensIn,
72
+ tokensOut: loop.cost.tokensOut
73
+ },
74
+ attemptsUsed: loop.attempts.length
75
+ });
76
+ loops.push({
77
+ loopId: loop.loopId,
78
+ title: loop.task?.title ?? loop.loopId,
79
+ objective: loop.task?.objective ?? "Loop record summary",
80
+ status: loop.status,
81
+ lifecycleState: loop.lifecycleState,
82
+ ...(loop.createdAt ? { createdAt: loop.createdAt } : {}),
83
+ ...(loop.updatedAt ? { updatedAt: loop.updatedAt } : {}),
84
+ attempts: loop.attempts.length,
85
+ costUsd: loop.cost.actualUsd,
86
+ avoidedUsd: loop.cost.avoidedUsd ?? 0,
87
+ pressure: costState.pressure,
88
+ shouldStop: costState.shouldStop,
89
+ remainingBudgetUsd: costState.remainingBudgetUsd,
90
+ remainingIterations: costState.remainingIterations,
91
+ remainingTokens: costState.remainingTokens
92
+ });
93
+ }
94
+ catch {
95
+ warnings.push(`Skipped unreadable loop record for '${entry.name}'.`);
96
+ }
97
+ }
98
+ loops.sort((left, right) => {
99
+ const leftTime = Date.parse(left.updatedAt ?? left.createdAt ?? "");
100
+ const rightTime = Date.parse(right.updatedAt ?? right.createdAt ?? "");
101
+ return (Number.isFinite(rightTime) ? rightTime : 0) - (Number.isFinite(leftTime) ? leftTime : 0);
102
+ });
103
+ return {
104
+ exists: true,
105
+ loopCount: loops.length,
106
+ ...(loops[0] ? { latestRun: loops[0] } : {}),
107
+ warnings
108
+ };
109
+ }
110
+ //# sourceMappingURL=tool-support.js.map
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@martinloop/mcp",
3
- "version": "0.1.3",
4
- "mcpName": "io.github.keesan12/martin-loop",
3
+ "version": "0.1.4",
4
+ "mcpName": "io.github.Keesan12/martin-loop",
5
5
  "private": false,
6
6
  "type": "module",
7
- "description": "Governed MCP server for AI coding agents with budgets, verifier gates, policy checks, and audit trails.",
8
- "license": "MIT",
7
+ "description": "Governed MCP server for AI coding agents with budgets, verifier gates, and inspectable runs.",
8
+ "license": "Apache-2.0",
9
9
  "author": "Vakeesan Mahalingam and Gobi Shanthan",
10
10
  "homepage": "https://martinloop.com/",
11
11
  "repository": {
@@ -22,7 +22,9 @@
22
22
  "martin-loop",
23
23
  "ai-agent",
24
24
  "claude",
25
- "codex"
25
+ "codex",
26
+ "martin_doctor",
27
+ "martin_preflight"
26
28
  ],
27
29
  "bin": {
28
30
  "mcp": "./dist/server.js",
@@ -49,6 +51,7 @@
49
51
  "smoke:pack": "node ./scripts/smoke-package.mjs",
50
52
  "smoke:published": "node ./scripts/smoke-published-package.mjs",
51
53
  "smoke:published:pack": "node ./scripts/smoke-published-package.mjs --package-spec=pack",
54
+ "verify:release": "node --test ../../scripts/tests/publish-mcp-workflow.test.mjs ../../scripts/tests/mcp-publish-reliability.test.mjs ../../scripts/tests/mcp-release-docs.test.mjs",
52
55
  "test": "vitest run",
53
56
  "lint": "tsc -p tsconfig.json --noEmit",
54
57
  "start": "node dist/server.js"
package/server.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
- "name": "io.github.keesan12/martin-loop",
3
+ "name": "io.github.Keesan12/martin-loop",
4
4
  "title": "Martin Loop",
5
- "description": "Governed MCP server for AI coding agents with budgets, verifier gates, policy checks, and audit trails.",
5
+ "description": "Governed MCP server for AI coding agents with budgets, verifier gates, and inspectable runs.",
6
6
  "repository": {
7
7
  "url": "https://github.com/Keesan12/martin-loop",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.1.3",
10
+ "version": "0.1.4",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@martinloop/mcp",
15
- "version": "0.1.3",
15
+ "version": "0.1.4",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }