1
+ package io .modelcontextprotocol .server ;
2
+
3
+ import java .util .List ;
4
+ import java .util .Map ;
5
+ import java .util .concurrent .atomic .AtomicReference ;
6
+ import java .util .function .BiFunction ;
7
+
8
+ import org .apache .catalina .LifecycleException ;
9
+ import org .apache .catalina .LifecycleState ;
10
+ import org .apache .catalina .startup .Tomcat ;
11
+ import static org .assertj .core .api .Assertions .assertThat ;
12
+ import org .junit .jupiter .api .AfterEach ;
13
+ import org .junit .jupiter .api .BeforeEach ;
14
+ import org .junit .jupiter .api .Test ;
15
+ import static org .assertj .core .api .Assertions .assertThatExceptionOfType ;
16
+
17
+ import com .fasterxml .jackson .databind .ObjectMapper ;
18
+
19
+ import io .modelcontextprotocol .client .McpClient ;
20
+ import io .modelcontextprotocol .client .transport .HttpClientSseClientTransport ;
21
+ import io .modelcontextprotocol .server .transport .HttpServletSseServerTransportProvider ;
22
+ import io .modelcontextprotocol .server .transport .TomcatTestUtil ;
23
+ import io .modelcontextprotocol .spec .McpSchema ;
24
+ import io .modelcontextprotocol .spec .McpSchema .CompleteRequest ;
25
+ import io .modelcontextprotocol .spec .McpSchema .CompleteResult ;
26
+ import io .modelcontextprotocol .spec .McpSchema .InitializeResult ;
27
+ import io .modelcontextprotocol .spec .McpSchema .Prompt ;
28
+ import io .modelcontextprotocol .spec .McpSchema .PromptArgument ;
29
+ import io .modelcontextprotocol .spec .McpSchema .ReadResourceResult ;
30
+ import io .modelcontextprotocol .spec .McpSchema .ResourceReference ;
31
+ import io .modelcontextprotocol .spec .McpSchema .PromptReference ;
32
+ import io .modelcontextprotocol .spec .McpSchema .ServerCapabilities ;
33
+ import io .modelcontextprotocol .spec .McpError ;
34
+
35
+ /**
36
+ * Tests for completion functionality with context support.
37
+ *
38
+ * @author Surbhi Bansal
39
+ */
40
+ class McpCompletionTests {
41
+
42
+ private HttpServletSseServerTransportProvider mcpServerTransportProvider ;
43
+
44
+ private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message" ;
45
+
46
+ McpClient .SyncSpec clientBuilder ;
47
+
48
+ private Tomcat tomcat ;
49
+
50
+ @ BeforeEach
51
+ public void before () {
52
+ // Create and con figure the transport provider
53
+ mcpServerTransportProvider = HttpServletSseServerTransportProvider .builder ()
54
+ .objectMapper (new ObjectMapper ())
55
+ .messageEndpoint (CUSTOM_MESSAGE_ENDPOINT )
56
+ .build ();
57
+
58
+ tomcat = TomcatTestUtil .createTomcatServer ("" , 3400 , mcpServerTransportProvider );
59
+ try {
60
+ tomcat .start ();
61
+ assertThat (tomcat .getServer ().getState ()).isEqualTo (LifecycleState .STARTED );
62
+ }
63
+ catch (Exception e ) {
64
+ throw new RuntimeException ("Failed to start Tomcat" , e );
65
+ }
66
+
67
+ this .clientBuilder = McpClient .sync (HttpClientSseClientTransport .builder ("http://localhost:" + 3400 ).build ());
68
+ }
69
+
70
+ @ AfterEach
71
+ public void after () {
72
+ if (mcpServerTransportProvider != null ) {
73
+ mcpServerTransportProvider .closeGracefully ().block ();
74
+ }
75
+ if (tomcat != null ) {
76
+ try {
77
+ tomcat .stop ();
78
+ tomcat .destroy ();
79
+ }
80
+ catch (LifecycleException e ) {
81
+ throw new RuntimeException ("Failed to stop Tomcat" , e );
82
+ }
83
+ }
84
+ }
85
+
86
+ @ Test
87
+ void testCompletionHandlerReceivesContext () {
88
+ AtomicReference <CompleteRequest > receivedRequest = new AtomicReference <>();
89
+ BiFunction <McpSyncServerExchange , CompleteRequest , CompleteResult > completionHandler = (exchange , request ) -> {
90
+ receivedRequest .set (request );
91
+ return new CompleteResult (new CompleteResult .CompleteCompletion (List .of ("test-completion" ), 1 , false ));
92
+ };
93
+
94
+ ResourceReference resourceRef = new ResourceReference ("ref/resource" , "test://resource/{param}" );
95
+
96
+ McpSchema .Resource resource = new McpSchema .Resource ("test://resource/{param}" , "Test Resource" ,
97
+ "A resource for testing" , "text/plain" , 123L , null );
98
+
99
+ var mcpServer = McpServer .sync (mcpServerTransportProvider )
100
+ .capabilities (ServerCapabilities .builder ().completions ().build ())
101
+ .resources (new McpServerFeatures .SyncResourceSpecification (resource ,
102
+ (exchange , req ) -> new ReadResourceResult (List .of ())))
103
+ .completions (new McpServerFeatures .SyncCompletionSpecification (resourceRef , completionHandler ))
104
+ .build ();
105
+
106
+ try (var mcpClient = clientBuilder .clientInfo (new McpSchema .Implementation ("Sample " + "client" , "0.0.0" ))
107
+ .build ();) {
108
+ InitializeResult initResult = mcpClient .initialize ();
109
+ assertThat (initResult ).isNotNull ();
110
+
111
+ // Test with context
112
+ CompleteRequest request = new CompleteRequest (resourceRef ,
113
+ new CompleteRequest .CompleteArgument ("param" , "test" ), null ,
114
+ new CompleteRequest .CompleteContext (Map .of ("previous" , "value" )));
115
+
116
+ CompleteResult result = mcpClient .completeCompletion (request );
117
+
118
+ // Verify handler received the context
119
+ assertThat (receivedRequest .get ().context ()).isNotNull ();
120
+ assertThat (receivedRequest .get ().context ().arguments ()).containsEntry ("previous" , "value" );
121
+ assertThat (result .completion ().values ()).containsExactly ("test-completion" );
122
+ }
123
+
124
+ mcpServer .close ();
125
+ }
126
+
127
+ @ Test
128
+ void testCompletionBackwardCompatibility () {
129
+ AtomicReference <Boolean > contextWasNull = new AtomicReference <>(false );
130
+ BiFunction <McpSyncServerExchange , CompleteRequest , CompleteResult > completionHandler = (exchange , request ) -> {
131
+ contextWasNull .set (request .context () == null );
132
+ return new CompleteResult (
133
+ new CompleteResult .CompleteCompletion (List .of ("no-context-completion" ), 1 , false ));
134
+ };
135
+
136
+ McpSchema .Prompt prompt = new Prompt ("test-prompt" , "this is a test prompt" ,
137
+ List .of (new PromptArgument ("arg" , "string" , false )));
138
+
139
+ var mcpServer = McpServer .sync (mcpServerTransportProvider )
140
+ .capabilities (ServerCapabilities .builder ().completions ().build ())
141
+ .prompts (new McpServerFeatures .SyncPromptSpecification (prompt ,
142
+ (mcpSyncServerExchange , getPromptRequest ) -> null ))
143
+ .completions (new McpServerFeatures .SyncCompletionSpecification (
144
+ new PromptReference ("ref/prompt" , "test-prompt" ), completionHandler ))
145
+ .build ();
146
+
147
+ try (var mcpClient = clientBuilder .clientInfo (new McpSchema .Implementation ("Sample " + "client" , "0.0.0" ))
148
+ .build ();) {
149
+ InitializeResult initResult = mcpClient .initialize ();
150
+ assertThat (initResult ).isNotNull ();
151
+
152
+ // Test without context
153
+ CompleteRequest request = new CompleteRequest (new PromptReference ("ref/prompt" , "test-prompt" ),
154
+ new CompleteRequest .CompleteArgument ("arg" , "val" ));
155
+
156
+ CompleteResult result = mcpClient .completeCompletion (request );
157
+
158
+ // Verify context was null
159
+ assertThat (contextWasNull .get ()).isTrue ();
160
+ assertThat (result .completion ().values ()).containsExactly ("no-context-completion" );
161
+ }
162
+
163
+ mcpServer .close ();
164
+ }
165
+
166
+ @ Test
167
+ void testDependentCompletionScenario () {
168
+ BiFunction <McpSyncServerExchange , CompleteRequest , CompleteResult > completionHandler = (exchange , request ) -> {
169
+ // Simulate database/table completion scenario
170
+ if (request .ref () instanceof ResourceReference resourceRef ) {
171
+ if ("db://{database}/{table}" .equals (resourceRef .uri ())) {
172
+ if ("database" .equals (request .argument ().name ())) {
173
+ // Complete database names
174
+ return new CompleteResult (new CompleteResult .CompleteCompletion (
175
+ List .of ("users_db" , "products_db" , "analytics_db" ), 3 , false ));
176
+ }
177
+ else if ("table" .equals (request .argument ().name ())) {
178
+ // Complete table names based on selected database
179
+ if (request .context () != null && request .context ().arguments () != null ) {
180
+ String db = request .context ().arguments ().get ("database" );
181
+ if ("users_db" .equals (db )) {
182
+ return new CompleteResult (new CompleteResult .CompleteCompletion (
183
+ List .of ("users" , "sessions" , "permissions" ), 3 , false ));
184
+ }
185
+ else if ("products_db" .equals (db )) {
186
+ return new CompleteResult (new CompleteResult .CompleteCompletion (
187
+ List .of ("products" , "categories" , "inventory" ), 3 , false ));
188
+ }
189
+ }
190
+ }
191
+ }
192
+ }
193
+ return new CompleteResult (new CompleteResult .CompleteCompletion (List .of (), 0 , false ));
194
+ };
195
+
196
+ McpSchema .Resource resource = new McpSchema .Resource ("db://{database}/{table}" , "Database Table" ,
197
+ "Resource representing a table in a database" , "application/json" , 456L , null );
198
+
199
+ var mcpServer = McpServer .sync (mcpServerTransportProvider )
200
+ .capabilities (ServerCapabilities .builder ().completions ().build ())
201
+ .resources (new McpServerFeatures .SyncResourceSpecification (resource ,
202
+ (exchange , req ) -> new ReadResourceResult (List .of ())))
203
+ .completions (new McpServerFeatures .SyncCompletionSpecification (
204
+ new ResourceReference ("ref/resource" , "db://{database}/{table}" ), completionHandler ))
205
+ .build ();
206
+
207
+ try (var mcpClient = clientBuilder .clientInfo (new McpSchema .Implementation ("Sample " + "client" , "0.0.0" ))
208
+ .build ();) {
209
+ InitializeResult initResult = mcpClient .initialize ();
210
+ assertThat (initResult ).isNotNull ();
211
+
212
+ // First, complete database
213
+ CompleteRequest dbRequest = new CompleteRequest (
214
+ new ResourceReference ("ref/resource" , "db://{database}/{table}" ),
215
+ new CompleteRequest .CompleteArgument ("database" , "" ));
216
+
217
+ CompleteResult dbResult = mcpClient .completeCompletion (dbRequest );
218
+ assertThat (dbResult .completion ().values ()).contains ("users_db" , "products_db" );
219
+
220
+ // Then complete table with database context
221
+ CompleteRequest tableRequest = new CompleteRequest (
222
+ new ResourceReference ("ref/resource" , "db://{database}/{table}" ),
223
+ new CompleteRequest .CompleteArgument ("table" , "" ),
224
+ new CompleteRequest .CompleteContext (Map .of ("database" , "users_db" )));
225
+
226
+ CompleteResult tableResult = mcpClient .completeCompletion (tableRequest );
227
+ assertThat (tableResult .completion ().values ()).containsExactly ("users" , "sessions" , "permissions" );
228
+
229
+ // Different database gives different tables
230
+ CompleteRequest tableRequest2 = new CompleteRequest (
231
+ new ResourceReference ("ref/resource" , "db://{database}/{table}" ),
232
+ new CompleteRequest .CompleteArgument ("table" , "" ),
233
+ new CompleteRequest .CompleteContext (Map .of ("database" , "products_db" )));
234
+
235
+ CompleteResult tableResult2 = mcpClient .completeCompletion (tableRequest2 );
236
+ assertThat (tableResult2 .completion ().values ()).containsExactly ("products" , "categories" , "inventory" );
237
+ }
238
+
239
+ mcpServer .close ();
240
+ }
241
+
242
+ @ Test
243
+ void testCompletionErrorOnMissingContext () {
244
+ BiFunction <McpSyncServerExchange , CompleteRequest , CompleteResult > completionHandler = (exchange , request ) -> {
245
+ if (request .ref () instanceof ResourceReference resourceRef ) {
246
+ if ("db://{database}/{table}" .equals (resourceRef .uri ())) {
247
+ if ("table" .equals (request .argument ().name ())) {
248
+ // Check if database context is provided
249
+ if (request .context () == null || request .context ().arguments () == null
250
+ || !request .context ().arguments ().containsKey ("database" )) {
251
+ throw new McpError ("Please select a database first to see available tables" );
252
+ }
253
+ // Normal completion if context is provided
254
+ String db = request .context ().arguments ().get ("database" );
255
+ if ("test_db" .equals (db )) {
256
+ return new CompleteResult (new CompleteResult .CompleteCompletion (
257
+ List .of ("users" , "orders" , "products" ), 3 , false ));
258
+ }
259
+ }
260
+ }
261
+ }
262
+ return new CompleteResult (new CompleteResult .CompleteCompletion (List .of (), 0 , false ));
263
+ };
264
+
265
+ McpSchema .Resource resource = new McpSchema .Resource ("db://{database}/{table}" , "Database Table" ,
266
+ "Resource representing a table in a database" , "application/json" , 456L , null );
267
+
268
+ var mcpServer = McpServer .sync (mcpServerTransportProvider )
269
+ .capabilities (ServerCapabilities .builder ().completions ().build ())
270
+ .resources (new McpServerFeatures .SyncResourceSpecification (resource ,
271
+ (exchange , req ) -> new ReadResourceResult (List .of ())))
272
+ .completions (new McpServerFeatures .SyncCompletionSpecification (
273
+ new ResourceReference ("ref/resource" , "db://{database}/{table}" ), completionHandler ))
274
+ .build ();
275
+
276
+ try (var mcpClient = clientBuilder .clientInfo (new McpSchema .Implementation ("Sample" + "client" , "0.0.0" ))
277
+ .build ();) {
278
+ InitializeResult initResult = mcpClient .initialize ();
279
+ assertThat (initResult ).isNotNull ();
280
+
281
+ // Try to complete table without database context - should raise error
282
+ CompleteRequest requestWithoutContext = new CompleteRequest (
283
+ new ResourceReference ("ref/resource" , "db://{database}/{table}" ),
284
+ new CompleteRequest .CompleteArgument ("table" , "" ));
285
+
286
+ assertThatExceptionOfType (McpError .class )
287
+ .isThrownBy (() -> mcpClient .completeCompletion (requestWithoutContext ))
288
+ .withMessageContaining ("Please select a database first" );
289
+
290
+ // Now complete with proper context - should work normally
291
+ CompleteRequest requestWithContext = new CompleteRequest (
292
+ new ResourceReference ("ref/resource" , "db://{database}/{table}" ),
293
+ new CompleteRequest .CompleteArgument ("table" , "" ),
294
+ new CompleteRequest .CompleteContext (Map .of ("database" , "test_db" )));
295
+
296
+ CompleteResult resultWithContext = mcpClient .completeCompletion (requestWithContext );
297
+ assertThat (resultWithContext .completion ().values ()).containsExactly ("users" , "orders" , "products" );
298
+ }
299
+
300
+ mcpServer .close ();
301
+ }
302
+
303
+ }
0 commit comments