fix(server): return empty completion when handler is absent
This commit is contained in:
parent
492c859eb8
commit
301f8b06c7
4 changed files with 204 additions and 7 deletions
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue