@martinloop/mcp 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -5
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.js +84 -0
- package/dist/resources.d.ts +7 -0
- package/dist/resources.js +89 -0
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +121 -1
- package/dist/server.d.ts +6 -4
- package/dist/server.js +271 -6
- package/dist/tools/cockpit-support.d.ts +69 -0
- package/dist/tools/cockpit-support.js +108 -0
- package/dist/tools/doctor.d.ts +35 -0
- package/dist/tools/doctor.js +56 -0
- package/dist/tools/get-attempt.d.ts +8 -0
- package/dist/tools/get-attempt.js +6 -0
- package/dist/tools/get-run.d.ts +17 -0
- package/dist/tools/get-run.js +14 -0
- package/dist/tools/get-verification-results.d.ts +11 -0
- package/dist/tools/get-verification-results.js +9 -0
- package/dist/tools/list-runs.d.ts +9 -0
- package/dist/tools/list-runs.js +7 -0
- package/dist/tools/preflight.d.ts +62 -0
- package/dist/tools/preflight.js +76 -0
- package/dist/tools/run-dossier.d.ts +8 -0
- package/dist/tools/run-dossier.js +6 -0
- package/dist/tools/tool-support.d.ts +37 -0
- package/dist/tools/tool-support.js +110 -0
- package/package.json +10 -5
- package/server.json +4 -4
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RunSummary } from "./cockpit-support.js";
|
|
2
|
+
export interface ListRunsInput {
|
|
3
|
+
runsDir?: string;
|
|
4
|
+
limit?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ListRunsOutput {
|
|
7
|
+
runs: RunSummary[];
|
|
8
|
+
}
|
|
9
|
+
export declare function listRunsTool(input: ListRunsInput): Promise<ListRunsOutput>;
|
|
@@ -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,8 @@
|
|
|
1
|
+
import { buildRunDossier } from "./cockpit-support.js";
|
|
2
|
+
export interface RunDossierInput {
|
|
3
|
+
loopId?: string;
|
|
4
|
+
runsDir?: string;
|
|
5
|
+
latest?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export type RunDossierOutput = ReturnType<typeof buildRunDossier>;
|
|
8
|
+
export declare function runDossierTool(input: RunDossierInput): Promise<RunDossierOutput>;
|
|
@@ -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.
|
|
4
|
-
"mcpName": "io.github.
|
|
3
|
+
"version": "0.2.0",
|
|
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,7 +22,11 @@
|
|
|
22
22
|
"martin-loop",
|
|
23
23
|
"ai-agent",
|
|
24
24
|
"claude",
|
|
25
|
-
"codex"
|
|
25
|
+
"codex",
|
|
26
|
+
"martin_doctor",
|
|
27
|
+
"martin_preflight",
|
|
28
|
+
"martin_list_runs",
|
|
29
|
+
"martin_run_dossier"
|
|
26
30
|
],
|
|
27
31
|
"bin": {
|
|
28
32
|
"mcp": "./dist/server.js",
|
|
@@ -49,6 +53,7 @@
|
|
|
49
53
|
"smoke:pack": "node ./scripts/smoke-package.mjs",
|
|
50
54
|
"smoke:published": "node ./scripts/smoke-published-package.mjs",
|
|
51
55
|
"smoke:published:pack": "node ./scripts/smoke-published-package.mjs --package-spec=pack",
|
|
56
|
+
"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
57
|
"test": "vitest run",
|
|
53
58
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
54
59
|
"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.
|
|
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,
|
|
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.
|
|
10
|
+
"version": "0.2.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "@martinloop/mcp",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.2.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|