Skip to content

Commit ad17c67

Browse files
feat(mcp): register loopover_get_automation_state as a local stdio tool
loopover_get_automation_state is a remote MCP tool (src/mcp/server.ts) and has a CLI mirror (maintain automation-state), but no local stdio MCP tool registration existed in packages/loopover-mcp/bin/loopover-mcp.ts -- so a self-host operator on the local MCP server could not call it. #6742 added the REST route + CLI subcommand but never the matching stdio registration. Register it following the exact pattern of its five #6382 siblings: a STDIO_TOOL_DESCRIPTORS entry (category 'agent', mirroring the remote tool) plus a registerStdioTool block that reuses the same GET {repoBase}/ automation-state call the maintain CLI already makes, via ownerRepoShape and toolRepoBase. No HTTP logic is duplicated. Tests: add it to the maintain stdio-proxy suite (proxy + API-failure cases, auto-covered by the existing MAINTAIN_TOOLS loop) and bump the discovery tool-count invariant from 87 to 88. Closes #7752
1 parent c6d28ab commit ad17c67

3 files changed

Lines changed: 35 additions & 8 deletions

File tree

packages/loopover-mcp/bin/loopover-mcp.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,6 +1373,12 @@ const STDIO_TOOL_DESCRIPTORS = [
13731373
category: "maintainer",
13741374
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.",
13751375
},
1376+
{
1377+
name: "loopover_get_automation_state",
1378+
category: "agent",
1379+
description:
1380+
"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.",
1381+
},
13761382
{
13771383
name: "loopover_plan_repo_issues",
13781384
category: "maintainer",
@@ -2772,6 +2778,24 @@ registerStdioTool(
27722778
},
27732779
);
27742780

2781+
// #7752: read-side counterpart to the pause/resume/set-level write tools above. Proxies the same
2782+
// GET {repoBase}/automation-state the `maintain automation-state` CLI already calls (loopover-mcp.ts),
2783+
// exposing the DERIVED mode/permissionReadiness/acting-classes/pending view the raw settings row omits.
2784+
registerStdioTool(
2785+
"loopover_get_automation_state",
2786+
{
2787+
description: stdioToolDescription("loopover_get_automation_state"),
2788+
inputSchema: ownerRepoShape,
2789+
},
2790+
async ({ owner, repo }: any) => {
2791+
const payload = await apiGet(`${toolRepoBase(owner, repo)}/automation-state`);
2792+
return toolResult(
2793+
`Agent automation state for ${owner}/${repo}: mode ${payload.mode ?? "unknown"}, ${(payload.actingActionClasses ?? []).length} acting class(es), ${payload.pendingActionCount ?? 0} pending.`,
2794+
payload,
2795+
);
2796+
},
2797+
);
2798+
27752799
registerStdioTool(
27762800
"loopover_plan_repo_issues",
27772801
{

test/unit/mcp-cli-maintain-tools.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async function connect() {
2222
const apiUrl = await startFixtureServer({
2323
onApiRequest: (request) => {
2424
const url = request.url ?? "";
25-
if (/pending-actions|settings|gate-precision|outcome-calibration/.test(url)) capturedRequests.push({ url, method: request.method ?? "GET" });
25+
if (/pending-actions|settings|gate-precision|outcome-calibration|automation-state/.test(url)) capturedRequests.push({ url, method: request.method ?? "GET" });
2626
},
2727
});
2828
transport = new StdioClientTransport({
@@ -60,16 +60,17 @@ const MAINTAIN_TOOLS = [
6060
{ name: "loopover_set_action_autonomy", args: { ...REPO, action: "merge", level: "auto" }, contains: "autonomy" },
6161
{ name: "loopover_get_gate_precision", args: REPO, contains: "falsePositiveRate" },
6262
{ name: "loopover_get_outcome_calibration", args: REPO, contains: "positiveRate" },
63+
{ name: "loopover_get_automation_state", args: REPO, contains: "permissionReadiness" },
6364
] as const;
6465

6566
describe("loopover-mcp maintain stdio proxies (#6152)", () => {
66-
it("registers all 6 maintain tools in the stdio server tool list", async () => {
67+
it("registers all 7 maintain tools in the stdio server tool list", async () => {
6768
await connect();
6869
const names = (await client!.listTools()).tools.map((tool) => tool.name);
6970
for (const tool of MAINTAIN_TOOLS) expect(names).toContain(tool.name);
7071
});
7172

72-
it("lists all 6 maintain tools via `loopover-mcp tools --json` with non-empty descriptions", async () => {
73+
it("lists all 7 maintain tools via `loopover-mcp tools --json` with non-empty descriptions", async () => {
7374
await connect();
7475
const payload = JSON.parse(run(["tools", "--json"])) as { tools: Array<{ name: string; description: string; category?: string }> };
7576
for (const tool of MAINTAIN_TOOLS) {

test/unit/mcp-tool-rename-aliases.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
// (#7801 registered the loopover_get_live_gate_thresholds remote+stdio tool, taking the count from 84 to 85.)
3030
// (#7802 registered the loopover_get_pr_maintainer_packet remote+stdio tool, taking the count from 85 to 86.)
3131
// (#7800 registered the loopover_get_gate_config_effective remote+stdio tool, taking the count from 86 to 87.)
32+
// (#7752 registered the loopover_get_automation_state stdio tool -- the remote tool already existed (#6742) --
33+
// taking the count from 87 to 88.)
3234
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3335
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3436
import { mkdtempSync, rmSync } from "node:fs";
@@ -75,14 +77,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => {
7577
});
7678
afterEach(disconnect);
7779

78-
it("lists exactly 87 loopover_ tools and zero gittensory_-prefixed aliases", async () => {
80+
it("lists exactly 88 loopover_ tools and zero gittensory_-prefixed aliases", async () => {
7981
const { tools } = await client.listTools();
8082
const names = tools.map((t) => t.name);
8183
const primary = names.filter((n) => n.startsWith("loopover_"));
8284
const legacy = names.filter((n) => n.startsWith("gittensory_"));
83-
expect(primary.length).toBe(87);
85+
expect(primary.length).toBe(88);
8486
expect(legacy.length).toBe(0);
85-
expect(names.length).toBe(87);
87+
expect(names.length).toBe(88);
8688
});
8789

8890
it("no loopover_ tool's description carries a stale deprecation notice", async () => {
@@ -94,14 +96,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => {
9496
}
9597
});
9698

97-
it("`loopover-mcp tools --json` reports the same 87-tool count the live server registers", async () => {
99+
it("`loopover-mcp tools --json` reports the same 88-tool count the live server registers", async () => {
98100
const { tools } = await client.listTools();
99101
const payload = JSON.parse(run(["tools", "--json"])) as {
100102
count: number;
101103
tools: Array<{ name: string }>;
102104
};
103105
expect(payload.count).toBe(tools.length);
104-
expect(payload.count).toBe(87);
106+
expect(payload.count).toBe(88);
105107
expect([...payload.tools.map((t) => t.name)].sort()).toEqual(
106108
[...tools.map((t) => t.name)].sort(),
107109
);

0 commit comments

Comments
 (0)