Repository navigation
Let prompts and resource templates define their own completions #1316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| /* | ||
| * Copyright 2026-2026 the original author or authors. | ||
| */ | ||
|
|
||
| package io.modelcontextprotocol.modern.server; | ||
|
|
||
| import java.util.Map; | ||
| import java.util.Set; | ||
|
|
||
| import io.modelcontextprotocol.json.McpJsonMapper; | ||
| import io.modelcontextprotocol.modern.McpException; | ||
| import io.modelcontextprotocol.modern.McpSchema; | ||
| import io.modelcontextprotocol.modern.McpSchema.CompleteRequest; | ||
| import io.modelcontextprotocol.modern.McpSchema.CompleteResult; | ||
| import io.modelcontextprotocol.modern.McpSchema.CompleteResult.Completion; | ||
| import io.modelcontextprotocol.modern.McpSchema.Result; | ||
| import io.modelcontextprotocol.modern.McpSchema.ServerCapabilities; | ||
| import reactor.core.publisher.Mono; | ||
|
|
||
| /** | ||
| * Serves {@code completion/complete} by routing each request to the feature that answers | ||
| * its ref type. | ||
| * | ||
| * @author Dariusz Jędrzejczyk | ||
| */ | ||
| final class CompletionsFeature implements McpFeature { | ||
|
|
||
| private static final int MAX_VALUES = 100; | ||
|
|
||
| private final McpJsonMapper jsonMapper; | ||
|
|
||
| private final Map<String, McpFeature> byRefType; | ||
|
|
||
| CompletionsFeature(McpJsonMapper jsonMapper, Map<String, McpFeature> byRefType) { | ||
| this.jsonMapper = jsonMapper; | ||
| this.byRefType = byRefType; | ||
| } | ||
|
|
||
| @Override | ||
| public Set<String> methods() { | ||
| return Set.of(McpSchema.METHOD_COMPLETION_COMPLETE); | ||
| } | ||
|
|
||
| @Override | ||
| public Mono<McpAsyncResponse<Result>> handle(McpRequestContext ctx, Object params) { | ||
| if (!(params instanceof Map<?, ?> map) || !(map.get("ref") instanceof Map<?, ?> ref)) { | ||
| return Mono.error(McpException.invalidParams("params.ref is required")); | ||
| } | ||
| // Checked here because CompleteRequest's JSON creator fills a missing one in. | ||
| if (map.get("argument") == null) { | ||
| return Mono.error(McpException.invalidParams("params.argument is required")); | ||
| } | ||
| McpFeature feature = this.byRefType.get(ref.get("type")); | ||
| if (feature == null) { | ||
| return Mono.error(McpException.invalidParams("Unsupported ref type: " + ref.get("type"))); | ||
| } | ||
| CompleteRequest request; | ||
| try { | ||
| request = this.jsonMapper.convertValue(params, CompleteRequest.class); | ||
| } | ||
| catch (RuntimeException ex) { | ||
| return Mono.error(McpException.invalidParams("Malformed CompleteRequest")); | ||
| } | ||
| return feature.complete(ctx, request).map(result -> McpAsyncResponse.result(capValues(result))); | ||
| } | ||
|
|
||
| private static CompleteResult capValues(CompleteResult result) { | ||
| Completion completion = result.completion(); | ||
| if (completion.values().size() <= MAX_VALUES) { | ||
| return result; | ||
| } | ||
| Integer total = completion.total() != null ? completion.total() : completion.values().size(); | ||
| Completion capped = new Completion(completion.values().subList(0, MAX_VALUES), total, true); | ||
| return new CompleteResult(capped, result.resultType(), result.meta()); | ||
| } | ||
|
|
||
| @Override | ||
| public void capabilities(ServerCapabilities.Builder builder) { | ||
| builder.completions(); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,15 +7,18 @@ | |
| import java.util.Set; | ||
|
|
||
| import io.modelcontextprotocol.modern.McpException; | ||
| import io.modelcontextprotocol.modern.McpSchema.CompleteRequest; | ||
| import io.modelcontextprotocol.modern.McpSchema.CompleteResult; | ||
| import io.modelcontextprotocol.modern.McpSchema.InputRequired; | ||
| import io.modelcontextprotocol.modern.McpSchema.Result; | ||
| import io.modelcontextprotocol.modern.McpSchema.ServerCapabilities; | ||
| import reactor.core.publisher.Mono; | ||
|
|
||
| /** | ||
| * A composable unit of server behaviour: the methods it serves, and the capabilities it | ||
| * contributes to {@code server/discover}. Core primitives (tools, resources, prompts, | ||
| * completions) are features; so is anything an extension adds. | ||
| * A composable unit of server behaviour: the methods it serves, the capabilities it | ||
| * contributes to {@code server/discover}, and the references it completes for | ||
| * {@code completion/complete}. Core primitives (tools, resources, prompts) are features; | ||
| * so is anything an extension adds. | ||
| * | ||
| * @author Dariusz Jędrzejczyk | ||
| */ | ||
|
|
@@ -46,4 +49,22 @@ default Set<String> inputRequiredMethods() { | |
| return Set.of(); | ||
| } | ||
|
|
||
| /** | ||
| * The {@code completion/complete} reference types, such as {@code ref/prompt}, this | ||
| * feature answers through {@link #complete}. No two features may answer the same one; | ||
| * the server serves {@code completion/complete} and advertises {@code completions} | ||
| * when any feature answers one. Default: none. | ||
| */ | ||
| default Set<String> completionRefTypes() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The #1312 promises that "Custom features can complete their own references" (as it the Though, I'm not sure if the MCP spec requires support of custom features?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be true. What do you propose?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure. Perhaps we can support unknown reference types by a fallback Or let each feature decode its own params: |
||
| return Set.of(); | ||
| } | ||
|
|
||
| /** | ||
| * Answer a {@code completion/complete} whose ref type is one of | ||
| * {@link #completionRefTypes()}, with the same error rules as {@link #handle}. | ||
| */ | ||
| default Mono<CompleteResult> complete(McpRequestContext ctx, CompleteRequest request) { | ||
| return Mono.error(new UnsupportedOperationException(getClass().getName() + " does not complete")); | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A
refwith notypeis likely to get an internal error here instead of invalid params?byRefTypecomes fromMap.copyOf, andget(null)on an immutable map throws an NPE and the{"ref": {"name": "x"}}will never reaches the "Unsupported ref type".Perhaps something like this would do:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, thanks for catching it.