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
14 changes: 7 additions & 7 deletions packages/loopover-mcp/bin/loopover-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ const agentRunIdShape = {
const STDIO_TOOL_DESCRIPTORS = [
{
name: "loopover_get_repo_context",
description: "Return the canonical repo intelligence bundle from the private LoopOver API.",
description: "Return the LoopOver repo-context bundle for a repo — registration state, recommended contribution lane, queue health, duplicate-PR collisions, and config quality — from the private LoopOver API. Takes owner and repo.",
},
{
name: "loopover_get_pr_reviewability",
Expand Down Expand Up @@ -496,7 +496,7 @@ const STDIO_TOOL_DESCRIPTORS = [
},
{
name: "loopover_get_registry_changes",
description: "Return latest cached Gittensor registry change report.",
description: "Return the latest cached report of changes to the Gittensor repo registry — repositories added, removed, or re-registered upstream. Read-only; takes no parameters.",
},
{
name: "loopover_get_upstream_drift",
Expand All @@ -522,11 +522,11 @@ const STDIO_TOOL_DESCRIPTORS = [
},
{
name: "loopover_get_decision_pack",
description: "Return the canonical private contributor decision pack for a GitHub login.",
description: "Return the private decision pack for a contributor: the ranked repos and issues to work on next, with per-repo go/raise/avoid guidance. Takes login (the contributor's GitHub username).",
},
{
name: "loopover_explain_repo_decision",
description: "Return the contributor/repo decision from the canonical decision pack.",
description: "Return the go/raise/avoid decision for one specific contributor-and-repo pair, drawn from that contributor's decision pack — narrower than loopover_get_decision_pack, which returns the whole pack. Takes login (GitHub username), owner, and repo.",
},
{
name: "loopover_compare_pr_variants",
Expand Down Expand Up @@ -570,15 +570,15 @@ const STDIO_TOOL_DESCRIPTORS = [
},
{
name: "loopover_agent_plan_next_work",
description: "Run the deterministic LoopOver base-agent planner for a GitHub login.",
description: "Run the deterministic LoopOver planner for a contributor and return the single recommended next unit of work (repo, issue, and action). Planning only — does not queue or start a run. Takes login (GitHub username); optional objective and repoFullName narrow the result.",
},
{
name: "loopover_agent_start_run",
description: "Create a queued copilot-only LoopOver base-agent run.",
description: "Queue a new LoopOver automated-agent run for a contributor. Copilot mode only: it proposes and records work but takes no GitHub actions on its own. Takes objective (what to accomplish) and actorLogin (the contributor's GitHub username); returns the new run's id and status.",
},
{
name: "loopover_agent_get_run",
description: "Fetch a persisted LoopOver base-agent run.",
description: "Fetch a previously queued LoopOver agent run by its id, including current status and planned actions. Takes runId (the id returned by loopover_agent_start_run).",
},
{
name: "loopover_agent_explain_next_action",
Expand Down
28 changes: 28 additions & 0 deletions test/unit/mcp-discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,34 @@ describe("MCP workspace root boundaries", () => {
});
});

describe("MCP tool discovery", () => {
beforeEach(connect);
afterEach(disconnect);

it("every tool exposes a non-empty description unique enough to disambiguate from its siblings", async () => {
const { tools } = await client.listTools();
expect(tools.length).toBeGreaterThan(0);
// An LLM client picks a tool from its description text, so two tools sharing identical
// wording are indistinguishable and get mis-selected (#6245).
const owners = new Map<string, string>();
for (const tool of tools) {
const description = (tool.description ?? "").trim();
expect(description.length, `empty description for ${tool.name}`).toBeGreaterThan(0);
const clash = owners.get(description);
expect(clash, `${tool.name} duplicates the description of ${clash}`).toBeUndefined();
owners.set(description, tool.name);
}
});

it("tool descriptions do not expose forbidden public terms", async () => {
const { tools } = await client.listTools();
for (const tool of tools) {
const text = [tool.name, tool.description ?? ""].join(" ");
expect(text).not.toMatch(FORBIDDEN_PUBLIC_TERMS);
}
});
});

describe("MCP resource discovery", () => {
beforeEach(connect);
afterEach(disconnect);
Expand Down