feat: add conformance scenario for SEP-2164 resource-not-found error code (#200)
* feat: add conformance scenario for SEP-2164 resource-not-found error code Adds a draft-scoped scenario verifying servers return -32602 (Invalid Params) for non-existent resources instead of the legacy -32002 or an empty contents array. Includes a sep-2164.yaml traceability file mapping each MUST/MUST NOT/SHOULD to its check per the proposed SEP-2484 process. * fix: align SEP-2164 check severities with spec text (#246) Splits sep-2164-error-code into: - sep-2164-no-empty-contents (MUST NOT -> FAILURE) - sep-2164-error-code (SHOULD -> WARNING) Updates sep-2164.yaml to quote the spec text verbatim and adds an excluded row for the client-side -32002 backcompat SHOULD. See #245 for the SHOULD/WARNING tier-scoring policy discussion. --------- Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>
This commit is contained in:
parent
64e6c7c0dd
commit
7ee58ef780
3 changed files with 175 additions and 2 deletions
|
|
@ -45,7 +45,8 @@ import {
|
||||||
ResourcesReadBinaryScenario,
|
ResourcesReadBinaryScenario,
|
||||||
ResourcesTemplateReadScenario,
|
ResourcesTemplateReadScenario,
|
||||||
ResourcesSubscribeScenario,
|
ResourcesSubscribeScenario,
|
||||||
ResourcesUnsubscribeScenario
|
ResourcesUnsubscribeScenario,
|
||||||
|
ResourcesNotFoundErrorScenario
|
||||||
} from './server/resources';
|
} from './server/resources';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -123,6 +124,9 @@ const allClientScenariosList: ClientScenario[] = [
|
||||||
new ResourcesSubscribeScenario(),
|
new ResourcesSubscribeScenario(),
|
||||||
new ResourcesUnsubscribeScenario(),
|
new ResourcesUnsubscribeScenario(),
|
||||||
|
|
||||||
|
// Resources error handling (SEP-2164)
|
||||||
|
new ResourcesNotFoundErrorScenario(),
|
||||||
|
|
||||||
// Prompts scenarios
|
// Prompts scenarios
|
||||||
new PromptsListScenario(),
|
new PromptsListScenario(),
|
||||||
new PromptsGetSimpleScenario(),
|
new PromptsGetSimpleScenario(),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types';
|
||||||
import { connectToServer } from './client-helper';
|
import { connectToServer } from './client-helper';
|
||||||
import {
|
import {
|
||||||
TextResourceContents,
|
TextResourceContents,
|
||||||
BlobResourceContents
|
BlobResourceContents,
|
||||||
|
McpError
|
||||||
} from '@modelcontextprotocol/sdk/types.js';
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
|
||||||
export class ResourcesListScenario implements ClientScenario {
|
export class ResourcesListScenario implements ClientScenario {
|
||||||
|
|
@ -435,6 +436,165 @@ Example request:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ResourcesNotFoundErrorScenario implements ClientScenario {
|
||||||
|
name = 'sep-2164-resource-not-found';
|
||||||
|
specVersions: SpecVersion[] = ['draft'];
|
||||||
|
description = `Test error handling for non-existent resources (SEP-2164).
|
||||||
|
|
||||||
|
**Server Implementation Requirements:**
|
||||||
|
|
||||||
|
**Endpoint**: \`resources/read\`
|
||||||
|
|
||||||
|
When a client requests a URI that does not correspond to any resource, the server:
|
||||||
|
|
||||||
|
- **MUST NOT** return a result with an empty \`contents\` array
|
||||||
|
- **SHOULD** return a JSON-RPC error with code \`-32602\` (Invalid Params)
|
||||||
|
- **SHOULD** include the requested \`uri\` in the error \`data\` field
|
||||||
|
|
||||||
|
Example error response:
|
||||||
|
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 2,
|
||||||
|
"error": {
|
||||||
|
"code": -32602,
|
||||||
|
"message": "Resource not found",
|
||||||
|
"data": {
|
||||||
|
"uri": "test://nonexistent-resource-for-conformance-testing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
This scenario does not require the server to register any specific resource — it tests behavior when reading a URI the server does not recognize.`;
|
||||||
|
|
||||||
|
async run(serverUrl: string): Promise<ConformanceCheck[]> {
|
||||||
|
const checks: ConformanceCheck[] = [];
|
||||||
|
const nonexistentUri =
|
||||||
|
'test://nonexistent-resource-for-conformance-testing';
|
||||||
|
const specReferences = [
|
||||||
|
{
|
||||||
|
id: 'SEP-2164',
|
||||||
|
url: 'https://modelcontextprotocol.io/specification/draft/server/resources#error-handling'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let connection;
|
||||||
|
try {
|
||||||
|
connection = await connectToServer(serverUrl);
|
||||||
|
} catch (error) {
|
||||||
|
checks.push({
|
||||||
|
id: 'sep-2164-error-code',
|
||||||
|
name: 'ResourcesNotFoundErrorCode',
|
||||||
|
description:
|
||||||
|
'Server returns -32602 (Invalid Params) for non-existent resource',
|
||||||
|
status: 'FAILURE',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
errorMessage: `Failed to connect: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
specReferences
|
||||||
|
});
|
||||||
|
return checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
let caughtError: unknown;
|
||||||
|
let result: { contents: unknown[] } | undefined;
|
||||||
|
try {
|
||||||
|
result = await connection.client.readResource({ uri: nonexistentUri });
|
||||||
|
} catch (error) {
|
||||||
|
caughtError = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 1: MUST NOT return an empty contents array
|
||||||
|
const returnedEmptyContents =
|
||||||
|
result !== undefined && (result.contents?.length ?? 0) === 0;
|
||||||
|
checks.push({
|
||||||
|
id: 'sep-2164-no-empty-contents',
|
||||||
|
name: 'ResourcesNotFoundNoEmptyContents',
|
||||||
|
description:
|
||||||
|
'Server does not return an empty contents array for a non-existent resource',
|
||||||
|
status: returnedEmptyContents ? 'FAILURE' : 'SUCCESS',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
errorMessage: returnedEmptyContents
|
||||||
|
? 'Server returned a result with an empty contents array. Servers MUST NOT return an empty contents array for non-existent resources.'
|
||||||
|
: undefined,
|
||||||
|
specReferences,
|
||||||
|
details: {
|
||||||
|
requestedUri: nonexistentUri,
|
||||||
|
receivedResult: result !== undefined,
|
||||||
|
contentsLength: result?.contents?.length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check 2: SHOULD return JSON-RPC error with code -32602
|
||||||
|
const errorCode =
|
||||||
|
caughtError instanceof McpError ? caughtError.code : undefined;
|
||||||
|
let errorCodeMessage: string | undefined;
|
||||||
|
if (result !== undefined) {
|
||||||
|
errorCodeMessage = `Server returned a result instead of an error (contents length: ${result.contents?.length ?? 'undefined'}). Servers SHOULD return a JSON-RPC error for non-existent resources.`;
|
||||||
|
} else if (!(caughtError instanceof McpError)) {
|
||||||
|
errorCodeMessage = `Expected a JSON-RPC error, got: ${caughtError instanceof Error ? caughtError.message : String(caughtError)}`;
|
||||||
|
} else if (errorCode !== -32602) {
|
||||||
|
errorCodeMessage =
|
||||||
|
`Expected error code -32602 (Invalid Params), got ${errorCode}. ` +
|
||||||
|
(errorCode === -32002
|
||||||
|
? 'Code -32002 was used in earlier spec versions but SEP-2164 standardizes on -32602.'
|
||||||
|
: '');
|
||||||
|
}
|
||||||
|
|
||||||
|
checks.push({
|
||||||
|
id: 'sep-2164-error-code',
|
||||||
|
name: 'ResourcesNotFoundErrorCode',
|
||||||
|
description:
|
||||||
|
'Server returns -32602 (Invalid Params) for non-existent resource (SHOULD)',
|
||||||
|
status: errorCodeMessage === undefined ? 'SUCCESS' : 'WARNING',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
errorMessage: errorCodeMessage,
|
||||||
|
specReferences,
|
||||||
|
details: {
|
||||||
|
requestedUri: nonexistentUri,
|
||||||
|
receivedErrorCode: errorCode,
|
||||||
|
receivedResult: result !== undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check 3: SHOULD include uri in error data field
|
||||||
|
const errorData =
|
||||||
|
caughtError instanceof McpError
|
||||||
|
? (caughtError.data as { uri?: string } | undefined)
|
||||||
|
: undefined;
|
||||||
|
const dataUriMatches = errorData?.uri === nonexistentUri;
|
||||||
|
|
||||||
|
checks.push({
|
||||||
|
id: 'sep-2164-data-uri',
|
||||||
|
name: 'ResourcesNotFoundDataUri',
|
||||||
|
description:
|
||||||
|
'Server includes the requested URI in the error data field (SHOULD)',
|
||||||
|
status:
|
||||||
|
caughtError instanceof McpError
|
||||||
|
? dataUriMatches
|
||||||
|
? 'SUCCESS'
|
||||||
|
: 'WARNING'
|
||||||
|
: 'FAILURE',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
errorMessage:
|
||||||
|
caughtError instanceof McpError
|
||||||
|
? dataUriMatches
|
||||||
|
? undefined
|
||||||
|
: `Error data.uri is ${JSON.stringify(errorData?.uri)}, expected "${nonexistentUri}". This is a SHOULD requirement.`
|
||||||
|
: 'No JSON-RPC error received; cannot evaluate data field.',
|
||||||
|
specReferences,
|
||||||
|
details: {
|
||||||
|
requestedUri: nonexistentUri,
|
||||||
|
receivedDataUri: errorData?.uri
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await connection.close();
|
||||||
|
return checks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ResourcesUnsubscribeScenario implements ClientScenario {
|
export class ResourcesUnsubscribeScenario implements ClientScenario {
|
||||||
name = 'resources-unsubscribe';
|
name = 'resources-unsubscribe';
|
||||||
specVersions: SpecVersion[] = ['2025-06-18', '2025-11-25'];
|
specVersions: SpecVersion[] = ['2025-06-18', '2025-11-25'];
|
||||||
|
|
|
||||||
9
src/scenarios/server/sep-2164.yaml
Normal file
9
src/scenarios/server/sep-2164.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
sep: 2164
|
||||||
|
spec_url: https://modelcontextprotocol.io/specification/draft/server/resources#error-handling
|
||||||
|
requirements:
|
||||||
|
- text: 'Servers MUST NOT return an empty contents array for a non-existent resource'
|
||||||
|
check: sep-2164-no-empty-contents
|
||||||
|
- text: 'Servers SHOULD return standard JSON-RPC errors for common failure cases: Resource not found: -32602 (Invalid Params)'
|
||||||
|
check: sep-2164-error-code
|
||||||
|
- text: 'clients SHOULD also accept -32002 as a resource not found error'
|
||||||
|
excluded: 'Client-side error handling is implementation-defined; not protocol-observable'
|
||||||
Loading…
Reference in a new issue