From e0cb7076c6a51b33472f006c67261174c823d089 Mon Sep 17 00:00:00 2001 From: jitokim Date: Fri, 11 Apr 2025 13:36:37 +0900 Subject: [PATCH 1/4] implement handler to support serverCapabilities completions Signed-off-by: jitokim --- .../WebMvcSseServerTransportProvider.java | 2 +- .../server/McpAsyncServer.java | 50 ++++++++++++++++++ .../server/McpServer.java | 28 ++++++++-- .../server/McpServerFeatures.java | 52 +++++++++++++++++-- .../server/McpSyncServer.java | 2 - .../server/McpSyncServerExchange.java | 1 - .../modelcontextprotocol/spec/McpSchema.java | 14 ++++- 7 files changed, 138 insertions(+), 11 deletions(-) diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java index 7bd1aa6c9..fc86cfaa0 100644 --- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java @@ -300,7 +300,7 @@ private ServerResponse handleMessage(ServerRequest request) { return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down"); } - if (!request.param("sessionId").isPresent()) { + if (request.param("sessionId").isEmpty()) { return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint")); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index ec2a04c9e..44763d040 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -67,6 +67,7 @@ * * @author Christian Tzolov * @author Dariusz Jędrzejczyk + * @author Jihoon Kim * @see McpServer * @see McpSchema * @see McpClientSession @@ -257,6 +258,8 @@ private static class AsyncServerImpl extends McpAsyncServer { private final ConcurrentHashMap prompts = new ConcurrentHashMap<>(); + private final ConcurrentHashMap completions = new ConcurrentHashMap<>(); + private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG; private List protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION); @@ -272,6 +275,7 @@ private static class AsyncServerImpl extends McpAsyncServer { this.resources.putAll(features.resources()); this.resourceTemplates.addAll(features.resourceTemplates()); this.prompts.putAll(features.prompts()); + this.completions.putAll(features.completions()); Map> requestHandlers = new HashMap<>(); @@ -304,6 +308,11 @@ private static class AsyncServerImpl extends McpAsyncServer { requestHandlers.put(McpSchema.METHOD_LOGGING_SET_LEVEL, setLoggerRequestHandler()); } + // Add completion API handlers if the completion capability is enabled + if (this.serverCapabilities.completions() != null) { + requestHandlers.put(McpSchema.METHOD_COMPLETION_COMPLETE, completionCompleteRequestHandler()); + } + Map notificationHandlers = new HashMap<>(); notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (exchange, params) -> Mono.empty()); @@ -686,6 +695,47 @@ private McpServerSession.RequestHandler setLoggerRequestHandler() { }; } + private McpServerSession.RequestHandler completionCompleteRequestHandler() { + return (exchange, params) -> { + McpSchema.CompleteRequest request = objectMapper.convertValue(params, McpSchema.CompleteRequest.class); + + if (request.ref() == null) { + return Mono.error(new McpError("ref must not be null")); + } + + if (request.ref().type() == null) { + return Mono.error(new McpError("type must not be null")); + } + + String type = request.ref().type(); + + if (type.equals("ref/prompt") + && request.ref() instanceof McpSchema.CompleteRequest.PromptReference promptReference) { + McpServerFeatures.AsyncPromptSpecification prompt = this.prompts.get(promptReference.name()); + if (prompt == null) { + return Mono.error(new McpError("Prompt not found: " + promptReference.name())); + } + } + + if (type.equals("ref/resource") + && request.ref() instanceof McpSchema.CompleteRequest.ResourceReference resourceReference) { + McpServerFeatures.AsyncResourceSpecification resource = this.resources.get(resourceReference.uri()); + if (resource == null) { + return Mono.error(new McpError("Resource not found: " + resourceReference.uri())); + } + } + + McpServerFeatures.CompletionRefKey key = McpServerFeatures.CompletionRefKey.from(request); + McpServerFeatures.AsyncCompletionSpecification specification = this.completions.get(key); + + if (specification == null) { + return Mono.error(new McpError("AsyncCompletionSpecification not found: " + key)); + } + + return specification.completionHandler().apply(exchange, request); + }; + } + // --------------------------------------- // Sampling // --------------------------------------- diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java index d5427335d..42e3f3716 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java @@ -114,6 +114,7 @@ * * @author Christian Tzolov * @author Dariusz Jędrzejczyk + * @author Jihoon Kim * @see McpAsyncServer * @see McpSyncServer * @see McpServerTransportProvider @@ -191,6 +192,8 @@ class AsyncSpecification { */ private final Map prompts = new HashMap<>(); + private final Map completions = new HashMap<>(); + private final List, Mono>> rootsChangeHandlers = new ArrayList<>(); private AsyncSpecification(McpServerTransportProvider transportProvider) { @@ -563,7 +566,8 @@ public AsyncSpecification objectMapper(ObjectMapper objectMapper) { */ public McpAsyncServer build() { var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools, - this.resources, this.resourceTemplates, this.prompts, this.rootsChangeHandlers, this.instructions); + this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers, + this.instructions); var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper(); return new McpAsyncServer(this.transportProvider, mapper, features); } @@ -617,6 +621,8 @@ class SyncSpecification { */ private final Map prompts = new HashMap<>(); + private final Map completions = new HashMap<>(); + private final List>> rootsChangeHandlers = new ArrayList<>(); private SyncSpecification(McpServerTransportProvider transportProvider) { @@ -922,6 +928,22 @@ public SyncSpecification prompts(McpServerFeatures.SyncPromptSpecification... pr return this; } + public SyncSpecification completions(List completions) { + Assert.notNull(completions, "Completions list must not be null"); + for (McpServerFeatures.SyncCompletionSpecification completion : completions) { + this.completions.put(completion.referenceKey(), completion); + } + return this; + } + + public SyncSpecification completions(McpServerFeatures.SyncCompletionSpecification... completions) { + Assert.notNull(completions, "Completions list must not be null"); + for (McpServerFeatures.SyncCompletionSpecification completion : completions) { + this.completions.put(completion.referenceKey(), completion); + } + return this; + } + /** * Registers a consumer that will be notified when the list of roots changes. This * is useful for updating resource availability dynamically, such as when new @@ -988,8 +1010,8 @@ public SyncSpecification objectMapper(ObjectMapper objectMapper) { */ public McpSyncServer build() { McpServerFeatures.Sync syncFeatures = new McpServerFeatures.Sync(this.serverInfo, this.serverCapabilities, - this.tools, this.resources, this.resourceTemplates, this.prompts, this.rootsChangeHandlers, - this.instructions); + this.tools, this.resources, this.resourceTemplates, this.prompts, this.completions, + this.rootsChangeHandlers, this.instructions); McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures); var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper(); var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures); diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java index e0f337b78..da81dc462 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java @@ -21,6 +21,7 @@ * MCP server features specification that a particular server can choose to support. * * @author Dariusz Jędrzejczyk + * @author Jihoon Kim */ public class McpServerFeatures { @@ -41,6 +42,7 @@ record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities s List tools, Map resources, List resourceTemplates, Map prompts, + Map completions, List, Mono>> rootsChangeConsumers, String instructions) { @@ -60,6 +62,7 @@ record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities s List tools, Map resources, List resourceTemplates, Map prompts, + Map completions, List, Mono>> rootsChangeConsumers, String instructions) { @@ -67,7 +70,8 @@ record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities s this.serverInfo = serverInfo; this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities - : new McpSchema.ServerCapabilities(null, // experimental + : new McpSchema.ServerCapabilities(null, // completions + null, // experimental new McpSchema.ServerCapabilities.LoggingCapabilities(), // Enable // logging // by @@ -81,6 +85,7 @@ record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities s this.resources = (resources != null) ? resources : Map.of(); this.resourceTemplates = (resourceTemplates != null) ? resourceTemplates : List.of(); this.prompts = (prompts != null) ? prompts : Map.of(); + this.completions = (completions != null) ? completions : Map.of(); this.rootsChangeConsumers = (rootsChangeConsumers != null) ? rootsChangeConsumers : List.of(); this.instructions = instructions; } @@ -109,6 +114,11 @@ static Async fromSync(Sync syncSpec) { prompts.put(key, AsyncPromptSpecification.fromSync(prompt)); }); + Map completions = new HashMap<>(); + syncSpec.completions().forEach((key, completion) -> { + completions.put(key, AsyncCompletionSpecification.fromSync(completion)); + }); + List, Mono>> rootChangeConsumers = new ArrayList<>(); for (var rootChangeConsumer : syncSpec.rootsChangeConsumers()) { @@ -118,7 +128,7 @@ static Async fromSync(Sync syncSpec) { } return new Async(syncSpec.serverInfo(), syncSpec.serverCapabilities(), tools, resources, - syncSpec.resourceTemplates(), prompts, rootChangeConsumers, syncSpec.instructions()); + syncSpec.resourceTemplates(), prompts, completions, rootChangeConsumers, syncSpec.instructions()); } } @@ -140,6 +150,7 @@ record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities se Map resources, List resourceTemplates, Map prompts, + Map completions, List>> rootsChangeConsumers, String instructions) { /** @@ -159,6 +170,7 @@ record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities se Map resources, List resourceTemplates, Map prompts, + Map completions, List>> rootsChangeConsumers, String instructions) { @@ -166,7 +178,8 @@ record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities se this.serverInfo = serverInfo; this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities - : new McpSchema.ServerCapabilities(null, // experimental + : new McpSchema.ServerCapabilities(null, // completions + null, // experimental new McpSchema.ServerCapabilities.LoggingCapabilities(), // Enable // logging // by @@ -180,6 +193,7 @@ record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities se this.resources = (resources != null) ? resources : new HashMap<>(); this.resourceTemplates = (resourceTemplates != null) ? resourceTemplates : new ArrayList<>(); this.prompts = (prompts != null) ? prompts : new HashMap<>(); + this.completions = (completions != null) ? completions : new HashMap<>(); this.rootsChangeConsumers = (rootsChangeConsumers != null) ? rootsChangeConsumers : new ArrayList<>(); this.instructions = instructions; } @@ -325,6 +339,19 @@ static AsyncPromptSpecification fromSync(SyncPromptSpecification prompt) { } } + public record AsyncCompletionSpecification( + BiFunction> completionHandler) { + + static AsyncCompletionSpecification fromSync(SyncCompletionSpecification completion) { + if (completion == null) { + return null; + } + return new AsyncCompletionSpecification((exchange, request) -> Mono + .fromCallable(() -> completion.completionHandler().apply(new McpSyncServerExchange(exchange), request)) + .subscribeOn(Schedulers.boundedElastic())); + } + } + /** * Specification of a tool with its synchronous handler function. Tools are the * primary way for MCP servers to expose functionality to AI models. Each tool @@ -431,4 +458,23 @@ public record SyncPromptSpecification(McpSchema.Prompt prompt, BiFunction promptHandler) { } + public record SyncCompletionSpecification(CompletionRefKey referenceKey, + BiFunction completionHandler) { + } + + public record CompletionRefKey(String type, String identifier) { + public static CompletionRefKey from(McpSchema.CompleteRequest request) { + var ref = request.ref(); + if (ref instanceof McpSchema.CompleteRequest.PromptReference pr) { + return new CompletionRefKey(ref.type(), pr.name()); + } + else if (ref instanceof McpSchema.CompleteRequest.ResourceReference rr) { + return new CompletionRefKey(ref.type(), rr.uri()); + } + else { + throw new IllegalArgumentException("Unsupported reference type: " + ref); + } + } + } + } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java index 72eba8b86..293d6cfc8 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java @@ -4,9 +4,7 @@ package io.modelcontextprotocol.server; -import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; import io.modelcontextprotocol.util.Assert; diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java index f121db552..3a53c6baa 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java @@ -1,6 +1,5 @@ package io.modelcontextprotocol.server; -import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.spec.McpSchema; /** diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 4c596b628..3ea2b5d5b 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -79,6 +79,8 @@ private McpSchema() { public static final String METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED = "notifications/prompts/list_changed"; + public static final String METHOD_COMPLETION_COMPLETE = "completions/complete"; + // Logging Methods public static final String METHOD_LOGGING_SET_LEVEL = "logging/setLevel"; @@ -314,12 +316,16 @@ public ClientCapabilities build() { @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record ServerCapabilities( // @formatter:off + @JsonProperty("completions") CompletionCapabilities completions, @JsonProperty("experimental") Map experimental, @JsonProperty("logging") LoggingCapabilities logging, @JsonProperty("prompts") PromptCapabilities prompts, @JsonProperty("resources") ResourceCapabilities resources, @JsonProperty("tools") ToolCapabilities tools) { + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public record CompletionCapabilities() { + } @JsonInclude(JsonInclude.Include.NON_ABSENT) public record LoggingCapabilities() { @@ -347,12 +353,18 @@ public static Builder builder() { public static class Builder { + private CompletionCapabilities completions; private Map experimental; private LoggingCapabilities logging = new LoggingCapabilities(); private PromptCapabilities prompts; private ResourceCapabilities resources; private ToolCapabilities tools; + public Builder completions(CompletionCapabilities completions) { + this.completions = completions; + return this; + } + public Builder experimental(Map experimental) { this.experimental = experimental; return this; @@ -379,7 +391,7 @@ public Builder tools(Boolean listChanged) { } public ServerCapabilities build() { - return new ServerCapabilities(experimental, logging, prompts, resources, tools); + return new ServerCapabilities(completions, experimental, logging, prompts, resources, tools); } } } // @formatter:on From 5d036f050210efa0113478a51bc51da3abb802a3 Mon Sep 17 00:00:00 2001 From: jitokim Date: Fri, 11 Apr 2025 15:03:52 +0900 Subject: [PATCH 2/4] fix typo METHOD_COMPLETION_COMPLETE Signed-off-by: jitokim --- mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 2e6a6add4..c72bc7653 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -79,7 +79,7 @@ private McpSchema() { public static final String METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED = "notifications/prompts/list_changed"; - public static final String METHOD_COMPLETION_COMPLETE = "completions/complete"; + public static final String METHOD_COMPLETION_COMPLETE = "completion/complete"; // Logging Methods public static final String METHOD_LOGGING_SET_LEVEL = "logging/setLevel"; From 09cbe0a6b31d856209867b6ed459174e93816ed6 Mon Sep 17 00:00:00 2001 From: jitokim Date: Fri, 11 Apr 2025 17:49:28 +0900 Subject: [PATCH 3/4] implement completion request and add integration test Signed-off-by: jitokim --- .../WebFluxSseIntegrationTests.java | 60 +++++++++++++++---- .../client/McpAsyncClient.java | 12 ++++ .../client/McpSyncClient.java | 5 ++ .../server/McpAsyncServer.java | 26 +++++++- .../spec/McpClientSession.java | 1 + 5 files changed, 92 insertions(+), 12 deletions(-) diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java index 76f908b8a..ff98f02c4 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java @@ -9,6 +9,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; @@ -18,19 +19,12 @@ import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.server.McpSyncServerExchange; import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; -import io.modelcontextprotocol.spec.McpSchema.InitializeResult; -import io.modelcontextprotocol.spec.McpSchema.ModelPreferences; -import io.modelcontextprotocol.spec.McpSchema.Role; -import io.modelcontextprotocol.spec.McpSchema.Root; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpSchema.Tool; +import io.modelcontextprotocol.spec.McpSchema.*; +import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities.CompletionCapabilities; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; @@ -620,4 +614,48 @@ void testLoggingNotification(String clientType) { mcpServer.close(); } -} + @ParameterizedTest(name = "{0} : Completion call") + @ValueSource(strings = { "httpclient", "webflux" }) + void testCompletionShouldReturnExpectedSuggestions(String clientType) { + var clientBuilder = clientBuilders.get(clientType); + + var expectedValues = List.of("python", "pytorch", "pyside"); + var completionResponse = new McpSchema.CompleteResult(new CompleteResult.CompleteCompletion(expectedValues, 10, // total + true // hasMore + )); + + BiFunction completionHandler = (mcpSyncServerExchange, + request) -> { + assertThat(request.argument().name()).isEqualTo("language"); + assertThat(request.argument().value()).isEqualTo("py"); + assertThat(request.ref().type()).isEqualTo("ref/prompt"); + return completionResponse; + }; + + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().completions(new CompletionCapabilities()).build()) + .prompts(new McpServerFeatures.SyncPromptSpecification( + new Prompt("code_review", "this is code review prompt", List.of()), + (mcpSyncServerExchange, getPromptRequest) -> null)) + .completions(new McpServerFeatures.SyncCompletionSpecification( + new McpServerFeatures.CompletionRefKey("ref/prompt", "code_review"), completionHandler)) + .build(); + + try (var mcpClient = clientBuilder.build()) { + + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + + CompleteRequest request = new CompleteRequest( + new CompleteRequest.PromptReference("ref/prompt", "code_review"), + new CompleteRequest.CompleteArgument("language", "py")); + + CompleteResult result = mcpClient.completeCompletion(request); + + assertThat(result).isNotNull(); + } + + mcpServer.close(); + } + +} \ No newline at end of file diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index df099836d..45a61cc45 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -71,6 +71,7 @@ * * @author Dariusz Jędrzejczyk * @author Christian Tzolov + * @author Jihoon Kim * @see McpClient * @see McpSchema * @see McpClientSession @@ -801,4 +802,15 @@ void setProtocolVersions(List protocolVersions) { this.protocolVersions = protocolVersions; } + // -------------------------- + // Completions + // -------------------------- + private static final TypeReference COMPLETION_COMPLETE_RESULT_TYPE_REF = new TypeReference<>() { + }; + + public Mono completeCompletion(McpSchema.CompleteRequest completeRequest) { + return this.withInitializationCheck("complete completions", initializedResult -> this.mcpSession + .sendRequest(McpSchema.METHOD_COMPLETION_COMPLETE, completeRequest, COMPLETION_COMPLETE_RESULT_TYPE_REF)); + } + } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java index 32cf325e9..1cecec1dc 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java @@ -46,6 +46,7 @@ * * @author Dariusz Jędrzejczyk * @author Christian Tzolov + * @author Jihoon Kim * @see McpClient * @see McpAsyncClient * @see McpSchema @@ -325,4 +326,8 @@ public void setLoggingLevel(McpSchema.LoggingLevel loggingLevel) { this.delegate.setLoggingLevel(loggingLevel).block(); } + public McpSchema.CompleteResult completeCompletion(McpSchema.CompleteRequest completeRequest) { + return this.delegate.completeCompletion(completeRequest).block(); + } + } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 03cb20fc8..7bd4b35cb 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -716,7 +716,7 @@ private McpServerSession.RequestHandler setLoggerRequestHandler() { private McpServerSession.RequestHandler completionCompleteRequestHandler() { return (exchange, params) -> { - McpSchema.CompleteRequest request = objectMapper.convertValue(params, McpSchema.CompleteRequest.class); + McpSchema.CompleteRequest request = parseCompletionParams(params); if (request.ref() == null) { return Mono.error(new McpError("ref must not be null")); @@ -755,6 +755,30 @@ private McpServerSession.RequestHandler completionComp }; } + @SuppressWarnings("unchecked") + private McpSchema.CompleteRequest parseCompletionParams(Object object) { + Map params = (Map) object; + Map refMap = (Map) params.get("ref"); + Map argMap = (Map) params.get("argument"); + + String refType = (String) refMap.get("type"); + + McpSchema.CompleteRequest.PromptOrResourceReference ref = switch (refType) { + case "ref/prompt" -> + new McpSchema.CompleteRequest.PromptReference(refType, (String) refMap.get("name")); + case "ref/resource" -> + new McpSchema.CompleteRequest.ResourceReference(refType, (String) refMap.get("uri")); + default -> throw new IllegalArgumentException("Invalid ref type: " + refType); + }; + + String argName = (String) argMap.get("name"); + String argValue = (String) argMap.get("value"); + McpSchema.CompleteRequest.CompleteArgument argument = new McpSchema.CompleteRequest.CompleteArgument( + argName, argValue); + + return new McpSchema.CompleteRequest(ref, argument); + } + // --------------------------------------- // Sampling // --------------------------------------- diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java index 0895e02b0..c1f42e3fb 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java @@ -238,6 +238,7 @@ public Mono sendRequest(String method, Object requestParams, TypeReferenc }); }).timeout(this.requestTimeout).handle((jsonRpcResponse, sink) -> { if (jsonRpcResponse.error() != null) { + logger.error("Error handling request: {}", jsonRpcResponse.error()); sink.error(new McpError(jsonRpcResponse.error())); } else { From 768d1231477891a8d81f16f31f1241a1d1bff3d6 Mon Sep 17 00:00:00 2001 From: jitokim Date: Fri, 11 Apr 2025 18:23:05 +0900 Subject: [PATCH 4/4] add comments to new lines Signed-off-by: jitokim --- .../client/McpAsyncClient.java | 10 +++ .../client/McpSyncClient.java | 6 ++ .../server/McpAsyncServer.java | 14 ++++ .../server/McpServer.java | 15 +++++ .../server/McpServerFeatures.java | 65 +++++++++++++++++++ 5 files changed, 110 insertions(+) diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index 45a61cc45..0d13a0d07 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -808,6 +808,16 @@ void setProtocolVersions(List protocolVersions) { private static final TypeReference COMPLETION_COMPLETE_RESULT_TYPE_REF = new TypeReference<>() { }; + /** + * Sends a completion/complete request to generate value suggestions based on a given + * reference and argument. This is typically used to provide auto-completion options + * for user input fields. + * @param completeRequest The request containing the prompt or resource reference and + * argument for which to generate completions. + * @return A Mono that completes with the result containing completion suggestions. + * @see McpSchema.CompleteRequest + * @see McpSchema.CompleteResult + */ public Mono completeCompletion(McpSchema.CompleteRequest completeRequest) { return this.withInitializationCheck("complete completions", initializedResult -> this.mcpSession .sendRequest(McpSchema.METHOD_COMPLETION_COMPLETE, completeRequest, COMPLETION_COMPLETE_RESULT_TYPE_REF)); diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java index 1cecec1dc..f2573dff2 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java @@ -326,6 +326,12 @@ public void setLoggingLevel(McpSchema.LoggingLevel loggingLevel) { this.delegate.setLoggingLevel(loggingLevel).block(); } + /** + * Send a completion/complete request. + * @param completeRequest the completion request contains the prompt or resource + * reference and arguments for generating suggestions. + * @return the completion result containing suggested values. + */ public McpSchema.CompleteResult completeCompletion(McpSchema.CompleteRequest completeRequest) { return this.delegate.completeCompletion(completeRequest).block(); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 7bd4b35cb..46546269b 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -728,6 +728,7 @@ private McpServerSession.RequestHandler completionComp String type = request.ref().type(); + // check if the referenced resource exists if (type.equals("ref/prompt") && request.ref() instanceof McpSchema.CompleteRequest.PromptReference promptReference) { McpServerFeatures.AsyncPromptSpecification prompt = this.prompts.get(promptReference.name()); @@ -755,6 +756,19 @@ private McpServerSession.RequestHandler completionComp }; } + /** + * Parses the raw JSON-RPC request parameters into a + * {@link McpSchema.CompleteRequest} object. + *

+ * This method manually extracts the `ref` and `argument` fields from the input + * map, determines the correct reference type (either prompt or resource), and + * constructs a fully-typed {@code CompleteRequest} instance. + * @param object the raw request parameters, expected to be a Map containing "ref" + * and "argument" entries. + * @return a {@link McpSchema.CompleteRequest} representing the structured + * completion request. + * @throws IllegalArgumentException if the "ref" type is not recognized. + */ @SuppressWarnings("unchecked") private McpSchema.CompleteRequest parseCompletionParams(Object object) { Map params = (Map) object; diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java index 42e3f3716..44b795198 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java @@ -928,6 +928,14 @@ public SyncSpecification prompts(McpServerFeatures.SyncPromptSpecification... pr return this; } + /** + * Registers multiple completions with their handlers using a List. This method is + * useful when completions need to be added in bulk from a collection. + * @param completions List of completion specifications. Must not be null. + * @return This builder instance for method chaining + * @throws IllegalArgumentException if completions is null + * @see #completions(McpServerFeatures.SyncCompletionSpecification...) + */ public SyncSpecification completions(List completions) { Assert.notNull(completions, "Completions list must not be null"); for (McpServerFeatures.SyncCompletionSpecification completion : completions) { @@ -936,6 +944,13 @@ public SyncSpecification completions(List + *

  • Customizable response generation logic + *
  • Parameter-driven template expansion + *
  • Dynamic interaction with connected clients + * + * + *

    + * Example completion specification:

    {@code
    +	 * new McpServerFeatures.AsyncCompletionSpecification(
    +	 *     (exchange, request) -> {
    +	 *     	   String name = request.argument().name(); // e.g., "language"
    +	 *         String language = request.argument().value(); // e.g., "py"
    +	 *         List suggestions = List.of(
    +	 *         	   "python",
    +	 *         	   "pytorch",
    +	 *         	   "pyside"
    +	 *         );
    +	 *         CompleteResult.CompleteCompletion completion = new CompleteResult.CompleteCompletion(
    +	 *             suggestions, suggestions.size(), false
    +	 *         );
    +	 *         return Mono.just(new CompleteResult(completion));
    +	 *     }
    +	 * )
    +	 * }
    + * + * @param completionHandler The asynchronous function that processes completion + * requests and returns results. The first argument is an + * {@link McpAsyncServerExchange} used to interact with the client. The second + * argument is a {@link io.modelcontextprotocol.spec.McpSchema.CompleteRequest}. + */ public record AsyncCompletionSpecification( BiFunction> completionHandler) { + /** + * Converts a synchronous {@link SyncCompletionSpecification} into an + * {@link AsyncCompletionSpecification} by wrapping the handler in a bounded + * elastic scheduler for safe non-blocking execution. + * @param completion the synchronous completion specification + * @return an asynchronous wrapper of the provided sync specification, or + * {@code null} if input is null + */ static AsyncCompletionSpecification fromSync(SyncCompletionSpecification completion) { if (completion == null) { return null; @@ -462,7 +504,30 @@ public record SyncCompletionSpecification(CompletionRefKey referenceKey, BiFunction completionHandler) { } + /** + * A unique key representing a completion reference, composed of its type and + * identifier. This key is used to look up asynchronous completion specifications in a + * map-like structure. + * + *

    + * The {@code type} typically corresponds to the kind of reference, such as + * {@code "ref/prompt"} or {@code "ref/resource"}, while the {@code identifier} is the + * name or URI associated with the specific reference. + * + * @param type the reference type (e.g., "ref/prompt", "ref/resource") + * @param identifier the reference identifier (e.g., prompt name or resource URI) + */ public record CompletionRefKey(String type, String identifier) { + + /** + * Creates a {@code CompletionRefKey} from a {@link McpSchema.CompleteRequest}. + * The key is derived from the request's reference type and its associated name or + * URI. + * @param request the {@code CompleteRequest} containing a prompt or resource + * reference + * @return a unique key based on the request's reference + * @throws IllegalArgumentException if the reference type is unsupported + */ public static CompletionRefKey from(McpSchema.CompleteRequest request) { var ref = request.ref(); if (ref instanceof McpSchema.CompleteRequest.PromptReference pr) {