@@ -1945,15 +1945,25 @@ describe('outputSchema validation', () => {
19451945 * or the use of other tools (SEP-2106 safety-guard isolation).
19461946 */
19471947 test ( 'preserves outputSchema validation metadata across paginated tool listings' , async ( ) => {
1948- const server = new Server ( { name : 'test-server' , version : '1.0.0' } , { capabilities : { tools : { } } } ) ;
1948+ const server = new Server ( { name : 'test-server' , version : '1.0.0' } , { capabilities : { tools : { listChanged : true } } } ) ;
1949+ const listRequests : Array < string | undefined > = [ ] ;
1950+ const listChangedNotifications : Array < [ Error | null , Tool [ ] | null ] > = [ ] ;
1951+ let resolveListChangedNotification : ( ) => void = ( ) => { } ;
1952+ const listChangedNotification = new Promise < void > ( resolve => {
1953+ resolveListChangedNotification = resolve ;
1954+ } ) ;
1955+ let lastPageToolReturnsInvalid = false ;
1956+ let lastPageBadToolCalled = false ;
19491957
19501958 server . setRequestHandler ( 'initialize' , async request => ( {
19511959 protocolVersion : request . params . protocolVersion ,
1952- capabilities : { tools : { } } ,
1960+ capabilities : { tools : { listChanged : true } } ,
19531961 serverInfo : { name : 'test-server' , version : '1.0.0' }
19541962 } ) ) ;
19551963
19561964 server . setRequestHandler ( 'tools/list' , async request => {
1965+ listRequests . push ( request . params ?. cursor ) ;
1966+
19571967 if ( request . params ?. cursor === 'page-2' ) {
19581968 return {
19591969 tools : [
@@ -1962,6 +1972,12 @@ describe('outputSchema validation', () => {
19621972 description : 'a tool on the final page' ,
19631973 inputSchema : { type : 'object' , properties : { } } ,
19641974 outputSchema : { type : 'object' , properties : { ok : { type : 'boolean' } } , required : [ 'ok' ] }
1975+ } ,
1976+ {
1977+ name : 'last-page-bad-tool' ,
1978+ description : 'a final-page tool with an outputSchema the SEP-2106 guard rejects' ,
1979+ inputSchema : { type : 'object' , properties : { } } ,
1980+ outputSchema : { $ref : 'https://evil.example/final-page-schema.json' }
19651981 }
19661982 ]
19671983 } ;
@@ -1989,23 +2005,40 @@ describe('outputSchema validation', () => {
19892005
19902006 server . setRequestHandler ( 'tools/call' , async request => {
19912007 if ( request . params . name === 'last-page-tool' ) {
1992- return { content : [ ] , structuredContent : { ok : true } } ;
2008+ return { content : [ ] , structuredContent : { ok : lastPageToolReturnsInvalid ? 'not-a-boolean' : true } } ;
2009+ }
2010+ if ( request . params . name === 'last-page-bad-tool' ) {
2011+ lastPageBadToolCalled = true ;
2012+ return { content : [ ] , structuredContent : { irrelevant : true } } ;
19932013 }
19942014 if ( request . params . name === 'validated-tool' ) {
19952015 return { content : [ ] , structuredContent : { ok : 'not-a-boolean' } } ;
19962016 }
19972017 return { content : [ ] , structuredContent : { irrelevant : true } } ;
19982018 } ) ;
19992019
2000- const client = new Client ( { name : 'test-client' , version : '1.0.0' } ) ;
2020+ const client = new Client (
2021+ { name : 'test-client' , version : '1.0.0' } ,
2022+ {
2023+ listChanged : {
2024+ tools : {
2025+ debounceMs : 0 ,
2026+ onChanged : ( error , tools ) => {
2027+ listChangedNotifications . push ( [ error , tools ] ) ;
2028+ resolveListChangedNotification ( ) ;
2029+ }
2030+ }
2031+ }
2032+ }
2033+ ) ;
20012034 const [ clientTransport , serverTransport ] = InMemoryTransport . createLinkedPair ( ) ;
20022035 await Promise . all ( [ client . connect ( clientTransport ) , server . connect ( serverTransport ) ] ) ;
20032036
20042037 // listTools() must NOT reject just because one tool's schema is uncompilable.
20052038 const firstPage = await client . listTools ( ) ;
20062039 expect ( firstPage . tools . map ( t => t . name ) . toSorted ( ) ) . toEqual ( [ 'bad-tool' , 'validated-tool' ] ) ;
20072040 const secondPage = await client . listTools ( { cursor : firstPage . nextCursor } ) ;
2008- expect ( secondPage . tools . map ( t => t . name ) ) . toEqual ( [ 'last-page-tool' ] ) ;
2041+ expect ( secondPage . tools . map ( t => t . name ) . toSorted ( ) ) . toEqual ( [ 'last-page-bad-tool' , 'last-page-tool' ] ) ;
20092042
20102043 // The final page's tool is fully usable.
20112044 const lastPage = await client . callTool ( { name : 'last-page-tool' } ) ;
@@ -2016,6 +2049,30 @@ describe('outputSchema validation', () => {
20162049
20172050 // An earlier-page schema compile error also survives pagination and is scoped to that tool.
20182051 await expect ( client . callTool ( { name : 'bad-tool' } ) ) . rejects . toThrow ( / o u t p u t s c h e m a t h a t c o u l d n o t b e c o m p i l e d / i) ;
2052+
2053+ // A final-page schema compile error also survives pagination and is scoped to that tool.
2054+ await expect ( client . callTool ( { name : 'last-page-bad-tool' } ) ) . rejects . toThrow ( / o u t p u t s c h e m a t h a t c o u l d n o t b e c o m p i l e d / i) ;
2055+ expect ( lastPageBadToolCalled ) . toBe ( false ) ;
2056+
2057+ listRequests . length = 0 ;
2058+ lastPageToolReturnsInvalid = true ;
2059+ await server . notification ( { method : 'notifications/tools/list_changed' } ) ;
2060+ await listChangedNotification ;
2061+
2062+ expect ( listRequests ) . toEqual ( [ undefined , 'page-2' ] ) ;
2063+ expect ( listChangedNotifications ) . toHaveLength ( 1 ) ;
2064+ expect ( listChangedNotifications [ 0 ] ! [ 0 ] ) . toBeNull ( ) ;
2065+ expect ( listChangedNotifications [ 0 ] ! [ 1 ] ?. map ( t => t . name ) . toSorted ( ) ) . toEqual ( [
2066+ 'bad-tool' ,
2067+ 'last-page-bad-tool' ,
2068+ 'last-page-tool' ,
2069+ 'validated-tool'
2070+ ] ) ;
2071+
2072+ await expect ( client . callTool ( { name : 'last-page-tool' } ) ) . rejects . toThrow ( / S t r u c t u r e d c o n t e n t d o e s n o t m a t c h / ) ;
2073+ lastPageBadToolCalled = false ;
2074+ await expect ( client . callTool ( { name : 'last-page-bad-tool' } ) ) . rejects . toThrow ( / o u t p u t s c h e m a t h a t c o u l d n o t b e c o m p i l e d / i) ;
2075+ expect ( lastPageBadToolCalled ) . toBe ( false ) ;
20192076 } ) ;
20202077
20212078 /***
0 commit comments