diff --git a/packages/gittensory-mcp/bin/gittensory-mcp.js b/packages/gittensory-mcp/bin/gittensory-mcp.js index de6bc1caed..298f3b60cb 100644 --- a/packages/gittensory-mcp/bin/gittensory-mcp.js +++ b/packages/gittensory-mcp/bin/gittensory-mcp.js @@ -413,6 +413,11 @@ const STDIO_TOOL_DESCRIPTORS = [ 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.", }, + { + name: "gittensory_get_burden_forecast", + description: + "Return the repo's cached maintainer burden forecast (projected review load, queue-growth risk, and stale-PR signals) with a freshness marker, from the private Gittensory API.", + }, { name: "gittensory_preview_local_pr_score", description: "Inspect local diff metadata and request a private Gittensory scoring preview. No source contents are uploaded.", @@ -689,6 +694,24 @@ server.registerTool( }, ); +server.registerTool( + "gittensory_get_burden_forecast", + { + description: stdioToolDescription("gittensory_get_burden_forecast"), + inputSchema: ownerRepoShape, + }, + async ({ owner, repo }) => { + const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; + const intelligence = await apiGet(`${prefix}/intelligence`); + return toolResult("Gittensory burden forecast.", { + repoFullName: intelligence?.repoFullName ?? `${owner}/${repo}`, + generatedAt: intelligence?.generatedAt, + burdenForecast: intelligence?.burdenForecast ?? null, + burdenForecastFreshness: intelligence?.burdenForecastFreshness ?? null, + }); + }, +); + server.registerTool( "gittensory_preview_local_pr_score", { diff --git a/test/unit/mcp-cli-burden-forecast.test.ts b/test/unit/mcp-cli-burden-forecast.test.ts new file mode 100644 index 0000000000..8e44c5060e --- /dev/null +++ b/test/unit/mcp-cli-burden-forecast.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-burden-forecast-")); + 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: "burden-forecast-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_burden_forecast 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_burden_forecast"); + }); + + it("proxies owner/repo to /v1/repos/:owner/:repo/intelligence via apiGet and returns the burden forecast", async () => { + const result = await client.callTool({ name: "gittensory_get_burden_forecast", 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("burdenForecast"); + expect(text).toContain("queueGrowthRisk"); + expect(text).toContain("owner/repo"); + }); +}); diff --git a/test/unit/support/mcp-cli-harness.ts b/test/unit/support/mcp-cli-harness.ts index 8155f1805d..ff99f37c5c 100644 --- a/test/unit/support/mcp-cli-harness.ts +++ b/test/unit/support/mcp-cli-harness.ts @@ -374,6 +374,17 @@ export async function startFixtureServer( suspiciousLabels: ["visual"], trustedLabelPipelineReady: false, }, + burdenForecast: { + projectedReviewLoad: "elevated", + queueGrowthRisk: "medium", + stalePrSignals: ["#101 idle 21d"], + }, + burdenForecastFreshness: { + source: "cache", + generatedAt: "2026-05-30T00:00:00.000Z", + ageSeconds: 120, + freshness: "fresh", + }, }), ); return;