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());
|
McpServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
|
||||||
|
|
||||||
if (specification == null) {
|
if (specification == null) {
|
||||||
return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
|
return EMPTY_COMPLETION_RESULT;
|
||||||
.message("AsyncCompletionSpecification not found: " + request.ref())
|
|
||||||
.build());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Mono.defer(() -> specification.completionHandler().apply(exchange, request));
|
return Mono.defer(() -> specification.completionHandler().apply(exchange, request));
|
||||||
|
|
|
||||||
|
|
@ -815,9 +815,7 @@ public class McpStatelessAsyncServer {
|
||||||
McpStatelessServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
|
McpStatelessServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
|
||||||
|
|
||||||
if (specification == null) {
|
if (specification == null) {
|
||||||
return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
|
return EMPTY_COMPLETION_RESULT;
|
||||||
.message("AsyncCompletionSpecification not found: " + request.ref())
|
|
||||||
.build());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return specification.completionHandler().apply(ctx, request);
|
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.Prompt;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
|
import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.PromptReference;
|
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.ServerCapabilities;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.TextContent;
|
import io.modelcontextprotocol.spec.McpSchema.TextContent;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
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
|
// 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.ReadResourceResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Resource;
|
import io.modelcontextprotocol.spec.McpSchema.Resource;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ResourceReference;
|
import io.modelcontextprotocol.spec.McpSchema.ResourceReference;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.PromptReference;
|
import io.modelcontextprotocol.spec.McpSchema.PromptReference;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
|
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
|
|
@ -179,6 +180,99 @@ class McpCompletionTests {
|
||||||
mcpServer.close();
|
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
|
@Test
|
||||||
void testDependentCompletionScenario() {
|
void testDependentCompletionScenario() {
|
||||||
BiFunction<McpSyncServerExchange, CompleteRequest, CompleteResult> completionHandler = (exchange, request) -> {
|
BiFunction<McpSyncServerExchange, CompleteRequest, CompleteResult> completionHandler = (exchange, request) -> {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue