Skip to content

Commit bdacaeb

Browse files
committed
fix(server): return empty completion when handler is absent
1 parent dbb9bda commit bdacaeb

4 files changed

Lines changed: 204 additions & 7 deletions

File tree

mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,9 +1089,7 @@ private McpRequestHandler<McpSchema.CompleteResult> completionCompleteRequestHan
10891089
McpServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
10901090

10911091
if (specification == null) {
1092-
return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
1093-
.message("AsyncCompletionSpecification not found: " + request.ref())
1094-
.build());
1092+
return EMPTY_COMPLETION_RESULT;
10951093
}
10961094

10971095
return Mono.defer(() -> specification.completionHandler().apply(exchange, request));

mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -813,9 +813,7 @@ private McpStatelessRequestHandler<McpSchema.CompleteResult> completionCompleteR
813813
McpStatelessServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
814814

815815
if (specification == null) {
816-
return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
817-
.message("AsyncCompletionSpecification not found: " + request.ref())
818-
.build());
816+
return EMPTY_COMPLETION_RESULT;
819817
}
820818

821819
return specification.completionHandler().apply(ctx, request);

mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
import io.modelcontextprotocol.spec.McpSchema.Prompt;
2929
import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
3030
import io.modelcontextprotocol.spec.McpSchema.PromptReference;
31+
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
32+
import io.modelcontextprotocol.spec.McpSchema.ResourceReference;
33+
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
3134
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
3235
import io.modelcontextprotocol.spec.McpSchema.TextContent;
3336
import io.modelcontextprotocol.spec.McpSchema.Tool;
@@ -230,6 +233,110 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) {
230233
}
231234
}
232235

