@@ -7,6 +7,9 @@ import { tmpdir } from "node:os";
77import { join } from "node:path" ;
88import { pathToFileURL } from "node:url" ;
99import { afterEach , beforeEach , describe , expect , it } from "vitest" ;
10+ import { closeFixtureServer , startFixtureServer } from "./support/mcp-cli-harness" ;
11+ import { ENRICHMENT_ANALYZERS_URI } from "../../src/review/enrichment-analyzers-taxonomy" ;
12+ import { FINDING_TAXONOMY_URI } from "../../src/review/finding-taxonomy" ;
1013
1114const bin = join ( process . cwd ( ) , "packages/loopover-mcp/bin/loopover-mcp.js" ) ;
1215
@@ -115,6 +118,8 @@ describe("MCP resource discovery", () => {
115118 const uris = resources . map ( ( r ) => r . uri ) ;
116119 expect ( uris ) . toContain ( "loopover://changelog" ) ;
117120 expect ( uris ) . toContain ( "loopover://compatibility" ) ;
121+ expect ( uris ) . toContain ( FINDING_TAXONOMY_URI ) ;
122+ expect ( uris ) . toContain ( ENRICHMENT_ANALYZERS_URI ) ;
118123 } ) ;
119124
120125 it ( "resource descriptions do not expose forbidden public terms" , async ( ) => {
@@ -151,13 +156,99 @@ describe("MCP resource discovery", () => {
151156 expect ( ( ) => JSON . parse ( content . text ?? "" ) ) . not . toThrow ( ) ;
152157 } ) ;
153158
159+ it ( "can read the finding-taxonomy resource and get structured JSON" , async ( ) => {
160+ const result = await client . readResource ( { uri : FINDING_TAXONOMY_URI } ) ;
161+ expect ( result . contents ) . toHaveLength ( 1 ) ;
162+ const content = result . contents [ 0 ] ;
163+ expect ( content ?. mimeType ) . toBe ( "application/json" ) ;
164+ if ( ! content || ! ( "text" in content ) ) throw new Error ( "expected text content" ) ;
165+ expect ( ( ) => JSON . parse ( content . text ?? "" ) ) . not . toThrow ( ) ;
166+ } ) ;
167+
168+ it ( "can read the enrichment-analyzers resource and get structured JSON" , async ( ) => {
169+ const result = await client . readResource ( { uri : ENRICHMENT_ANALYZERS_URI } ) ;
170+ expect ( result . contents ) . toHaveLength ( 1 ) ;
171+ const content = result . contents [ 0 ] ;
172+ expect ( content ?. mimeType ) . toBe ( "application/json" ) ;
173+ if ( ! content || ! ( "text" in content ) ) throw new Error ( "expected text content" ) ;
174+ expect ( ( ) => JSON . parse ( content . text ?? "" ) ) . not . toThrow ( ) ;
175+ } ) ;
176+
154177 it ( "decision-pack resource template is discoverable" , async ( ) => {
155178 const { resourceTemplates } = await client . listResourceTemplates ( ) ;
156179 const names = resourceTemplates . map ( ( t ) => t . name ) ;
157180 expect ( names ) . toContain ( "loopover_decision_pack" ) ;
158181 } ) ;
159182} ) ;
160183
184+ describe ( "MCP taxonomy resource mirrors (#6620)" , ( ) => {
185+ let fixtureUrl : string ;
186+ let fixtureConfigDir : string ;
187+ let fixtureClient : Client ;
188+ let fixtureTransport : StdioClientTransport ;
189+
190+ async function connectFixtureClient ( ) {
191+ fixtureConfigDir = mkdtempSync ( join ( tmpdir ( ) , "gittensory-taxonomy-mirror-" ) ) ;
192+ fixtureTransport = new StdioClientTransport ( {
193+ command : "node" ,
194+ args : [ bin , "--stdio" ] ,
195+ env : {
196+ ...process . env ,
197+ LOOPOVER_API_URL : fixtureUrl ,
198+ LOOPOVER_CONFIG_DIR : fixtureConfigDir ,
199+ LOOPOVER_API_TIMEOUT_MS : "1000" ,
200+ } ,
201+ } ) ;
202+ fixtureClient = new Client ( { name : "taxonomy-mirror-test" , version : "0.0.1" } ) ;
203+ await fixtureClient . connect ( fixtureTransport ) ;
204+ }
205+
206+ afterEach ( async ( ) => {
207+ await fixtureClient ?. close ( ) . catch ( ( ) => undefined ) ;
208+ if ( fixtureConfigDir ) rmSync ( fixtureConfigDir , { recursive : true , force : true } ) ;
209+ await closeFixtureServer ( ) ;
210+ } ) ;
211+
212+ it ( "reads finding taxonomy from the API mirror when available" , async ( ) => {
213+ fixtureUrl = await startFixtureServer ( { } ) ;
214+ await connectFixtureClient ( ) ;
215+ const result = await fixtureClient . readResource ( { uri : FINDING_TAXONOMY_URI } ) ;
216+ const content = result . contents [ 0 ] ;
217+ if ( ! content || ! ( "text" in content ) ) throw new Error ( "expected text content" ) ;
218+ const body = JSON . parse ( content . text ?? "" ) as { categories : string [ ] ; severities : string [ ] } ;
219+ expect ( body . categories . length ) . toBeGreaterThan ( 0 ) ;
220+ expect ( body . severities . length ) . toBeGreaterThan ( 0 ) ;
221+ } ) ;
222+
223+ it ( "falls back to unavailable when finding-taxonomy API fetch fails" , async ( ) => {
224+ fixtureUrl = await startFixtureServer ( { findingTaxonomyStatus : 503 } ) ;
225+ await connectFixtureClient ( ) ;
226+ const result = await fixtureClient . readResource ( { uri : FINDING_TAXONOMY_URI } ) ;
227+ const content = result . contents [ 0 ] ;
228+ if ( ! content || ! ( "text" in content ) ) throw new Error ( "expected text content" ) ;
229+ expect ( JSON . parse ( content . text ?? "" ) ) . toMatchObject ( { status : "unavailable" } ) ;
230+ } ) ;
231+
232+ it ( "reads enrichment analyzers from the API mirror when available" , async ( ) => {
233+ fixtureUrl = await startFixtureServer ( { } ) ;
234+ await connectFixtureClient ( ) ;
235+ const result = await fixtureClient . readResource ( { uri : ENRICHMENT_ANALYZERS_URI } ) ;
236+ const content = result . contents [ 0 ] ;
237+ if ( ! content || ! ( "text" in content ) ) throw new Error ( "expected text content" ) ;
238+ const body = JSON . parse ( content . text ?? "" ) as { analyzers : Array < { name : string } > } ;
239+ expect ( body . analyzers . length ) . toBeGreaterThan ( 0 ) ;
240+ } ) ;
241+
242+ it ( "falls back to unavailable when enrichment-analyzers API fetch fails" , async ( ) => {
243+ fixtureUrl = await startFixtureServer ( { enrichmentAnalyzersStatus : 503 } ) ;
244+ await connectFixtureClient ( ) ;
245+ const result = await fixtureClient . readResource ( { uri : ENRICHMENT_ANALYZERS_URI } ) ;
246+ const content = result . contents [ 0 ] ;
247+ if ( ! content || ! ( "text" in content ) ) throw new Error ( "expected text content" ) ;
248+ expect ( JSON . parse ( content . text ?? "" ) ) . toMatchObject ( { status : "unavailable" } ) ;
249+ } ) ;
250+ } ) ;
251+
161252describe ( "MCP prompt discovery" , ( ) => {
162253 beforeEach ( connect ) ;
163254 afterEach ( disconnect ) ;
0 commit comments