Skip to content

Commit 90da00d

Browse files
feat(mcp-cli): add stdio tool gittensory_get_burden_forecast (#4324)
Register gittensory_get_burden_forecast in the stdio bin, proxying the public GET /v1/repos/:owner/:repo/intelligence route via apiGet and surfacing the burdenForecast + burdenForecastFreshness slice. Mirrors the sibling gittensory_get_label_audit tool. Adds a subprocess stdio test and a fixture intelligence route. Bin + test only, no src changes. Closes #2230 Co-authored-by: e11734937-beep <e11734937-beep@users.noreply.github.com>
1 parent c4432b4 commit 90da00d

3 files changed

Lines changed: 105 additions & 0 deletions

File tree

packages/gittensory-mcp/bin/gittensory-mcp.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,11 @@ const STDIO_TOOL_DESCRIPTORS = [
413413
description:
414414
"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.",
415415
},
416+
{
417+
name: "gittensory_get_burden_forecast",
418+
description:
419+
"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.",
420+
},
416421
{
417422
name: "gittensory_preview_local_pr_score",
418423
description: "Inspect local diff metadata and request a private Gittensory scoring preview. No source contents are uploaded.",
@@ -689,6 +694,24 @@ server.registerTool(
689694
},
690695
);
691696

697+
server.registerTool(
698+
"gittensory_get_burden_forecast",
699+
{
700+
description: stdioToolDescription("gittensory_get_burden_forecast"),
701+
inputSchema: ownerRepoShape,
702+
},
703+
async ({ owner, repo }) => {
704+
const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
705+
const intelligence = await apiGet(`${prefix}/intelligence`);
706+
return toolResult("Gittensory burden forecast.", {
707+
repoFullName: intelligence?.repoFullName ?? `${owner}/${repo}`,
708+
generatedAt: intelligence?.generatedAt,
709+
burdenForecast: intelligence?.burdenForecast ?? null,
710+
burdenForecastFreshness: intelligence?.burdenForecastFreshness ?? null,
711+
});
712+
},
713+
);
714+
692715
server.registerTool(
693716
"gittensory_preview_local_pr_score",
694717
{
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3+
import { mkdtempSync, rmSync } from "node:fs";
4+
import { tmpdir } from "node:os";
5+
import { join } from "node:path";
6+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
7+
import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harness";
8+
9+
const bin = join(process.cwd(), "packages/gittensory-mcp/bin/gittensory-mcp.js");
10+
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;
11+
12+
let client: Client;
13+
let transport: StdioClientTransport;
14+
let configDir: string;
15+
let apiUrl: string;
16+
let capturedRequests: Array<{ url: string; method: string }>;
17+
18+
async function connect() {
19+
configDir = mkdtempSync(join(tmpdir(), "gittensory-burden-forecast-"));
20+
capturedRequests = [];
21+
apiUrl = await startFixtureServer({
22+
onApiRequest: (request) => {
23+
if (request.url && request.url.includes("/intelligence")) {
24+
capturedRequests.push({ url: request.url ?? "", method: request.method ?? "GET" });
25+
}
26+
},
27+
});
28+
transport = new StdioClientTransport({
29+
command: "node",
30+
args: [bin, "--stdio"],
31+
env: {
32+
...process.env,
33+
GITTENSORY_CONFIG_DIR: configDir,
34+
GITTENSORY_API_URL: apiUrl,
35+
GITTENSORY_TOKEN: "session-token",
36+
GITTENSORY_API_TIMEOUT_MS: "5000",
37+
},
38+
});
39+
client = new Client({ name: "burden-forecast-test", version: "0.0.1" });
40+
await client.connect(transport);
41+
}
42+
43+
async function disconnect() {
44+
await client.close().catch(() => undefined);
45+
await closeFixtureServer();
46+
if (configDir) rmSync(configDir, { recursive: true, force: true });
47+
}
48+
49+
describe("gittensory_get_burden_forecast stdio proxy", () => {
50+
beforeEach(connect);
51+
afterEach(disconnect);
52+
53+
it("registers the tool in the stdio server tool list", async () => {
54+
const { tools } = await client.listTools();
55+
expect(tools.map((t) => t.name)).toContain("gittensory_get_burden_forecast");
56+
});
57+
58+
it("proxies owner/repo to /v1/repos/:owner/:repo/intelligence via apiGet and returns the burden forecast", async () => {
59+
const result = await client.callTool({ name: "gittensory_get_burden_forecast", arguments: { owner: "owner", repo: "repo" } });
60+
expect(capturedRequests.length).toBe(1);
61+
const captured = capturedRequests[0]!;
62+
expect(captured.url).toContain("/v1/repos/owner/repo/intelligence");
63+
expect(captured.method).toBe("GET");
64+
expect(result.isError).toBeFalsy();
65+
const text = JSON.stringify(result);
66+
expect(text).not.toMatch(FORBIDDEN_PUBLIC_TERMS);
67+
expect(text).toContain("burdenForecast");
68+
expect(text).toContain("queueGrowthRisk");
69+
expect(text).toContain("owner/repo");
70+
});
71+
});

test/unit/support/mcp-cli-harness.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,17 @@ export async function startFixtureServer(
374374
suspiciousLabels: ["visual"],
375375
trustedLabelPipelineReady: false,
376376
},
377+
burdenForecast: {
378+
projectedReviewLoad: "elevated",
379+
queueGrowthRisk: "medium",
380+
stalePrSignals: ["#101 idle 21d"],
381+
},
382+
burdenForecastFreshness: {
383+
source: "cache",
384+
generatedAt: "2026-05-30T00:00:00.000Z",
385+
ageSeconds: 120,
386+
freshness: "fresh",
387+
},
377388
}),
378389
);
379390
return;

0 commit comments

Comments
 (0)