diff --git a/examples/servers/typescript/sep-2322-mrtr-broken-server.ts b/examples/servers/typescript/sep-2322-mrtr-broken-server.ts index 49fcdb6d..72cf6021 100644 --- a/examples/servers/typescript/sep-2322-mrtr-broken-server.ts +++ b/examples/servers/typescript/sep-2322-mrtr-broken-server.ts @@ -8,6 +8,11 @@ * 2. Returns InputRequiredResult on `tools/list` (unsupported method) * 3. Accepts tampered requestState without integrity verification * + * Plus one case that is not a violation at all: + * + * 4. Answers with a conformant input_required result that names no input + * request, leaving the capability check with nothing to verify. + * * The conformance scenarios should emit FAILURE against this server. */ @@ -54,6 +59,11 @@ handlers['tools/list'] = () => ({ name: 'test_input_required_result_tampered_state', description: 'Test tool for tampered state', inputSchema: { type: 'object' as const, properties: {} } + }, + { + name: 'test_input_required_result_capabilities', + description: 'Test tool for client capability handling', + inputSchema: { type: 'object' as const, properties: {} } } ] }); @@ -94,6 +104,16 @@ handlers['tools/call'] = (params) => { }; } + case 'test_input_required_result_capabilities': { + // BUG 4: Conformant on its face — `requestState` satisfies "at least one + // of inputRequests or requestState" — but it names no input request, so + // the capability restriction under test is never exercised. + return { + resultType: 'input_required', + requestState: 'no-input-requested' + }; + } + case 'test_input_required_result_tampered_state': { if (inputResponses) { // BUG 3: Accepts ANY requestState without verification diff --git a/src/scenarios/server/input-required-result.ts b/src/scenarios/server/input-required-result.ts index 66efb3f5..64ad4ba9 100644 --- a/src/scenarios/server/input-required-result.ts +++ b/src/scenarios/server/input-required-result.ts @@ -22,6 +22,7 @@ import { mockListRootsResponse, MRTR_SPEC_REFERENCES } from './input-required-result-helpers'; +import { notTestable } from '../untestable'; // ─── A1: Basic Elicitation ──────────────────────────────────────────────────── @@ -1424,14 +1425,28 @@ Only include inputRequests for methods the client supports. For example, if the const result = resp.result; const errors: string[] = []; + let untestable = false; if (resp.error) { errors.push(`JSON-RPC error: ${resp.error.message}`); } else if (!result) { errors.push('No result in response'); - } else if (isInputRequiredResult(result) && result.inputRequests) { + } else if (isInputRequiredResult(result)) { + const inputRequests = Object.entries(result.inputRequests ?? {}); + // `inputRequests` is optional (a result carrying only `requestState` is + // still valid), so naming none does not violate the MUST NOT this check + // scores — but it leaves nothing to scan, and the loop below would then + // record no error at all, scoring SUCCESS without having verified it. + if (inputRequests.length === 0) { + untestable = true; + errors.push( + notTestable( + 'server returned no inputRequests, so the capability restriction was never exercised' + ) + ); + } // Check that no elicitation requests are included (client didn't declare it) - for (const [key, req] of Object.entries(result.inputRequests)) { + for (const [key, req] of inputRequests) { if (req.method === 'elicitation/create') { errors.push( `Server included elicitation/create inputRequest (key: "${key}") ` + @@ -1454,7 +1469,7 @@ Only include inputRequests for methods the client supports. For example, if the timestamp: new Date().toISOString(), errorMessage: errors.length > 0 ? errors.join('; ') : undefined, specReferences: MRTR_SPEC_REFERENCES, - details: { result } + details: untestable ? { result, untestable: true } : { result } }); } catch (error) { checks.push({ diff --git a/src/scenarios/server/negative-mrtr.test.ts b/src/scenarios/server/negative-mrtr.test.ts index 30228400..871ad73b 100644 --- a/src/scenarios/server/negative-mrtr.test.ts +++ b/src/scenarios/server/negative-mrtr.test.ts @@ -13,7 +13,8 @@ import path from 'path'; import { InputRequiredResultResultTypeScenario, InputRequiredResultUnsupportedMethodsScenario, - InputRequiredResultTamperedStateScenario + InputRequiredResultTamperedStateScenario, + InputRequiredResultCapabilityCheckScenario } from './input-required-result'; import { formatWireViolation, @@ -142,4 +143,18 @@ describe('SEP-2322 MRTR negative tests', () => { expect(tamperedCheck).toBeDefined(); expect(tamperedCheck?.status).toBe('FAILURE'); }, 10000); + + it('reports sep-2322-respect-client-capabilities as untestable against a server whose input_required result requests nothing', async () => { + const scenario = new InputRequiredResultCapabilityCheckScenario(); + const checks = await scenario.run(testContext(SERVER_URL)); + + const capabilityCheck = checks.find( + (c) => c.id === 'sep-2322-respect-client-capabilities' + ); + expect(capabilityCheck).toBeDefined(); + expect(capabilityCheck?.status).toBe('FAILURE'); + // The requirement was not violated, it could not be exercised (#248). + expect(capabilityCheck?.errorMessage).toContain('Not testable:'); + expect(capabilityCheck?.details?.untestable).toBe(true); + }, 10000); });