@@ -8,7 +8,11 @@ import {
88 CreateMessageRequestSchema ,
99 type CreateMessageResult ,
1010 ElicitRequestSchema ,
11+ ProgressNotificationSchema ,
12+ PromptListChangedNotificationSchema ,
13+ ResourceListChangedNotificationSchema ,
1114 ResourceUpdatedNotificationSchema ,
15+ ToolListChangedNotificationSchema ,
1216} from '@modelcontextprotocol/sdk/types.js'
1317import { test , expect } from 'vitest'
1418import { type z } from 'zod'
@@ -55,7 +59,7 @@ test('Tool Definition', async () => {
5559 const [ firstTool ] = list . tools
5660 invariant ( firstTool , '🚨 No tools found' )
5761
58- expect ( firstTool ) . toEqual (
62+ expect ( firstTool , '🚨 firstTool should be a create_entry tool' ) . toEqual (
5963 expect . objectContaining ( {
6064 name : expect . stringMatching ( / ^ c r e a t e _ e n t r y $ / i) ,
6165 description : expect . stringMatching ( / ^ c r e a t e a n e w j o u r n a l e n t r y $ / i) ,
@@ -138,7 +142,10 @@ test('Sampling', async () => {
138142 const request = await messageRequestDeferred . promise
139143
140144 try {
141- expect ( request ) . toEqual (
145+ expect (
146+ request ,
147+ '🚨 request should be a sampling/createMessage request' ,
148+ ) . toEqual (
142149 expect . objectContaining ( {
143150 method : 'sampling/createMessage' ,
144151 params : expect . objectContaining ( {
@@ -334,8 +341,14 @@ test('Resource subscriptions: entry and tag', async () => {
334341 entryNotification . promise ,
335342 ] )
336343
337- expect ( tagNotif . params . uri ) . toBe ( tagUri )
338- expect ( entryNotif . params . uri ) . toBe ( entryUri )
344+ expect (
345+ tagNotif . params . uri ,
346+ '🚨 Tag notification uri should be the tag URI' ,
347+ ) . toBe ( tagUri )
348+ expect (
349+ entryNotif . params . uri ,
350+ '🚨 Entry notification uri should be the entry URI' ,
351+ ) . toBe ( entryUri )
339352
340353 // Unsubscribe and trigger another update
341354 notifications . length = 0
@@ -351,7 +364,10 @@ test('Resource subscriptions: entry and tag', async () => {
351364 } )
352365 // Wait a short time to ensure no notifications are received
353366 await new Promise ( ( r ) => setTimeout ( r , 200 ) )
354- expect ( notifications ) . toHaveLength ( 0 )
367+ expect (
368+ notifications ,
369+ '🚨 No notifications should be received after unsubscribing' ,
370+ ) . toHaveLength ( 0 )
355371} )
356372
357373test ( 'Elicitation: delete_entry confirmation' , async ( ) => {
@@ -395,43 +411,24 @@ test('Elicitation: delete_entry confirmation', async () => {
395411 'success' in structuredContent ,
396412 '🚨 structuredContent missing success field' ,
397413 )
398- expect ( structuredContent . success ) . toBe ( true )
414+ expect (
415+ structuredContent . success ,
416+ '🚨 structuredContent.success should be true after deleting an entry' ,
417+ ) . toBe ( true )
399418
400419 invariant ( elicitationRequest , '🚨 No elicitation request was sent' )
401420 const params = elicitationRequest . params
402421 invariant ( params , '🚨 elicitationRequest missing params' )
403- invariant (
404- typeof params . message === 'string' ,
405- '🚨 elicitationRequest.params.message must be a string' ,
406- )
407- invariant (
408- params . message . match ( / A r e y o u s u r e y o u w a n t t o d e l e t e e n t r y / i) ,
409- '🚨 elicitationRequest.params.message does not match expected confirmation prompt' ,
410- )
411- expect ( params . message ) . toMatch ( / A r e y o u s u r e y o u w a n t t o d e l e t e e n t r y / i)
412422
413- invariant (
423+ expect (
424+ params . message ,
425+ '🚨 elicitationRequest.params.message should match expected confirmation prompt' ,
426+ ) . toMatch ( / A r e y o u s u r e y o u w a n t t o d e l e t e e n t r y / i)
427+
428+ expect (
414429 params . requestedSchema ,
415- '🚨 elicitationRequest.params.requestedSchema is missing' ,
416- )
417- invariant (
418- params . requestedSchema . type === 'object' ,
419- '🚨 elicitationRequest.params.requestedSchema.type must be "object"' ,
420- )
421- invariant (
422- params . requestedSchema . properties &&
423- typeof params . requestedSchema . properties === 'object' ,
424- '🚨 elicitationRequest.params.requestedSchema.properties must be an object' ,
425- )
426- invariant (
427- 'confirmed' in params . requestedSchema . properties ,
428- '🚨 elicitationRequest.params.requestedSchema.properties must include confirmed' ,
429- )
430- invariant (
431- params . requestedSchema . properties . confirmed . type === 'boolean' ,
432- '🚨 elicitationRequest.params.requestedSchema.properties.confirmed.type must be boolean' ,
433- )
434- expect ( params . requestedSchema ) . toEqual (
430+ '🚨 elicitationRequest.params.requestedSchema should match expected schema' ,
431+ ) . toEqual (
435432 expect . objectContaining ( {
436433 type : 'object' ,
437434 properties : expect . objectContaining ( {
@@ -470,18 +467,222 @@ test('Elicitation: delete_tag decline', async () => {
470467 arguments : { id : tag . id } ,
471468 } )
472469 const structuredContent = deleteResult . structuredContent as any
473- invariant (
474- structuredContent ,
475- '🚨 No structuredContent returned from delete_tag' ,
476- )
477- invariant (
478- 'success' in structuredContent ,
479- '🚨 structuredContent missing success field' ,
480- )
481- expect ( structuredContent . success ) . toBe ( false )
470+
471+ expect (
472+ structuredContent . success ,
473+ '🚨 structuredContent.success should be false after declining to delete a tag' ,
474+ ) . toBe ( false )
482475 invariant (
483476 'message' in structuredContent ,
484477 '🚨 structuredContent missing message field' ,
485478 )
486- expect ( structuredContent . message ) . toMatch ( / c a n c e l l e d / i)
479+ expect (
480+ structuredContent . message ,
481+ '🚨 structuredContent.message should include "cancelled"' ,
482+ ) . toMatch ( / c a n c e l l e d / i)
483+ } )
484+
485+ test ( 'ListChanged notification: resources' , async ( ) => {
486+ await using setup = await setupClient ( )
487+ const { client } = setup
488+
489+ const resourceListChanged = await deferred < any > ( )
490+ client . setNotificationHandler (
491+ ResourceListChangedNotificationSchema ,
492+ ( notification ) => {
493+ resourceListChanged . resolve ( notification )
494+ } ,
495+ )
496+
497+ // Trigger a DB change that should enable resources
498+ await client . callTool ( {
499+ name : 'create_tag' ,
500+ arguments : {
501+ name : faker . lorem . word ( ) ,
502+ description : faker . lorem . sentence ( ) ,
503+ } ,
504+ } )
505+ await client . callTool ( {
506+ name : 'create_entry' ,
507+ arguments : {
508+ title : faker . lorem . words ( 3 ) ,
509+ content : faker . lorem . paragraphs ( 2 ) ,
510+ } ,
511+ } )
512+
513+ let resourceNotif
514+ try {
515+ resourceNotif = await Promise . race ( [
516+ resourceListChanged . promise ,
517+ AbortSignal . timeout ( 2000 ) ,
518+ ] )
519+ } catch {
520+ throw new Error (
521+ '🚨 Did not receive resources/listChanged notification when expected. Make sure your server calls sendResourceListChanged when resources change.' ,
522+ )
523+ }
524+ expect (
525+ resourceNotif ,
526+ '🚨 Did not receive resources/listChanged notification when expected. Make sure your server calls sendResourceListChanged when resources change.' ,
527+ ) . toBeDefined ( )
528+ } )
529+
530+ test ( 'ListChanged notification: tools' , async ( ) => {
531+ await using setup = await setupClient ( )
532+ const { client } = setup
533+
534+ const toolListChanged = await deferred < any > ( )
535+ client . setNotificationHandler (
536+ ToolListChangedNotificationSchema ,
537+ ( notification ) => {
538+ toolListChanged . resolve ( notification )
539+ } ,
540+ )
541+
542+ // Trigger a DB change that should enable tools
543+ await client . callTool ( {
544+ name : 'create_tag' ,
545+ arguments : {
546+ name : faker . lorem . word ( ) ,
547+ description : faker . lorem . sentence ( ) ,
548+ } ,
549+ } )
550+ await client . callTool ( {
551+ name : 'create_entry' ,
552+ arguments : {
553+ title : faker . lorem . words ( 3 ) ,
554+ content : faker . lorem . paragraphs ( 2 ) ,
555+ } ,
556+ } )
557+
558+ let toolNotif
559+ try {
560+ toolNotif = await Promise . race ( [
561+ toolListChanged . promise ,
562+ AbortSignal . timeout ( 2000 ) ,
563+ ] )
564+ } catch {
565+ throw new Error (
566+ '🚨 Did not receive tools/listChanged notification when expected. Make sure your server notifies clients when tools are enabled/disabled.' ,
567+ )
568+ }
569+ expect (
570+ toolNotif ,
571+ '🚨 Did not receive tools/listChanged notification when expected. Make sure your server notifies clients when tools are enabled/disabled.' ,
572+ ) . toBeDefined ( )
573+ } )
574+
575+ test ( 'ListChanged notification: prompts' , async ( ) => {
576+ await using setup = await setupClient ( )
577+ const { client } = setup
578+
579+ const promptListChanged = await deferred < any > ( )
580+ client . setNotificationHandler (
581+ PromptListChangedNotificationSchema ,
582+ ( notification ) => {
583+ promptListChanged . resolve ( notification )
584+ } ,
585+ )
586+
587+ // Trigger a DB change that should enable prompts
588+ await client . callTool ( {
589+ name : 'create_tag' ,
590+ arguments : {
591+ name : faker . lorem . word ( ) ,
592+ description : faker . lorem . sentence ( ) ,
593+ } ,
594+ } )
595+ await client . callTool ( {
596+ name : 'create_entry' ,
597+ arguments : {
598+ title : faker . lorem . words ( 3 ) ,
599+ content : faker . lorem . paragraphs ( 2 ) ,
600+ } ,
601+ } )
602+
603+ let promptNotif
604+ try {
605+ promptNotif = await Promise . race ( [
606+ promptListChanged . promise ,
607+ AbortSignal . timeout ( 2000 ) ,
608+ ] )
609+ } catch {
610+ throw new Error (
611+ '🚨 Did not receive prompts/listChanged notification when expected. Make sure your server notifies clients when prompts are enabled/disabled.' ,
612+ )
613+ }
614+ expect (
615+ promptNotif ,
616+ '🚨 Did not receive prompts/listChanged notification when expected. Make sure your server notifies clients when prompts are enabled/disabled.' ,
617+ ) . toBeDefined ( )
618+ } )
619+
620+ test ( 'Progress notification: create_wrapped_video (mock)' , async ( ) => {
621+ await using setup = await setupClient ( )
622+ const { client } = setup
623+
624+ const progressDeferred = await deferred < any > ( )
625+ client . setNotificationHandler ( ProgressNotificationSchema , ( notification ) => {
626+ progressDeferred . resolve ( notification )
627+ } )
628+
629+ // Ensure the tool is enabled by creating a tag and an entry first
630+ await client . callTool ( {
631+ name : 'create_tag' ,
632+ arguments : {
633+ name : faker . lorem . word ( ) ,
634+ description : faker . lorem . sentence ( ) ,
635+ } ,
636+ } )
637+ await client . callTool ( {
638+ name : 'create_entry' ,
639+ arguments : {
640+ title : faker . lorem . words ( 3 ) ,
641+ content : faker . lorem . paragraphs ( 2 ) ,
642+ } ,
643+ } )
644+
645+ // Call the tool with mockTime: 500
646+ const progressToken = faker . string . uuid ( )
647+ await client . callTool ( {
648+ name : 'create_wrapped_video' ,
649+ arguments : {
650+ mockTime : 500 ,
651+ } ,
652+ _meta : {
653+ progressToken,
654+ } ,
655+ } )
656+
657+ let progressNotif
658+ try {
659+ progressNotif = await Promise . race ( [
660+ progressDeferred . promise ,
661+ AbortSignal . timeout ( 2000 ) ,
662+ ] )
663+ } catch {
664+ throw new Error (
665+ '🚨 Did not receive progress notification for create_wrapped_video (mock). Make sure your tool sends progress updates when running in mock mode.' ,
666+ )
667+ }
668+ expect (
669+ progressNotif ,
670+ '🚨 Did not receive progress notification for create_wrapped_video (mock).' ,
671+ ) . toBeDefined ( )
672+ expect (
673+ typeof progressNotif . params . progress ,
674+ '🚨 progress should be a number' ,
675+ ) . toBe ( 'number' )
676+ expect (
677+ progressNotif . params . progress ,
678+ '🚨 progress should be a number between 0 and 1' ,
679+ ) . toBeGreaterThanOrEqual ( 0 )
680+ expect (
681+ progressNotif . params . progress ,
682+ '🚨 progress should be a number between 0 and 1' ,
683+ ) . toBeLessThanOrEqual ( 1 )
684+ expect (
685+ progressNotif . params . progressToken ,
686+ '🚨 progressToken should be a string' ,
687+ ) . toBe ( progressToken )
487688} )
0 commit comments