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
16 changes: 16 additions & 0 deletions packages/gittensory-mcp/bin/gittensory-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ const STDIO_TOOL_DESCRIPTORS = [
name: "gittensory_get_repo_context",
description: "Return the canonical repo intelligence bundle from the private Gittensory API.",
},
{
name: "gittensory_get_maintainer_noise",
description: "Return the maintainer queue-noise triage report for a repo: a noise score/level, the specific noise sources to clear first, and recommended maintainer actions. Maintainer-authenticated; advisory only.",
},
{
name: "gittensory_preflight_pr",
description: "Preflight planned PR metadata against lane, duplicate, linked issue, test, and queue signals.",
Expand Down Expand Up @@ -513,6 +517,18 @@ server.registerTool(
},
);

server.registerTool(
"gittensory_get_maintainer_noise",
{
description: stdioToolDescription("gittensory_get_maintainer_noise"),
inputSchema: ownerRepoShape,
},
async ({ owner, repo }) => {
const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
return toolResult("Gittensory maintainer noise report.", await apiGet(`${prefix}/maintainer-noise`));
},
);

server.registerTool(
"gittensory_preflight_pr",
{
Expand Down
15 changes: 15 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ import {
} from "../services/weekly-value-report";
import { generateAndSendReviewRecap } from "../services/review-recap";
import { loadOrComputeIssueQualityResponse } from "../services/issue-quality";
import { loadMaintainerNoiseReport } from "../services/maintainer-noise";
import { loadOrComputeBurdenForecastResponse } from "../services/burden-forecast";
import { buildUnavailableQueueTrendReport } from "../services/queue-trends";
import { loadOrComputeRepoOutcomePatternsResponse } from "../services/repo-outcome-patterns";
Expand Down Expand Up @@ -2504,6 +2505,15 @@ export function createApp() {
return c.json(await loadGatePrecisionReport(c.env, fullName, windowDays !== undefined ? { windowDays } : {}));
});

// #2228 maintainer queue-noise triage: read-only report for MCP stdio proxy + maintainer tooling.
// Maintainer-authenticated, repo-scoped; replaces the removed legacy public route with the same path shape.
app.get("/v1/repos/:owner/:repo/maintainer-noise", async (c) => {
const fullName = `${c.req.param("owner")}/${c.req.param("repo")}`;
const gate = await requireRepoMaintainer(c, fullName);
if (gate instanceof Response) return gate;
return c.json(await loadMaintainerNoiseReport(c.env, fullName));
});

// One-click "enable advisory mode" — turns on the gate + deterministic rules in advisory (non-blocking)
// mode. Merges onto current settings so unrelated fields are preserved.
app.post("/v1/repos/:owner/:repo/activation", async (c) => {
Expand Down Expand Up @@ -5258,6 +5268,7 @@ function canSessionAccessPath(env: Env, identity: Extract<AuthIdentity, { kind:
if (isRepoActivationPath(path)) return true;
if (isRepoOutcomeCalibrationPath(path)) return true;
if (isRepoGatePrecisionPath(path)) return true;
if (isRepoMaintainerNoisePath(path)) return true;
if (isRepoSettingsPreviewPath(path)) return true;
if (isRepoOnboardingPackPreviewPath(path)) return true;
if (isRepoFocusManifestPath(path)) return true;
Expand Down Expand Up @@ -5293,6 +5304,10 @@ function isRepoGatePrecisionPath(path: string): boolean {
return /^\/v1\/repos\/[^/]+\/[^/]+\/gate-precision$/.test(path);
}

function isRepoMaintainerNoisePath(path: string): boolean {
return /^\/v1\/repos\/[^/]+\/[^/]+\/maintainer-noise$/.test(path);
}

function isRepoSettingsPreviewPath(path: string): boolean {
return /^\/v1\/repos\/[^/]+\/[^/]+\/settings-preview$/.test(path);
}
Expand Down
12 changes: 11 additions & 1 deletion test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,17 @@ describe("api routes", () => {
const gatePrecisionNoWindow = await app.request("/v1/repos/entrius/allways-ui/gate-precision", { headers: apiHeaders(env) }, env);
await expect(gatePrecisionNoWindow.json()).resolves.toMatchObject({ windowDays: null });

const maintainerNoiseUnauthenticated = await app.request("/v1/repos/entrius/allways-ui/maintainer-noise", {}, env);
expect(maintainerNoiseUnauthenticated.status).toBe(401);
const maintainerNoise = await app.request("/v1/repos/entrius/allways-ui/maintainer-noise", { headers: apiHeaders(env) }, env);
expect(maintainerNoise.status).toBe(200);
await expect(maintainerNoise.json()).resolves.toMatchObject({
repoFullName: "entrius/allways-ui",
score: expect.any(Number),
level: expect.any(String),
noiseSources: expect.any(Array),
});

const settingsPreviewUnauthenticated = await app.request("/v1/repos/entrius/allways-ui/settings-preview", { method: "POST", body: "{}" }, env);
expect(settingsPreviewUnauthenticated.status).toBe(401);

Expand Down Expand Up @@ -931,7 +942,6 @@ describe("api routes", () => {
"/v1/repos/entrius/allways-ui/maintainer-lane",
"/v1/repos/entrius/allways-ui/maintainer-cut-readiness",
"/v1/repos/entrius/allways-ui/contributor-intake-health",
"/v1/repos/entrius/allways-ui/maintainer-noise",
]) {
const legacy = await app.request(path, { headers: apiHeaders(env) }, env);
expect(legacy.status).toBe(404);
Expand Down
83 changes: 83 additions & 0 deletions test/unit/mcp-cli-maintainer-noise.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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, run, 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-maintainer-noise-"));
capturedRequests = [];
apiUrl = await startFixtureServer({
onApiRequest: (request) => {
if (request.url && request.url.includes("/maintainer-noise")) {
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: "maintainer-noise-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_maintainer_noise 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((tool) => tool.name)).toContain("gittensory_get_maintainer_noise");
});

it("proxies the call to /maintainer-noise via apiGet and returns the payload", async () => {
const result = await client.callTool({
name: "gittensory_get_maintainer_noise",
arguments: { owner: "owner", repo: "repo" },
});
expect(capturedRequests.length).toBe(1);
const captured = capturedRequests[0]!;
expect(captured.url).toContain("/v1/repos/owner/repo/maintainer-noise");
expect(captured.method).toBe("GET");
expect(result.isError).toBeFalsy();
const text = JSON.stringify(result);
expect(text).not.toMatch(FORBIDDEN_PUBLIC_TERMS);
expect(text).toContain("owner/repo");
expect(text).toContain("noiseSources");
expect(text).toContain("medium");
});

it("lists the tool via gittensory-mcp tools", () => {
const payload = JSON.parse(run(["tools", "--json"])) as {
tools: Array<{ name: string; description: string }>;
};
const tool = payload.tools.find((entry) => entry.name === "gittensory_get_maintainer_noise");
expect(tool?.description).toMatch(/maintainer queue-noise triage report/i);
expect(tool?.description.trim().length).toBeGreaterThan(0);
});
});
15 changes: 15 additions & 0 deletions test/unit/support/mcp-cli-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,21 @@ export async function startFixtureServer(
response.end(JSON.stringify({ repoFullName: "owner/repo", pendingActions: [{ id: "pa-1", actionClass: "merge", pullNumber: 7, reason: "clean", status: "pending" }] }));
return;
}
if (request.url === "/v1/repos/owner/repo/maintainer-noise" && request.method === "GET") {
response.end(
JSON.stringify({
repoFullName: "owner/repo",
generatedAt: "2026-06-01T00:00:00.000Z",
score: 42,
level: "medium",
noiseSources: ["3 open PRs lack linked issue context."],
maintainerActions: ["review_now"],
queueHealth: { signals: { openPullRequests: 2 } },
summary: "Gittensory maintainer noise report for owner/repo: medium noise (score 42); 1 source(s) to triage.",
}),
);
return;
}
if (request.url?.startsWith("/v1/repos/owner/repo/agent/pending-actions/") && request.method === "POST") {
const accepted = request.url.endsWith("/accept");
response.end(JSON.stringify(accepted ? { status: "accepted", executionOutcome: "completed" } : { status: "rejected" }));
Expand Down