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-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/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index df099836d..0d13a0d07 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,25 @@ void setProtocolVersions(List protocolVersions) { this.protocolVersions = protocolVersions; } + // -------------------------- + // Completions + // -------------------------- + 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 32cf325e9..f2573dff2 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,14 @@ 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 062de13ed..46546269b 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -68,6 +68,7 @@ * * @author Christian Tzolov * @author Dariusz Jędrzejczyk + * @author Jihoon Kim * @see McpServer * @see McpSchema * @see McpClientSession @@ -264,6 +265,8 @@ private static class AsyncServerImpl extends McpAsyncServer { private final ConcurrentHashMap prompts = new ConcurrentHashMap<>(); + private final ConcurrentHashMap completions = new ConcurrentHashMap<>(); + // FIXME: this field is deprecated and should be remvoed together with the // broadcasting loggingNotification. private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG; @@ -281,6 +284,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<>(); @@ -313,6 +317,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()); @@ -705,6 +714,85 @@ private McpServerSession.RequestHandler setLoggerRequestHandler() { }; } + private McpServerSession.RequestHandler completionCompleteRequestHandler() { + return (exchange, params) -> { + McpSchema.CompleteRequest request = parseCompletionParams(params); + + 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(); + + // 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()); + 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); + }; + } + + /** + * 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; + 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/server/McpServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java index d5427335d..44b795198 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,37 @@ 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) { + this.completions.put(completion.referenceKey(), completion); + } + return this; + } + + /** + * Registers multiple completions with their handlers using varargs. This method + * is useful when completions are defined inline and added directly. + * @param completions Array of completion specifications. Must not be null. + * @return This builder instance for method chaining + * @throws IllegalArgumentException if completions is null + */ + 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 +1025,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..f373edbff 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,61 @@ static AsyncPromptSpecification fromSync(SyncPromptSpecification prompt) { } } + /** + * Specification of a completion handler function with asynchronous execution support. + * Completions generate AI model outputs based on prompt or resource references and + * user-provided arguments. This abstraction enables: + *

    + *
  • 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; + } + 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 +500,46 @@ public record SyncPromptSpecification(McpSchema.Prompt prompt, BiFunction promptHandler) { } + 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) { + 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/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 { diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index e621ac19b..c72bc7653 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 = "completion/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