diff --git a/packages/loopover-mcp/bin/loopover-mcp.ts b/packages/loopover-mcp/bin/loopover-mcp.ts index aa89ef236c..ee1cf61176 100644 --- a/packages/loopover-mcp/bin/loopover-mcp.ts +++ b/packages/loopover-mcp/bin/loopover-mcp.ts @@ -1041,6 +1041,12 @@ const STDIO_TOOL_DESCRIPTORS = [ category: "maintainer", description: "Return the maintainer queue-noise triage report for a repo: a noise score/level, the specific noise sources to clear first, and recommended maintainer actions. Maintainer-authenticated; advisory only.", }, + { + name: "loopover_refresh_repo_docs", + category: "maintainer", + description: + "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.", + }, { name: "loopover_get_ams_miner_cohort", category: "maintainer", @@ -1671,6 +1677,21 @@ registerStdioTool( }, ); +// #7754: stdio mirror of the remote loopover_refresh_repo_docs + the `maintain refresh-docs` CLI. Thin POST +// proxy of the same {repoBase}/repo-docs/refresh route (empty body -- the route only ever opens a PR, never +// merges/commits, so there is no create-safety flag to forward). Same ownerRepoShape pattern as maintainer_noise. +registerStdioTool( + "loopover_refresh_repo_docs", + { + description: stdioToolDescription("loopover_refresh_repo_docs"), + inputSchema: ownerRepoShape, + }, + async ({ owner, repo }: any) => { + const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; + return toolResult(`LoopOver repo-doc refresh for ${owner}/${repo}.`, await apiPost(`${prefix}/repo-docs/refresh`, {})); + }, +); + registerStdioTool( "loopover_get_ams_miner_cohort", { diff --git a/test/unit/mcp-cli-refresh-repo-docs.test.ts b/test/unit/mcp-cli-refresh-repo-docs.test.ts new file mode 100644 index 0000000000..2ce9b07cb7 --- /dev/null +++ b/test/unit/mcp-cli-refresh-repo-docs.test.ts @@ -0,0 +1,72 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harness"; + +// #7754: in-process coverage for the loopover_refresh_repo_docs stdio tool. Same #7764 entrypoint-guard +// pattern as mcp-cli-repo-focus-manifest -- import the .ts, hold the exported `server`, connect an +// InMemoryTransport so v8/Codecov attributes the registerStdioTool block (a subprocess spawn can't be +// instrumented). The tool is a thin POST proxy, so one call exercises the whole handler. +const MODULES = ["../../packages/loopover-mcp/bin/loopover-mcp.ts"] as const; + +type BinModule = { + server: { connect: (transport: unknown) => Promise }; +}; + +let tempDir = ""; +const refreshCalls: Array<{ url: string; method: string }> = []; +const loaded = new Map(); + +beforeAll(async () => { + tempDir = mkdtempSync(join(tmpdir(), "loopover-refresh-repo-docs-")); + const apiUrl = await startFixtureServer({ + onApiRequest: (r) => { + if (r.url && r.url.includes("/repo-docs/refresh")) refreshCalls.push({ url: r.url ?? "", method: r.method ?? "" }); + }, + }); + process.env.LOOPOVER_API_URL = apiUrl; + process.env.LOOPOVER_API_TOKEN = "in-process-token"; + process.env.LOOPOVER_API_TIMEOUT_MS = "2000"; + process.env.LOOPOVER_CONFIG_DIR = tempDir; + process.env.LOOPOVER_SKIP_NPM_VERSION_CHECK = "1"; + for (const specifier of MODULES) { + loaded.set(specifier, (await import(specifier)) as unknown as BinModule); + } +}, 120_000); + +afterAll(async () => { + await closeFixtureServer(); + if (tempDir) rmSync(tempDir, { recursive: true, force: true }); + delete process.env.LOOPOVER_API_URL; + delete process.env.LOOPOVER_API_TOKEN; + delete process.env.LOOPOVER_CONFIG_DIR; + delete process.env.LOOPOVER_SKIP_NPM_VERSION_CHECK; +}); + +describe("bin loopover_refresh_repo_docs stdio tool (in-process, #7754)", () => { + it.each(MODULES)("proxies POST .../repo-docs/refresh and returns the PR result — %s", async (specifier) => { + refreshCalls.length = 0; + const mod = loaded.get(specifier)!; + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await mod.server.connect(serverTransport); + const client = new Client({ name: "refresh-repo-docs-test", version: "0.1.0" }, { capabilities: {} }); + await client.connect(clientTransport); + try { + const tool = (await client.listTools()).tools.find((entry) => entry.name === "loopover_refresh_repo_docs"); + expect(tool).toBeDefined(); + expect(tool?.description).toMatch(/repo-doc refresh|opens a pull request/i); + + const result = await client.callTool({ name: "loopover_refresh_repo_docs", arguments: { owner: "owner", repo: "repo" } }); + expect(result.isError).toBeFalsy(); + expect(refreshCalls).toEqual([{ url: "/v1/repos/owner/repo/repo-docs/refresh", method: "POST" }]); + const text = JSON.stringify(result); + expect(text).toContain("opened"); + expect(text).toContain("pullNumber"); + } finally { + await client.close().catch(() => undefined); + } + }); +}); diff --git a/test/unit/mcp-tool-rename-aliases.test.ts b/test/unit/mcp-tool-rename-aliases.test.ts index bb1ab73809..04795b5658 100644 --- a/test/unit/mcp-tool-rename-aliases.test.ts +++ b/test/unit/mcp-tool-rename-aliases.test.ts @@ -36,7 +36,7 @@ // (#7763 registered the loopover_watch_issues stdio tool, taking the count from 91 to 92.) // (#7759 registered the loopover_check_improvement_potential stdio tool, taking the count from 92 to 93.) // (#7761 registered the loopover_list_notifications stdio tool, taking the count from 93 to 94.) -// (#7752 registered the loopover_get_automation_state stdio tool, taking the count from 94 to 95.) +// (#7754 registered the loopover_refresh_repo_docs stdio tool, taking the count from 94 to 95.) import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { mkdtempSync, rmSync } from "node:fs";