1616
1717package org .springframework .ai .mcp .client .common .autoconfigure .annotations ;
1818
19+ import java .util .ArrayList ;
20+ import java .util .Collections ;
1921import java .util .List ;
2022
2123import io .modelcontextprotocol .spec .McpSchema ;
24+ import org .junit .Test ;
2225import org .junit .jupiter .params .ParameterizedTest ;
2326import org .junit .jupiter .params .provider .ValueSource ;
2427import org .springaicommunity .mcp .annotation .McpPromptListChanged ;
2528import org .springaicommunity .mcp .annotation .McpResourceListChanged ;
2629import org .springaicommunity .mcp .annotation .McpToolListChanged ;
30+ import reactor .core .publisher .Mono ;
2731
32+ import org .springframework .ai .mcp .annotation .spring .ClientMcpAsyncHandlersRegistry ;
33+ import org .springframework .ai .mcp .annotation .spring .ClientMcpSyncHandlersRegistry ;
2834import org .springframework .boot .autoconfigure .AutoConfigurations ;
2935import org .springframework .boot .test .context .runner .ApplicationContextRunner ;
3036import org .springframework .context .annotation .Bean ;
@@ -47,25 +53,65 @@ public class McpClientListChangedAnnotationsScanningIT {
4753 private final ApplicationContextRunner contextRunner = new ApplicationContextRunner ()
4854 .withConfiguration (AutoConfigurations .of (McpClientAnnotationScannerAutoConfiguration .class ));
4955
50- @ ParameterizedTest
51- @ ValueSource (strings = { "SYNC" , "ASYNC" })
52- void shouldScanAllThreeListChangedAnnotations (String clientType ) {
53- String prefix = clientType .toLowerCase ();
56+ @ Test
57+ public void shouldScanAllThreeListChangedAnnotationsSync () {
58+ this .contextRunner .withUserConfiguration (AllListChangedConfiguration .class )
59+ .withPropertyValues ("spring.ai.mcp.client.type=SYNC" )
60+ .run (context -> {
61+ // Verify all three annotations were scanned
62+ var registry = context .getBean (ClientMcpSyncHandlersRegistry .class );
63+ var handlers = context .getBean (TestListChangedHandlers .class );
64+ assertThat (registry ).isNotNull ();
65+
66+ List <McpSchema .Tool > updatedTools = List .of (McpSchema .Tool .builder ().name ("tool-1" ).build (),
67+ McpSchema .Tool .builder ().name ("tool-2" ).build ());
68+ List <McpSchema .Prompt > updatedPrompts = List .of (
69+ new McpSchema .Prompt ("prompt-1" , "a test prompt" , Collections .emptyList ()),
70+ new McpSchema .Prompt ("prompt-2" , "another test prompt" , Collections .emptyList ()));
71+ List <McpSchema .Resource > updatedResources = List .of (
72+ McpSchema .Resource .builder ().name ("resource-1" ).uri ("file:///resource/1" ).build (),
73+ McpSchema .Resource .builder ().name ("resource-2" ).uri ("file:///resource/2" ).build ());
74+
75+ registry .handleToolListChanged ("test-client" , updatedTools );
76+ registry .handleResourceListChanged ("test-client" , updatedResources );
77+ registry .handlePromptListChanged ("test-client" , updatedPrompts );
78+
79+ assertThat (handlers .getCalls ()).hasSize (3 )
80+ .containsExactlyInAnyOrder (
81+ new TestListChangedHandlers .Call ("resource-list-changed" , updatedResources ),
82+ new TestListChangedHandlers .Call ("prompt-list-changed" , updatedPrompts ),
83+ new TestListChangedHandlers .Call ("tool-list-changed" , updatedTools ));
84+ });
85+ }
5486
87+ @ Test
88+ public void shouldScanAllThreeListChangedAnnotationsAsync () {
5589 this .contextRunner .withUserConfiguration (AllListChangedConfiguration .class )
56- .withPropertyValues ("spring.ai.mcp.client.type=" + clientType )
90+ .withPropertyValues ("spring.ai.mcp.client.type=ASYNC" )
5791 .run (context -> {
5892 // Verify all three annotations were scanned
59- McpClientAnnotationScannerAutoConfiguration .ClientMcpAnnotatedBeans annotatedBeans = context
60- .getBean (McpClientAnnotationScannerAutoConfiguration .ClientMcpAnnotatedBeans .class );
61- assertThat (annotatedBeans .getBeansByAnnotation (McpToolListChanged .class )).hasSize (1 );
62- assertThat (annotatedBeans .getBeansByAnnotation (McpResourceListChanged .class )).hasSize (1 );
63- assertThat (annotatedBeans .getBeansByAnnotation (McpPromptListChanged .class )).hasSize (1 );
64-
65- // Verify all three specification beans were created
66- assertThat (context ).hasBean (prefix + "ToolListChangedSpecs" );
67- assertThat (context ).hasBean (prefix + "ResourceListChangedSpecs" );
68- assertThat (context ).hasBean (prefix + "PromptListChangedSpecs" );
93+ var registry = context .getBean (ClientMcpAsyncHandlersRegistry .class );
94+ var handlers = context .getBean (TestListChangedHandlers .class );
95+ assertThat (registry ).isNotNull ();
96+
97+ List <McpSchema .Tool > updatedTools = List .of (McpSchema .Tool .builder ().name ("tool-1" ).build (),
98+ McpSchema .Tool .builder ().name ("tool-2" ).build ());
99+ List <McpSchema .Prompt > updatedPrompts = List .of (
100+ new McpSchema .Prompt ("prompt-1" , "a test prompt" , Collections .emptyList ()),
101+ new McpSchema .Prompt ("prompt-2" , "another test prompt" , Collections .emptyList ()));
102+ List <McpSchema .Resource > updatedResources = List .of (
103+ McpSchema .Resource .builder ().name ("resource-1" ).uri ("file:///resource/1" ).build (),
104+ McpSchema .Resource .builder ().name ("resource-2" ).uri ("file:///resource/2" ).build ());
105+
106+ registry .handleToolListChanged ("test-client" , updatedTools ).block ();
107+ registry .handleResourceListChanged ("test-client" , updatedResources ).block ();
108+ registry .handlePromptListChanged ("test-client" , updatedPrompts ).block ();
109+
110+ assertThat (handlers .getCalls ()).hasSize (3 )
111+ .containsExactlyInAnyOrder (
112+ new TestListChangedHandlers .Call ("resource-list-changed" , updatedResources ),
113+ new TestListChangedHandlers .Call ("prompt-list-changed" , updatedPrompts ),
114+ new TestListChangedHandlers .Call ("tool-list-changed" , updatedTools ));
69115 });
70116 }
71117
@@ -79,10 +125,8 @@ void shouldNotScanAnnotationsWhenScannerDisabled(String clientType) {
79125 "spring.ai.mcp.client.annotation-scanner.enabled=false" )
80126 .run (context -> {
81127 // Verify scanner beans were not created
82- assertThat (context ).doesNotHaveBean (McpClientAnnotationScannerAutoConfiguration .class );
83- assertThat (context ).doesNotHaveBean (prefix + "ToolListChangedSpecs" );
84- assertThat (context ).doesNotHaveBean (prefix + "ResourceListChangedSpecs" );
85- assertThat (context ).doesNotHaveBean (prefix + "PromptListChangedSpecs" );
128+ assertThat (context ).doesNotHaveBean (ClientMcpSyncHandlersRegistry .class );
129+ assertThat (context ).doesNotHaveBean (ClientMcpAsyncHandlersRegistry .class );
86130 });
87131 }
88132
@@ -98,19 +142,47 @@ TestListChangedHandlers testHandlers() {
98142
99143 static class TestListChangedHandlers {
100144
145+ private final List <Call > calls = new ArrayList <>();
146+
147+ public List <Call > getCalls () {
148+ return this .calls ;
149+ }
150+
101151 @ McpToolListChanged (clients = "test-client" )
102152 public void onToolListChanged (List <McpSchema .Tool > updatedTools ) {
103- // Test handler for tool list changes
153+ this . calls . add ( new Call ( " tool- list-changed" , updatedTools ));
104154 }
105155
106156 @ McpResourceListChanged (clients = "test-client" )
107157 public void onResourceListChanged (List <McpSchema .Resource > updatedResources ) {
108- // Test handler for resource list changes
158+ this . calls . add ( new Call ( " resource- list-changed" , updatedResources ));
109159 }
110160
111161 @ McpPromptListChanged (clients = "test-client" )
112162 public void onPromptListChanged (List <McpSchema .Prompt > updatedPrompts ) {
113- // Test handler for prompt list changes
163+ this .calls .add (new Call ("prompt-list-changed" , updatedPrompts ));
164+ }
165+
166+ @ McpToolListChanged (clients = "test-client" )
167+ public Mono <Void > onToolListChangedReactive (List <McpSchema .Tool > updatedTools ) {
168+ this .calls .add (new Call ("tool-list-changed" , updatedTools ));
169+ return Mono .empty ();
170+ }
171+
172+ @ McpResourceListChanged (clients = "test-client" )
173+ public Mono <Void > onResourceListChangedReactive (List <McpSchema .Resource > updatedResources ) {
174+ this .calls .add (new Call ("resource-list-changed" , updatedResources ));
175+ return Mono .empty ();
176+ }
177+
178+ @ McpPromptListChanged (clients = "test-client" )
179+ public Mono <Void > onPromptListChangedReactive (List <McpSchema .Prompt > updatedPrompts ) {
180+ this .calls .add (new Call ("prompt-list-changed" , updatedPrompts ));
181+ return Mono .empty ();
182+ }
183+
184+ // Record calls made to this object
185+ record Call (String name , Object callRequest ) {
114186 }
115187
116188 }
0 commit comments