Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions examples/servers/typescript/sep-2322-mrtr-broken-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/

Expand Down Expand Up @@ -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: {} }
}
]
});
Expand Down Expand Up @@ -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
Expand Down
21 changes: 18 additions & 3 deletions src/scenarios/server/input-required-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
mockListRootsResponse,
MRTR_SPEC_REFERENCES
} from './input-required-result-helpers';
import { notTestable } from '../untestable';

// ─── A1: Basic Elicitation ────────────────────────────────────────────────────

Expand Down Expand Up @@ -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}") ` +
Expand All @@ -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({
Expand Down
17 changes: 16 additions & 1 deletion src/scenarios/server/negative-mrtr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import path from 'path';
import {
InputRequiredResultResultTypeScenario,
InputRequiredResultUnsupportedMethodsScenario,
InputRequiredResultTamperedStateScenario
InputRequiredResultTamperedStateScenario,
InputRequiredResultCapabilityCheckScenario
} from './input-required-result';
import {
formatWireViolation,
Expand Down Expand Up @@ -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);
});