Skip to content

Commit e7b5b6b

Browse files
committed
Migrate language-specific MCP resources
1 parent f482909 commit e7b5b6b

24 files changed

+5757
-321
lines changed

extensions/vscode/esbuild.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const testSuiteConfig = {
3636
'test/suite/index.ts',
3737
'test/suite/bridge.integration.test.ts',
3838
'test/suite/extension.integration.test.ts',
39+
'test/suite/mcp-resource-e2e.integration.test.ts',
3940
'test/suite/mcp-server.integration.test.ts',
4041
'test/suite/mcp-tool-e2e.integration.test.ts',
4142
'test/suite/workspace-scenario.integration.test.ts',
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/**
2+
* Integration tests for MCP server resource endpoints.
3+
*
4+
* These run inside the Extension Development Host with the REAL VS Code API.
5+
* They spawn the actual `ql-mcp` server process, connect via
6+
* StdioClientTransport, list all resources, and read each one to verify
7+
* that no resource returns fallback "not found" content.
8+
*
9+
* This test suite exists to catch the class of bugs where:
10+
* - Resource files are configured with wrong paths (e.g. `ql/languages/<lang>/` vs `ql/<lang>/`)
11+
* - Resource content is not embedded in the bundle (esbuild `.md: 'text'` loader misconfigured)
12+
* - Resource file extensions are wrong (e.g. `.prompt.md` vs `.md`)
13+
*
14+
* These bugs manifest in VS Code as: Command Palette → "MCP: Browse Resources..."
15+
* → selecting any ql-mcp resource → seeing "Resource file not found or could not be loaded."
16+
*/
17+
18+
import * as assert from 'assert';
19+
import * as fs from 'fs';
20+
import * as path from 'path';
21+
import * as vscode from 'vscode';
22+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
23+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
24+
25+
const EXTENSION_ID = 'advanced-security.vscode-codeql-development-mcp-server';
26+
27+
/** Sentinel text returned when a language resource file cannot be loaded. */
28+
const NOT_FOUND_SENTINEL = 'Resource file not found or could not be loaded';
29+
30+
/** Extract text from a readResource content item (text or blob union). */
31+
function extractText(item: { uri: string; text?: string; blob?: string }): string {
32+
return (item as { text?: string }).text ?? '';
33+
}
34+
35+
/**
36+
* Resolve the MCP server entry point.
37+
*/
38+
function resolveServerPath(): string {
39+
const extPath = vscode.extensions.getExtension(EXTENSION_ID)?.extensionUri.fsPath;
40+
if (!extPath) throw new Error('Extension not found');
41+
42+
const monorepo = path.resolve(extPath, '..', '..', 'server', 'dist', 'codeql-development-mcp-server.js');
43+
try {
44+
fs.accessSync(monorepo);
45+
return monorepo;
46+
} catch {
47+
// Fall through
48+
}
49+
50+
const vsix = path.resolve(extPath, 'server', 'dist', 'codeql-development-mcp-server.js');
51+
try {
52+
fs.accessSync(vsix);
53+
return vsix;
54+
} catch {
55+
throw new Error(`MCP server not found at ${monorepo} or ${vsix}`);
56+
}
57+
}
58+
59+
suite('MCP Resource Integration Tests', () => {
60+
let client: Client;
61+
let transport: StdioClientTransport;
62+
63+
suiteSetup(async function () {
64+
this.timeout(30_000);
65+
66+
const ext = vscode.extensions.getExtension(EXTENSION_ID);
67+
assert.ok(ext, `Extension ${EXTENSION_ID} not found`);
68+
if (!ext.isActive) await ext.activate();
69+
70+
const serverPath = resolveServerPath();
71+
72+
const env: Record<string, string> = {
73+
...process.env as Record<string, string>,
74+
TRANSPORT_MODE: 'stdio',
75+
};
76+
77+
transport = new StdioClientTransport({
78+
command: 'node',
79+
args: [serverPath],
80+
env,
81+
stderr: 'pipe',
82+
});
83+
84+
client = new Client({ name: 'resource-integration-test', version: '1.0.0' });
85+
await client.connect(transport);
86+
console.log('[mcp-resource-e2e] Connected to MCP server');
87+
});
88+
89+
suiteTeardown(async function () {
90+
this.timeout(10_000);
91+
try { if (client) await client.close(); } catch { /* best-effort */ }
92+
try { if (transport) await transport.close(); } catch { /* best-effort */ }
93+
});
94+
95+
test('Server should list resources', async function () {
96+
this.timeout(15_000);
97+
98+
const response = await client.listResources();
99+
assert.ok(response.resources, 'Server should return resources');
100+
assert.ok(response.resources.length > 0, 'Server should have at least one resource');
101+
102+
console.log(`[mcp-resource-e2e] Server provides ${response.resources.length} resources`);
103+
});
104+
105+
test('Static resources should return non-empty content', async function () {
106+
this.timeout(30_000);
107+
108+
const response = await client.listResources();
109+
const staticUris = response.resources
110+
.map(r => r.uri)
111+
.filter(uri => uri.startsWith('codeql://server/') || uri.startsWith('codeql://templates/') || uri.startsWith('codeql://patterns/') || uri.startsWith('codeql://learning/'));
112+
113+
assert.ok(staticUris.length > 0, 'Should have at least one static resource');
114+
115+
for (const uri of staticUris) {
116+
const result = await client.readResource({ uri });
117+
const text = extractText(result.contents?.[0] ?? { uri });
118+
assert.ok(
119+
text.length > 0,
120+
`Static resource ${uri} returned empty content`,
121+
);
122+
assert.ok(
123+
!text.includes(NOT_FOUND_SENTINEL),
124+
`Static resource ${uri} returned fallback "not found" content`,
125+
);
126+
}
127+
});
128+
129+
test('Language-specific resources should return actual content, not fallback', async function () {
130+
this.timeout(60_000);
131+
132+
const response = await client.listResources();
133+
const langUris = response.resources
134+
.map(r => r.uri)
135+
.filter(uri => uri.startsWith('codeql://languages/'));
136+
137+
// If no language resources are listed, the registration-time existence check
138+
// correctly filtered them out (which is acceptable). But if they ARE listed,
139+
// they MUST return real content.
140+
if (langUris.length === 0) {
141+
console.log('[mcp-resource-e2e] No language resources registered (files may be missing in this layout)');
142+
return;
143+
}
144+
145+
console.log(`[mcp-resource-e2e] Found ${langUris.length} language-specific resources`);
146+
147+
for (const uri of langUris) {
148+
const result = await client.readResource({ uri });
149+
const text = extractText(result.contents?.[0] ?? { uri });
150+
151+
assert.ok(
152+
text.length > 100,
153+
`Language resource ${uri} returned suspiciously short content (${text.length} chars)`,
154+
);
155+
156+
assert.ok(
157+
!text.includes(NOT_FOUND_SENTINEL),
158+
`Language resource ${uri} returned fallback "not found" content. ` +
159+
`This means the resource content was not embedded at build time. ` +
160+
`Check LANGUAGE_RESOURCES in language-types.ts and ensure the ` +
161+
`corresponding .md file exists under server/src/resources/languages/.`,
162+
);
163+
}
164+
165+
console.log(`[mcp-resource-e2e] All ${langUris.length} language resources returned real content`);
166+
});
167+
168+
test('Every listed resource should be readable without error', async function () {
169+
this.timeout(60_000);
170+
171+
const response = await client.listResources();
172+
173+
for (const resource of response.resources) {
174+
const result = await client.readResource({ uri: resource.uri });
175+
const text = extractText(result.contents?.[0] ?? { uri: resource.uri });
176+
177+
assert.ok(
178+
text.length > 0,
179+
`Resource "${resource.name}" (${resource.uri}) returned empty content`,
180+
);
181+
182+
assert.ok(
183+
!text.includes(NOT_FOUND_SENTINEL),
184+
`Resource "${resource.name}" (${resource.uri}) returned fallback "not found" content`,
185+
);
186+
}
187+
188+
console.log(`[mcp-resource-e2e] All ${response.resources.length} resources are readable`);
189+
});
190+
191+
test('No resource content should contain YAML frontmatter', async function () {
192+
this.timeout(60_000);
193+
194+
const response = await client.listResources();
195+
196+
for (const resource of response.resources) {
197+
const result = await client.readResource({ uri: resource.uri });
198+
const text = extractText(result.contents?.[0] ?? { uri: resource.uri });
199+
200+
assert.ok(
201+
!text.startsWith('---'),
202+
`Resource "${resource.name}" (${resource.uri}) starts with YAML frontmatter. ` +
203+
`Resources are not prompts and should not contain prompt metadata.`,
204+
);
205+
}
206+
207+
console.log(`[mcp-resource-e2e] No resources contain YAML frontmatter`);
208+
});
209+
});

server/dist/codeql-development-mcp-server.js

Lines changed: 113 additions & 123 deletions
Large diffs are not rendered by default.

server/dist/codeql-development-mcp-server.js.map

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)