Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/gittensory-miner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,9 @@ It exposes these read-only tools:

- `gittensory_miner_get_governor_decisions` (#5159) — read-only projection of the governor decision log (`id`, `ts`, `eventType`, `repoFullName`, `actionClass`, `decision`, `reason`), optionally filtered by `repoFullName`. The projection **excludes the sensitive `payload_json` column by construction** — `governor-ledger.js` reads it with an explicit named-column SELECT, never `SELECT *`.

Further AMS-state-reading tools (status/doctor diagnostics) land as follow-up PRs on top of this server.
- `gittensory_miner_status` (#5154) — read-only status + doctor diagnostics, returning `{ status, doctor }`: `status` = package/engine versions (and skew), node version, state-dir + config-file paths, and the resolved coding-agent driver (provider name, the model **env-var NAME** never its value, CLI-present boolean); `doctor` = the checks `gittensory-miner doctor` runs (Docker/CLI presence, config validity, …) as `{ name, ok, detail }`. Reuses `collectStatus` / `runDoctorChecks` so it can't drift from the CLI, and returns only names / booleans / paths — never any env-var value, token, or credential.

This completes the read-only AMS MCP tool surface (status, portfolio, claims, event-ledger, governor-ledger, run-state, plan-store).

## Version check

Expand Down
7 changes: 6 additions & 1 deletion packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,17 @@ export interface MinerMcpServerOptions {
readGovernorDecisions(filter?: { repoFullName?: string | null }): unknown[];
close(): void;
};
/** Override the status reader (defaults to status.js's collectStatus); injection seam for tests. */
collectStatus?: () => unknown;
/** Override the doctor-checks reader (defaults to status.js's runDoctorChecks); injection seam for tests. */
runDoctorChecks?: () => unknown[];
}

/**
* Build the miner MCP server with its tools registered (gittensory_miner_ping,
* gittensory_miner_get_portfolio_dashboard, gittensory_miner_list_claims, gittensory_miner_get_audit_feed,
* gittensory_miner_get_run_state, gittensory_miner_list_plans, gittensory_miner_get_plan,
* gittensory_miner_get_governor_decisions). `options` supplies test injection seams; production callers pass nothing.
* gittensory_miner_get_governor_decisions, gittensory_miner_status). `options` supplies test injection seams;
* production callers pass nothing.
*/
export function createMinerMcpServer(options?: MinerMcpServerOptions): McpServer;
27 changes: 24 additions & 3 deletions packages/gittensory-miner/bin/gittensory-miner-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { initPortfolioQueueStore } from "../lib/portfolio-queue.js";
import { initRunStateStore } from "../lib/run-state.js";
import { PLAN_STATUSES, openPlanStore } from "../lib/plan-store.js";
import { initGovernorLedger } from "../lib/governor-ledger.js";
import { collectStatus, runDoctorChecks } from "../lib/status.js";

// MCP stdio server for @jsonbored/gittensory-miner (scaffold #5153). Mirrors the packages/gittensory-mcp
// harness (MCP SDK server + stdio transport). Tools:
Expand All @@ -31,7 +32,8 @@ import { initGovernorLedger } from "../lib/governor-ledger.js";
// plan store via plan-store.js's listPlans/loadPlan (distinct from ORB's stateless gittensory_plan_status).
// - gittensory_miner_get_governor_decisions (#5159): read-only governor decision-log projection via
// governor-ledger.js's readGovernorDecisions -- an explicit named-column read that excludes payload_json.
// Remaining AMS-state-reading tools (status/doctor, etc.) land as follow-ups.
// - gittensory_miner_status (#5154): read-only status + doctor diagnostics via status.js's collectStatus/
// runDoctorChecks (names/booleans/paths only -- never any env-var value, token, key, or credential).

// Read the version from this package's own package.json (always shipped) rather than a hand-synced
// literal, so a release bump never has a second place to forget -- same approach as the mcp harness.
Expand All @@ -50,8 +52,9 @@ export const MINER_PING_STATUS = { status: "ok", tool: "gittensory_miner_ping" }
/**
* Build the miner MCP server with its tools registered. `options.initPortfolioQueue`, `options.openClaimLedger`,
* `options.initEventLedger`, `options.initRunStateStore`, `options.openPlanStore`, `options.initGovernorLedger`,
* and `options.nowMs` are injection seams for tests (default to the real stores and the wall clock); the ping
* tool needs none. Each store-backed tool opens its store only when invoked and closes any store it opened.
* `options.collectStatus`, `options.runDoctorChecks`, and `options.nowMs` are injection seams for tests (default
* to the real stores/readers and the wall clock); the ping tool needs none. Each store-backed tool opens its
* store only when invoked and closes any store it opened.
*/
export function createMinerMcpServer(options = {}) {
const server = new McpServer({ name: "gittensory-miner", version: ownPackageJson.version });
Expand Down Expand Up @@ -242,6 +245,24 @@ export function createMinerMcpServer(options = {}) {
}
},
);
server.registerTool(
"gittensory_miner_status",
{
description:
"Read-only miner status + doctor diagnostics. Returns { status, doctor }: status = package/engine versions " +
"(+ skew), node version, state-dir path, config-file path, and the resolved coding-agent driver (provider " +
"name, the model ENV-VAR NAME -- never its value -- and a CLI-present boolean); doctor = the same checks " +
"`gittensory-miner doctor` runs (Docker/CLI presence, config validity, ...) as { name, ok, detail }. Reuses " +
"collectStatus/runDoctorChecks so it can never drift from the CLI. Only names / booleans / paths -- never " +
"any env-var value, token, key, or credential. Read-only; no writes or state changes.",
inputSchema: {},
},
async () => {
const status = (options.collectStatus ?? collectStatus)();
const doctor = (options.runDoctorChecks ?? runDoctorChecks)();
return { content: [{ type: "text", text: JSON.stringify({ status, doctor }) }] };
},
);
return server;
}