236+
@ParameterizedTest(name = "{0} : Completion call without matching handler")
237+
@ValueSource(strings = { "httpclient" })
238+
void testCompletionWithoutMatchingHandlerReturnsEmptyResult(String clientType) {
239+
var clientBuilder = clientBuilders.get(clientType);
240+
241+
BiFunction<McpTransportContext, CompleteRequest, CompleteResult> completionHandler = (transportContext,
242+
request) -> new CompleteResult(new CompleteResult.CompleteCompletion(List.of("java"), 1, false));
243+
244+
var prompt = Prompt.builder("code_review")
245+
.title("Code review")
246+
.description("this is code review prompt")
247+
.arguments(List
248+
.of(PromptArgument.builder("language").title("Language").description("string").required(false).build()))
249+
.build();
250+
251+
var otherPrompt = Prompt.builder("other_prompt")
252+
.title("Other prompt")
253+
.description("this prompt has completions")
254+
.arguments(List
255+
.of(PromptArgument.builder("topic").title("Topic").description("string").required(false).build()))
256+
.build();
257+
258+
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
259+
.capabilities(ServerCapabilities.builder().completions().build())
260+
.prompts(
261+
new McpStatelessServerFeatures.SyncPromptSpecification(prompt,
262+
(transportContext, getPromptRequest) -> null),
263+
new McpStatelessServerFeatures.SyncPromptSpecification(otherPrompt,
264+
(transportContext, getPromptRequest) -> null))
265+
.completions(new McpStatelessServerFeatures.SyncCompletionSpecification(
266+
PromptReference.builder("other_prompt").title("Other prompt").build(), completionHandler))
267+
.build();
268+
269+
try (var mcpClient = clientBuilder.build()) {
270+
InitializeResult initResult = mcpClient.initialize();
271+
assertThat(initResult).isNotNull();
272+
273+
CompleteRequest request = CompleteRequest
274+
.builder(PromptReference.builder("code_review").title("Code review").build(),
275+
new CompleteRequest.CompleteArgument("language", "ja"))
276+
.build();
277+
278+
CompleteResult result = mcpClient.completeCompletion(request);
279+
280+
assertThat(result.completion().values()).isEmpty();
281+
assertThat(result.completion().total()).isZero();
282+
assertThat(result.completion().hasMore()).isFalse();
283+
}
284+
finally {
285+
mcpServer.close();
286+
}
287+
}
288+
289+
@ParameterizedTest(name = "{0} : Resource template completion call without matching handler")
290+
@ValueSource(strings = { "httpclient" })
291+
void testResourceTemplateCompletionWithoutMatchingHandlerReturnsEmptyResult(String clientType) {
292+
var clientBuilder = clientBuilders.get(clientType);
293+
294+
BiFunction<McpTransportContext, CompleteRequest, CompleteResult> completionHandler = (transportContext,
295+
request) -> new CompleteResult(new CompleteResult.CompleteCompletion(List.of("java"), 1, false));
296+
297+
var template = ResourceTemplate.builder("test://resource/{param}", "Test Resource")
298+
.title("Test resource")
299+
.description("A resource template for testing")
300+
.mimeType("text/plain")
301+
.build();
302+
303+
var otherTemplate = ResourceTemplate.builder("test://other/{param}", "Other Resource")
304+
.title("Other resource")
305+
.description("A resource template with completions")
306+
.mimeType("text/plain")
307+
.build();
308+
309+
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
310+
.capabilities(ServerCapabilities.builder().completions().build())
311+
.resourceTemplates(
312+
new McpStatelessServerFeatures.SyncResourceTemplateSpecification(template,
313+
(transportContext, req) -> ReadResourceResult.builder(List.of()).build()),
314+
new McpStatelessServerFeatures.SyncResourceTemplateSpecification(otherTemplate,
315+
(transportContext, req) -> ReadResourceResult.builder(List.of()).build()))
316+
.completions(new McpStatelessServerFeatures.SyncCompletionSpecification(
317+
new ResourceReference("test://other/{param}"), completionHandler))
318+
.build();
319+
320+
try (var mcpClient = clientBuilder.build()) {
321+
InitializeResult initResult = mcpClient.initialize();
322+
assertThat(initResult).isNotNull();
323+
324+
CompleteRequest request = CompleteRequest
325+
.builder(new ResourceReference("test://resource/{param}"),
326+
new CompleteRequest.CompleteArgument("param", "ja"))
327+
.build();
328+
329+
CompleteResult result = mcpClient.completeCompletion(request);
330+
331+
assertThat(result.completion().values()).isEmpty();
332+
assertThat(result.completion().total()).isZero();
333+
assertThat(result.completion().hasMore()).isFalse();
334+
}
335+
finally {
336+
mcpServer.close();
337+
}
338+
}
339+
233340
// ---------------------------------------
234341
// Tool Structured Output Schema Tests
235342
// ---------------------------------------

mcp-test/src/test/java/io/modelcontextprotocol/server/McpCompletionTests.java

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
3232
import io.modelcontextprotocol.spec.McpSchema.Resource;
3333
import io.modelcontextprotocol.spec.McpSchema.ResourceReference;
34+
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
3435
import io.modelcontextprotocol.spec.McpSchema.PromptReference;
3536
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
3637
import io.modelcontextprotocol.spec.McpError;
@@ -179,6 +180,99 @@ void testCompletionBackwardCompatibility() {
179180
mcpServer.close();
180181
}
181182

