Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions packages/loopover-mcp/bin/loopover-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,42 @@ const checkSlopRiskShape = {
testFiles: z.array(z.string().max(400)).max(2000).optional(),
};

// #7759: mirrors checkImprovementPotentialShape in src/mcp/server.ts — same optional local-metadata fields the
// CLI / REST route already accept. Stdio proxies POST /v1/lint/improvement-potential (builders stay app-side).
const checkImprovementPotentialShape = {
changedFiles: z
.array(z.object({ path: z.string().min(1).max(400), additions: z.number().int().min(0).optional(), deletions: z.number().int().min(0).optional() }))
.max(2000)
.optional(),
tests: z.array(z.string().max(400)).max(2000).optional(),
testFiles: z.array(z.string().max(400)).max(2000).optional(),
patchCoverageDeltaPercent: z.number().optional(),
complexityDeltas: z
.array(
z.object({
file: z.string().min(1).max(400),
line: z.number().int().min(1),
name: z.string().min(1).max(400),
before: z.number().int().min(0),
after: z.number().int().min(0),
delta: z.number().int(),
}),
)
.max(2000)
.optional(),
duplicationDeltas: z
.array(
z.object({
file: z.string().min(1).max(400),
line: z.number().int().min(1),
duplicateOfLine: z.number().int().min(1),
lines: z.number().int().min(1),
}),
)
.max(2000)
.optional(),
};

const checkIssueSlopShape = {
title: z.string().max(500).optional(),
body: z.string().max(40000).optional(),
Expand Down Expand Up @@ -1079,6 +1115,12 @@ const STDIO_TOOL_DESCRIPTORS = [
category: "review",
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.",
},
{
name: "loopover_check_improvement_potential",
category: "review",
description:
"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.",
},
{
name: "loopover_simulate_open_pr_pressure",
category: "discovery",
Expand Down Expand Up @@ -1857,6 +1899,18 @@ registerStdioTool(
(input: any) => toolResult("LoopOver slop-risk self-check.", { ...buildSlopAssessment(input), rubric: SLOP_RUBRIC_MARKDOWN }),
);

// #7759: CLI already proxies POST /v1/lint/improvement-potential (#6748); register the matching stdio tool.
// Proxies rather than computing in-process (same rationale as the CLI): builders live app-side, not in
// @loopover/engine. Forward the validated input object as the POST body — no local branching.
registerStdioTool(
"loopover_check_improvement_potential",
{
description: stdioToolDescription("loopover_check_improvement_potential"),
inputSchema: checkImprovementPotentialShape,
},
async (input: any) => toolResult("LoopOver improvement-potential self-check.", await apiPost("/v1/lint/improvement-potential", input)),
);

// #6751: CLI mirror of the remote server's loopover_simulate_open_pr_pressure. Proxies rather than computing
// in-process (like the boundary-tests mirror, #6750): simulateOpenPrPressure lives app-side in
// src/services/open-pr-pressure-scenarios.ts, not in @loopover/engine, so POST /v1/lint/open-pr-pressure stays
Expand Down
84 changes: 84 additions & 0 deletions test/unit/mcp-cli-improvement-potential-stdio.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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";

// #7759: in-process coverage for the loopover_check_improvement_potential stdio tool.
// Same #7764 entrypoint-guard pattern as sibling maintainer tools — import .ts, hold exported `server`,
// connect InMemoryTransport so v8/Codecov attributes registerStdioTool.
const MODULES = ["../../packages/loopover-mcp/bin/loopover-mcp.ts"] as const;

type BinModule = {
server: { connect: (transport: unknown) => Promise<void> };
};

let tempDir = "";
const capturedRequests: Array<{ url: string; method: string }> = [];
const loaded = new Map<string, BinModule>();

beforeAll(async () => {
tempDir = mkdtempSync(join(tmpdir(), "loopover-improvement-potential-stdio-"));
const apiUrl = await startFixtureServer({
onApiRequest: (request) => {
if (request.url && request.url.includes("/lint/improvement-potential")) {
capturedRequests.push({ url: request.url ?? "", method: request.method ?? "GET" });
}
},
});
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_check_improvement_potential stdio tool (in-process, #7759)", () => {
it.each(MODULES)("registers and proxies POST /v1/lint/improvement-potential - %s", async (specifier) => {
capturedRequests.length = 0;
const mod = loaded.get(specifier)!;
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await mod.server.connect(serverTransport);
const client = new Client({ name: "improvement-potential-stdio-test", version: "0.1.0" }, { capabilities: {} });
await client.connect(clientTransport);
try {
const { tools } = await client.listTools();
const tool = tools.find((entry) => entry.name === "loopover_check_improvement_potential");
expect(tool).toBeDefined();
expect(tool?.description).toMatch(/improvement/i);

const result = await client.callTool({
name: "loopover_check_improvement_potential",
arguments: {
changedFiles: [{ path: "src/widget.ts", additions: 80, deletions: 2 }],
testFiles: ["test/unit/widget.test.ts"],
},
});
expect(capturedRequests.length).toBe(1);
const captured = capturedRequests[0]!;
expect(captured.url).toContain("/v1/lint/improvement-potential");
expect(captured.method).toBe("POST");
expect(result.isError).toBeFalsy();
const text = JSON.stringify(result);
expect(text).toContain("improvementScore");
expect(text).toContain("minor");
expect(text).not.toMatch(/wallet|hotkey|reward|trust score/i);
} finally {
await client.close().catch(() => undefined);
}
});
});
11 changes: 6 additions & 5 deletions test/unit/mcp-tool-rename-aliases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
// (#7762 registered the loopover_mark_notifications_read stdio tool, taking the count from 89 to 90.)
// (#7760 registered the loopover_get_contributor_profile stdio tool, taking the count from 90 to 91.)
// (#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.)
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { mkdtempSync, rmSync } from "node:fs";
Expand Down Expand Up @@ -80,14 +81,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => {
});
afterEach(disconnect);

it("lists exactly 92 loopover_ tools and zero gittensory_-prefixed aliases", async () => {
it("lists exactly 93 loopover_ tools and zero gittensory_-prefixed aliases", async () => {
const { tools } = await client.listTools();
const names = tools.map((t) => t.name);
const primary = names.filter((n) => n.startsWith("loopover_"));
const legacy = names.filter((n) => n.startsWith("gittensory_"));
expect(primary.length).toBe(92);
expect(primary.length).toBe(93);
expect(legacy.length).toBe(0);
expect(names.length).toBe(92);
expect(names.length).toBe(93);
});

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

it("`loopover-mcp tools --json` reports the same 92-tool count the live server registers", async () => {
it("`loopover-mcp tools --json` reports the same 93-tool count the live server registers", async () => {
const { tools } = await client.listTools();
const payload = JSON.parse(run(["tools", "--json"])) as {
count: number;
tools: Array<{ name: string }>;
};
expect(payload.count).toBe(tools.length);
expect(payload.count).toBe(92);
expect(payload.count).toBe(93);
expect([...payload.tools.map((t) => t.name)].sort()).toEqual(
[...tools.map((t) => t.name)].sort(),
);
Expand Down