Skip to content
Closed
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
38 changes: 38 additions & 0 deletions packages/gittensory-mcp/bin/gittensory-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,29 @@
repo: z.string().min(1),
issueNumber: z.number().int().positive().optional(),
title: z.string().min(1).optional(),
plannedPaths: z.array(z.string()).optional(),

Check notice on line 183 in packages/gittensory-mcp/bin/gittensory-mcp.js

View check run for this annotation

Loopover ORB / Gittensory Context

Review queue is busy

This repo has a busy review queue in the local Gittensory cache.
};

const findOpportunitiesShape = {
targets: z
.array(
z.object({
owner: z.string().min(1),
repo: z.string().min(1),
}),
)
.optional(),
searchQuery: z.string().min(1).max(500).optional(),
goalSpec: z
.object({
lane: z.string().min(1).optional(),
minRankScore: z.number().min(0).max(100).optional(),
languages: z.array(z.string()).optional(),
})
.optional(),
limit: z.number().int().min(1).max(50).optional(),
};

const lintPrTextShape = {
commitMessages: z.array(z.string()).max(50).optional(),
prBody: z.string().optional(),
Expand Down Expand Up @@ -379,6 +399,24 @@
},
);

server.registerTool(
"gittensory_find_opportunities",
{
description:
"Cross-repo discovery: find high-fit contribution opportunities across registered Gittensor repos. Returns a ranked, public-safe list filtered by your MinerGoalSpec (lane, min rank score, languages). Metadata-only, no GitHub writes.",
inputSchema: findOpportunitiesShape,
},
async ({ targets, searchQuery, goalSpec, limit }) => {
const body = {
...(targets && targets.length > 0 ? { targets } : {}),
...(searchQuery ? { searchQuery } : {}),
...(goalSpec ? { goalSpec } : {}),
...(limit != null ? { limit } : {}),
};
return toolResult("Gittensory cross-repo opportunities.", await apiPost("/v1/opportunities/find", body));
},
);

