diff --git a/packages/gittensory-mcp/bin/gittensory-mcp.js b/packages/gittensory-mcp/bin/gittensory-mcp.js index a46b9b550b..cc669001f5 100755 --- a/packages/gittensory-mcp/bin/gittensory-mcp.js +++ b/packages/gittensory-mcp/bin/gittensory-mcp.js @@ -517,6 +517,24 @@ server.registerTool( async () => toolResult("Gittensory upstream drift status.", await apiGet("/v1/upstream/drift")), ); +server.registerTool( + "gittensory_get_label_audit", + { + description: + "Return the repo's label-policy audit (configured-vs-live labels, missing configured labels, suspicious status/source-style labels, and trusted-label-pipeline readiness) from the private Gittensory API.", + inputSchema: ownerRepoShape, + }, + async ({ owner, repo }) => { + const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; + const intelligence = await apiGet(`${prefix}/intelligence`); + return toolResult("Gittensory label audit.", { + repoFullName: intelligence?.repoFullName ?? `${owner}/${repo}`, + generatedAt: intelligence?.generatedAt, + labelAudit: intelligence?.labelAudit ?? null, + }); + }, +); + server.registerTool( "gittensory_preview_local_pr_score", { diff --git a/test/unit/mcp-cli-label-audit.test.ts b/test/unit/mcp-cli-label-audit.test.ts new file mode 100644 index 0000000000..a9bcd414ca --- /dev/null +++ b/test/unit/mcp-cli-label-audit.test.ts @@ -0,0 +1,71 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +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 }>; + +async function connect() { + configDir = mkdtempSync(join(tmpdir(), "gittensory-label-audit-")); + capturedRequests = []; + apiUrl = await startFixtureServer({ + onApiRequest: (request) => { + if (request.url && request.url.includes("/intelligence")) { + capturedRequests.push({ url: request.url ?? "", method: request.method ?? "GET" }); + } + }, + }); + 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: "label-audit-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_get_label_audit stdio proxy", () => { + beforeEach(connect); + afterEach(disconnect); + + it("registers the tool in the stdio server tool list", async () => { + const { tools } = await client.listTools(); + expect(tools.map((t) => t.name)).toContain("gittensory_get_label_audit"); + }); + + it("proxies owner/repo to /v1/repos/:owner/:repo/intelligence via apiGet and returns the label audit", async () => { + const result = await client.callTool({ name: "gittensory_get_label_audit", arguments: { owner: "owner", repo: "repo" } }); + expect(capturedRequests.length).toBe(1); + const captured = capturedRequests[0]!; + expect(captured.url).toContain("/v1/repos/owner/repo/intelligence"); + expect(captured.method).toBe("GET"); + expect(result.isError).toBeFalsy(); + const text = JSON.stringify(result); + expect(text).not.toMatch(FORBIDDEN_PUBLIC_TERMS); + expect(text).toContain("labelAudit"); + expect(text).toContain("trustedLabelPipelineReady"); + expect(text).toContain("owner/repo"); + }); +}); diff --git a/test/unit/support/mcp-cli-harness.ts b/test/unit/support/mcp-cli-harness.ts index 2cb60e608b..9becf8d3d0 100644 --- a/test/unit/support/mcp-cli-harness.ts +++ b/test/unit/support/mcp-cli-harness.ts @@ -327,6 +327,24 @@ export async function startFixtureServer( ); return; } + if (request.url === "/v1/repos/owner/repo/intelligence" && request.method === "GET") { + response.end( + JSON.stringify({ + status: "ready", + source: "computed", + repoFullName: "owner/repo", + generatedAt: "2026-05-30T00:00:00.000Z", + labelAudit: { + configuredLabels: ["gittensor:feature", "gittensor:bug"], + liveLabels: ["gittensor:feature", "visual"], + missingConfiguredLabels: ["gittensor:bug"], + suspiciousLabels: ["visual"], + trustedLabelPipelineReady: false, + }, + }), + ); + return; + } response.statusCode = 404; response.end(JSON.stringify({ error: "not_found" })); });