fix(server): return empty completion when handler is absent

This commit is contained in:
King Star 2026-05-30 19:52:24 +08:00 • committed by Daniel Garnier-Moiroux
commit 301f8b06c7
4 changed files with 204 additions and 7 deletions

View file

@ -1090,9 +1090,7 @@ public class McpAsyncServer {
McpServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
if (specification == null) {
return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
.message("AsyncCompletionSpecification not found: " + request.ref())
.build());
return EMPTY_COMPLETION_RESULT;
}
return Mono.defer(() -> specification.completionHandler().apply(exchange, request));

View file

@ -815,9 +815,7 @@ public class McpStatelessAsyncServer {
McpStatelessServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
if (specification == null) {
return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
.message("AsyncCompletionSpecification not found: " + request.ref())
.build());
return EMPTY_COMPLETION_RESULT;
}
return specification.completionHandler().apply(ctx, request);

View file

@ -28,6 +28,9 @@ import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
import io.modelcontextprotocol.spec.McpSchema.Prompt;
import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
import io.modelcontextprotocol.spec.McpSchema.PromptReference;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
import io.modelcontextprotocol.spec.McpSchema.ResourceReference;
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool;
@ -230,6 +233,110 @@ class HttpServletStatelessIntegrationTests {
}
}
@ParameterizedTest(name = "{0} : Completion call without matching handler")
@ValueSource(strings = { "httpclient" })
void testCompletionWithoutMatchingHandlerReturnsEmptyResult(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
BiFunction<McpTransportContext, CompleteRequest, CompleteResult> completionHandler = (transportContext,
request) -> new CompleteResult(new CompleteResult.CompleteCompletion(List.of("java"), 1, false));
var prompt = Prompt.builder("code_review")
.title("Code review")
.description("this is code review prompt")
.arguments(List
.of(PromptArgument.builder("language").title("Language").description("string").required(false).build()))
.build();
var otherPrompt = Prompt.builder("other_prompt")
.title("Other prompt")
.description("this prompt has completions")
.arguments(List
.of(PromptArgument.builder("topic").title("Topic").description("string").required(false).build()))
.build();
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
.capabilities(ServerCapabilities.builder().completions().build())
.prompts(
new McpStatelessServerFeatures.SyncPromptSpecification(prompt,
(transportContext, getPromptRequest) -> null),
new McpStatelessServerFeatures.SyncPromptSpecification(otherPrompt,
(transportContext, getPromptRequest) -> null))
.completions(new McpStatelessServerFeatures.SyncCompletionSpecification(
PromptReference.builder("other_prompt").title("Other prompt").build(), completionHandler))
.build();
try (var mcpClient = clientBuilder.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
CompleteRequest request = CompleteRequest
.builder(PromptReference.builder("code_review").title("Code review").build(),
new CompleteRequest.CompleteArgument("language", "ja"))
.build();
CompleteResult result = mcpClient.completeCompletion(request);
assertThat(result.completion().values()).isEmpty();
assertThat(result.completion().total()).isZero();
assertThat(result.completion().hasMore()).isFalse();
}
finally {
mcpServer.close();
}
}
@ParameterizedTest(name = "{0} : Resource template completion call without matching handler")
@ValueSource(strings = { "httpclient" })
void testResourceTemplateCompletionWithoutMatchingHandlerReturnsEmptyResult(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
BiFunction<McpTransportContext, CompleteRequest, CompleteResult> completionHandler = (transportContext,
request) -> new CompleteResult(new CompleteResult.CompleteCompletion(List.of("java"), 1, false));
var template = ResourceTemplate.builder("test://resource/{param}", "Test Resource")
.title("Test resource")
.description("A resource template for testing")
.mimeType("text/plain")
.build();
var otherTemplate = ResourceTemplate.builder("test://other/{param}", "Other Resource")
.title("Other resource")
.description("A resource template with completions")
.mimeType("text/plain")
.build();
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
.capabilities(ServerCapabilities.builder().completions().build())
.resourceTemplates(
new McpStatelessServerFeatures.SyncResourceTemplateSpecification(template,
(transportContext, req) -> ReadResourceResult.builder(List.of()).build()),
new McpStatelessServerFeatures.SyncResourceTemplateSpecification(otherTemplate,
(transportContext, req) -> ReadResourceResult.builder(List.of()).build()))
.completions(new McpStatelessServerFeatures.SyncCompletionSpecification(
new ResourceReference("test://other/{param}"), completionHandler))
.build();
try (var mcpClient = clientBuilder.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
CompleteRequest request = CompleteRequest
.builder(new ResourceReference("test://resource/{param}"),
new CompleteRequest.CompleteArgument("param", "ja"))
.build();
CompleteResult result = mcpClient.completeCompletion(request);
assertThat(result.completion().values()).isEmpty();
assertThat(result.completion().total()).isZero();
assertThat(result.completion().hasMore()).isFalse();
}
finally {
mcpServer.close();
}
}
// ---------------------------------------
// Tool Structured Output Schema Tests
// ---------------------------------------