server.registerTool(
"gittensory_lint_pr_text",
{
Expand Down
106 changes: 106 additions & 0 deletions test/unit/mcp-cli-find-opportunities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";

Check notice on line 1 in test/unit/mcp-cli-find-opportunities.test.ts

View check run for this annotation

Loopover ORB / Gittensory Context

Review queue is busy

This repo has a busy review queue in the local Gittensory cache.
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harness";

const bin = join(process.cwd(), "packages/gittensory-mcp/bin/gittensory-mcp.js");
const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i;

let client: Client;
let transport: StdioClientTransport;
let configDir: string;
let apiUrl: string;
let capturedRequests: Array<{ url: string; method: string; body: string }>;

async function connect() {
configDir = mkdtempSync(join(tmpdir(), "gittensory-find-opp-"));
capturedRequests = [];
apiUrl = await startFixtureServer({
onApiRequest: (request) => {
if (request.url && request.url.includes("/v1/opportunities/find")) {
const chunks: Buffer[] = [];
request.on("data", (chunk: Buffer) => chunks.push(chunk));
request.on("end", () => {
capturedRequests.push({
url: request.url ?? "",
method: request.method ?? "GET",
body: Buffer.concat(chunks).toString("utf8"),
});
});
}
},
});
transport = new StdioClientTransport({
command: "node",
args: [bin, "--stdio"],
env: {
...process.env,
GITTENSORY_CONFIG_DIR: configDir,
GITTENSORY_API_URL: apiUrl,
GITTENSORY_TOKEN: "session-token",
GITTENSORY_API_TIMEOUT_MS: "5000",
},
});
client = new Client({ name: "find-opportunities-test", version: "0.0.1" });
await client.connect(transport);
}

async function disconnect() {
await client.close().catch(() => undefined);
await closeFixtureServer();
if (configDir) rmSync(configDir, { recursive: true, force: true });
}

describe("gittensory_find_opportunities stdio proxy", () => {
beforeEach(connect);
afterEach(disconnect);

it("registers the tool in the stdio server's tool list", async () => {
const { tools } = await client.listTools();
const names = tools.map((t) => t.name);
expect(names).toContain("gittensory_find_opportunities");
});

it("proxies the call to /v1/opportunities/find via apiPost", async () => {
await client.callTool({
name: "gittensory_find_opportunities",
arguments: { searchQuery: "test coverage", limit: 3 },
});
expect(capturedRequests.length).toBe(1);
const captured = capturedRequests[0]!;
expect(captured.url).toContain("/v1/opportunities/find");
expect(captured.method).toBe("POST");
const parsedBody = JSON.parse(captured.body) as { searchQuery?: string; limit?: number };
expect(parsedBody.searchQuery).toBe("test coverage");
expect(parsedBody.limit).toBe(3);
});

it("returns a ranked, public-safe list of opportunities", async () => {
const result = await client.callTool({
name: "gittensory_find_opportunities",
arguments: { searchQuery: "scoring", limit: 2 },
});
expect(result.isError).toBeFalsy();
const text = JSON.stringify(result);
expect(text).not.toMatch(FORBIDDEN_PUBLIC_TERMS);
expect(text).toContain("rankScore");
expect(text).toContain("laneFit");
expect(text).toContain("aiPolicyAllowed");
});

it("strips undefined optional fields from the proxied body", async () => {
await client.callTool({
name: "gittensory_find_opportunities",
arguments: { searchQuery: "minimum" },
});
expect(capturedRequests.length).toBe(1);
const parsedBody = JSON.parse(capturedRequests[0]!.body) as Record<string, unknown>;
expect(parsedBody.searchQuery).toBe("minimum");
expect("targets" in parsedBody).toBe(false);
expect("goalSpec" in parsedBody).toBe(false);
expect("limit" in parsedBody).toBe(false);
});
});
21 changes: 21 additions & 0 deletions test/unit/support/mcp-cli-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,30 @@
}
if (request.url === "/v1/lint/pr-text" && request.method === "POST") {
const body = (await readJsonRequest(request)) as { commitMessages?: string[]; prBody?: string; linkedIssue?: number };
response.end(JSON.stringify(lintPrTextFixture(body)));

Check notice on line 234 in test/unit/support/mcp-cli-harness.ts

View check run for this annotation

Loopover ORB / Gittensory Context

Review queue is busy

This repo has a busy review queue in the local Gittensory cache.
return;
}
if (request.url === "/v1/opportunities/find" && request.method === "POST") {
const body = (await readJsonRequest(request)) as {
targets?: Array<{ owner: string; repo: string }>;
searchQuery?: string;
goalSpec?: { lane?: string; minRankScore?: number; languages?: string[] };
limit?: number;
};
const limit = body.limit ?? 5;
const lane = body.goalSpec?.lane ?? "default";
const minRank = body.goalSpec?.minRankScore ?? 0;
const candidates = [
{ owner: "JSONbored", repo: "gittensory", issueNumber: 100, title: "Improve REES test retry", rankScore: 85, laneFit: lane, freshness: 0.9, dupRisk: 0.1, aiPolicyAllowed: true },
{ owner: "JSONbored", repo: "gittensory", issueNumber: 101, title: "Add label-audit coverage", rankScore: 72, laneFit: lane, freshness: 0.7, dupRisk: 0.2, aiPolicyAllowed: true },
{ owner: "JSONbored", repo: "gittensory", issueNumber: 102, title: "Fix flaky buildBrief test", rankScore: 68, laneFit: lane, freshness: 0.5, dupRisk: 0.3, aiPolicyAllowed: true },
{ owner: "JSONbored", repo: "gittensory", issueNumber: 103, title: "Normalize path matchers", rankScore: 55, laneFit: lane, freshness: 0.4, dupRisk: 0.1, aiPolicyAllowed: true },
{ owner: "JSONbored", repo: "gittensory", issueNumber: 104, title: "Document score breakdown", rankScore: 45, laneFit: lane, freshness: 0.3, dupRisk: 0.1, aiPolicyAllowed: true },
];
const ranked = candidates.filter((c) => c.rankScore >= minRank).slice(0, limit);
response.end(JSON.stringify({ ranked, totalCandidates: candidates.length, appliedLane: lane, appliedMinRankScore: minRank }));
return;
}
// #784 maintainer controls (agent approval queue + kill-switch).
if (request.url === "/v1/repos/owner/repo/agent/pending-actions" && request.method === "GET") {
response.end(JSON.stringify({ repoFullName: "owner/repo", pendingActions: [{ id: "pa-1", actionClass: "merge", pullNumber: 7, reason: "clean", status: "pending" }] }));
Expand Down
Loading