Skip to content

Commit 455de14

Browse files
fix(mcp): register loopover_get_automation_state as a local stdio tool
Mirror the maintain-family registerStdioTool pattern (#6382/#7877) and reuse the same REST endpoint the existing `maintain automation-state` CLI already calls. Bump the stdio tool-count invariant to 83 (correcting the stale pin left after #7887). Closes #7752. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b3e1bc3 commit 455de14

3 files changed

Lines changed: 37 additions & 13 deletions

File tree

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,12 @@ const STDIO_TOOL_DESCRIPTORS = [
13331333
category: "agent",
13341334
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 <action> <level>`. Maintainer access required.",
13351335
},
1336+
{
1337+
name: "loopover_get_automation_state",
1338+
category: "agent",
1339+
description:
1340+
"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.",
1341+
},
13361342
{
13371343
name: "loopover_get_outcome_calibration",
13381344
category: "maintainer",
@@ -2658,6 +2664,23 @@ registerStdioTool(
26582664
},
26592665
);
26602666

2667+
registerStdioTool(
2668+
"loopover_get_automation_state",
2669+
{
2670+
description: stdioToolDescription("loopover_get_automation_state"),
2671+
inputSchema: ownerRepoShape,
2672+
},
2673+
async ({ owner, repo }: any) => {
2674+
// Same GET {repoBase}/automation-state the `maintain automation-state` CLI subcommand already calls (#6742).
2675+
const payload = await apiGet(`${toolRepoBase(owner, repo)}/automation-state`);
2676+
const acting = payload.actingActionClasses ?? [];
2677+
return toolResult(
2678+
`Agent automation for ${owner}/${repo}: mode=${payload.mode}, ${acting.length} acting class(es), ${payload.pendingActionCount ?? 0} pending approval(s).`,
2679+
payload,
2680+
);
2681+
},
2682+
);
2683+
26612684
registerStdioTool(
26622685
"loopover_get_outcome_calibration",
26632686
{

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import { mkdtempSync, rmSync } from "node:fs";
44
import { tmpdir } from "node:os";
55
import { join } from "node:path";
66
import { afterEach, describe, expect, it } from "vitest";
7-
import { closeFixtureServer, run, startFixtureServer } from "./support/mcp-cli-harness";
8-
9-
const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js");
7+
import { closeFixtureServer, run, startFixtureServer, bin } from "./support/mcp-cli-harness";
108

119
// #6152: the maintain CLI's REST surface, exposed as stdio tools. These assert the proxy contract -- that each
1210
// tool reaches the endpoint its CLI subcommand already calls, with the same method and body -- rather than
@@ -22,7 +20,7 @@ async function connect() {
2220
const apiUrl = await startFixtureServer({
2321
onApiRequest: (request) => {
2422
const url = request.url ?? "";
25-
if (/pending-actions|settings|gate-precision|outcome-calibration/.test(url)) capturedRequests.push({ url, method: request.method ?? "GET" });
23+
if (/pending-actions|settings|gate-precision|outcome-calibration|automation-state/.test(url)) capturedRequests.push({ url, method: request.method ?? "GET" });
2624
},
2725
});
2826
transport = new StdioClientTransport({
@@ -51,25 +49,26 @@ afterEach(async () => {
5149

5250
const REPO = { owner: "owner", repo: "repo" };
5351

54-
/** Every #6152 tool (plus #7758's outcome-calibration sibling), with an argument set the fixture serves
55-
* and a field its real payload carries. */
52+
/** Every #6152 tool (plus #7758's outcome-calibration and #7752's automation-state siblings), with an
53+
* argument set the fixture serves and a field its real payload carries. */
5654
const MAINTAIN_TOOLS = [
5755
{ name: "loopover_list_pending_actions", args: REPO, contains: "pa-1" },
5856
{ name: "loopover_decide_pending_action", args: { ...REPO, id: "pa-1", decision: "accept" }, contains: "accepted" },
5957
{ name: "loopover_set_agent_paused", args: { ...REPO, paused: true }, contains: "agentPaused" },
6058
{ name: "loopover_set_action_autonomy", args: { ...REPO, action: "merge", level: "auto" }, contains: "autonomy" },
59+
{ name: "loopover_get_automation_state", args: REPO, contains: "permissionReadiness" },
6160
{ name: "loopover_get_gate_precision", args: REPO, contains: "falsePositiveRate" },
6261
{ name: "loopover_get_outcome_calibration", args: REPO, contains: "positiveRate" },
6362
] as const;
6463

6564
describe("loopover-mcp maintain stdio proxies (#6152)", () => {
66-
it("registers all 6 maintain tools in the stdio server tool list", async () => {
65+
it("registers all 7 maintain tools in the stdio server tool list", async () => {
6766
await connect();
6867
const names = (await client!.listTools()).tools.map((tool) => tool.name);
6968
for (const tool of MAINTAIN_TOOLS) expect(names).toContain(tool.name);
7069
});
7170

72-
it("lists all 6 maintain tools via `loopover-mcp tools --json` with non-empty descriptions", async () => {
71+
it("lists all 7 maintain tools via `loopover-mcp tools --json` with non-empty descriptions", async () => {
7372
await connect();
7473
const payload = JSON.parse(run(["tools", "--json"])) as { tools: Array<{ name: string; description: string; category?: string }> };
7574
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
@@ -23,6 +23,8 @@
2323
// (#6980 registered the loopover_explain_review_risk CLI mirror, taking the count from 78 to 79.)
2424
// (#7758 registered the loopover_get_outcome_calibration stdio tool, taking the count from 79 to 80.)
2525
// (#7764 registered the loopover_plan_repo_issues stdio + CLI + REST tool, taking the count from 80 to 81.)
26+
// (#7887 registered the loopover_get_activation_preview stdio tool without bumping this pin — live count became 82.)
27+
// (#7752 registered the loopover_get_automation_state stdio tool, taking the count from 82 to 83.)
2628
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2729
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2830
import { mkdtempSync, rmSync } from "node:fs";
@@ -70,14 +72,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => {
7072
});
7173
afterEach(disconnect);
7274

73-
it("lists exactly 81 loopover_ tools and zero gittensory_-prefixed aliases", async () => {
75+
it("lists exactly 83 loopover_ tools and zero gittensory_-prefixed aliases", async () => {
7476
const { tools } = await client.listTools();
7577
const names = tools.map((t) => t.name);
7678
const primary = names.filter((n) => n.startsWith("loopover_"));
7779
const legacy = names.filter((n) => n.startsWith("gittensory_"));
78-
expect(primary.length).toBe(81);
80+
expect(primary.length).toBe(83);
7981
expect(legacy.length).toBe(0);
80-
expect(names.length).toBe(81);
82+
expect(names.length).toBe(83);
8183
});
8284

8385
it("no loopover_ tool's description carries a stale deprecation notice", async () => {
@@ -89,14 +91,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => {
8991
}
9092
});
9193

92-
it("`loopover-mcp tools --json` reports the same 81-tool count the live server registers", async () => {
94+
it("`loopover-mcp tools --json` reports the same 83-tool count the live server registers", async () => {
9395
const { tools } = await client.listTools();
9496
const payload = JSON.parse(run(["tools", "--json"])) as {
9597
count: number;
9698
tools: Array<{ name: string }>;
9799
};
98100
expect(payload.count).toBe(tools.length);
99-
expect(payload.count).toBe(81);
101+
expect(payload.count).toBe(83);
100102
expect([...payload.tools.map((t) => t.name)].sort()).toEqual(
101103
[...tools.map((t) => t.name)].sort(),
102104
);

0 commit comments

Comments
 (0)