Improve prompt error handling and relative path support#153
Improve prompt error handling and relative path support#153data-douser merged 27 commits intomainfrom
Conversation
Dependency ReviewThe following issues were found:
Snapshot WarningsEnsure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice. License Issues.github/workflows/copilot-setup-steps.yml
OpenSSF Scorecard
Scanned Files
|
There was a problem hiding this comment.
Pull request overview
This PR aims to improve MCP workflow prompt UX by preventing raw MCP protocol errors for invalid prompt inputs (especially invalid enums and problematic file paths), and by adding coverage to ensure prompts return user-friendly inline warnings/errors.
Changes:
- Added prompt-side validation/error recovery (
createSafePromptHandler) and permissive prompt-argument registration (toPermissiveShape) to avoid protocol-layer validation failures. - Implemented workspace-relative file path resolution with warnings (
resolvePromptFilePath) and surfaced these warnings in prompt output. - Expanded automated coverage with new server unit tests and a new VS Code Extension Development Host E2E suite for prompt error handling.
Reviewed changes
Copilot reviewed 8 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| server/src/prompts/workflow-prompts.ts | Adds permissive schema registration + safe handler wrapper; introduces file-path resolution/warnings; adjusts prompt schemas/requiredness. |
| server/test/src/prompts/workflow-prompts.test.ts | Updates schema expectations and adds unit tests for permissive shapes, validation formatting, safe handler behavior, and path resolution. |
| extensions/vscode/test/suite/mcp-prompt-e2e.integration.test.ts | New end-to-end prompt error-handling tests running against a real spawned MCP server. |
| extensions/vscode/esbuild.config.js | Registers the new E2E test entrypoint for bundling. |
| client/src/lib/mcp-test-suite.js | Adds a client-side prompt invalid-path test case to the basic MCP test suite runner. |
| client/integration-tests/primitives/prompts/explain_codeql_query/invalid_query_path/README.md | Documents the new invalid-path integration test scenario. |
| client/integration-tests/primitives/prompts/explain_codeql_query/invalid_query_path/before/monitoring-state.json | Adds before fixture for the integration test. |
| client/integration-tests/primitives/prompts/explain_codeql_query/invalid_query_path/after/monitoring-state.json | Adds after fixture for the integration test. |
| package-lock.json | Lockfile updates from dependency install/re-lock. |
| server/dist/codeql-development-mcp-server.js | Rebuilt bundle reflecting the prompt changes. |
30c4379 to
553f549
Compare
extensions/vscode/test/suite/mcp-prompt-e2e.integration.test.ts
Outdated
Show resolved
Hide resolved
client/integration-tests/primitives/prompts/explain_codeql_query/invalid_query_path/README.md
Show resolved
Hide resolved
|
@copilot open a new pull request to apply changes based on the comments in this thread |
|
@data-douser I've opened a new pull request, #155, to work on those changes. Once the pull request is ready, I'll request review from you. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 12 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
server/src/prompts/workflow-prompts.ts:65
- The doc comment says
resolvedPathis “always set (to the best-effort absolute path)”, but for the empty/whitespace path case the function returnsresolvedPathas the raw (empty) input. Either adjust the docs to reflect that empty input returns the original value, or return an absolute fallback (e.g. workspace root) so the invariant holds.
/**
* Result of resolving a user-supplied file path in a prompt parameter.
*
* `resolvedPath` is always set (to the best-effort absolute path).
* `warning` is set only when the path is problematic — the caller should
* embed it in the prompt response so the user sees a clear message.
*/
export interface PromptFilePathResult {
resolvedPath: string;
warning?: string;
}
/**
* Resolve a user-supplied file path for a prompt parameter.
*
* Relative paths are resolved against `workspaceRoot` (which defaults to
* `getUserWorkspaceDir()`). The function never throws — it returns a
* `warning` string when the path is empty, contains traversal sequences, or
* does not exist on disk.
*
* @param filePath - The raw path value from the prompt parameter.
* @param workspaceRoot - Directory to resolve relative paths against.
* @returns An object with the resolved absolute path and an optional warning.
*/
export function resolvePromptFilePath(
filePath: string,
workspaceRoot?: string,
): PromptFilePathResult {
if (!filePath || filePath.trim() === '') {
return {
resolvedPath: filePath ?? '',
warning: '⚠ **File path is empty.** Please provide a valid file path.',
};
server/src/prompts/workflow-prompts.ts:538
- Not all workflow prompts appear to be using the new
toPermissiveShape(...)+createSafePromptHandler(...)pattern yet. In particular,check_for_duplicated_codeandfind_overlapping_queriesare still registered with their strict schema shapes and unwrapped handlers, so they can still surface raw MCP protocol validation errors (and don’t get the new path-resolution/warning behavior). Consider updating those registrations too so prompt behavior is consistent across the whole set.
server.prompt(
'test_driven_development',
'Test-driven development workflow for CodeQL queries using MCP tools',
toPermissiveShape(testDrivenDevelopmentSchema.shape),
createSafePromptHandler(
'test_driven_development',
...tests/primitives/prompts/explain_codeql_query/invalid_query_path/after/monitoring-state.json
Outdated
Show resolved
Hide resolved
extensions/vscode/test/suite/mcp-prompt-e2e.integration.test.ts
Outdated
Show resolved
Hide resolved
- Add resolvePromptFilePath() utility that resolves relative paths against workspace root and returns user-friendly warnings for invalid paths - Update all 9 prompt handlers with file path parameters to resolve paths and embed warnings in the response instead of throwing protocol errors - Add integration test in mcp-test-suite.js for explain_codeql_query with invalid path - Add file-based integration test fixtures in primitives/prompts/ - Add 10 unit tests for resolvePromptFilePath and handler path resolution Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com>
… assertions Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com>
Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com>
…alid paths Add mcp-prompt-e2e.integration.test.ts to the VS Code extension test suite that spawns the real MCP server inside the Extension Development Host and verifies that prompts return user-friendly warnings (not protocol errors) when given invalid file paths: - explain_codeql_query with nonexistent relative path returns "does not exist" - explain_codeql_query with valid absolute path returns no warning - document_codeql_query with nonexistent path returns "does not exist" - Server lists prompts including explain_codeql_query Register the new test in esbuild.config.js so it is bundled for CI. Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com>
The MCP SDK validates prompt arguments before calling handlers. When validation fails (e.g. unsupported language like "rust"), the SDK throws McpError(-32602) with a raw JSON dump — poor UX for VS Code slash commands. Server-side changes (server/src/prompts/workflow-prompts.ts): - Add toPermissiveShape() to widen z.enum() to z.string() in registered schemas so the SDK never rejects user input at the protocol layer - Add createSafePromptHandler() to wrap all 11 prompt handlers with: (1) strict Zod validation that returns user-friendly inline errors (2) exception recovery that catches handler crashes gracefully - Add formatValidationError() to convert ZodError into actionable markdown with field names, received values, and valid options listed Integration tests (extensions/vscode/test/suite/mcp-prompt-e2e.integration.test.ts): - Expand from 4 to 15 e2e tests covering all prompts with file-path params - Add test for invalid language returning inline error (not protocol error) - Add test for path traversal detection - Add tests for all-optional prompts with empty args Unit tests (server/test/src/prompts/workflow-prompts.test.ts): - Add 17 new tests for toPermissiveShape, formatValidationError, createSafePromptHandler, and handler-level invalid argument handling - Update schema-to-registration consistency tests for permissive shapes
Make databasePath, language, queryPath, and sarifPath required where the prompt depends on them. VS Code now correctly marks these fields as required in the slash-command input dialog.
|
@data-douser I've opened a new pull request, #162, to work on those changes. Once the pull request is ready, I'll request review from you. |
… tests in prompt handlers (#162) * Initial plan * Fix markdown injection and platform-dependent path tests in prompt handlers - Add sanitizeForInlineCode() helper to escape backticks and newlines in user-supplied values embedded in markdown code spans - Apply sanitizer to resolvePromptFilePath 'does not exist' warning (filePath and absolutePath) - Apply sanitizer to formatValidationError issue.received display - Fix POSIX path separator assumptions in tests: use basename only ('mydb', 'database') - Rename createSafePromptHandler tests to clarify they validate the handler wrapper, not MCP SDK validation Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com> Agent-Logs-Url: https://github.com/advanced-security/codeql-development-mcp-server/sessions/2660567b-5822-4505-91c2-37fe3ef00f4e * Replace sanitizeForInlineCode with CommonMark-compliant markdownInlineCode - markdownInlineCode() uses a fence length = maxRun+1 per CommonMark spec, preserving the original string (no information loss from backtick→apostrophe) - Normalises CR/CRLF to LF before wrapping (inline spans can't span lines) - Export markdownInlineCode for testability - Add 6 unit tests for markdownInlineCode (plain text, single/double backtick, CRLF normalisation, backtick-only values) - Add regression test for formatValidationError with backtick in received value - Add regression test for resolvePromptFilePath warning with backtick in path Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com> Agent-Logs-Url: https://github.com/advanced-security/codeql-development-mcp-server/sessions/ec7c534b-93ac-40e5-bcb6-023bc7496940 * Fix markdownInlineCode to replace newlines with spaces for single-line output Replace \r\n, \r, and \n with a space (not just normalize CRLF to LF) so the returned inline code span never contains a literal newline character. Update docstring and test to reflect space-replacement behavior. Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com> Agent-Logs-Url: https://github.com/advanced-security/codeql-development-mcp-server/sessions/87cfd54e-9d66-4871-a581-601aff3c6c8d --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 11 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (3)
server/test/src/prompts/workflow-prompts.test.ts:1234
- Same issue as above: this test expects an inline validation error when required fields are missing, but in production the SDK will reject missing required args before the handler runs. Update the test strategy so it matches the actual invocation path (schema validation before handler).
it('ql_tdd_advanced handler (createSafePromptHandler) should return inline validation error when required parameters are missing', async () => {
const handler = getRegisteredHandler(mockServer, 'ql_tdd_advanced');
const result: PromptResult = await handler({});
expect(result.messages[0].content.text).toContain('Invalid input');
});
server/test/src/prompts/workflow-prompts.test.ts:1262
- These tests expect inline validation errors for missing required
sarifPath, butsarifPathis required in the registered schema, so real MCP clients will be rejected by SDK validation before the handler runs. Consider replacing these with schema requiredness assertions and rely on E2E coverage for protocol-layer behavior.
it('sarif_rank handlers should return inline error with no parameters', async () => {
for (const name of ['sarif_rank_false_positives', 'sarif_rank_true_positives']) {
const handler = getRegisteredHandler(mockServer, name);
const result: PromptResult = await handler({});
expect(result.messages[0].content.text).toContain('Invalid input');
server/test/src/prompts/workflow-prompts.test.ts:1279
- This test expects an inline validation error when
queryPathis missing, butqueryPathis required in the registered schema, so real MCP clients will get a protocol-level validation error before the handler executes. Adjust the test to align with real behavior (schema requiredness vs handler behavior).
it('run_query_and_summarize_false_positives handler should return inline error with no parameters', async () => {
const handler = getRegisteredHandler(mockServer, 'run_query_and_summarize_false_positives');
const result: PromptResult = await handler({});
expect(result.messages[0].content.text).toContain('Invalid input');
});
Adds a common cache for actions workflows that download the latest instance of VS Code for integration testing purposes.
- Add retry logic (3 attempts) with backoff to download-vscode.js - Handle unhandled promise rejections from @vscode/test-electron stream errors - Clean partial downloads between retries to avoid checksum mismatches - Default to 'stable' version to match .vscode-test.mjs configuration - Chain download:vscode before vscode-test in test:integration script - Cache VS Code downloads in build-and-test-extension.yml and copilot-setup-steps.yml - Increase download timeout from 15s (default) to 120s Fixes intermittent CI failures from ECONNRESET and empty-file checksum errors when downloading VS Code for Extension Host integration tests.
- resolvePromptFilePath: only enforce workspace-root boundary for
relative path inputs; absolute paths pass through since users
legitimately reference databases/queries outside the workspace
- download-vscode.js: use fileURLToPath() instead of URL.pathname
for cross-platform compatibility
- integration-test-runner.js: track execution counts separately
from pass/fail; runWorkflowIntegrationTests and
runPromptIntegrationTests return { executed, passed } objects
- Add test for absolute paths outside workspace being allowed
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Nathan Randall <70299490+data-douser@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 17 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
client/src/lib/integration-test-runner.js:1325
runWorkflowIntegrationTests()always returns{ passed: true }when it completes, even if one or more workflow test cases failed (sincerunSingleWorkflowIntegrationTestcatches errors and only logs). This makes theworkflowTestsPassedsignal unreliable and can causerunIntegrationTests()to report success despite workflow failures. Consider having workflow test execution return booleans and aggregate pass/fail like the new prompt fixture runner does.
// Run tests for each workflow
let totalWorkflowTests = 0;
for (const workflowName of workflowDirs) {
const testCount = await this.runWorkflowTests(workflowName, workflowTestsDir);
totalWorkflowTests += testCount;
}
this.logger.log(`Total workflow integration tests executed: ${totalWorkflowTests}`);
return { executed: totalWorkflowTests, passed: true };
} catch (error) {
...nt/integration-tests/primitives/tools/codeql_bqrs_interpret/sarif_format/after/results.sarif
Show resolved
Hide resolved
- Remove absolute path from 'does not exist' warning to avoid leaking local machine paths into prompt context sent to LLMs - Add blockedPathError() helper and check blocked flag at all 12 resolvePromptFilePath call sites so traversal attempts short-circuit with a clear inline error instead of silently proceeding - Add tests for both behaviors
| ): Promise<PromptFilePathResult> { | ||
| if (!filePath || filePath.trim() === '') { | ||
| return { | ||
| resolvedPath: filePath ?? '', |
There was a problem hiding this comment.
In the empty/whitespace-only path case, resolvedPath is set to the original filePath value (which can be just whitespace). This can leak a meaningless value into downstream context; consider returning resolvedPath: '' (or filePath.trim()) to keep the result consistent with other “blocked/invalid” outcomes.
| resolvedPath: filePath ?? '', | |
| resolvedPath: '', |
| // Track execution counts and pass/fail separately. | ||
| // *Count* tracks whether the suite discovered & ran tests. | ||
| // *Succeeded* tracks whether those tests passed. | ||
| const toolTestsExecuted = totalIntegrationTests; | ||
| const toolTestsPassed = totalIntegrationTests > 0; | ||
|
|
There was a problem hiding this comment.
toolTestsPassed is currently computed as totalIntegrationTests > 0, which only checks that some tests executed, not that they passed. This makes the aggregated pass/fail result from runIntegrationTests() unreliable; consider tracking failures from runSingleIntegrationTest() / runToolIntegrationTests() and using that to compute toolTestsPassed.
| // Run tests for each workflow | ||
| let totalWorkflowTests = 0; | ||
| for (const workflowName of workflowDirs) { | ||
| const testCount = await this.runWorkflowTests(workflowName, workflowTestsDir); | ||
| totalWorkflowTests += testCount; | ||
| } | ||
|
|
||
| this.logger.log(`Total workflow integration tests executed: ${totalWorkflowTests}`); | ||
| return true; | ||
| return { executed: totalWorkflowTests, passed: true }; | ||
| } catch (error) { |
There was a problem hiding this comment.
runWorkflowIntegrationTests() always returns { passed: true } when it completes, even if individual workflow tests were marked failed via logTest. This prevents callers from reliably using the returned passed flag; consider tracking whether any workflow test failed (or remove the passed field and rely solely on executed).
| // @vscode/test-electron can emit unhandled rejections from internal stream | ||
| // errors (e.g. ECONNRESET). Suppress them here so the retry loop can handle | ||
| // the failure via the caught exception from the await. | ||
| let suppressedError = null; | ||
| process.on('unhandledRejection', (reason) => { | ||
| suppressedError = reason; | ||
| const msg = reason instanceof Error ? reason.message : String(reason); | ||
| console.error(` (suppressed unhandled rejection: ${msg})`); | ||
| }); | ||
|
|
||
| /** | ||
| * Remove partially-downloaded VS Code artifacts so the next attempt starts | ||
| * fresh instead of hitting a checksum mismatch on a truncated archive. | ||
| */ | ||
| function cleanPartialDownload() { | ||
| const cacheDir = join(fileURLToPath(new URL('..', import.meta.url)), '.vscode-test'); | ||
| if (existsSync(cacheDir)) { | ||
| console.log(' Cleaning .vscode-test/ to remove partial downloads...'); | ||
| rmSync(cacheDir, { recursive: true, force: true }); | ||
| } | ||
| return 'stable'; | ||
| } | ||
|
|
||
| const version = process.argv[2] || getEnginesVscodeVersion(); | ||
|
|
||
| console.log(`Downloading VS Code (${version}) for integration tests...`); | ||
|
|
||
| try { | ||
| const vscodeExecutablePath = await downloadAndUnzipVSCode(version); | ||
| console.log(`✅ VS Code downloaded to: ${vscodeExecutablePath}`); | ||
| } catch (error) { | ||
| console.error( | ||
| '❌ Failed to download VS Code:', | ||
| error instanceof Error ? error.message : String(error), | ||
| ); | ||
| process.exit(1); | ||
| for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { | ||
| suppressedError = null; | ||
| try { |
There was a problem hiding this comment.
suppressedError is assigned in the unhandledRejection handler and reset on each attempt, but it’s never read. This is likely to trip no-unused-vars/lint rules; either remove the variable or include it in the retry error reporting so it serves a purpose.
Fixes #143
Problems solved
develop_codeql_query,document_codeql_query,ql_lsp_iterative_development, etc.) now return inline error messages instead of throwing unhandled MCP protocol errors when given invalid or non-existent paths.queryPath,testPath,workspaceUri) are now resolved relative to the workspace root viaresolvePromptFilePath, so workspace-relative paths work as expected.createSafePromptHandler, which wraps each prompt with Zod schema parsing and catches errors before template rendering. Invalid inputs fail fast with a user-facing message.language,queryPath) promoted from optional to required where appropriate, so missing values are caught at input time.Changes
server/src/prompts/workflow-prompts.ts— AddedresolvePromptFilePath,createSafePromptHandler, andtoPermissiveShapeutilities; refactored all prompt registrations to use them.server/test/src/prompts/workflow-prompts.test.ts— Expanded unit tests for path resolution, error handling, and schema validation.extensions/vscode/test/suite/mcp-prompt-e2e.integration.test.ts— New E2E integration tests for prompt error handling with invalid paths.client/src/lib/integration-test-runner.js,client/src/lib/mcp-test-suite.js— New prompt fixture test runner and test suite utilities.client/integration-tests/invalid_query_path/— Test fixture for invalid path scenario.