Skip to content

Let prompts and resource templates define their own completions - #1316

Open
chemicL wants to merge 1 commit into
mainfrom
modern-completions
Open

chemicL wants to merge 1 commit into
mainfrom
modern-completions

Conversation

@chemicL

@chemicL chemicL commented Nov 6, 2026

Copy link
Copy Markdown
Member

Completion in the modern server was a single function registered through CompletionsFeature. It received every completion/complete request, so it had to know about every prompt and resource template and keep its own copy of which arguments each of them takes.

A prompt now carries its completion handler, set with McpSyncPrompt.withCompletionHandler, and a resource template does the same through the new McpSyncResourceTemplate, which a resource repository returns from findTemplate(ctx, uriTemplate). A repository opts in with supportsCompletion(), as it already does for subscriptions with supportsSubscribe(). The server then serves completion/complete and advertises completions on its own; there is no feature to register.

The server rejects a reference to an unknown prompt or template, or an argument it does not declare, with invalid params, and answers with no values when a prompt or template has no completion handler. Custom features can complete their own reference types through
McpFeature.completionRefTypes() and McpFeature.complete().

CompletionsFeature and McpSync/AsyncCompletionRepository are removed; the completion function itself is now McpSync/AsyncCompletionHandler.

This brings completions in line with the rest of the modern feature model, where each feature is responsible for its own primitives.

Fixes #1312

@chemicL chemicL added this to the 2.2.0-M3 milestone Nov 6, 2026
Completion in the modern server was a single function registered through
CompletionsFeature. It received every completion/complete request, so it had
to know about every prompt and resource template and keep its own copy of
which arguments each of them takes.

A prompt now carries its completion handler, set with
McpSyncPrompt.withCompletionHandler, and a resource template does the same
through the new McpSyncResourceTemplate, which a resource repository returns
from findTemplate(ctx, uriTemplate). A repository opts in with
supportsCompletion(), as it already does for subscriptions with
supportsSubscribe(). The server then serves completion/complete and
advertises completions on its own; there is no feature to register.

The server rejects a reference to an unknown prompt or template, or an
argument it does not declare, with invalid params, and answers with no
values when a prompt or template has no completion handler. Custom features
can complete their own reference types through
McpFeature.completionRefTypes() and McpFeature.complete().

CompletionsFeature and McpSync/AsyncCompletionRepository are removed; the
completion function itself is now McpSync/AsyncCompletionHandler.

This brings completions in line with the rest of the modern feature model,
where each feature is responsible for its own primitives.

Fixes #1312

Signed-off-by: Dariusz Jedrzejczyk <2554306+chemicL@users.noreply.github.com>
@chemicL
chemicL force-pushed the modern-completions branch from 17913c2 to 8177c3d Compare November 6, 2026 12:47

@tzolov tzolov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @chemicL , looks like a handy improvement.
Please check the comments inline

if (map.get("argument") == null) {
return Mono.error(McpException.invalidParams("params.argument is required"));
}
McpFeature feature = this.byRefType.get(ref.get("type"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A ref with no type is likely to get an internal error here instead of invalid params?
byRefType comes from Map.copyOf, and get(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:

Suggested change
McpFeature feature = this.byRefType.get(ref.get("type"));
McpFeature feature = ref.get("type") instanceof String type ? this.byRefType.get(type) : null;

Copy link
Copy Markdown
Member Author

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.

* the server serves {@code completion/complete} and advertises {@code completions}
* when any feature answers one. Default: none.
*/
default Set<String> completionRefTypes() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 docs/server-modern.md). But it seems this extension point won't work for a new reference type. The CompletionsFeature decodes every request into CompleteRequest, and CompleteReference only lists ref/prompt and ref/resource in @JsonSubTypes.

Though, I'm not sure if the MCP spec requires support of custom features?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be true. What do you propose?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure. Perhaps we can support unknown reference types by a fallback CompleteReference, so complete() can receive them. But this means changes it the McpSchema.

Or let each feature decode its own params: complete(ctx, Object params), matching handle(). Custom features could then use their own types without changing McpSchema, and the built-in features would go back to Params.decode.

public Mono<CompleteResult> complete(McpRequestContext ctx, CompleteRequest request) {
String uriTemplate = ((ResourceReference) request.ref()).uri();
String argument = request.argument().name();
return this.repository.findTemplate(ctx, uriTemplate)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is rejecting a concrete resource URI intended here? ref/resource is only resolved through an findTemplate match, but a reference to an existing resource like test://static-text gets -32602
According to the spec the field is "The URI or URI template of the resource", so to me it looks like a valid reference?
Perhaps for empty findTemplate, we can try repository.find(ctx, uri), return no values if the resource exists, and keep the invalid-params error otherwise?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand. If the uri is not a template, what is there to complete?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript SDK and our legacy server return an empty result here. But I guess -32602 if well documented can be fine

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let prompts and resource templates define their own completions

2 participants