@martinloop/mcp 0.1.2 → 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 +105 -42
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +52 -0
- package/dist/server.d.ts +6 -4
- package/dist/server.js +134 -16
- package/dist/tools/doctor.d.ts +35 -0
- package/dist/tools/doctor.js +56 -0
- package/dist/tools/preflight.d.ts +62 -0
- package/dist/tools/preflight.js +76 -0
- package/dist/tools/tool-support.d.ts +37 -0
- package/dist/tools/tool-support.js +110 -0
- package/package.json +12 -12
- package/server.json +21 -0
package/README.md
CHANGED
|
@@ -1,81 +1,127 @@
|
|
|
1
1
|
# @martinloop/mcp
|
|
2
2
|
|
|
3
|
-
Governed MCP server for AI coding agents
|
|
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
|
-
|
|
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
|
|
|
11
|
-
|
|
13
|
+
Recommended flow:
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
1. `martin_doctor`
|
|
16
|
+
2. `martin_preflight`
|
|
17
|
+
3. `martin_run`
|
|
18
|
+
4. `martin_inspect` or `martin_status`
|
|
19
|
+
|
|
20
|
+
## What This Server Is For
|
|
21
|
+
|
|
22
|
+
Use this MCP when a host already knows how to delegate coding work, but you want Martin Loop to bound that work with:
|
|
23
|
+
|
|
24
|
+
- a hard budget ceiling (`maxUsd`)
|
|
25
|
+
- an attempt ceiling (`maxIterations`)
|
|
26
|
+
- a total token ceiling (`maxTokens`)
|
|
27
|
+
- verifier commands (`verificationPlan`)
|
|
28
|
+
- allowed and denied file globs
|
|
29
|
+
- persisted run records you can inspect afterward
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
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
34
|
|
|
20
35
|
## Quickstart
|
|
21
36
|
|
|
22
37
|
Run the packaged server directly:
|
|
23
38
|
|
|
24
39
|
```sh
|
|
25
|
-
npx @martinloop/mcp
|
|
40
|
+
npx -y @martinloop/mcp
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Add it to Codex:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
codex mcp add martin-loop -- npx -y @martinloop/mcp
|
|
26
47
|
```
|
|
27
48
|
|
|
28
49
|
Add it to Claude Code:
|
|
29
50
|
|
|
30
51
|
```sh
|
|
31
52
|
# macOS/Linux
|
|
32
|
-
claude mcp add --scope user martin-loop -- npx @martinloop/mcp
|
|
53
|
+
claude mcp add --transport stdio --scope user martin-loop -- npx -y @martinloop/mcp
|
|
33
54
|
|
|
34
55
|
# Windows PowerShell/cmd
|
|
35
|
-
claude mcp add --scope user martin-loop cmd /c
|
|
56
|
+
claude mcp add --transport stdio --scope user martin-loop -- cmd /c npx -y @martinloop/mcp
|
|
36
57
|
```
|
|
37
58
|
|
|
38
|
-
Generic stdio configuration
|
|
59
|
+
Generic stdio configuration:
|
|
39
60
|
|
|
40
61
|
```json
|
|
41
62
|
{
|
|
42
63
|
"type": "stdio",
|
|
43
64
|
"command": "npx",
|
|
44
|
-
"args": ["@martinloop/mcp"]
|
|
65
|
+
"args": ["-y", "@martinloop/mcp"]
|
|
45
66
|
}
|
|
46
67
|
```
|
|
47
68
|
|
|
48
|
-
|
|
69
|
+
Codex host configuration in `~/.codex/config.toml`:
|
|
49
70
|
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
71
|
+
```toml
|
|
72
|
+
[mcp_servers.martin-loop]
|
|
73
|
+
command = "npx"
|
|
74
|
+
args = ["-y", "@martinloop/mcp"]
|
|
75
|
+
```
|
|
53
76
|
|
|
54
77
|
## Requirements
|
|
55
78
|
|
|
56
79
|
- Node 20+
|
|
57
|
-
- For live
|
|
58
|
-
- For
|
|
80
|
+
- For live `martin_run` usage, either the `claude` CLI or the `codex` CLI must be available on `PATH`
|
|
81
|
+
- For stub or smoke flows, set `MARTIN_LIVE=false`
|
|
59
82
|
|
|
60
|
-
|
|
83
|
+
Example stub launch:
|
|
61
84
|
|
|
62
85
|
```sh
|
|
63
|
-
MARTIN_LIVE=false npx @martinloop/mcp
|
|
86
|
+
MARTIN_LIVE=false npx -y @martinloop/mcp
|
|
64
87
|
```
|
|
65
88
|
|
|
66
|
-
## Tool
|
|
89
|
+
## Tool Contract
|
|
67
90
|
|
|
68
|
-
|
|
91
|
+
| Tool | Purpose | Required input | Important optional input | Notes |
|
|
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. |
|
|
95
|
+
| `martin_run` | Run a governed coding loop | `objective` | `workingDirectory`, `engine`, `model`, `maxUsd`, `maxIterations`, `maxTokens`, `verificationPlan`, `allowedPaths`, `deniedPaths`, `workspaceId`, `projectId` | Unknown arguments are rejected. |
|
|
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. |
|
|
97
|
+
| `martin_status` | Report budget pressure and stop conditions | exactly one of `loopJson`, `file`, `loopId`, or `latest` | `runsDir` | `latest` must be `true` when used. |
|
|
98
|
+
|
|
99
|
+
## Safe-Root Path Model
|
|
100
|
+
|
|
101
|
+
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.
|
|
69
102
|
|
|
70
|
-
|
|
103
|
+
- `workingDirectory`
|
|
104
|
+
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.
|
|
105
|
+
- `file`
|
|
106
|
+
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.
|
|
107
|
+
- `runsDir`
|
|
108
|
+
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.
|
|
109
|
+
- `allowedPaths` and `deniedPaths`
|
|
110
|
+
These are relative glob patterns only. Absolute paths, drive-qualified paths, and patterns containing `..` are rejected.
|
|
111
|
+
|
|
112
|
+
Absolute paths can work only when they still resolve inside the corresponding safe root. Escapes above the workspace or runs root are rejected.
|
|
113
|
+
|
|
114
|
+
## Tool Examples
|
|
115
|
+
|
|
116
|
+
### `martin_run`
|
|
71
117
|
|
|
72
118
|
```json
|
|
73
119
|
{
|
|
74
120
|
"objective": "Fix the auth regression and prove it with tests",
|
|
75
121
|
"engine": "codex",
|
|
76
|
-
"
|
|
77
|
-
"softLimitUsd": 2.25,
|
|
122
|
+
"maxUsd": 3,
|
|
78
123
|
"maxIterations": 3,
|
|
124
|
+
"maxTokens": 20000,
|
|
79
125
|
"verificationPlan": ["pnpm test --filter auth"],
|
|
80
126
|
"workingDirectory": ".",
|
|
81
127
|
"allowedPaths": ["src/**", "tests/**"],
|
|
@@ -85,25 +131,25 @@ Example request body:
|
|
|
85
131
|
|
|
86
132
|
### `martin_inspect`
|
|
87
133
|
|
|
88
|
-
Inspect the default
|
|
134
|
+
Inspect the default runs root:
|
|
89
135
|
|
|
90
136
|
```json
|
|
91
137
|
{}
|
|
92
138
|
```
|
|
93
139
|
|
|
94
|
-
Inspect a
|
|
140
|
+
Inspect a specific saved loop record under the runs root:
|
|
95
141
|
|
|
96
142
|
```json
|
|
97
143
|
{
|
|
98
|
-
"file": "
|
|
144
|
+
"file": "loop-123/loop-record.json"
|
|
99
145
|
}
|
|
100
146
|
```
|
|
101
147
|
|
|
102
|
-
Inspect a
|
|
148
|
+
Inspect a subdirectory under the configured runs root:
|
|
103
149
|
|
|
104
150
|
```json
|
|
105
151
|
{
|
|
106
|
-
"runsDir": "
|
|
152
|
+
"runsDir": "team-a"
|
|
107
153
|
}
|
|
108
154
|
```
|
|
109
155
|
|
|
@@ -121,27 +167,36 @@ Status for a specific persisted loop:
|
|
|
121
167
|
|
|
122
168
|
```json
|
|
123
169
|
{
|
|
124
|
-
"loopId": "loop-123"
|
|
125
|
-
"runsDir": "C:/Users/you/.martin/runs"
|
|
170
|
+
"loopId": "loop-123"
|
|
126
171
|
}
|
|
127
172
|
```
|
|
128
173
|
|
|
129
|
-
|
|
174
|
+
Status from inline JSON:
|
|
130
175
|
|
|
131
|
-
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"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}}"
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Registry Metadata
|
|
183
|
+
|
|
184
|
+
The registry manifest artifact for this package is `server.json`. In this repository, that manifest is authored at `packages/mcp/server.json`.
|
|
185
|
+
|
|
186
|
+
Current metadata:
|
|
132
187
|
|
|
133
188
|
- npm package: `@martinloop/mcp`
|
|
134
|
-
- registry server name: `io.github.
|
|
135
|
-
- manifest
|
|
189
|
+
- registry server name: `io.github.Keesan12/martin-loop`
|
|
190
|
+
- manifest artifact name: `server.json`
|
|
136
191
|
|
|
137
|
-
|
|
192
|
+
Official MCP Registry publication is separate from npm publication. After publishing the package to npm, run the publisher from `packages/mcp`:
|
|
138
193
|
|
|
139
194
|
```sh
|
|
140
195
|
mcp-publisher login github
|
|
141
196
|
mcp-publisher publish
|
|
142
197
|
```
|
|
143
198
|
|
|
144
|
-
##
|
|
199
|
+
## Verification
|
|
145
200
|
|
|
146
201
|
From the repository root:
|
|
147
202
|
|
|
@@ -150,8 +205,16 @@ pnpm --filter @martinloop/mcp lint
|
|
|
150
205
|
pnpm --filter @martinloop/mcp test
|
|
151
206
|
pnpm --filter @martinloop/mcp build
|
|
152
207
|
pnpm --filter @martinloop/mcp smoke:pack
|
|
208
|
+
pnpm --filter @martinloop/mcp smoke:published:pack
|
|
209
|
+
pnpm --filter @martinloop/mcp verify:release
|
|
153
210
|
pnpm --filter @martinloop/mcp smoke:published
|
|
154
211
|
```
|
|
155
212
|
|
|
156
|
-
- `smoke:pack`
|
|
157
|
-
- `smoke:published`
|
|
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
|
|
216
|
+
- `smoke:published` verifies the npm-installed artifact through `npm install` plus live MCP tool calls
|
|
217
|
+
|
|
218
|
+
## Version Notes
|
|
219
|
+
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Exposes five tools over the Model Context Protocol (stdio transport):
|
|
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
|
package/dist/server.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Martin Loop MCP Server
|
|
4
4
|
*
|
|
5
|
-
* Exposes
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Exposes five tools over the Model Context Protocol (stdio transport):
|
|
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
|
|
@@ -17,24 +19,119 @@
|
|
|
17
19
|
* Manual start:
|
|
18
20
|
* node dist/server.js
|
|
19
21
|
*/
|
|
22
|
+
import { createRequire } from "node:module";
|
|
20
23
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
24
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
25
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
26
|
+
import { martinDoctorTool } from "./tools/doctor.js";
|
|
23
27
|
import { getStatusTool } from "./tools/get-status.js";
|
|
24
28
|
import { inspectLoopTool } from "./tools/inspect-loop.js";
|
|
29
|
+
import { martinPreflightTool } from "./tools/preflight.js";
|
|
25
30
|
import { runLoopTool } from "./tools/run-loop.js";
|
|
26
31
|
import { sanitizeToolErrorMessage, validateToolInput } from "./server-validation.js";
|
|
27
|
-
const
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
const packageJson = require("../package.json");
|
|
34
|
+
const server = new Server({ name: "martin-loop", version: packageJson.version }, { capabilities: { tools: {} } });
|
|
28
35
|
// ---------------------------------------------------------------------------
|
|
29
36
|
// Tool manifest
|
|
30
37
|
// ---------------------------------------------------------------------------
|
|
31
38
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
32
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
|
+
},
|
|
33
129
|
{
|
|
34
130
|
name: "martin_run",
|
|
35
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.",
|
|
36
132
|
inputSchema: {
|
|
37
133
|
type: "object",
|
|
134
|
+
additionalProperties: false,
|
|
38
135
|
properties: {
|
|
39
136
|
objective: {
|
|
40
137
|
type: "string",
|
|
@@ -42,7 +139,7 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
|
42
139
|
},
|
|
43
140
|
workingDirectory: {
|
|
44
141
|
type: "string",
|
|
45
|
-
description: "
|
|
142
|
+
description: "Optional repo-root override resolved under the MCP workspace root (or current working directory). Must stay within that safe root."
|
|
46
143
|
},
|
|
47
144
|
engine: {
|
|
48
145
|
type: "string",
|
|
@@ -55,14 +152,17 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
|
55
152
|
},
|
|
56
153
|
maxUsd: {
|
|
57
154
|
type: "number",
|
|
155
|
+
exclusiveMinimum: 0,
|
|
58
156
|
description: "Hard budget ceiling in USD. Defaults to 25."
|
|
59
157
|
},
|
|
60
158
|
maxIterations: {
|
|
61
|
-
type: "
|
|
159
|
+
type: "integer",
|
|
160
|
+
exclusiveMinimum: 0,
|
|
62
161
|
description: "Maximum number of loop attempts. Defaults to 8."
|
|
63
162
|
},
|
|
64
163
|
maxTokens: {
|
|
65
|
-
type: "
|
|
164
|
+
type: "integer",
|
|
165
|
+
exclusiveMinimum: 0,
|
|
66
166
|
description: "Maximum total tokens across all attempts. Defaults to 80000."
|
|
67
167
|
},
|
|
68
168
|
verificationPlan: {
|
|
@@ -73,12 +173,12 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
|
73
173
|
allowedPaths: {
|
|
74
174
|
type: "array",
|
|
75
175
|
items: { type: "string" },
|
|
76
|
-
description: "
|
|
176
|
+
description: "Repo-relative path globs Martin may modify, such as ['src/**', 'tests/**']. Absolute paths and '..' traversal are rejected."
|
|
77
177
|
},
|
|
78
178
|
deniedPaths: {
|
|
79
179
|
type: "array",
|
|
80
180
|
items: { type: "string" },
|
|
81
|
-
description: "
|
|
181
|
+
description: "Repo-relative path globs Martin must never modify, such as ['.env', 'docs/security/**']. Absolute paths and '..' traversal are rejected."
|
|
82
182
|
},
|
|
83
183
|
workspaceId: {
|
|
84
184
|
type: "string",
|
|
@@ -97,14 +197,15 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
|
97
197
|
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
198
|
inputSchema: {
|
|
99
199
|
type: "object",
|
|
200
|
+
additionalProperties: false,
|
|
100
201
|
properties: {
|
|
101
202
|
file: {
|
|
102
203
|
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."
|
|
204
|
+
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
205
|
},
|
|
105
206
|
runsDir: {
|
|
106
207
|
type: "string",
|
|
107
|
-
description: "Optional Martin runs
|
|
208
|
+
description: "Optional runs-root override resolved under the default Martin runs root. Defaults to MARTIN_RUNS_DIR or ~/.martin/runs."
|
|
108
209
|
}
|
|
109
210
|
}
|
|
110
211
|
}
|
|
@@ -114,6 +215,7 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
|
114
215
|
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
216
|
inputSchema: {
|
|
116
217
|
type: "object",
|
|
218
|
+
additionalProperties: false,
|
|
117
219
|
properties: {
|
|
118
220
|
loopJson: {
|
|
119
221
|
type: "string",
|
|
@@ -121,7 +223,7 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
|
121
223
|
},
|
|
122
224
|
file: {
|
|
123
225
|
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."
|
|
226
|
+
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
227
|
},
|
|
126
228
|
loopId: {
|
|
127
229
|
type: "string",
|
|
@@ -129,13 +231,19 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
|
129
231
|
},
|
|
130
232
|
runsDir: {
|
|
131
233
|
type: "string",
|
|
132
|
-
description: "Optional Martin runs
|
|
234
|
+
description: "Optional runs-root override resolved under the default Martin runs root. Defaults to MARTIN_RUNS_DIR or ~/.martin/runs."
|
|
133
235
|
},
|
|
134
236
|
latest: {
|
|
135
|
-
|
|
237
|
+
const: true,
|
|
136
238
|
description: "When true, loads the most recently updated loop record in the runs directory."
|
|
137
239
|
}
|
|
138
|
-
}
|
|
240
|
+
},
|
|
241
|
+
oneOf: [
|
|
242
|
+
{ required: ["loopJson"] },
|
|
243
|
+
{ required: ["file"] },
|
|
244
|
+
{ required: ["loopId"] },
|
|
245
|
+
{ required: ["latest"] }
|
|
246
|
+
]
|
|
139
247
|
}
|
|
140
248
|
}
|
|
141
249
|
]
|
|
@@ -146,6 +254,16 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
|
146
254
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
147
255
|
const { name, arguments: args } = request.params;
|
|
148
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
|
+
}
|
|
149
267
|
if (name === "martin_run") {
|
|
150
268
|
const input = validateToolInput("martin_run", args);
|
|
151
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.
|
|
4
|
-
"mcpName": "io.github.
|
|
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,
|
|
8
|
-
"license": "
|
|
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,24 +22,22 @@
|
|
|
22
22
|
"martin-loop",
|
|
23
23
|
"ai-agent",
|
|
24
24
|
"claude",
|
|
25
|
-
"codex"
|
|
25
|
+
"codex",
|
|
26
|
+
"martin_doctor",
|
|
27
|
+
"martin_preflight"
|
|
26
28
|
],
|
|
27
|
-
"main": "./dist/server.js",
|
|
28
|
-
"types": "./dist/server.d.ts",
|
|
29
29
|
"bin": {
|
|
30
30
|
"mcp": "./dist/server.js",
|
|
31
31
|
"martin-loop-mcp": "./dist/server.js"
|
|
32
32
|
},
|
|
33
33
|
"exports": {
|
|
34
|
-
".":
|
|
35
|
-
"types": "./dist/server.d.ts",
|
|
36
|
-
"default": "./dist/server.js"
|
|
37
|
-
},
|
|
34
|
+
"./server.json": "./server.json",
|
|
38
35
|
"./package.json": "./package.json"
|
|
39
36
|
},
|
|
40
37
|
"files": [
|
|
41
38
|
"dist",
|
|
42
|
-
"README.md"
|
|
39
|
+
"README.md",
|
|
40
|
+
"server.json"
|
|
43
41
|
],
|
|
44
42
|
"engines": {
|
|
45
43
|
"node": ">=20.0.0"
|
|
@@ -52,6 +50,8 @@
|
|
|
52
50
|
"prepack": "pnpm build",
|
|
53
51
|
"smoke:pack": "node ./scripts/smoke-package.mjs",
|
|
54
52
|
"smoke:published": "node ./scripts/smoke-published-package.mjs",
|
|
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",
|
|
55
55
|
"test": "vitest run",
|
|
56
56
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
57
57
|
"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, and inspectable runs.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/Keesan12/martin-loop",
|
|
8
|
+
"source": "github"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.1.4",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "@martinloop/mcp",
|
|
15
|
+
"version": "0.1.4",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|