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
23 changes: 23 additions & 0 deletions packages/gittensory-mcp/bin/gittensory-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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",
{
Expand Down
71 changes: 71 additions & 0 deletions test/unit/mcp-cli-burden-forecast.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
11 changes: 11 additions & 0 deletions test/unit/support/mcp-cli-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down