diff --git a/packages/loopover-mcp/bin/loopover-mcp.ts b/packages/loopover-mcp/bin/loopover-mcp.ts index 77b5c718fb..d9f33252d7 100644 --- a/packages/loopover-mcp/bin/loopover-mcp.ts +++ b/packages/loopover-mcp/bin/loopover-mcp.ts @@ -1373,6 +1373,12 @@ const STDIO_TOOL_DESCRIPTORS = [ category: "maintainer", description: "Return per-gate-type false-positive precision for a repo's recorded gate blocks — blocked / blocked-then-merged counts and false-positive rates with low-sample guards. Optionally bounded by windowDays. Maintainer-authenticated; measurement only.", }, + { + name: "loopover_get_automation_state", + category: "agent", + description: + "Return a repo's DERIVED agent automation state — the effective mode, permissionReadiness, acting action classes, and pending-action count computed over the raw settings row — same as `loopover-mcp maintain automation-state` and the read-side counterpart to the pause/resume/set-level write tools. Maintainer-authenticated; read-only.", + }, { name: "loopover_plan_repo_issues", category: "maintainer", @@ -2772,6 +2778,24 @@ registerStdioTool( }, ); +// #7752: read-side counterpart to the pause/resume/set-level write tools above. Proxies the same +// GET {repoBase}/automation-state the `maintain automation-state` CLI already calls (loopover-mcp.ts), +// exposing the DERIVED mode/permissionReadiness/acting-classes/pending view the raw settings row omits. +registerStdioTool( + "loopover_get_automation_state", + { + description: stdioToolDescription("loopover_get_automation_state"), + inputSchema: ownerRepoShape, + }, + async ({ owner, repo }: any) => { + const payload = await apiGet(`${toolRepoBase(owner, repo)}/automation-state`); + return toolResult( + `Agent automation state for ${owner}/${repo}: mode ${payload.mode ?? "unknown"}, ${(payload.actingActionClasses ?? []).length} acting class(es), ${payload.pendingActionCount ?? 0} pending.`, + payload, + ); + }, +); + registerStdioTool( "loopover_plan_repo_issues", { diff --git a/test/unit/mcp-cli-maintain-tools.test.ts b/test/unit/mcp-cli-maintain-tools.test.ts index 58ff8a7ced..edf86a7333 100644 --- a/test/unit/mcp-cli-maintain-tools.test.ts +++ b/test/unit/mcp-cli-maintain-tools.test.ts @@ -22,7 +22,7 @@ async function connect() { const apiUrl = await startFixtureServer({ onApiRequest: (request) => { const url = request.url ?? ""; - if (/pending-actions|settings|gate-precision|outcome-calibration/.test(url)) capturedRequests.push({ url, method: request.method ?? "GET" }); + if (/pending-actions|settings|gate-precision|outcome-calibration|automation-state/.test(url)) capturedRequests.push({ url, method: request.method ?? "GET" }); }, }); transport = new StdioClientTransport({ @@ -60,16 +60,17 @@ const MAINTAIN_TOOLS = [ { name: "loopover_set_action_autonomy", args: { ...REPO, action: "merge", level: "auto" }, contains: "autonomy" }, { name: "loopover_get_gate_precision", args: REPO, contains: "falsePositiveRate" }, { name: "loopover_get_outcome_calibration", args: REPO, contains: "positiveRate" }, + { name: "loopover_get_automation_state", args: REPO, contains: "permissionReadiness" }, ] as const; describe("loopover-mcp maintain stdio proxies (#6152)", () => { - it("registers all 6 maintain tools in the stdio server tool list", async () => { + it("registers all 7 maintain tools in the stdio server tool list", async () => { await connect(); const names = (await client!.listTools()).tools.map((tool) => tool.name); for (const tool of MAINTAIN_TOOLS) expect(names).toContain(tool.name); }); - it("lists all 6 maintain tools via `loopover-mcp tools --json` with non-empty descriptions", async () => { + it("lists all 7 maintain tools via `loopover-mcp tools --json` with non-empty descriptions", async () => { await connect(); const payload = JSON.parse(run(["tools", "--json"])) as { tools: Array<{ name: string; description: string; category?: string }> }; for (const tool of MAINTAIN_TOOLS) { diff --git a/test/unit/mcp-tool-rename-aliases.test.ts b/test/unit/mcp-tool-rename-aliases.test.ts index 773a45a5e8..fc729f8d34 100644 --- a/test/unit/mcp-tool-rename-aliases.test.ts +++ b/test/unit/mcp-tool-rename-aliases.test.ts @@ -29,6 +29,8 @@ // (#7801 registered the loopover_get_live_gate_thresholds remote+stdio tool, taking the count from 84 to 85.) // (#7802 registered the loopover_get_pr_maintainer_packet remote+stdio tool, taking the count from 85 to 86.) // (#7800 registered the loopover_get_gate_config_effective remote+stdio tool, taking the count from 86 to 87.) +// (#7752 registered the loopover_get_automation_state stdio tool -- the remote tool already existed (#6742) -- +// taking the count from 87 to 88.) import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { mkdtempSync, rmSync } from "node:fs"; @@ -75,14 +77,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => { }); afterEach(disconnect); - it("lists exactly 87 loopover_ tools and zero gittensory_-prefixed aliases", async () => { + it("lists exactly 88 loopover_ tools and zero gittensory_-prefixed aliases", async () => { const { tools } = await client.listTools(); const names = tools.map((t) => t.name); const primary = names.filter((n) => n.startsWith("loopover_")); const legacy = names.filter((n) => n.startsWith("gittensory_")); - expect(primary.length).toBe(87); + expect(primary.length).toBe(88); expect(legacy.length).toBe(0); - expect(names.length).toBe(87); + expect(names.length).toBe(88); }); it("no loopover_ tool's description carries a stale deprecation notice", async () => { @@ -94,14 +96,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => { } }); - it("`loopover-mcp tools --json` reports the same 87-tool count the live server registers", async () => { + it("`loopover-mcp tools --json` reports the same 88-tool count the live server registers", async () => { const { tools } = await client.listTools(); const payload = JSON.parse(run(["tools", "--json"])) as { count: number; tools: Array<{ name: string }>; }; expect(payload.count).toBe(tools.length); - expect(payload.count).toBe(87); + expect(payload.count).toBe(88); expect([...payload.tools.map((t) => t.name)].sort()).toEqual( [...tools.map((t) => t.name)].sort(), );