@martinloop/mcp 0.1.2 → 0.1.3

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
@@ -1,81 +1,110 @@
1
1
  # @martinloop/mcp
2
2
 
3
- Governed MCP server for AI coding agents with hard budgets, verifier gates, policy checks, and inspectable run records.
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
- Martin Loop helps MCP hosts run AI coding work inside a bounded runtime instead of an open-ended retry loop. The standalone MCP package exposes three focused tools over stdio:
5
+ `@martinloop/mcp` exposes three stdio tools:
6
6
 
7
7
  - `martin_run`
8
8
  - `martin_inspect`
9
9
  - `martin_status`
10
10
 
11
- ## What's new in 0.1.2
11
+ ## What This Server Is For
12
12
 
13
- - `martin_inspect` now reads both canonical `loop-record.json` runs and legacy `.jsonl` run-store files
14
- - `martin_status` now supports `file`, `loopId`, `runsDir`, and `latest` selectors in addition to inline `loopJson`
15
- - `martin_run` now persists loop records by default in the MCP path and preserves `allowedPaths`, `deniedPaths`, and resolved `repoRoot`
16
- - the packaged tarball now rebuilds vendored workspace dependencies before packing, so `npm` installs match current source instead of stale `dist/` output
17
- - the packaged artifact now rebuilds and vendors the Martin runtime dependencies needed by the standalone MCP server
18
- - release validation now includes both a packed-tarball smoke and a published-artifact smoke
13
+ Use this MCP when a host already knows how to delegate coding work, but you want Martin Loop to bound that work with:
14
+
15
+ - a hard budget ceiling (`maxUsd`)
16
+ - an attempt ceiling (`maxIterations`)
17
+ - a total token ceiling (`maxTokens`)
18
+ - verifier commands (`verificationPlan`)
19
+ - allowed and denied file globs
20
+ - persisted run records you can inspect afterward
21
+
22
+ It is a good fit for Claude Code, Codex-oriented hosts, and other MCP clients that want governed code-change execution instead of open-ended retry behavior.
23
+
24
+ For host-facing integration guidance, see [MCP for AI Agents](https://github.com/Keesan12/martin-loop/blob/main/docs/oss/MCP-FOR-AI-AGENTS.md).
19
25
 
20
26
  ## Quickstart
21
27
 
22
28
  Run the packaged server directly:
23
29
 
24
30
  ```sh
25
- npx @martinloop/mcp
31
+ npx -y @martinloop/mcp
26
32
  ```
27
33
 
28
34
  Add it to Claude Code:
29
35
 
30
36
  ```sh
31
37
  # macOS/Linux
32
- claude mcp add --scope user martin-loop -- npx @martinloop/mcp
38
+ claude mcp add --scope user martin-loop -- npx -y @martinloop/mcp
33
39
 
34
40
  # Windows PowerShell/cmd
35
- claude mcp add --scope user martin-loop cmd /c "npx @martinloop/mcp"
41
+ claude mcp add --scope user martin-loop cmd /c "npx -y @martinloop/mcp"
36
42
  ```
37
43
 
38
- Generic stdio configuration for non-Claude clients:
44
+ Generic stdio configuration:
39
45
 
40
46
  ```json
41
47
  {
42
48
  "type": "stdio",
43
49
  "command": "npx",
44
- "args": ["@martinloop/mcp"]
50
+ "args": ["-y", "@martinloop/mcp"]
45
51
  }
46
52
  ```
47
53
 
48
- ## What the tools do
54
+ Codex host configuration in `~/.codex/config.toml`:
49
55
 
50
- - `martin_run`: runs a governed coding loop with budget caps, verifier commands, engine selection, path scoping, and persisted loop records
51
- - `martin_inspect`: reads Martin Loop run-store data from a canonical `loop-record.json`, a legacy `.jsonl` file, or a full runs directory
52
- - `martin_status`: evaluates remaining budget pressure and stop conditions from inline JSON, a saved loop record, a loop id, or the latest persisted run
56
+ ```toml
57
+ [mcp_servers.martin-loop]
58
+ command = "npx"
59
+ args = ["-y", "@martinloop/mcp"]
60
+ ```
53
61
 
54
62
  ## Requirements
55
63
 
56
64
  - Node 20+
57
- - For live runs, either the `claude` CLI or the `codex` CLI must be on `PATH`
58
- - For dry-run and smoke-test flows, set `MARTIN_LIVE=false`
65
+ - For live `martin_run` usage, either the `claude` CLI or the `codex` CLI must be available on `PATH`
66
+ - For stub or smoke flows, set `MARTIN_LIVE=false`
59
67
 
60
- Live `martin_run` delegates to the configured CLI adapter. If no supported CLI is installed, use the stub path for testing:
68
+ Example stub launch:
61
69
 
62
70
  ```sh
63
- MARTIN_LIVE=false npx @martinloop/mcp
71
+ MARTIN_LIVE=false npx -y @martinloop/mcp
64
72
  ```
65
73
 
66
- ## Tool examples
74
+ ## Tool Contract
67
75
 
68
- ### `martin_run`
76
+ | Tool | Purpose | Required input | Important optional input | Notes |
77
+ | --- | --- | --- | --- | --- |
78
+ | `martin_run` | Run a governed coding loop | `objective` | `workingDirectory`, `engine`, `model`, `maxUsd`, `maxIterations`, `maxTokens`, `verificationPlan`, `allowedPaths`, `deniedPaths`, `workspaceId`, `projectId` | Unknown arguments are rejected. |
79
+ | `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
+ | `martin_status` | Report budget pressure and stop conditions | exactly one of `loopJson`, `file`, `loopId`, or `latest` | `runsDir` | `latest` must be `true` when used. |
81
+
82
+ ## Safe-Root Path Model
83
+
84
+ This MCP does not let tool callers point at arbitrary filesystem locations. The server resolves tool paths against safe roots chosen when the server starts.
85
+
86
+ - `workingDirectory`
87
+ Defaults to `MARTIN_MCP_WORKSPACE_ROOT` or the server process current directory. If you pass a value, it must still resolve inside that workspace root. `.` and repo-relative subpaths are the safest choices.
88
+ - `file`
89
+ For `martin_inspect` and `martin_status`, `file` resolves under the runs root, not the whole machine. Direct file targets must end in `.json` or `.jsonl`; run directories are also accepted where the tool supports them.
90
+ - `runsDir`
91
+ Defaults to `MARTIN_RUNS_DIR` or `~/.martin/runs`. Passing `runsDir` only re-states or narrows that safe runs root; it does not grant access outside it.
92
+ - `allowedPaths` and `deniedPaths`
93
+ These are relative glob patterns only. Absolute paths, drive-qualified paths, and patterns containing `..` are rejected.
94
+
95
+ Absolute paths can work only when they still resolve inside the corresponding safe root. Escapes above the workspace or runs root are rejected.
96
+
97
+ ## Tool Examples
69
98
 
70
- Example request body:
99
+ ### `martin_run`
71
100
 
72
101
  ```json
73
102
  {
74
103
  "objective": "Fix the auth regression and prove it with tests",
75
104
  "engine": "codex",
76
- "budgetUsd": 3,
77
- "softLimitUsd": 2.25,
105
+ "maxUsd": 3,
78
106
  "maxIterations": 3,
107
+ "maxTokens": 20000,
79
108
  "verificationPlan": ["pnpm test --filter auth"],
80
109
  "workingDirectory": ".",
81
110
  "allowedPaths": ["src/**", "tests/**"],
@@ -85,25 +114,25 @@ Example request body:
85
114
 
86
115
  ### `martin_inspect`
87
116
 
88
- Inspect the default run store:
117
+ Inspect the default runs root:
89
118
 
90
119
  ```json
91
120
  {}
92
121
  ```
93
122
 
94
- Inspect a legacy JSONL file directly:
123
+ Inspect a specific saved loop record under the runs root:
95
124
 
96
125
  ```json
97
126
  {
98
- "file": "C:/Users/you/.martin/runs/workspace.jsonl"
127
+ "file": "loop-123/loop-record.json"
99
128
  }
100
129
  ```
101
130
 
102
- Inspect a canonical runs directory:
131
+ Inspect a subdirectory under the configured runs root:
103
132
 
104
133
  ```json
105
134
  {
106
- "runsDir": "C:/Users/you/.martin/runs"
135
+ "runsDir": "team-a"
107
136
  }
108
137
  ```
109
138
 
@@ -121,27 +150,36 @@ Status for a specific persisted loop:
121
150
 
122
151
  ```json
123
152
  {
124
- "loopId": "loop-123",
125
- "runsDir": "C:/Users/you/.martin/runs"
153
+ "loopId": "loop-123"
154
+ }
155
+ ```
156
+
157
+ Status from inline JSON:
158
+
159
+ ```json
160
+ {
161
+ "loopJson": "{\"loopId\":\"loop-123\",\"status\":\"completed\",\"lifecycleState\":\"completed\",\"attempts\":[],\"budget\":{\"maxUsd\":5,\"softLimitUsd\":3,\"maxIterations\":2,\"maxTokens\":1000},\"cost\":{\"actualUsd\":1.25,\"avoidedUsd\":0,\"tokensIn\":20,\"tokensOut\":10}}"
126
162
  }
127
163
  ```
128
164
 
129
- ## Official MCP Registry
165
+ ## Registry Metadata
166
+
167
+ The registry manifest artifact for this package is `server.json`. In this repository, that manifest is authored at `packages/mcp/server.json`.
130
168
 
131
- This package is prepared for the official MCP Registry metadata flow:
169
+ Current metadata:
132
170
 
133
171
  - npm package: `@martinloop/mcp`
134
172
  - registry server name: `io.github.keesan12/martin-loop`
135
- - manifest file: `packages/mcp/server.json`
173
+ - manifest artifact name: `server.json`
136
174
 
137
- The official registry publish flow is separate from npm publication. After publishing the package to npm, run the publisher from `packages/mcp`:
175
+ Official MCP Registry publication is separate from npm publication. After publishing the package to npm, run the publisher from `packages/mcp`:
138
176
 
139
177
  ```sh
140
178
  mcp-publisher login github
141
179
  mcp-publisher publish
142
180
  ```
143
181
 
144
- ## Local verification
182
+ ## Verification
145
183
 
146
184
  From the repository root:
147
185
 
@@ -153,5 +191,9 @@ pnpm --filter @martinloop/mcp smoke:pack
153
191
  pnpm --filter @martinloop/mcp smoke:published
154
192
  ```
155
193
 
156
- - `smoke:pack` validates the local packed tarball before publish
157
- - `smoke:published` validates the npm-published artifact through `npm exec`
194
+ - `smoke:pack` verifies the packed tarball shape and a stdio MCP launch
195
+ - `smoke:published` verifies the npm-installed artifact through `npm install` plus live MCP tool calls
196
+
197
+ ## Version Notes
198
+
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/`.
package/dist/server.js CHANGED
@@ -17,6 +17,7 @@
17
17
  * Manual start:
18
18
  * node dist/server.js
19
19
  */
20
+ import { createRequire } from "node:module";
20
21
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
22
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
23
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -24,7 +25,9 @@ import { getStatusTool } from "./tools/get-status.js";
24
25
  import { inspectLoopTool } from "./tools/inspect-loop.js";
25
26
  import { runLoopTool } from "./tools/run-loop.js";
26
27
  import { sanitizeToolErrorMessage, validateToolInput } from "./server-validation.js";
27
- const server = new Server({ name: "martin-loop", version: "0.1.2" }, { capabilities: { tools: {} } });
28
+ const require = createRequire(import.meta.url);
29
+ const packageJson = require("../package.json");
30
+ const server = new Server({ name: "martin-loop", version: packageJson.version }, { capabilities: { tools: {} } });
28
31
  // ---------------------------------------------------------------------------
29
32
  // Tool manifest
30
33
  // ---------------------------------------------------------------------------
@@ -35,6 +38,7 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
35
38
  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.",
36
39
  inputSchema: {
37
40
  type: "object",
41
+ additionalProperties: false,
38
42
  properties: {
39
43
  objective: {
40
44
  type: "string",
@@ -42,7 +46,7 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
42
46
  },
43
47
  workingDirectory: {
44
48
  type: "string",
45
- description: "Absolute path to the project root. Defaults to the current working directory."
49
+ description: "Optional repo-root override resolved under the MCP workspace root (or current working directory). Must stay within that safe root."
46
50
  },
47
51
  engine: {
48
52
  type: "string",
@@ -55,14 +59,17 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
55
59
  },
56
60
  maxUsd: {
57
61
  type: "number",
62
+ exclusiveMinimum: 0,
58
63
  description: "Hard budget ceiling in USD. Defaults to 25."
59
64
  },
60
65
  maxIterations: {
61
- type: "number",
66
+ type: "integer",
67
+ exclusiveMinimum: 0,
62
68
  description: "Maximum number of loop attempts. Defaults to 8."
63
69
  },
64
70
  maxTokens: {
65
- type: "number",
71
+ type: "integer",
72
+ exclusiveMinimum: 0,
66
73
  description: "Maximum total tokens across all attempts. Defaults to 80000."
67
74
  },
68
75
  verificationPlan: {
@@ -73,12 +80,12 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
73
80
  allowedPaths: {
74
81
  type: "array",
75
82
  items: { type: "string" },
76
- description: "Relative path globs Martin may modify, such as ['src/**', 'tests/**']."
83
+ description: "Repo-relative path globs Martin may modify, such as ['src/**', 'tests/**']. Absolute paths and '..' traversal are rejected."
77
84
  },
78
85
  deniedPaths: {
79
86
  type: "array",
80
87
  items: { type: "string" },
81
- description: "Relative path globs Martin must never modify, such as ['.env', 'docs/security/**']."
88
+ description: "Repo-relative path globs Martin must never modify, such as ['.env', 'docs/security/**']. Absolute paths and '..' traversal are rejected."
82
89
  },
83
90
  workspaceId: {
84
91
  type: "string",
@@ -97,14 +104,15 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
97
104
  description: "Summarise Martin Loop run records from a saved loop file or run-store directory. Supports canonical loop-record.json files, legacy JSONL files, and full runs directories.",
98
105
  inputSchema: {
99
106
  type: "object",
107
+ additionalProperties: false,
100
108
  properties: {
101
109
  file: {
102
110
  type: "string",
103
- description: "Optional path under the Martin runs root to a loop-record.json file, a legacy .jsonl file, or a run-store directory."
111
+ description: "Optional path resolved under the Martin runs root to a loop-record.json file, a legacy .jsonl file, or a run-store directory."
104
112
  },
105
113
  runsDir: {
106
114
  type: "string",
107
- description: "Optional Martin runs directory. Defaults to MARTIN_RUNS_DIR or ~/.martin/runs."
115
+ description: "Optional runs-root override resolved under the default Martin runs root. Defaults to MARTIN_RUNS_DIR or ~/.martin/runs."
108
116
  }
109
117
  }
110
118
  }
@@ -114,6 +122,7 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
114
122
  description: "Return the current budget and cost state of a Martin loop record. Accepts inline JSON, a saved loop file, a loopId under the run store, or the latest run in the store.",
115
123
  inputSchema: {
116
124
  type: "object",
125
+ additionalProperties: false,
117
126
  properties: {
118
127
  loopJson: {
119
128
  type: "string",
@@ -121,7 +130,7 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
121
130
  },
122
131
  file: {
123
132
  type: "string",
124
- description: "Optional path under the Martin runs root to a loop-record.json file, a legacy .jsonl file, or a run-store directory."
133
+ description: "Optional path resolved under the Martin runs root to a loop-record.json file, a legacy .jsonl file, or a run-store directory."
125
134
  },
126
135
  loopId: {
127
136
  type: "string",
@@ -129,13 +138,19 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
129
138
  },
130
139
  runsDir: {
131
140
  type: "string",
132
- description: "Optional Martin runs directory. Defaults to MARTIN_RUNS_DIR or ~/.martin/runs."
141
+ description: "Optional runs-root override resolved under the default Martin runs root. Defaults to MARTIN_RUNS_DIR or ~/.martin/runs."
133
142
  },
134
143
  latest: {
135
- type: "boolean",
144
+ const: true,
136
145
  description: "When true, loads the most recently updated loop record in the runs directory."
137
146
  }
138
- }
147
+ },
148
+ oneOf: [
149
+ { required: ["loopJson"] },
150
+ { required: ["file"] },
151
+ { required: ["loopId"] },
152
+ { required: ["latest"] }
153
+ ]
139
154
  }
140
155
  }
141
156
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@martinloop/mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "mcpName": "io.github.keesan12/martin-loop",
5
5
  "private": false,
6
6
  "type": "module",
@@ -24,22 +24,18 @@
24
24
  "claude",
25
25
  "codex"
26
26
  ],
27
- "main": "./dist/server.js",
28
- "types": "./dist/server.d.ts",
29
27
  "bin": {
30
28
  "mcp": "./dist/server.js",
31
29
  "martin-loop-mcp": "./dist/server.js"
32
30
  },
33
31
  "exports": {
34
- ".": {
35
- "types": "./dist/server.d.ts",
36
- "default": "./dist/server.js"
37
- },
32
+ "./server.json": "./server.json",
38
33
  "./package.json": "./package.json"
39
34
  },
40
35
  "files": [
41
36
  "dist",
42
- "README.md"
37
+ "README.md",
38
+ "server.json"
43
39
  ],
44
40
  "engines": {
45
41
  "node": ">=20.0.0"
@@ -52,6 +48,7 @@
52
48
  "prepack": "pnpm build",
53
49
  "smoke:pack": "node ./scripts/smoke-package.mjs",
54
50
  "smoke:published": "node ./scripts/smoke-published-package.mjs",
51
+ "smoke:published:pack": "node ./scripts/smoke-published-package.mjs --package-spec=pack",
55
52
  "test": "vitest run",
56
53
  "lint": "tsc -p tsconfig.json --noEmit",
57
54
  "start": "node dist/server.js"
package/server.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.keesan12/martin-loop",
4
+ "title": "Martin Loop",
5
+ "description": "Governed MCP server for AI coding agents with budgets, verifier gates, policy checks, and audit trails.",
6
+ "repository": {
7
+ "url": "https://github.com/Keesan12/martin-loop",
8
+ "source": "github"
9
+ },
10
+ "version": "0.1.3",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "@martinloop/mcp",
15
+ "version": "0.1.3",
16
+ "transport": {
17
+ "type": "stdio"
18
+ }
19
+ }
20
+ ]
21
+ }