diff --git a/src/linter/rules/error-contract-rules.ts b/src/linter/rules/error-contract-rules.ts index fa0aa6dd..e973036e 100644 --- a/src/linter/rules/error-contract-rules.ts +++ b/src/linter/rules/error-contract-rules.ts @@ -316,8 +316,8 @@ export function lintErrorContractConformance( const observed = new Set(); - // Direct references: `JsonRpcErrorCode.NotFound` - for (const m of cleaned.matchAll(/JsonRpcErrorCode\.(\w+)/g)) { + // Direct throws: `throw new McpError(JsonRpcErrorCode.NotFound, ...)` + for (const m of cleaned.matchAll(/\bthrow\s+new\s+McpError\s*\(\s*JsonRpcErrorCode\.(\w+)/g)) { const value = m[1] ? CODE_NAME_TO_VALUE[m[1]] : undefined; if (value !== undefined) observed.add(value); } diff --git a/tests/unit/linter/error-contract-rules.test.ts b/tests/unit/linter/error-contract-rules.test.ts index 4ed06351..a5162456 100644 --- a/tests/unit/linter/error-contract-rules.test.ts +++ b/tests/unit/linter/error-contract-rules.test.ts @@ -383,6 +383,30 @@ describe('lintErrorContractConformance', () => { ); expect(d).toEqual([]); }); + + it('does not treat JsonRpcErrorCode comparisons as direct throws', () => { + const handler = new Function( + `return async () => { + try { + throw new Error("boom"); + } catch (err) { + if (err instanceof McpError && err.code === JsonRpcErrorCode.NotFound) { + throw ctx.fail("not_found", "Not found"); + } + throw err; + } + }`, + )(); + const d = lintErrorContractConformance( + { + handler, + errors: [{ code: JsonRpcErrorCode.NotFound, reason: 'not_found', when: 'no match' }], + }, + 'tool', + 'x', + ); + expect(d).toEqual([]); + }); }); it('produces no diagnostics for a clean handler that uses ctx.fail', () => {