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