diff --git a/packages/loopover-mcp/bin/loopover-mcp.ts b/packages/loopover-mcp/bin/loopover-mcp.ts index 9c6a26fd3..e33b23d15 100644 --- a/packages/loopover-mcp/bin/loopover-mcp.ts +++ b/packages/loopover-mcp/bin/loopover-mcp.ts @@ -1333,6 +1333,12 @@ const STDIO_TOOL_DESCRIPTORS = [ category: "agent", description: "Set the autonomy level for one action class via a read-merge-write, so the other classes are left untouched. Same as `loopover-mcp maintain set-level `. Maintainer access required.", }, + { + name: "loopover_get_automation_state", + category: "agent", + description: + "Return a repo's agent automation state: the per-action autonomy levels, kill-switch / dry-run mode, GitHub write-permission readiness, and how many auto_with_approval actions are awaiting a maintainer decision. Same as `loopover-mcp maintain automation-state`. Maintainer access required.", + }, { name: "loopover_get_outcome_calibration", category: "maintainer", @@ -2658,6 +2664,23 @@ registerStdioTool( }, ); +registerStdioTool( + "loopover_get_automation_state", + { + description: stdioToolDescription("loopover_get_automation_state"), + inputSchema: ownerRepoShape, + }, + async ({ owner, repo }: any) => { + // Same GET {repoBase}/automation-state the `maintain automation-state` CLI subcommand already calls (#6742). + const payload = await apiGet(`${toolRepoBase(owner, repo)}/automation-state`); + const acting = payload.actingActionClasses ?? []; + return toolResult( + `Agent automation for ${owner}/${repo}: mode=${payload.mode}, ${acting.length} acting class(es), ${payload.pendingActionCount ?? 0} pending approval(s).`, + payload, + ); + }, +); + registerStdioTool( "loopover_get_outcome_calibration", { diff --git a/test/unit/mcp-cli-maintain-tools.test.ts b/test/unit/mcp-cli-maintain-tools.test.ts index 58ff8a7ce..beb35f20c 100644 --- a/test/unit/mcp-cli-maintain-tools.test.ts +++ b/test/unit/mcp-cli-maintain-tools.test.ts @@ -4,9 +4,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { closeFixtureServer, run, startFixtureServer } from "./support/mcp-cli-harness"; - -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +import { closeFixtureServer, run, startFixtureServer, bin } from "./support/mcp-cli-harness"; // #6152: the maintain CLI's REST surface, exposed as stdio tools. These assert the proxy contract -- that each // tool reaches the endpoint its CLI subcommand already calls, with the same method and body -- rather than @@ -22,7 +20,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({ @@ -51,25 +49,26 @@ afterEach(async () => { const REPO = { owner: "owner", repo: "repo" }; -/** Every #6152 tool (plus #7758's outcome-calibration sibling), with an argument set the fixture serves - * and a field its real payload carries. */ +/** Every #6152 tool (plus #7758's outcome-calibration and #7752's automation-state siblings), with an + * argument set the fixture serves and a field its real payload carries. */ const MAINTAIN_TOOLS = [ { name: "loopover_list_pending_actions", args: REPO, contains: "pa-1" }, { name: "loopover_decide_pending_action", args: { ...REPO, id: "pa-1", decision: "accept" }, contains: "accepted" }, { name: "loopover_set_agent_paused", args: { ...REPO, paused: true }, contains: "agentPaused" }, { name: "loopover_set_action_autonomy", args: { ...REPO, action: "merge", level: "auto" }, contains: "autonomy" }, + { name: "loopover_get_automation_state", args: REPO, contains: "permissionReadiness" }, { name: "loopover_get_gate_precision", args: REPO, contains: "falsePositiveRate" }, { name: "loopover_get_outcome_calibration", args: REPO, contains: "positiveRate" }, ] 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 b1195bc93..2bd9297a0 100644 --- a/test/unit/mcp-tool-rename-aliases.test.ts +++ b/test/unit/mcp-tool-rename-aliases.test.ts @@ -23,6 +23,8 @@ // (#6980 registered the loopover_explain_review_risk CLI mirror, taking the count from 78 to 79.) // (#7758 registered the loopover_get_outcome_calibration stdio tool, taking the count from 79 to 80.) // (#7764 registered the loopover_plan_repo_issues stdio + CLI + REST tool, taking the count from 80 to 81.) +// (#7887 registered the loopover_get_activation_preview stdio tool without bumping this pin — live count became 82.) +// (#7752 registered the loopover_get_automation_state stdio tool, taking the count from 82 to 83.) import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { mkdtempSync, rmSync } from "node:fs"; @@ -70,14 +72,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => { }); afterEach(disconnect); - it("lists exactly 81 loopover_ tools and zero gittensory_-prefixed aliases", async () => { + it("lists exactly 83 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(81); + expect(primary.length).toBe(83); expect(legacy.length).toBe(0); - expect(names.length).toBe(81); + expect(names.length).toBe(83); }); it("no loopover_ tool's description carries a stale deprecation notice", async () => { @@ -89,14 +91,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => { } }); - it("`loopover-mcp tools --json` reports the same 81-tool count the live server registers", async () => { + it("`loopover-mcp tools --json` reports the same 83-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(81); + expect(payload.count).toBe(83); expect([...payload.tools.map((t) => t.name)].sort()).toEqual( [...tools.map((t) => t.name)].sort(), );