@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.
@@ -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,7 @@
1
+ import { listRunSummaries } from "./cockpit-support.js";
2
+ export async function listRunsTool(input) {
3
+ return {
4
+ runs: await listRunSummaries(input)
5
+ };
6
+ }
7
+ //# sourceMappingURL=list-runs.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,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,6 @@
1
+ import { buildRunDossier, loadSelectedRun } from "./cockpit-support.js";
2
+ export async function runDossierTool(input) {
3
+ const loop = await loadSelectedRun(input);
4
+ return buildRunDossier(loop);
5
+ }
6
+ //# sourceMappingURL=run-dossier.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.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, 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,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.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.2.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@martinloop/mcp",
15
- "version": "0.1.3",
15
+ "version": "0.2.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }