Skip to content

Commit 8ff2ec1

Browse files
committed
feat(mcp): register loopover_refresh_repo_docs as a local stdio tool
loopover_refresh_repo_docs has a remote MCP tool (src/mcp/server.ts) and a `maintain refresh-docs` CLI command, but no local stdio MCP tool registration. operator using the local MCP server couldn't call it. Adds the registerStdioTool block following the existing sibling pattern -- a thin POST proxy of the same {repoBase}/repo-docs/refresh route the CLI hits, with an empty body (the route only ever opens a PR -- never merges/commits -- so there is no create-safety flag to forward). Input reuses ownerRepoShape (matching the remote refreshRepoDocsShape); description via stdioToolDescription; category "maintainer". test/unit/mcp-cli-refresh-repo-docs.test.ts drives it in-process (#7764 entrypoint guard) so the registration + handler get real Codecov coverage. Count 94 -> 95. Closes #7754
1 parent 84a7878 commit 8ff2ec1

3 files changed

Lines changed: 99 additions & 5 deletions

File tree

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,12 @@ const STDIO_TOOL_DESCRIPTORS = [
10561056
description:
10571057
"Return a repo's agent audit feed: executed actions (agent.action.*) and approval-queue decisions (accepted/rejected), newest first. Read-only and public-safe (action posture only). Maintainer access required.",
10581058
},
1059+
{
1060+
name: "loopover_refresh_repo_docs",
1061+
category: "maintainer",
1062+
description:
1063+
"Force an immediate repo-doc refresh (AGENTS.md/CLAUDE.md, and a skill file when warranted) for one repo, without waiting for the scheduled interval. Only ever opens a pull request -- never a direct commit -- and only when repoDocGeneration is enabled for this repo and the generated content actually changed. Maintainer access required.",
1064+
},
10591065
{
10601066
name: "loopover_get_ams_miner_cohort",
10611067
category: "maintainer",
@@ -1704,6 +1710,21 @@ registerStdioTool(
17041710
},
17051711
);
17061712

1713+
// #7754: stdio mirror of the remote loopover_refresh_repo_docs + the `maintain refresh-docs` CLI. Thin POST
1714+
// proxy of the same {repoBase}/repo-docs/refresh route (empty body -- the route only ever opens a PR, never
1715+
// merges/commits, so there is no create-safety flag to forward). Same ownerRepoShape pattern as maintainer_noise.
1716+
registerStdioTool(
1717+
"loopover_refresh_repo_docs",
1718+
{
1719+
description: stdioToolDescription("loopover_refresh_repo_docs"),
1720+
inputSchema: ownerRepoShape,
1721+
},
1722+
async ({ owner, repo }: any) => {
1723+
const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
1724+
return toolResult(`LoopOver repo-doc refresh for ${owner}/${repo}.`, await apiPost(`${prefix}/repo-docs/refresh`, {}));
1725+
},
1726+
);
1727+
17071728
registerStdioTool(
17081729
"loopover_get_ams_miner_cohort",
17091730
{
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3+
import { mkdtempSync, rmSync } from "node:fs";
4+
import { tmpdir } from "node:os";
5+
import { join } from "node:path";
6+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
7+
import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harness";
8+
9+
// #7754: in-process coverage for the loopover_refresh_repo_docs stdio tool. Same #7764 entrypoint-guard
10+
// pattern as mcp-cli-repo-focus-manifest -- import the .ts, hold the exported `server`, connect an
11+
// InMemoryTransport so v8/Codecov attributes the registerStdioTool block (a subprocess spawn can't be
12+
// instrumented). The tool is a thin POST proxy, so one call exercises the whole handler.
13+
const MODULES = ["../../packages/loopover-mcp/bin/loopover-mcp.ts"] as const;
14+
15+
type BinModule = {
16+
server: { connect: (transport: unknown) => Promise<void> };
17+
};
18+
19+
let tempDir = "";
20+
const refreshCalls: Array<{ url: string; method: string }> = [];
21+
const loaded = new Map<string, BinModule>();
22+
23+
beforeAll(async () => {
24+
tempDir = mkdtempSync(join(tmpdir(), "loopover-refresh-repo-docs-"));
25+
const apiUrl = await startFixtureServer({
26+
onApiRequest: (r) => {
27+
if (r.url && r.url.includes("/repo-docs/refresh")) refreshCalls.push({ url: r.url ?? "", method: r.method ?? "" });
28+
},
29+
});
30+
process.env.LOOPOVER_API_URL = apiUrl;
31+
process.env.LOOPOVER_API_TOKEN = "in-process-token";
32+
process.env.LOOPOVER_API_TIMEOUT_MS = "2000";
33+
process.env.LOOPOVER_CONFIG_DIR = tempDir;
34+
process.env.LOOPOVER_SKIP_NPM_VERSION_CHECK = "1";
35+
for (const specifier of MODULES) {
36+
loaded.set(specifier, (await import(specifier)) as unknown as BinModule);
37+
}
38+
}, 120_000);
39+
40+
afterAll(async () => {
41+
await closeFixtureServer();
42+
if (tempDir) rmSync(tempDir, { recursive: true, force: true });
43+
delete process.env.LOOPOVER_API_URL;
44+
delete process.env.LOOPOVER_API_TOKEN;
45+
delete process.env.LOOPOVER_CONFIG_DIR;
46+
delete process.env.LOOPOVER_SKIP_NPM_VERSION_CHECK;
47+
});
48+
49+
describe("bin loopover_refresh_repo_docs stdio tool (in-process, #7754)", () => {
50+
it.each(MODULES)("proxies POST .../repo-docs/refresh and returns the PR result — %s", async (specifier) => {
51+
refreshCalls.length = 0;
52+
const mod = loaded.get(specifier)!;
53+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
54+
await mod.server.connect(serverTransport);
55+
const client = new Client({ name: "refresh-repo-docs-test", version: "0.1.0" }, { capabilities: {} });
56+
await client.connect(clientTransport);
57+
try {
58+
const tool = (await client.listTools()).tools.find((entry) => entry.name === "loopover_refresh_repo_docs");
59+
expect(tool).toBeDefined();
60+
expect(tool?.description).toMatch(/repo-doc refresh|opens a pull request/i);
61+
62+
const result = await client.callTool({ name: "loopover_refresh_repo_docs", arguments: { owner: "owner", repo: "repo" } });
63+
expect(result.isError).toBeFalsy();
64+
expect(refreshCalls).toEqual([{ url: "/v1/repos/owner/repo/repo-docs/refresh", method: "POST" }]);
65+
const text = JSON.stringify(result);
66+
expect(text).toContain("opened");
67+
expect(text).toContain("pullNumber");
68+
} finally {
69+
await client.close().catch(() => undefined);
70+
}
71+
});
72+
});

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
// (#7761 registered the loopover_list_notifications stdio tool, taking the count from 93 to 94.)
3939
// (#7752 registered the loopover_get_automation_state stdio tool, taking the count from 94 to 95.)
4040
// (#7757 registered the loopover_get_agent_audit_feed stdio tool, taking the count from 95 to 96.)
41+
// (#7754 registered the loopover_refresh_repo_docs stdio tool, taking the count from 96 to 97.)
4142
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4243
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4344
import { mkdtempSync, rmSync } from "node:fs";
@@ -84,14 +85,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => {
8485
});
8586
afterEach(disconnect);
8687

87-
it("lists exactly 96 loopover_ tools and zero gittensory_-prefixed aliases", async () => {
88+
it("lists exactly 97 loopover_ tools and zero gittensory_-prefixed aliases", async () => {
8889
const { tools } = await client.listTools();
8990
const names = tools.map((t) => t.name);
9091
const primary = names.filter((n) => n.startsWith("loopover_"));
9192
const legacy = names.filter((n) => n.startsWith("gittensory_"));
92-
expect(primary.length).toBe(96);
93+
expect(primary.length).toBe(97);
9394
expect(legacy.length).toBe(0);
94-
expect(names.length).toBe(96);
95+
expect(names.length).toBe(97);
9596
});
9697

9798
it("no loopover_ tool's description carries a stale deprecation notice", async () => {
@@ -103,14 +104,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => {
103104
}
104105
});
105106

106-
it("`loopover-mcp tools --json` reports the same 96-tool count the live server registers", async () => {
107+
it("`loopover-mcp tools --json` reports the same 97-tool count the live server registers", async () => {
107108
const { tools } = await client.listTools();
108109
const payload = JSON.parse(run(["tools", "--json"])) as {
109110
count: number;
110111
tools: Array<{ name: string }>;
111112
};
112113
expect(payload.count).toBe(tools.length);
113-
expect(payload.count).toBe(96);
114+
expect(payload.count).toBe(97);
114115
expect([...payload.tools.map((t) => t.name)].sort()).toEqual(
115116
[...tools.map((t) => t.name)].sort(),
116117
);

0 commit comments

Comments
 (0)