View file

@ -31,6 +31,7 @@ import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
import io.modelcontextprotocol.spec.McpSchema.Resource;
import io.modelcontextprotocol.spec.McpSchema.ResourceReference;
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
import io.modelcontextprotocol.spec.McpSchema.PromptReference;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpError;
@ -179,6 +180,99 @@ class McpCompletionTests {
mcpServer.close();
}
@Test
void testCompletionWithoutMatchingHandlerReturnsEmptyResult() {
BiFunction<McpSyncServerExchange, CompleteRequest, CompleteResult> completionHandler = (exchange,
request) -> new CompleteResult(new CompleteResult.CompleteCompletion(List.of("java"), 1, false));
McpSchema.Prompt prompt = Prompt.builder("code_review")
.description("this is a code review prompt")
.arguments(List.of(PromptArgument.builder("language").description("string").required(false).build()))
.build();
McpSchema.Prompt otherPrompt = Prompt.builder("other_prompt")
.description("this prompt has completions")
.arguments(List.of(PromptArgument.builder("topic").description("string").required(false).build()))
.build();
var mcpServer = McpServer.sync(mcpServerTransportProvider)
.capabilities(ServerCapabilities.builder().completions().build())
.prompts(
new McpServerFeatures.SyncPromptSpecification(prompt,
(mcpSyncServerExchange, getPromptRequest) -> null),
new McpServerFeatures.SyncPromptSpecification(otherPrompt,
(mcpSyncServerExchange, getPromptRequest) -> null))
.completions(new McpServerFeatures.SyncCompletionSpecification(new PromptReference("other_prompt"),
completionHandler))
.build();
try (var mcpClient = clientBuilder
.clientInfo(McpSchema.Implementation.builder("Sample " + "client", "0.0.0").build())
.build();) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
CompleteRequest request = CompleteRequest
.builder(new PromptReference("code_review"), new CompleteRequest.CompleteArgument("language", "ja"))
.build();
CompleteResult result = mcpClient.completeCompletion(request);
assertThat(result.completion().values()).isEmpty();
assertThat(result.completion().total()).isZero();
assertThat(result.completion().hasMore()).isFalse();
}
mcpServer.close();
}
@Test
void testResourceTemplateCompletionWithoutMatchingHandlerReturnsEmptyResult() {
BiFunction<McpSyncServerExchange, CompleteRequest, CompleteResult> completionHandler = (exchange,
request) -> new CompleteResult(new CompleteResult.CompleteCompletion(List.of("java"), 1, false));
ResourceTemplate template = ResourceTemplate.builder("test://resource/{param}", "Test Resource")
.description("A resource template for testing")
.mimeType("text/plain")
.build();
ResourceTemplate otherTemplate = ResourceTemplate.builder("test://other/{param}", "Other Resource")
.description("A resource template with completions")
.mimeType("text/plain")
.build();
var mcpServer = McpServer.sync(mcpServerTransportProvider)
.capabilities(ServerCapabilities.builder().completions().build())
.resourceTemplates(
new McpServerFeatures.SyncResourceTemplateSpecification(template,
(exchange, req) -> ReadResourceResult.builder(List.of()).build()),
new McpServerFeatures.SyncResourceTemplateSpecification(otherTemplate,
(exchange, req) -> ReadResourceResult.builder(List.of()).build()))
.completions(new McpServerFeatures.SyncCompletionSpecification(
new ResourceReference("test://other/{param}"), completionHandler))
.build();
try (var mcpClient = clientBuilder
.clientInfo(McpSchema.Implementation.builder("Sample " + "client", "0.0.0").build())
.build();) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
CompleteRequest request = CompleteRequest
.builder(new ResourceReference("test://resource/{param}"),
new CompleteRequest.CompleteArgument("param", "ja"))
.build();
CompleteResult result = mcpClient.completeCompletion(request);
assertThat(result.completion().values()).isEmpty();
assertThat(result.completion().total()).isZero();
assertThat(result.completion().hasMore()).isFalse();
}
mcpServer.close();
}
@Test
void testDependentCompletionScenario() {
BiFunction<McpSyncServerExchange, CompleteRequest, CompleteResult> completionHandler = (exchange, request) -> {
@ -365,4 +459,4 @@ class McpCompletionTests {
mcpServer.close();
}
}
}