-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmcp-prompt-e2e.integration.test.ts
More file actions
183 lines (148 loc) · 6.52 KB
/
mcp-prompt-e2e.integration.test.ts
File metadata and controls
183 lines (148 loc) · 6.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/**
* End-to-end integration tests for MCP server prompt error handling.
*
* These run inside the Extension Development Host with the REAL VS Code API.
* They spawn the actual `ql-mcp` server process, connect via
* StdioClientTransport, and invoke prompts with invalid file paths to verify
* that the server returns user-friendly warnings instead of throwing raw MCP
* protocol errors.
*
* This test suite exists to catch the class of bugs where:
* - A relative or nonexistent `queryPath` triggers a cryptic -32001 error
* - Invalid paths propagate silently into the LLM context without any warning
* - Path traversal attempts are not detected
*/
import * as assert from 'assert';
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
const EXTENSION_ID = 'advanced-security.vscode-codeql-development-mcp-server';
/**
* Resolve the MCP server entry point.
*/
function resolveServerPath(): string {
const extPath = vscode.extensions.getExtension(EXTENSION_ID)?.extensionUri.fsPath;
if (!extPath) throw new Error('Extension not found');
const monorepo = path.resolve(extPath, '..', '..', 'server', 'dist', 'codeql-development-mcp-server.js');
try {
fs.accessSync(monorepo);
return monorepo;
} catch {
// Fall through
}
const vsix = path.resolve(extPath, 'server', 'dist', 'codeql-development-mcp-server.js');
try {
fs.accessSync(vsix);
return vsix;
} catch {
throw new Error(`MCP server not found at ${monorepo} or ${vsix}`);
}
}
suite('MCP Prompt Error Handling Integration Tests', () => {
let client: Client;
let transport: StdioClientTransport;
suiteSetup(async function () {
this.timeout(30_000);
const ext = vscode.extensions.getExtension(EXTENSION_ID);
assert.ok(ext, `Extension ${EXTENSION_ID} not found`);
if (!ext.isActive) await ext.activate();
const serverPath = resolveServerPath();
const env: Record<string, string> = {
...process.env as Record<string, string>,
TRANSPORT_MODE: 'stdio',
};
transport = new StdioClientTransport({
command: 'node',
args: [serverPath],
env,
stderr: 'pipe',
});
client = new Client({ name: 'prompt-e2e-test', version: '1.0.0' });
await client.connect(transport);
console.log('[mcp-prompt-e2e] Connected to MCP server');
});
suiteTeardown(async function () {
this.timeout(10_000);
try { if (client) await client.close(); } catch { /* best-effort */ }
try { if (transport) await transport.close(); } catch { /* best-effort */ }
});
test('Server should list prompts including explain_codeql_query', async function () {
this.timeout(15_000);
const response = await client.listPrompts();
assert.ok(response.prompts, 'Server should return prompts');
assert.ok(response.prompts.length > 0, 'Server should have at least one prompt');
const names = response.prompts.map(p => p.name);
assert.ok(
names.includes('explain_codeql_query'),
`Prompts should include explain_codeql_query. Found: ${names.join(', ')}`,
);
console.log(`[mcp-prompt-e2e] Server provides ${response.prompts.length} prompts`);
});
test('explain_codeql_query with nonexistent relative path should return warning, not throw', async function () {
this.timeout(15_000);
// This simulates a user entering a relative path in the VS Code slash
// command input that does not exist on disk.
const result = await client.getPrompt({
name: 'explain_codeql_query',
arguments: {
queryPath: 'nonexistent/path/to/query.ql',
language: 'javascript',
},
});
// The prompt MUST return messages — not throw a protocol error.
assert.ok(result.messages, 'Prompt should return messages');
assert.ok(result.messages.length > 0, 'Prompt should return at least one message');
const text = result.messages[0]?.content as unknown as { type: string; text: string };
assert.ok(text?.text, 'First message should have text content');
// The response should contain a user-friendly warning about the invalid path.
assert.ok(
text.text.includes('does not exist'),
`Response should warn that the path does not exist. Got:\n${text.text.slice(0, 500)}`,
);
console.log('[mcp-prompt-e2e] explain_codeql_query correctly returned warning for nonexistent path');
});
test('explain_codeql_query with valid absolute path should not include a warning', async function () {
this.timeout(15_000);
// Use this very test file as a known-existing absolute path.
const extPath = vscode.extensions.getExtension(EXTENSION_ID)?.extensionUri.fsPath;
assert.ok(extPath, 'Extension path should be available');
const existingFile = path.resolve(extPath, 'package.json');
const result = await client.getPrompt({
name: 'explain_codeql_query',
arguments: {
queryPath: existingFile,
language: 'javascript',
},
});
assert.ok(result.messages, 'Prompt should return messages');
assert.ok(result.messages.length > 0, 'Prompt should return at least one message');
const text = result.messages[0]?.content as unknown as { type: string; text: string };
assert.ok(text?.text, 'First message should have text content');
// With a valid existing path, there should be no warning.
assert.ok(
!text.text.includes('does not exist'),
`Response should NOT contain a "does not exist" warning for valid path. Got:\n${text.text.slice(0, 500)}`,
);
console.log('[mcp-prompt-e2e] explain_codeql_query returned clean response for valid path');
});
test('document_codeql_query with nonexistent path should return warning, not throw', async function () {
this.timeout(15_000);
const result = await client.getPrompt({
name: 'document_codeql_query',
arguments: {
queryPath: 'does-not-exist/MyQuery.ql',
language: 'python',
},
});
assert.ok(result.messages, 'Prompt should return messages');
assert.ok(result.messages.length > 0, 'Prompt should return at least one message');
const text = result.messages[0]?.content as unknown as { type: string; text: string };
assert.ok(
text?.text?.includes('does not exist'),
`Response should warn that the path does not exist. Got:\n${(text?.text ?? '').slice(0, 500)}`,
);
console.log('[mcp-prompt-e2e] document_codeql_query correctly returned warning for nonexistent path');
});
});