Expand Down
44 changes: 44 additions & 0 deletions test/unit/miner-mcp-scaffold.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ describe("gittensory-miner MCP server (#5153 scaffold)", () => {
"gittensory_miner_list_claims",
"gittensory_miner_list_plans",
"gittensory_miner_ping",
"gittensory_miner_status",
]);
});

Expand Down Expand Up @@ -339,3 +340,46 @@ describe("gittensory_miner_list_plans / get_plan (#5161)", () => {
expect(store.calls).not.toContain("savePlan");
});
});

const FAKE_STATUS = {
package: { name: "@jsonbored/gittensory-miner", version: "0.1.0" },
engine: { name: "@jsonbored/gittensory-engine", version: "1.0.0" },
node: "v22.13.0",
stateDir: "/home/miner/.config/gittensory-miner",
configFile: null,
driver: { provider: "claude-code", modelEnvVar: "MINER_CODING_AGENT_CLAUDE_MODEL", cliPresent: true },
};
const FAKE_DOCTOR = [
{ name: "Node", ok: true, detail: "v22.13.0" },
{ name: "Docker", ok: false, detail: "not installed" },
{ name: "Claude CLI", ok: true, detail: "present" },
];

describe("gittensory_miner_status (#5154)", () => {
function statusClient(): Promise<Client> {
return connectedClient({ collectStatus: () => FAKE_STATUS, runDoctorChecks: () => FAKE_DOCTOR });
}
async function callStatus(client: Client): Promise<Record<string, unknown>> {
const result = (await client.callTool({ name: "gittensory_miner_status", arguments: {} })) as Content;
return JSON.parse(toolText(result)) as Record<string, unknown>;
}

it("returns { status, doctor } from the reused collectStatus / runDoctorChecks readers", async () => {
const out = await callStatus(await statusClient());
expect(out).toEqual({ status: FAKE_STATUS, doctor: FAKE_DOCTOR });
});

it("surfaces the driver's model ENV-VAR NAME and CLI-present boolean, never a secret value", async () => {
const out = await callStatus(await statusClient());
expect((out.status as { driver: unknown }).driver).toEqual({
provider: "claude-code",
modelEnvVar: "MINER_CODING_AGENT_CLAUDE_MODEL",
cliPresent: true,
});
// Only names / booleans / paths — no token/key-shaped secret anywhere in the serialized response.
const serialized = JSON.stringify(out);
for (const secretish of ["ghp_", "gho_", "github_pat_", "-----BEGIN"]) {
expect(serialized).not.toContain(secretish);
}
});
});