Skip to content

Commit 34418d7

Browse files
feat(mcp): register loopover_check_improvement_potential stdio tool
1 parent 3790ecd commit 34418d7

3 files changed

Lines changed: 144 additions & 5 deletions

File tree

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,42 @@ const checkSlopRiskShape = {
710710
testFiles: z.array(z.string().max(400)).max(2000).optional(),
711711
};
712712

713+
// #7759: mirrors checkImprovementPotentialShape in src/mcp/server.ts — same optional local-metadata fields the
714+
// CLI / REST route already accept. Stdio proxies POST /v1/lint/improvement-potential (builders stay app-side).
715+
const checkImprovementPotentialShape = {
716+
changedFiles: z
717+
.array(z.object({ path: z.string().min(1).max(400), additions: z.number().int().min(0).optional(), deletions: z.number().int().min(0).optional() }))
718+
.max(2000)
719+
.optional(),
720+
tests: z.array(z.string().max(400)).max(2000).optional(),
721+
testFiles: z.array(z.string().max(400)).max(2000).optional(),
722+
patchCoverageDeltaPercent: z.number().optional(),
723+
complexityDeltas: z
724+
.array(
725+
z.object({
726+
file: z.string().min(1).max(400),
727+
line: z.number().int().min(1),
728+
name: z.string().min(1).max(400),
729+
before: z.number().int().min(0),
730+
after: z.number().int().min(0),
731+
delta: z.number().int(),
732+
}),
733+
)
734+
.max(2000)
735+
.optional(),
736+
duplicationDeltas: z
737+
.array(
738+
z.object({
739+
file: z.string().min(1).max(400),
740+
line: z.number().int().min(1),
741+
duplicateOfLine: z.number().int().min(1),
742+
lines: z.number().int().min(1),
743+
}),
744+
)
745+
.max(2000)
746+
.optional(),
747+
};
748+
713749
const checkIssueSlopShape = {
714750
title: z.string().max(500).optional(),
715751
body: z.string().max(40000).optional(),
@@ -1069,6 +1105,12 @@ const STDIO_TOOL_DESCRIPTORS = [
10691105
category: "review",
10701106
description: "Assess the deterministic slop risk of a planned change from local diff metadata (paths + line counts) + the PR description — an agent-native, source-free quality self-check. Returns slopRisk (0-100), band, findings, and the rubric. Computed in-process; no repo data and no API round-trip.",
10711107
},
1108+
{
1109+
name: "loopover_check_improvement_potential",
1110+
category: "review",
1111+
description:
1112+
"Assess the deterministic structural-improvement potential of a planned change from local diff metadata plus optional complexity/duplication/patch-coverage deltas — mirrors loopover_check_slop_risk on the positive axis. Same as `loopover-mcp improvement-potential` / POST /v1/lint/improvement-potential.",
1113+
},
10721114
{
10731115
name: "loopover_simulate_open_pr_pressure",
10741116
category: "discovery",
@@ -1841,6 +1883,18 @@ registerStdioTool(
18411883
(input: any) => toolResult("LoopOver slop-risk self-check.", { ...buildSlopAssessment(input), rubric: SLOP_RUBRIC_MARKDOWN }),
18421884
);
18431885

1886+
// #7759: CLI already proxies POST /v1/lint/improvement-potential (#6748); register the matching stdio tool.
1887+
// Proxies rather than computing in-process (same rationale as the CLI): builders live app-side, not in
1888+
// @loopover/engine. Forward the validated input object as the POST body — no local branching.
1889+
registerStdioTool(
1890+
"loopover_check_improvement_potential",
1891+
{
1892+
description: stdioToolDescription("loopover_check_improvement_potential"),
1893+
inputSchema: checkImprovementPotentialShape,
1894+
},
1895+
async (input: any) => toolResult("LoopOver improvement-potential self-check.", await apiPost("/v1/lint/improvement-potential", input)),
1896+
);
1897+
18441898
// #6751: CLI mirror of the remote server's loopover_simulate_open_pr_pressure. Proxies rather than computing
18451899
// in-process (like the boundary-tests mirror, #6750): simulateOpenPrPressure lives app-side in
18461900
// src/services/open-pr-pressure-scenarios.ts, not in @loopover/engine, so POST /v1/lint/open-pr-pressure stays
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
// #7759: in-process coverage for the loopover_check_improvement_potential stdio tool.
10+
// Same #7764 entrypoint-guard pattern as sibling maintainer tools — import .ts, hold exported `server`,
11+
// connect InMemoryTransport so v8/Codecov attributes registerStdioTool.
12+
const MODULES = ["../../packages/loopover-mcp/bin/loopover-mcp.ts"] as const;
13+
14+
type BinModule = {
15+
server: { connect: (transport: unknown) => Promise<void> };
16+
};
17+
18+
let tempDir = "";
19+
const capturedRequests: Array<{ url: string; method: string }> = [];
20+
const loaded = new Map<string, BinModule>();
21+
22+
beforeAll(async () => {
23+
tempDir = mkdtempSync(join(tmpdir(), "loopover-improvement-potential-stdio-"));
24+
const apiUrl = await startFixtureServer({
25+
onApiRequest: (request) => {
26+
if (request.url && request.url.includes("/lint/improvement-potential")) {
27+
capturedRequests.push({ url: request.url ?? "", method: request.method ?? "GET" });
28+
}
29+
},
30+
});
31+
process.env.LOOPOVER_API_URL = apiUrl;
32+
process.env.LOOPOVER_API_TOKEN = "in-process-token";
33+
process.env.LOOPOVER_API_TIMEOUT_MS = "2000";
34+
process.env.LOOPOVER_CONFIG_DIR = tempDir;
35+
process.env.LOOPOVER_SKIP_NPM_VERSION_CHECK = "1";
36+
for (const specifier of MODULES) {
37+
loaded.set(specifier, (await import(specifier)) as unknown as BinModule);
38+
}
39+
}, 120_000);
40+
41+
afterAll(async () => {
42+
await closeFixtureServer();
43+
if (tempDir) rmSync(tempDir, { recursive: true, force: true });
44+
delete process.env.LOOPOVER_API_URL;
45+
delete process.env.LOOPOVER_API_TOKEN;
46+
delete process.env.LOOPOVER_CONFIG_DIR;
47+
delete process.env.LOOPOVER_SKIP_NPM_VERSION_CHECK;
48+
});
49+
50+
describe("bin loopover_check_improvement_potential stdio tool (in-process, #7759)", () => {
51+
it.each(MODULES)("registers and proxies POST /v1/lint/improvement-potential - %s", async (specifier) => {
52+
capturedRequests.length = 0;
53+
const mod = loaded.get(specifier)!;
54+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
55+
await mod.server.connect(serverTransport);
56+
const client = new Client({ name: "improvement-potential-stdio-test", version: "0.1.0" }, { capabilities: {} });
57+
await client.connect(clientTransport);
58+
try {
59+
const { tools } = await client.listTools();
60+
const tool = tools.find((entry) => entry.name === "loopover_check_improvement_potential");
61+
expect(tool).toBeDefined();
62+
expect(tool?.description).toMatch(/improvement/i);
63+
64+
const result = await client.callTool({
65+
name: "loopover_check_improvement_potential",
66+
arguments: {
67+
changedFiles: [{ path: "src/widget.ts", additions: 80, deletions: 2 }],
68+
testFiles: ["test/unit/widget.test.ts"],
69+
},
70+
});
71+
expect(capturedRequests.length).toBe(1);
72+
const captured = capturedRequests[0]!;
73+
expect(captured.url).toContain("/v1/lint/improvement-potential");
74+
expect(captured.method).toBe("POST");
75+
expect(result.isError).toBeFalsy();
76+
const text = JSON.stringify(result);
77+
expect(text).toContain("improvementScore");
78+
expect(text).toContain("minor");
79+
expect(text).not.toMatch(/wallet|hotkey|reward|trust score/i);
80+
} finally {
81+
await client.close().catch(() => undefined);
82+
}
83+
});
84+
});

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
// (#7808 registered the loopover_get_repo_focus_manifest remote+stdio tool, taking the count from 88 to 89.)
3434
// (#7762 registered the loopover_mark_notifications_read stdio tool, taking the count from 89 to 90.)
3535
// (#7760 registered the loopover_get_contributor_profile stdio tool, taking the count from 90 to 91.)
36+
// (#7759 registered the loopover_check_improvement_potential stdio tool, taking the count from 91 to 92.)
3637
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3738
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3839
import { mkdtempSync, rmSync } from "node:fs";
@@ -79,14 +80,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => {
7980
});
8081
afterEach(disconnect);
8182

82-
it("lists exactly 91 loopover_ tools and zero gittensory_-prefixed aliases", async () => {
83+
it("lists exactly 92 loopover_ tools and zero gittensory_-prefixed aliases", async () => {
8384
const { tools } = await client.listTools();
8485
const names = tools.map((t) => t.name);
8586
const primary = names.filter((n) => n.startsWith("loopover_"));
8687
const legacy = names.filter((n) => n.startsWith("gittensory_"));
87-
expect(primary.length).toBe(91);
88+
expect(primary.length).toBe(92);
8889
expect(legacy.length).toBe(0);
89-
expect(names.length).toBe(91);
90+
expect(names.length).toBe(92);
9091
});
9192

9293
it("no loopover_ tool's description carries a stale deprecation notice", async () => {
@@ -98,14 +99,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => {
9899
}
99100
});
100101

101-
it("`loopover-mcp tools --json` reports the same 91-tool count the live server registers", async () => {
102+
it("`loopover-mcp tools --json` reports the same 92-tool count the live server registers", async () => {
102103
const { tools } = await client.listTools();
103104
const payload = JSON.parse(run(["tools", "--json"])) as {
104105
count: number;
105106
tools: Array<{ name: string }>;
106107
};
107108
expect(payload.count).toBe(tools.length);
108-
expect(payload.count).toBe(91);
109+
expect(payload.count).toBe(92);
109110
expect([...payload.tools.map((t) => t.name)].sort()).toEqual(
110111
[...tools.map((t) => t.name)].sort(),
111112
);

0 commit comments

Comments
 (0)