@@ -6,7 +6,8 @@ import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types';
66import { connectToServer } from './client-helper' ;
77import {
88 TextResourceContents ,
9- BlobResourceContents
9+ BlobResourceContents ,
10+ McpError
1011} from '@modelcontextprotocol/sdk/types.js' ;
1112
1213export class ResourcesListScenario implements ClientScenario {
@@ -435,6 +436,150 @@ Example request:
435436 }
436437}
437438
439+ export class ResourcesNotFoundErrorScenario implements ClientScenario {
440+ name = 'sep-2164-resource-not-found' ;
441+ specVersions : SpecVersion [ ] = [ 'draft' ] ;
442+ description = `Test error handling for non-existent resources (SEP-2164).
443+
444+ **Server Implementation Requirements:**
445+
446+ **Endpoint**: \`resources/read\`
447+
448+ When a client requests a URI that does not correspond to any resource, the server:
449+
450+ - **MUST** return a JSON-RPC error with code \`-32602\` (Invalid Params)
451+ - **MUST NOT** return a result with an empty \`contents\` array
452+ - **SHOULD** include the requested \`uri\` in the error \`data\` field
453+
454+ Example error response:
455+
456+ \`\`\`json
457+ {
458+ "jsonrpc": "2.0",
459+ "id": 2,
460+ "error": {
461+ "code": -32602,
462+ "message": "Resource not found",
463+ "data": {
464+ "uri": "test://nonexistent-resource-for-conformance-testing"
465+ }
466+ }
467+ }
468+ \`\`\`
469+
470+ This scenario does not require the server to register any specific resource — it tests behavior when reading a URI the server does not recognize.` ;
471+
472+ async run ( serverUrl : string ) : Promise < ConformanceCheck [ ] > {
473+ const checks : ConformanceCheck [ ] = [ ] ;
474+ const nonexistentUri =
475+ 'test://nonexistent-resource-for-conformance-testing' ;
476+ const specReferences = [
477+ {
478+ id : 'SEP-2164' ,
479+ url : 'https://modelcontextprotocol.io/specification/draft/server/resources#error-handling'
480+ }
481+ ] ;
482+
483+ let connection ;
484+ try {
485+ connection = await connectToServer ( serverUrl ) ;
486+ } catch ( error ) {
487+ checks . push ( {
488+ id : 'sep-2164-error-code' ,
489+ name : 'ResourcesNotFoundErrorCode' ,
490+ description :
491+ 'Server returns -32602 (Invalid Params) for non-existent resource' ,
492+ status : 'FAILURE' ,
493+ timestamp : new Date ( ) . toISOString ( ) ,
494+ errorMessage : `Failed to connect: ${ error instanceof Error ? error . message : String ( error ) } ` ,
495+ specReferences
496+ } ) ;
497+ return checks ;
498+ }
499+
500+ let caughtError : unknown ;
501+ let result : { contents : unknown [ ] } | undefined ;
502+ try {
503+ result = await connection . client . readResource ( { uri : nonexistentUri } ) ;
504+ } catch ( error ) {
505+ caughtError = error ;
506+ }
507+
508+ // Check 1: MUST return -32602 error code (not empty contents, not other codes)
509+ const errorCode =
510+ caughtError instanceof McpError ? caughtError . code : undefined ;
511+ const errorCodeErrors : string [ ] = [ ] ;
512+ if ( result !== undefined ) {
513+ errorCodeErrors . push (
514+ `Server returned a result instead of an error (contents length: ${ result . contents ?. length ?? 'undefined' } ). Servers MUST NOT return an empty contents array for non-existent resources.`
515+ ) ;
516+ } else if ( ! ( caughtError instanceof McpError ) ) {
517+ errorCodeErrors . push (
518+ `Expected a JSON-RPC error, got: ${ caughtError instanceof Error ? caughtError . message : String ( caughtError ) } `
519+ ) ;
520+ } else if ( errorCode !== - 32602 ) {
521+ errorCodeErrors . push (
522+ `Expected error code -32602 (Invalid Params), got ${ errorCode } . ` +
523+ ( errorCode === - 32002
524+ ? 'Code -32002 was used in earlier spec versions but SEP-2164 standardizes on -32602.'
525+ : '' )
526+ ) ;
527+ }
528+
529+ checks . push ( {
530+ id : 'sep-2164-error-code' ,
531+ name : 'ResourcesNotFoundErrorCode' ,
532+ description :
533+ 'Server returns -32602 (Invalid Params) for non-existent resource' ,
534+ status : errorCodeErrors . length === 0 ? 'SUCCESS' : 'FAILURE' ,
535+ timestamp : new Date ( ) . toISOString ( ) ,
536+ errorMessage :
537+ errorCodeErrors . length > 0 ? errorCodeErrors . join ( '; ' ) : undefined ,
538+ specReferences,
539+ details : {
540+ requestedUri : nonexistentUri ,
541+ receivedErrorCode : errorCode ,
542+ receivedResult : result !== undefined
543+ }
544+ } ) ;
545+
546+ // Check 2: SHOULD include uri in error data field
547+ const errorData =
548+ caughtError instanceof McpError
549+ ? ( caughtError . data as { uri ?: string } | undefined )
550+ : undefined ;
551+ const dataUriMatches = errorData ?. uri === nonexistentUri ;
552+
553+ checks . push ( {
554+ id : 'sep-2164-data-uri' ,
555+ name : 'ResourcesNotFoundDataUri' ,
556+ description :
557+ 'Server includes the requested URI in the error data field (SHOULD)' ,
558+ status :
559+ caughtError instanceof McpError
560+ ? dataUriMatches
561+ ? 'SUCCESS'
562+ : 'WARNING'
563+ : 'FAILURE' ,
564+ timestamp : new Date ( ) . toISOString ( ) ,
565+ errorMessage :
566+ caughtError instanceof McpError
567+ ? dataUriMatches
568+ ? undefined
569+ : `Error data.uri is ${ JSON . stringify ( errorData ?. uri ) } , expected "${ nonexistentUri } ". This is a SHOULD requirement.`
570+ : 'No JSON-RPC error received; cannot evaluate data field.' ,
571+ specReferences,
572+ details : {
573+ requestedUri : nonexistentUri ,
574+ receivedDataUri : errorData ?. uri
575+ }
576+ } ) ;
577+
578+ await connection . close ( ) ;
579+ return checks ;
580+ }
581+ }
582+
438583export class ResourcesUnsubscribeScenario implements ClientScenario {
439584 name = 'resources-unsubscribe' ;
440585 specVersions : SpecVersion [ ] = [ '2025-06-18' , '2025-11-25' ] ;
0 commit comments