diff --git a/src/mcp/server.ts b/src/mcp/server.ts index efa8b3ff03..45a6614e33 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -149,6 +149,7 @@ import { PREFLIGHT_LIMITS } from "../signals/preflight-limits"; import { SCENARIO_MAX_BRANCH_REF_CHARS, SCENARIO_MAX_LINKED_ISSUE_NUMBERS, SCENARIO_MAX_REPO_FULL_NAME_CHARS } from "../scenarios/input-model"; import { loadUpstreamStatus } from "../upstream/ruleset"; import { simulateOpenPrPressure, type OpenPrPressureInput } from "../services/open-pr-pressure-scenarios"; +import { buildFindingTaxonomyDocument, FINDING_TAXONOMY_URI } from "../review/finding-taxonomy"; type AppContext = Context<{ Bindings: Env }>; type ToolPayload = { @@ -2007,6 +2008,26 @@ export class GittensoryMcp { }), ); + // #2225 — read-only taxonomy discovery for AI review finding categories + severity ladder. + server.registerResource( + "gittensory_finding_taxonomy", + FINDING_TAXONOMY_URI, + { + title: "Gittensory Finding Taxonomy", + description: "Canonical AI review finding categories and severity levels for discovery without hard-coding.", + mimeType: "application/json", + }, + async () => ({ + contents: [ + { + uri: FINDING_TAXONOMY_URI, + mimeType: "application/json", + text: JSON.stringify(buildFindingTaxonomyDocument(), null, 2), + }, + ], + }), + ); + return server; } diff --git a/src/review/finding-taxonomy.ts b/src/review/finding-taxonomy.ts new file mode 100644 index 0000000000..64d2f6cf4e --- /dev/null +++ b/src/review/finding-taxonomy.ts @@ -0,0 +1,18 @@ +import { FINDING_CATEGORIES } from "./finding-category-classify"; +import { REVIEW_FINDING_SEVERITY_LADDER } from "../signals/focus-manifest"; + +/** MCP resource URI for the canonical review finding taxonomy (#2225). */ +export const FINDING_TAXONOMY_URI = "gittensory://finding-taxonomy" as const; + +export interface FindingTaxonomyDocument { + categories: readonly (typeof FINDING_CATEGORIES)[number][]; + severities: readonly (typeof REVIEW_FINDING_SEVERITY_LADDER)[number][]; +} + +/** Static, machine-readable taxonomy for AI review findings — categories + severity ladder. */ +export function buildFindingTaxonomyDocument(): FindingTaxonomyDocument { + return { + categories: [...FINDING_CATEGORIES], + severities: [...REVIEW_FINDING_SEVERITY_LADDER], + }; +} diff --git a/test/unit/finding-taxonomy.test.ts b/test/unit/finding-taxonomy.test.ts new file mode 100644 index 0000000000..e4ea8e70e5 --- /dev/null +++ b/test/unit/finding-taxonomy.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { FINDING_CATEGORIES } from "../../src/review/finding-category-classify"; +import { buildFindingTaxonomyDocument, FINDING_TAXONOMY_URI } from "../../src/review/finding-taxonomy"; +import { REVIEW_FINDING_SEVERITY_LADDER } from "../../src/signals/focus-manifest"; + +describe("finding taxonomy document", () => { + it("exposes the canonical category enum and severity ladder", () => { + const doc = buildFindingTaxonomyDocument(); + expect(doc.categories).toEqual([...FINDING_CATEGORIES]); + expect(doc.severities).toEqual([...REVIEW_FINDING_SEVERITY_LADDER]); + }); + + it("includes every category and severity value exactly once", () => { + const doc = buildFindingTaxonomyDocument(); + for (const category of FINDING_CATEGORIES) { + expect(doc.categories).toContain(category); + } + for (const severity of REVIEW_FINDING_SEVERITY_LADDER) { + expect(doc.severities).toContain(severity); + } + expect(new Set(doc.categories).size).toBe(FINDING_CATEGORIES.length); + expect(new Set(doc.severities).size).toBe(REVIEW_FINDING_SEVERITY_LADDER.length); + }); + + it("uses the stable MCP resource URI", () => { + expect(FINDING_TAXONOMY_URI).toBe("gittensory://finding-taxonomy"); + }); +}); diff --git a/test/unit/mcp-finding-taxonomy.test.ts b/test/unit/mcp-finding-taxonomy.test.ts new file mode 100644 index 0000000000..6bdfc845ee --- /dev/null +++ b/test/unit/mcp-finding-taxonomy.test.ts @@ -0,0 +1,43 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { describe, expect, it } from "vitest"; +import { GittensoryMcp } from "../../src/mcp/server"; +import { FINDING_CATEGORIES } from "../../src/review/finding-category-classify"; +import { FINDING_TAXONOMY_URI } from "../../src/review/finding-taxonomy"; +import { REVIEW_FINDING_SEVERITY_LADDER } from "../../src/signals/focus-manifest"; +import { createTestEnv } from "../helpers/d1"; + +async function connectTestClient() { + const mcpServer = new GittensoryMcp(createTestEnv()).createServer(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await mcpServer.connect(serverTransport); + const client = new Client({ name: "gittensory-finding-taxonomy-test", version: "0.1.0" }, { capabilities: {} }); + await client.connect(clientTransport); + return { client, mcpServer }; +} + +describe("MCP finding-taxonomy resource (#2225)", () => { + it("discovers the finding-taxonomy resource", async () => { + const { client } = await connectTestClient(); + const { resources } = await client.listResources(); + expect(resources.map((r) => r.uri)).toContain(FINDING_TAXONOMY_URI); + }); + + it("returns the canonical categories and severities as JSON", async () => { + const { client } = await connectTestClient(); + const result = await client.readResource({ uri: FINDING_TAXONOMY_URI }); + expect(result.contents).toHaveLength(1); + const content = result.contents[0]; + expect(content?.mimeType).toBe("application/json"); + if (!content || !("text" in content)) throw new Error("expected text content"); + const body = JSON.parse(content.text ?? "") as { categories: string[]; severities: string[] }; + expect(body.categories).toEqual([...FINDING_CATEGORIES]); + expect(body.severities).toEqual([...REVIEW_FINDING_SEVERITY_LADDER]); + for (const category of FINDING_CATEGORIES) { + expect(body.categories).toContain(category); + } + for (const severity of REVIEW_FINDING_SEVERITY_LADDER) { + expect(body.severities).toContain(severity); + } + }); +});