diff --git a/packages/gittensory-miner/README.md b/packages/gittensory-miner/README.md index 06920f65b4..f8c6f38036 100644 --- a/packages/gittensory-miner/README.md +++ b/packages/gittensory-miner/README.md @@ -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 diff --git a/packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts b/packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts index 1ec9069640..fbe8257137 100644 --- a/packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts +++ b/packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts @@ -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; diff --git a/packages/gittensory-miner/bin/gittensory-miner-mcp.js b/packages/gittensory-miner/bin/gittensory-miner-mcp.js index fe8eb39f65..83909e7213 100755 --- a/packages/gittensory-miner/bin/gittensory-miner-mcp.js +++ b/packages/gittensory-miner/bin/gittensory-miner-mcp.js @@ -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: @@ -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. @@ -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 }); @@ -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; } diff --git a/test/unit/miner-mcp-scaffold.test.ts b/test/unit/miner-mcp-scaffold.test.ts index c6c3feffe7..fd6017b3ed 100644 --- a/test/unit/miner-mcp-scaffold.test.ts +++ b/test/unit/miner-mcp-scaffold.test.ts @@ -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", ]); }); @@ -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 { + return connectedClient({ collectStatus: () => FAKE_STATUS, runDoctorChecks: () => FAKE_DOCTOR }); + } + async function callStatus(client: Client): Promise> { + const result = (await client.callTool({ name: "gittensory_miner_status", arguments: {} })) as Content; + return JSON.parse(toolText(result)) as Record; + } + + 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); + } + }); +});