183+
@Test
184+
void testCompletionWithoutMatchingHandlerReturnsEmptyResult() {
185+
BiFunction<McpSyncServerExchange, CompleteRequest, CompleteResult> completionHandler = (exchange,
186+
request) -> new CompleteResult(new CompleteResult.CompleteCompletion(List.of("java"), 1, false));
187+
188+
McpSchema.Prompt prompt = Prompt.builder("code_review")
189+
.description("this is a code review prompt")
190+
.arguments(List.of(PromptArgument.builder("language").description("string").required(false).build()))
191+
.build();
192+
193+
McpSchema.Prompt otherPrompt = Prompt.builder("other_prompt")
194+
.description("this prompt has completions")
195+
.arguments(List.of(PromptArgument.builder("topic").description("string").required(false).build()))
196+
.build();
197+
198+
var mcpServer = McpServer.sync(mcpServerTransportProvider)
199+
.capabilities(ServerCapabilities.builder().completions().build())
200+
.prompts(
201+
new McpServerFeatures.SyncPromptSpecification(prompt,
202+
(mcpSyncServerExchange, getPromptRequest) -> null),
203+
new McpServerFeatures.SyncPromptSpecification(otherPrompt,
204+
(mcpSyncServerExchange, getPromptRequest) -> null))
205+
.completions(new McpServerFeatures.SyncCompletionSpecification(new PromptReference("other_prompt"),
206+
completionHandler))
207+
.build();
208+
209+
try (var mcpClient = clientBuilder
210+
.clientInfo(McpSchema.Implementation.builder("Sample " + "client", "0.0.0").build())
211+
.build();) {
212+
InitializeResult initResult = mcpClient.initialize();
213+
assertThat(initResult).isNotNull();
214+
215+
CompleteRequest request = CompleteRequest
216+
.builder(new PromptReference("code_review"), new CompleteRequest.CompleteArgument("language", "ja"))
217+
.build();
218+
219+
CompleteResult result = mcpClient.completeCompletion(request);
220+
221+
assertThat(result.completion().values()).isEmpty();
222+
assertThat(result.completion().total()).isZero();
223+
assertThat(result.completion().hasMore()).isFalse();
224+
}
225+
226+
mcpServer.close();
227+
}
228+
229+
@Test
230+
void testResourceTemplateCompletionWithoutMatchingHandlerReturnsEmptyResult() {
231+
BiFunction<McpSyncServerExchange, CompleteRequest, CompleteResult> completionHandler = (exchange,
232+
request) -> new CompleteResult(new CompleteResult.CompleteCompletion(List.of("java"), 1, false));
233+
234+
ResourceTemplate template = ResourceTemplate.builder("test://resource/{param}", "Test Resource")
235+
.description("A resource template for testing")
236+
.mimeType("text/plain")
237+
.build();
238+
239+
ResourceTemplate otherTemplate = ResourceTemplate.builder("test://other/{param}", "Other Resource")
240+
.description("A resource template with completions")
241+
.mimeType("text/plain")
242+
.build();
243+
244+
var mcpServer = McpServer.sync(mcpServerTransportProvider)
245+
.capabilities(ServerCapabilities.builder().completions().build())
246+
.resourceTemplates(
247+
new McpServerFeatures.SyncResourceTemplateSpecification(template,
248+
(exchange, req) -> ReadResourceResult.builder(List.of()).build()),
249+
new McpServerFeatures.SyncResourceTemplateSpecification(otherTemplate,
250+
(exchange, req) -> ReadResourceResult.builder(List.of()).build()))
251+
.completions(new McpServerFeatures.SyncCompletionSpecification(
252+
new ResourceReference("test://other/{param}"), completionHandler))
253+
.build();
254+
255+
try (var mcpClient = clientBuilder
256+
.clientInfo(McpSchema.Implementation.builder("Sample " + "client", "0.0.0").build())
257+
.build();) {
258+
InitializeResult initResult = mcpClient.initialize();
259+
assertThat(initResult).isNotNull();
260+
261+
CompleteRequest request = CompleteRequest
262+
.builder(new ResourceReference("test://resource/{param}"),
263+
new CompleteRequest.CompleteArgument("param", "ja"))
264+
.build();
265+
266+
CompleteResult result = mcpClient.completeCompletion(request);
267+
268+
assertThat(result.completion().values()).isEmpty();
269+
assertThat(result.completion().total()).isZero();
270+
assertThat(result.completion().hasMore()).isFalse();
271+
}
272+
273+
mcpServer.close();
274+
}
275+
182276
@Test
183277
void testDependentCompletionScenario() {
184278
BiFunction<McpSyncServerExchange, CompleteRequest, CompleteResult> completionHandler = (exchange, request) -> {
@@ -365,4 +459,4 @@ void testPromptWithoutArgumentsCompletionForArgument() {
365459
mcpServer.close();
366460
}
367461

368-
}
462+
}

0 commit comments

Comments
 (0)