From 1a9ebe19535e1bc912d8a84a4a840f8c6ac838eb Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 17 Jan 2025 12:43:26 +0100 Subject: [PATCH] Implement completion for resource templates - resolves #71 --- .../mcp/server/deployment/DotNames.java | 2 + .../deployment/FeatureMethodBuildItem.java | 4 + .../server/deployment/McpServerProcessor.java | 78 ++++++++++++++++--- .../mcp/server/CompleteResourceTemplate.java | 43 ++++++++++ ...ler.java => CompletionMessageHandler.java} | 22 +++--- .../mcp/server/runtime/FeatureMetadata.java | 3 +- .../mcp/server/runtime/McpMessageHandler.java | 12 ++- .../mcp/server/runtime/McpMetadata.java | 2 + .../runtime/PromptCompleteMessageHandler.java | 21 +++++ .../server/runtime/PromptMessageHandler.java | 12 +-- .../runtime/ResourceMessageHandler.java | 11 +-- .../ResourceTemplateCompleteManager.java | 43 ++++++++++ ...esourceTemplateCompleteMessageHandler.java | 21 +++++ .../mcp/server/runtime/ResultMappers.java | 8 +- .../server/runtime/ToolMessageHandler.java | 11 +-- .../quarkus-mcp-server-core_quarkus.adoc | 2 +- docs/modules/ROOT/pages/index.adoc | 41 +++++++++- .../InvalidResourceTemplateCompleteTest.java | 50 ++++++++++++ .../test/complete/MyResourceTemplates.java | 31 ++++++++ .../ResourceTemplateCompleteTest.java | 78 +++++++++++++++++++ .../sse/runtime/SseMcpMessageHandler.java | 5 +- .../sse/runtime/SseMcpServerRecorder.java | 4 +- .../stdio/runtime/StdioMcpMessageHandler.java | 5 +- .../stdio/runtime/StdioMcpServerRecorder.java | 4 +- 24 files changed, 457 insertions(+), 56 deletions(-) create mode 100644 core/runtime/src/main/java/io/quarkiverse/mcp/server/CompleteResourceTemplate.java rename core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/{PromptCompletionMessageHandler.java => CompletionMessageHandler.java} (72%) create mode 100644 core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/PromptCompleteMessageHandler.java create mode 100644 core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResourceTemplateCompleteManager.java create mode 100644 core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResourceTemplateCompleteMessageHandler.java create mode 100644 transports/sse/deployment/src/test/java/io/quarkiverse/mcp/server/test/complete/InvalidResourceTemplateCompleteTest.java create mode 100644 transports/sse/deployment/src/test/java/io/quarkiverse/mcp/server/test/complete/MyResourceTemplates.java create mode 100644 transports/sse/deployment/src/test/java/io/quarkiverse/mcp/server/test/complete/ResourceTemplateCompleteTest.java diff --git a/core/deployment/src/main/java/io/quarkiverse/mcp/server/deployment/DotNames.java b/core/deployment/src/main/java/io/quarkiverse/mcp/server/deployment/DotNames.java index e441d70..484284f 100644 --- a/core/deployment/src/main/java/io/quarkiverse/mcp/server/deployment/DotNames.java +++ b/core/deployment/src/main/java/io/quarkiverse/mcp/server/deployment/DotNames.java @@ -7,6 +7,7 @@ import io.quarkiverse.mcp.server.BlobResourceContents; import io.quarkiverse.mcp.server.CompleteArg; import io.quarkiverse.mcp.server.CompletePrompt; +import io.quarkiverse.mcp.server.CompleteResourceTemplate; import io.quarkiverse.mcp.server.CompletionResponse; import io.quarkiverse.mcp.server.Content; import io.quarkiverse.mcp.server.ImageContent; @@ -64,6 +65,7 @@ class DotNames { static final DotName BLOB_RESOURCE_CONTENTS = DotName.createSimple(BlobResourceContents.class); static final DotName STRING = DotName.createSimple(String.class); static final DotName COMPLETE_PROMPT = DotName.createSimple(CompletePrompt.class); + static final DotName COMPLETE_RESOURCE_TEMPLATE = DotName.createSimple(CompleteResourceTemplate.class); static final DotName COMPLETE_ARG = DotName.createSimple(CompleteArg.class); static final DotName COMPLETE_RESPONSE = DotName.createSimple(CompletionResponse.class); static final DotName RESOURCE_TEMPLATE = DotName.createSimple(ResourceTemplate.class); diff --git a/core/deployment/src/main/java/io/quarkiverse/mcp/server/deployment/FeatureMethodBuildItem.java b/core/deployment/src/main/java/io/quarkiverse/mcp/server/deployment/FeatureMethodBuildItem.java index 65c844d..d869887 100644 --- a/core/deployment/src/main/java/io/quarkiverse/mcp/server/deployment/FeatureMethodBuildItem.java +++ b/core/deployment/src/main/java/io/quarkiverse/mcp/server/deployment/FeatureMethodBuildItem.java @@ -93,6 +93,10 @@ boolean isResourceTemplate() { return feature == Feature.RESOURCE_TEMPLATE; } + boolean isResourceTemplateComplete() { + return feature == Feature.RESOURCE_TEMPLATE_COMPLETE; + } + @Override public String toString() { return "FeatureMethodBuildItem [name=" + name + ", method=" + method.declaringClass() + "#" diff --git a/core/deployment/src/main/java/io/quarkiverse/mcp/server/deployment/McpServerProcessor.java b/core/deployment/src/main/java/io/quarkiverse/mcp/server/deployment/McpServerProcessor.java index b335d8e..0fc5a01 100644 --- a/core/deployment/src/main/java/io/quarkiverse/mcp/server/deployment/McpServerProcessor.java +++ b/core/deployment/src/main/java/io/quarkiverse/mcp/server/deployment/McpServerProcessor.java @@ -4,6 +4,7 @@ import static io.quarkiverse.mcp.server.runtime.FeatureMetadata.Feature.PROMPT_COMPLETE; import static io.quarkiverse.mcp.server.runtime.FeatureMetadata.Feature.RESOURCE; import static io.quarkiverse.mcp.server.runtime.FeatureMetadata.Feature.RESOURCE_TEMPLATE; +import static io.quarkiverse.mcp.server.runtime.FeatureMetadata.Feature.RESOURCE_TEMPLATE_COMPLETE; import static io.quarkiverse.mcp.server.runtime.FeatureMetadata.Feature.TOOL; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; @@ -53,6 +54,7 @@ import io.quarkiverse.mcp.server.runtime.PromptCompleteManager; import io.quarkiverse.mcp.server.runtime.PromptManager; import io.quarkiverse.mcp.server.runtime.ResourceManager; +import io.quarkiverse.mcp.server.runtime.ResourceTemplateCompleteManager; import io.quarkiverse.mcp.server.runtime.ResourceTemplateManager; import io.quarkiverse.mcp.server.runtime.ResultMappers; import io.quarkiverse.mcp.server.runtime.ToolManager; @@ -92,6 +94,7 @@ class McpServerProcessor { DotNames.COMPLETE_PROMPT, PROMPT_COMPLETE, DotNames.RESOURCE, RESOURCE, DotNames.RESOURCE_TEMPLATE, RESOURCE_TEMPLATE, + DotNames.COMPLETE_RESOURCE_TEMPLATE, RESOURCE_TEMPLATE_COMPLETE, DotNames.TOOL, TOOL); @BuildStep @@ -99,7 +102,7 @@ void addBeans(BuildProducer additionalBeans) { additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf("io.quarkiverse.mcp.server.runtime.ConnectionManager")); additionalBeans.produce(AdditionalBeanBuildItem.builder().setUnremovable() .addBeanClasses(PromptManager.class, ToolManager.class, ResourceManager.class, PromptCompleteManager.class, - ResourceTemplateManager.class) + ResourceTemplateManager.class, ResourceTemplateCompleteManager.class) .build()); } @@ -124,7 +127,7 @@ void collectFeatureMethods(BeanDiscoveryFinishedBuildItem beanDiscovery, Invoker Feature feature = getFeature(featureAnnotation); validateFeatureMethod(method, feature); String name; - if (feature == PROMPT_COMPLETE) { + if (feature == PROMPT_COMPLETE || feature == RESOURCE_TEMPLATE_COMPLETE) { name = featureAnnotation.value().asString(); } else { AnnotationValue nameValue = featureAnnotation.value("name"); @@ -201,7 +204,7 @@ void collectFeatureMethods(BeanDiscoveryFinishedBuildItem beanDiscovery, Invoker } } - // Check existing prompts for prompt completions + // Check existing prompts for completions List prompts = found.get(PROMPT); List promptCompletions = found.get(PROMPT_COMPLETE); if (promptCompletions != null) { @@ -213,10 +216,24 @@ void collectFeatureMethods(BeanDiscoveryFinishedBuildItem beanDiscovery, Invoker } } } + + // Check existing resource templates for completions + List resourceTemplates = found.get(RESOURCE_TEMPLATE); + List resourceTemplateCompletions = found.get(RESOURCE_TEMPLATE_COMPLETE); + if (resourceTemplateCompletions != null) { + for (FeatureMethodBuildItem completion : resourceTemplateCompletions) { + if (resourceTemplates == null + || resourceTemplates.stream().noneMatch(p -> p.getName().equals(completion.getName()))) { + String message = "Resource template %s does not exist for completion: %s" + .formatted(completion.getName(), completion); + errors.produce(new ValidationErrorBuildItem(new IllegalStateException(message))); + } + } + } } private String getDuplicateValidationName(FeatureMethodBuildItem featureMethod) { - if (featureMethod.getFeature() == PROMPT_COMPLETE) { + if (featureMethod.getFeature() == PROMPT_COMPLETE || featureMethod.getFeature() == RESOURCE_TEMPLATE_COMPLETE) { MethodParameterInfo argument = featureMethod.getMethod().parameters().stream() .filter(p -> providerFrom(p.type()) == Provider.PARAMS).findFirst().orElseThrow(); String argumentName = argument.name(); @@ -316,6 +333,20 @@ void generateMetadata(McpServerRecorder recorder, RecorderContext recorderContex } resourceTemplatesMethod.returnValue(retResourceTemplates); + // io.quarkiverse.mcp.server.runtime.McpMetadata.resourceTemplateCompletions() + MethodCreator resourceTemplateCompletionsMethod = metadataCreator.getMethodCreator("resourceTemplateCompletions", + List.class); + ResultHandle retResourceTemplateCompletions = Gizmo.newArrayList(resourceTemplateCompletionsMethod); + for (FeatureMethodBuildItem resourceTemplateCompletion : featureMethods.stream() + .filter(FeatureMethodBuildItem::isResourceTemplateComplete) + .toList()) { + processFeatureMethod(counter, metadataCreator, resourceTemplateCompletionsMethod, resourceTemplateCompletion, + retResourceTemplateCompletions, + transformedAnnotations, + DotNames.COMPLETE_ARG); + } + resourceTemplateCompletionsMethod.returnValue(retResourceTemplateCompletions); + metadataCreator.close(); syntheticBeans.produce(SyntheticBeanBuildItem.configure(McpMetadata.class) @@ -362,6 +393,7 @@ private void validateFeatureMethod(MethodInfo method, Feature feature) { case TOOL -> validateToolMethod(method); case RESOURCE -> validateResourceMethod(method); case RESOURCE_TEMPLATE -> validateResourceTemplateMethod(method); + case RESOURCE_TEMPLATE_COMPLETE -> validateResourceTemplateCompleteMethod(method); default -> throw new IllegalArgumentException("Unsupported feature: " + feature); } } @@ -390,7 +422,7 @@ private void validatePromptMethod(MethodInfo method) { } } - private static final Set PROMPT_COMPLETE_TYPES = Set.of(ClassType.create(DotNames.COMPLETE_RESPONSE), + private static final Set COMPLETE_TYPES = Set.of(ClassType.create(DotNames.COMPLETE_RESPONSE), ClassType.create(DotNames.STRING)); private void validatePromptCompleteMethod(MethodInfo method) { @@ -401,7 +433,7 @@ private void validatePromptCompleteMethod(MethodInfo method) { if (DotNames.LIST.equals(type.name()) && type.kind() == Kind.PARAMETERIZED_TYPE) { type = type.asParameterizedType().arguments().get(0); } - if (!PROMPT_COMPLETE_TYPES.contains(type)) { + if (!COMPLETE_TYPES.contains(type)) { throw new IllegalStateException("Unsupported Prompt complete method return type: " + method); } @@ -412,6 +444,25 @@ private void validatePromptCompleteMethod(MethodInfo method) { } } + private void validateResourceTemplateCompleteMethod(MethodInfo method) { + org.jboss.jandex.Type type = method.returnType(); + if (DotNames.UNI.equals(type.name()) && type.kind() == Kind.PARAMETERIZED_TYPE) { + type = type.asParameterizedType().arguments().get(0); + } + if (DotNames.LIST.equals(type.name()) && type.kind() == Kind.PARAMETERIZED_TYPE) { + type = type.asParameterizedType().arguments().get(0); + } + if (!COMPLETE_TYPES.contains(type)) { + throw new IllegalStateException("Unsupported Resource template complete method return type: " + method); + } + + List arguments = method.parameters().stream() + .filter(p -> providerFrom(p.type()) == Provider.PARAMS).toList(); + if (arguments.size() != 1 || !arguments.get(0).type().name().equals(DotNames.STRING)) { + throw new IllegalStateException("Resource template complete must consume exactly one String argument: " + method); + } + } + private static final Set TOOL_TYPES = Set.of(ClassType.create(DotNames.TOOL_RESPONSE), ClassType.create(DotNames.CONTENT), ClassType.create(DotNames.TEXT_CONTENT), ClassType.create(DotNames.IMAGE_CONTENT), ClassType.create(DotNames.RESOURCE_CONTENT), @@ -475,7 +526,8 @@ private boolean hasFeatureMethod(BeanInfo bean) { || beanClass.hasAnnotation(DotNames.COMPLETE_PROMPT) || beanClass.hasAnnotation(DotNames.TOOL) || beanClass.hasAnnotation(DotNames.RESOURCE) - || beanClass.hasAnnotation(DotNames.RESOURCE_TEMPLATE); + || beanClass.hasAnnotation(DotNames.RESOURCE_TEMPLATE) + || beanClass.hasAnnotation(DotNames.COMPLETE_RESOURCE_TEMPLATE); } private void processFeatureMethod(AtomicInteger counter, ClassCreator clazz, MethodCreator method, @@ -561,6 +613,8 @@ private ResultHandle getMapper(BytecodeCreator bytecode, org.jboss.jandex.Type r })); case RESOURCE, RESOURCE_TEMPLATE -> readResultMapper(bytecode, createMapperField(RESOURCE, returnType, DotNames.RESOURCE_RESPONSE, c -> "CONTENT")); + case RESOURCE_TEMPLATE_COMPLETE -> readResultMapper(bytecode, + createMapperField(RESOURCE_TEMPLATE_COMPLETE, returnType, DotNames.COMPLETE_RESPONSE, c -> "STRING")); default -> throw new IllegalArgumentException("Unsupported feature: " + feature); }; } @@ -571,9 +625,13 @@ static String createMapperField(FeatureMetadata.Feature feature, org.jboss.jande return "TO_UNI"; } org.jboss.jandex.Type type = returnType; - StringBuilder ret = new StringBuilder(feature.toString()) - .append("_"); - + StringBuilder ret; + if (feature == PROMPT_COMPLETE || feature == RESOURCE_TEMPLATE_COMPLETE) { + ret = new StringBuilder("COMPLETE_"); + } else { + ret = new StringBuilder(feature.toString()) + .append("_"); + } if (DotNames.UNI.equals(type.name())) { type = type.asParameterizedType().arguments().get(0); if (type.name().equals(baseType)) { diff --git a/core/runtime/src/main/java/io/quarkiverse/mcp/server/CompleteResourceTemplate.java b/core/runtime/src/main/java/io/quarkiverse/mcp/server/CompleteResourceTemplate.java new file mode 100644 index 0000000..1185be0 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkiverse/mcp/server/CompleteResourceTemplate.java @@ -0,0 +1,43 @@ +package io.quarkiverse.mcp.server; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.List; + +import io.smallrye.mutiny.Uni; + +/** + * Annotates a business method of a CDI bean used to complete an expression of a URI template of a resource template. + *

+ * The result of a "complete" operation is always represented as a {@link CompletionResponse}. However, the annotated method can + * also return other types that are converted according to the following rules. + *

    + *
  • If the method returns {@link String} then the reponse contains the single value.
  • + *
  • If the method returns a {@link List} of {@link String}s then the reponse contains the list of values.
  • + *
  • The method may return a {@link Uni} that wraps any of the type mentioned above.
  • + *
+ * In other words, the return type must be one of the following list: + *
    + *
  • {@code CompletionResponse}
  • + *
  • {@code String}
  • + *
  • {@code List}
  • + *
  • {@code Uni}
  • + *
  • {@code Uni}
  • + *
  • {@code Uni>}
  • + *
+ * + * @see ResourceTemplate#name() + */ +@Retention(RUNTIME) +@Target(METHOD) +public @interface CompleteResourceTemplate { + + /** + * The name reference to a resource template. If not such {@link ResourceTemplate} exists then the build fails. + */ + String value(); + +} diff --git a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/PromptCompletionMessageHandler.java b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/CompletionMessageHandler.java similarity index 72% rename from core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/PromptCompletionMessageHandler.java rename to core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/CompletionMessageHandler.java index 32a99f5..99d1a31 100644 --- a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/PromptCompletionMessageHandler.java +++ b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/CompletionMessageHandler.java @@ -11,29 +11,25 @@ import io.vertx.core.Handler; import io.vertx.core.json.JsonObject; -class PromptCompletionMessageHandler { +public abstract class CompletionMessageHandler { - private static final Logger LOG = Logger.getLogger(PromptCompletionMessageHandler.class); + private static final Logger LOG = Logger.getLogger(CompletionMessageHandler.class); - private final PromptCompleteManager manager; + protected abstract Future execute(String key, ArgumentProviders argProviders) throws McpException; - PromptCompletionMessageHandler(PromptCompleteManager manager) { - this.manager = manager; - } - - void promptComplete(Object id, JsonObject ref, JsonObject argument, Responder responder, McpConnection connection) { - String promptName = ref.getString("name"); + void complete(Object id, JsonObject ref, JsonObject argument, Responder responder, McpConnection connection) { + String referenceName = ref.getString("name"); String argumentName = argument.getString("name"); - LOG.debugf("Complete prompt %s for argument %s [id: %s]", promptName, argumentName, id); + LOG.debugf("Complete %s for argument %s [id: %s]", referenceName, argumentName, id); - String key = promptName + "_" + argumentName; + String key = referenceName + "_" + argumentName; ArgumentProviders argProviders = new ArgumentProviders( Map.of(argumentName, argument.getString("value")), connection, id, responder); try { - Future fu = manager.execute(key, argProviders); + Future fu = execute(key, argProviders); fu.onComplete(new Handler>() { @Override public void handle(AsyncResult ar) { @@ -51,7 +47,7 @@ public void handle(AsyncResult ar) { result.put("completion", completion); responder.sendResult(id, result); } else { - LOG.errorf(ar.cause(), "Unable to complete prompt %s", promptName); + LOG.errorf(ar.cause(), "Unable to complete prompt %s", referenceName); responder.sendInternalError(id); } } diff --git a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/FeatureMetadata.java b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/FeatureMetadata.java index 42afcb7..41fd7d1 100644 --- a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/FeatureMetadata.java +++ b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/FeatureMetadata.java @@ -45,7 +45,8 @@ public enum Feature { TOOL, RESOURCE, RESOURCE_TEMPLATE, - PROMPT_COMPLETE; + PROMPT_COMPLETE, + RESOURCE_TEMPLATE_COMPLETE; public boolean requiresUri() { return this == RESOURCE || this == RESOURCE_TEMPLATE; diff --git a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/McpMessageHandler.java b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/McpMessageHandler.java index e1898f5..7965522 100644 --- a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/McpMessageHandler.java +++ b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/McpMessageHandler.java @@ -24,9 +24,10 @@ public class McpMessageHandler { private final ToolMessageHandler toolHandler; private final PromptMessageHandler promptHandler; - private final PromptCompletionMessageHandler promptCompleteHandler; + private final PromptCompleteMessageHandler promptCompleteHandler; private final ResourceMessageHandler resourceHandler; private final ResourceTemplateMessageHandler resourceTemplateHandler; + private final ResourceTemplateCompleteMessageHandler resourceTemplateCompleteHandler; protected final McpRuntimeConfig config; @@ -34,13 +35,14 @@ public class McpMessageHandler { protected McpMessageHandler(McpRuntimeConfig config, ConnectionManager connectionManager, PromptManager promptManager, ToolManager toolManager, ResourceManager resourceManager, PromptCompleteManager promptCompleteManager, - ResourceTemplateManager resourceTemplateManager) { + ResourceTemplateManager resourceTemplateManager, ResourceTemplateCompleteManager resourceTemplateCompleteManager) { this.connectionManager = connectionManager; this.toolHandler = new ToolMessageHandler(toolManager); this.promptHandler = new PromptMessageHandler(promptManager); - this.promptCompleteHandler = new PromptCompletionMessageHandler(promptCompleteManager); + this.promptCompleteHandler = new PromptCompleteMessageHandler(promptCompleteManager); this.resourceHandler = new ResourceMessageHandler(resourceManager); this.resourceTemplateHandler = new ResourceTemplateMessageHandler(resourceTemplateManager); + this.resourceTemplateCompleteHandler = new ResourceTemplateCompleteMessageHandler(resourceTemplateCompleteManager); this.config = config; this.serverInfo = serverInfo(promptManager, toolManager, resourceManager, resourceTemplateManager); } @@ -167,7 +169,9 @@ private void complete(JsonObject message, Responder responder, McpConnection con responder.sendError(id, JsonRPC.INVALID_REQUEST, "Argument not found"); } else { if ("ref/prompt".equals(referenceType)) { - promptCompleteHandler.promptComplete(id, ref, argument, responder, connection); + promptCompleteHandler.complete(id, ref, argument, responder, connection); + } else if ("ref/resource".equals(referenceType)) { + resourceTemplateCompleteHandler.complete(id, ref, argument, responder, connection); } else { responder.sendError(id, JsonRPC.INVALID_REQUEST, "Unsupported reference found: " + ref.getString("type")); diff --git a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/McpMetadata.java b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/McpMetadata.java index dc107a8..42aa093 100644 --- a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/McpMetadata.java +++ b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/McpMetadata.java @@ -19,4 +19,6 @@ public interface McpMetadata { List> resourceTemplates(); + List> resourceTemplateCompletions(); + } diff --git a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/PromptCompleteMessageHandler.java b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/PromptCompleteMessageHandler.java new file mode 100644 index 0000000..c6c6277 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/PromptCompleteMessageHandler.java @@ -0,0 +1,21 @@ +package io.quarkiverse.mcp.server.runtime; + +import java.util.Objects; + +import io.quarkiverse.mcp.server.CompletionResponse; +import io.vertx.core.Future; + +class PromptCompleteMessageHandler extends CompletionMessageHandler { + + private final PromptCompleteManager manager; + + PromptCompleteMessageHandler(PromptCompleteManager manager) { + this.manager = Objects.requireNonNull(manager); + } + + @Override + protected Future execute(String key, ArgumentProviders argProviders) throws McpException { + return manager.execute(key, argProviders); + } + +} diff --git a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/PromptMessageHandler.java b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/PromptMessageHandler.java index 17dd561..4594879 100644 --- a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/PromptMessageHandler.java +++ b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/PromptMessageHandler.java @@ -1,5 +1,7 @@ package io.quarkiverse.mcp.server.runtime; +import java.util.Objects; + import org.jboss.logging.Logger; import io.quarkiverse.mcp.server.McpConnection; @@ -14,17 +16,17 @@ class PromptMessageHandler { private static final Logger LOG = Logger.getLogger(PromptMessageHandler.class); - private final PromptManager promptManager; + private final PromptManager manager; - PromptMessageHandler(PromptManager promptManager) { - this.promptManager = promptManager; + PromptMessageHandler(PromptManager manager) { + this.manager = Objects.requireNonNull(manager); } void promptsList(JsonObject message, Responder responder) { Object id = message.getValue("id"); LOG.debugf("List prompts [id: %s]", id); JsonArray prompts = new JsonArray(); - for (FeatureMetadata resource : promptManager.list()) { + for (FeatureMetadata resource : manager.list()) { prompts.add(resource.asJson()); } responder.sendResult(id, new JsonObject().put("prompts", prompts)); @@ -40,7 +42,7 @@ void promptsGet(JsonObject message, Responder responder, McpConnection connectio responder); try { - Future fu = promptManager.execute(promptName, argProviders); + Future fu = manager.execute(promptName, argProviders); fu.onComplete(new Handler>() { @Override public void handle(AsyncResult ar) { diff --git a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResourceMessageHandler.java b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResourceMessageHandler.java index 65d2a86..d522090 100644 --- a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResourceMessageHandler.java +++ b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResourceMessageHandler.java @@ -1,6 +1,7 @@ package io.quarkiverse.mcp.server.runtime; import java.util.Map; +import java.util.Objects; import org.jboss.logging.Logger; @@ -16,17 +17,17 @@ class ResourceMessageHandler { private static final Logger LOG = Logger.getLogger(ResourceMessageHandler.class); - private final ResourceManager resourceManager; + private final ResourceManager manager; - ResourceMessageHandler(ResourceManager resourceManager) { - this.resourceManager = resourceManager; + ResourceMessageHandler(ResourceManager manager) { + this.manager = Objects.requireNonNull(manager); } void resourcesList(JsonObject message, Responder responder) { Object id = message.getValue("id"); LOG.debugf("List resources [id: %s]", id); JsonArray resources = new JsonArray(); - for (FeatureMetadata resource : resourceManager.list()) { + for (FeatureMetadata resource : manager.list()) { resources.add(resource.asJson()); } responder.sendResult(id, new JsonObject().put("resources", resources)); @@ -45,7 +46,7 @@ void resourcesRead(JsonObject message, Responder responder, McpConnection connec ArgumentProviders argProviders = new ArgumentProviders(Map.of("uri", resourceUri), connection, id, responder); try { - Future fu = resourceManager.execute(resourceUri, argProviders); + Future fu = manager.execute(resourceUri, argProviders); fu.onComplete(new Handler>() { @Override public void handle(AsyncResult ar) { diff --git a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResourceTemplateCompleteManager.java b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResourceTemplateCompleteManager.java new file mode 100644 index 0000000..9d47837 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResourceTemplateCompleteManager.java @@ -0,0 +1,43 @@ +package io.quarkiverse.mcp.server.runtime; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkiverse.mcp.server.CompletionResponse; +import io.vertx.core.Vertx; + +public class ResourceTemplateCompleteManager extends FeatureManager { + + // key = resource template name + "_" + argument name + final Map> completions; + + protected ResourceTemplateCompleteManager(McpMetadata metadata, Vertx vertx, ObjectMapper mapper) { + super(vertx, mapper); + this.completions = metadata.resourceTemplateCompletions().stream() + .collect(Collectors.toMap( + m -> m.info().name() + "_" + + m.info().arguments().stream().filter(FeatureArgument::isParam).findFirst().orElseThrow() + .name(), + Function.identity())); + } + + @Override + public List> list() { + return completions.values().stream().sorted().toList(); + } + + @Override + protected FeatureMetadata getMetadata(String id) { + return completions.get(id); + } + + @Override + protected McpException notFound(String id) { + return new McpException("Resource template completion does not exist: " + id, JsonRPC.INVALID_PARAMS); + } + +} diff --git a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResourceTemplateCompleteMessageHandler.java b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResourceTemplateCompleteMessageHandler.java new file mode 100644 index 0000000..7a9cb12 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResourceTemplateCompleteMessageHandler.java @@ -0,0 +1,21 @@ +package io.quarkiverse.mcp.server.runtime; + +import java.util.Objects; + +import io.quarkiverse.mcp.server.CompletionResponse; +import io.vertx.core.Future; + +class ResourceTemplateCompleteMessageHandler extends CompletionMessageHandler { + + private final ResourceTemplateCompleteManager manager; + + ResourceTemplateCompleteMessageHandler(ResourceTemplateCompleteManager manager) { + this.manager = Objects.requireNonNull(manager); + } + + @Override + protected Future execute(String key, ArgumentProviders argProviders) throws McpException { + return manager.execute(key, argProviders); + } + +} diff --git a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResultMappers.java b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResultMappers.java index 1146e66..d8049d9 100644 --- a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResultMappers.java +++ b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ResultMappers.java @@ -69,16 +69,16 @@ public class ResultMappers { public static final Function>, Uni> RESOURCE_UNI_LIST_CONTENT = uni -> uni .map(l -> new ResourceResponse(l)); - public static final Function> PROMPT_COMPLETE_STRING = str -> Uni.createFrom() + public static final Function> COMPLETE_STRING = str -> Uni.createFrom() .item(new CompletionResponse(List.of(str), null, null)); - public static final Function, Uni> PROMPT_COMPLETE_LIST_STRING = list -> Uni.createFrom() + public static final Function, Uni> COMPLETE_LIST_STRING = list -> Uni.createFrom() .item(new CompletionResponse(list, null, null)); - public static final Function, Uni> PROMPT_COMPLETE_UNI_STRING = uni -> uni + public static final Function, Uni> COMPLETE_UNI_STRING = uni -> uni .map(str -> new CompletionResponse(List.of(str), null, null)); - public static final Function>, Uni> PROMPT_COMPLETE_UNI_LIST_STRING = uni -> uni + public static final Function>, Uni> COMPLETE_UNI_LIST_STRING = uni -> uni .map(list -> new CompletionResponse(list, null, null)); } diff --git a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ToolMessageHandler.java b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ToolMessageHandler.java index 5c7bd4c..6f85259 100644 --- a/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ToolMessageHandler.java +++ b/core/runtime/src/main/java/io/quarkiverse/mcp/server/runtime/ToolMessageHandler.java @@ -1,6 +1,7 @@ package io.quarkiverse.mcp.server.runtime; import java.lang.reflect.Type; +import java.util.Objects; import org.jboss.logging.Logger; @@ -24,12 +25,12 @@ class ToolMessageHandler { private static final Logger LOG = Logger.getLogger(ToolMessageHandler.class); - private final ToolManager toolManager; + private final ToolManager manager; private final SchemaGenerator schemaGenerator; - ToolMessageHandler(ToolManager toolManager) { - this.toolManager = toolManager; + ToolMessageHandler(ToolManager manager) { + this.manager = Objects.requireNonNull(manager); this.schemaGenerator = new SchemaGenerator( new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON).build()); } @@ -39,7 +40,7 @@ void toolsList(JsonObject message, Responder responder) { LOG.debugf("List tools [id: %s]", id); JsonArray tools = new JsonArray(); - for (FeatureMetadata toolMetadata : toolManager.list()) { + for (FeatureMetadata toolMetadata : manager.list()) { JsonObject tool = toolMetadata.asJson(); JsonObject properties = new JsonObject(); JsonArray required = new JsonArray(); @@ -80,7 +81,7 @@ void toolsCall(JsonObject message, Responder responder, McpConnection connection responder); try { - Future fu = toolManager.execute(toolName, argProviders); + Future fu = manager.execute(toolName, argProviders); fu.onComplete(new Handler>() { @Override public void handle(AsyncResult ar) { diff --git a/docs/modules/ROOT/pages/includes/quarkus-mcp-server-core_quarkus.adoc b/docs/modules/ROOT/pages/includes/quarkus-mcp-server-core_quarkus.adoc index 273be3d..7f5a033 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-mcp-server-core_quarkus.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-mcp-server-core_quarkus.adoc @@ -93,7 +93,7 @@ ifndef::add-copy-button-to-env-var[] Environment variable: `+++QUARKUS_CLIENT_LOGGING_DEFAULT_LEVEL+++` endif::add-copy-button-to-env-var[] -- -a|`alert`, `critical`, `debug`, `emergency`, `error`, `info`, `notice`, `warning` +a|`debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency` |`info` |=== diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 4186d58..b20d37e 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -150,7 +150,7 @@ public class MyPrompts { } ---- <1> `"foo"` is the name reference to a prompt. If not such prompt exists then the build fails. -<2> The method returns a list of values. +<2> The method returns a list of matching values. <3> The `@CompleteArg` can be used to customize the name of an argument. The result of a "prompt complete" operation is always represented as a `CompleteResponse`. @@ -209,8 +209,9 @@ You can also use https://spec.modelcontextprotocol.io/specification/2024-11-05/s [source,java] ---- import io.quarkiverse.mcp.server.ResourceTemplate; +import io.quarkiverse.mcp.server.TextResourceContents; + import jakarta.inject.Inject; -import java.nio.file.Files; // @Singleton <1> public class MyResourceTemplates { @@ -238,6 +239,42 @@ However, the annotated method can also return other types that are converted acc * If the method returns a `List` of `ResourceContents` implementations then the reponse contains the list of contents objects. * The method may return a `Uni` that wraps any of the type mentioned above. +Arguments of a `@ResourceTemplate` method may be auto-completed through the completion API. + +[source,java] +---- +import io.quarkiverse.mcp.server.ResourceTemplate; +import io.quarkiverse.mcp.server.TextResourceContents; + +import jakarta.inject.Inject; + +public class MyTemplates { + + @Inject + ProjectService projectService; + + @ResourceTemplate(uriTemplate = "file:///project/{name}") + TextResourceContents project(String name) { + return TextResourceContents.create(uri, projectService.readProject(name))); + } + + @CompleteResourceTemplate("project") <1> + List completeName(String name) { <2> + return projectService.getNames().stream().filter(n -> n.startsWith(name)).toList(); + } + +} +---- +<1> `"project"` is the name reference to a resource template. If not such resource template exists then the build fails. +<2> The method returns a list of matching values. + +The result of a "prompt complete" operation is always represented as a `CompleteResponse`. +However, the annotated method can also return other types that are converted according to the following rules. + +* If the method returns `java.lang.String` then the reponse contains a single value. +* If the method returns a `List` of `String`s then the reponse contains the list of values. +* The method may return a `Uni` that wraps any of the type mentioned above. + === Tools MCP provides a https://spec.modelcontextprotocol.io/specification/server/tools/[standardized way] for servers to expose tools that can be invoked by clients. diff --git a/transports/sse/deployment/src/test/java/io/quarkiverse/mcp/server/test/complete/InvalidResourceTemplateCompleteTest.java b/transports/sse/deployment/src/test/java/io/quarkiverse/mcp/server/test/complete/InvalidResourceTemplateCompleteTest.java new file mode 100644 index 0000000..4da16c5 --- /dev/null +++ b/transports/sse/deployment/src/test/java/io/quarkiverse/mcp/server/test/complete/InvalidResourceTemplateCompleteTest.java @@ -0,0 +1,50 @@ +package io.quarkiverse.mcp.server.test.complete; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkiverse.mcp.server.runtime.JsonRPC; +import io.quarkiverse.mcp.server.test.McpServerTest; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.http.ContentType; +import io.vertx.core.json.JsonObject; + +public class InvalidResourceTemplateCompleteTest extends McpServerTest { + + @RegisterExtension + static final QuarkusUnitTest config = defaultConfig() + .withApplicationRoot( + root -> root.addClasses(MyPrompts.class)); + + @Test + public void testError() throws URISyntaxException { + URI endpoint = initClient(); + + JsonObject completeMessage = newMessage("completion/complete") + .put("params", new JsonObject() + .put("ref", new JsonObject() + .put("type", "ref/prompt") + .put("name", "bar")) + .put("argument", new JsonObject() + .put("name", "name") + .put("value", "Vo"))); + + given().contentType(ContentType.JSON) + .when() + .body(completeMessage.encode()) + .post(endpoint) + .then() + .statusCode(200); + + JsonObject response = waitForLastResponse(); + assertEquals(JsonRPC.INVALID_PARAMS, response.getJsonObject("error").getInteger("code")); + assertEquals("Prompt completion does not exist: bar_name", response.getJsonObject("error").getString("message")); + } + +} diff --git a/transports/sse/deployment/src/test/java/io/quarkiverse/mcp/server/test/complete/MyResourceTemplates.java b/transports/sse/deployment/src/test/java/io/quarkiverse/mcp/server/test/complete/MyResourceTemplates.java new file mode 100644 index 0000000..7507c05 --- /dev/null +++ b/transports/sse/deployment/src/test/java/io/quarkiverse/mcp/server/test/complete/MyResourceTemplates.java @@ -0,0 +1,31 @@ +package io.quarkiverse.mcp.server.test.complete; + +import java.util.List; + +import io.quarkiverse.mcp.server.CompleteArg; +import io.quarkiverse.mcp.server.CompleteResourceTemplate; +import io.quarkiverse.mcp.server.ResourceTemplate; +import io.quarkiverse.mcp.server.TextResourceContents; +import io.quarkus.logging.Log; + +public class MyResourceTemplates { + + static final List NAMES = List.of("Martin", "Lu", "Jachym", "Vojtik", "Onda"); + + @ResourceTemplate(uriTemplate = "file:///{foo}/{bar}") + TextResourceContents foo_template(String foo, String bar, String uri) { + return TextResourceContents.create(uri, foo + ":" + bar); + } + + @CompleteResourceTemplate("foo_template") + List completeFoo(@CompleteArg(name = "foo") String val) { + Log.infof("Complete foo: %s", val); + return NAMES.stream().filter(n -> n.startsWith(val)).toList(); + } + + @CompleteResourceTemplate("foo_template") + String completeBar(String bar) { + Log.infof("Complete bar: %s", bar); + return "_bar"; + } +} diff --git a/transports/sse/deployment/src/test/java/io/quarkiverse/mcp/server/test/complete/ResourceTemplateCompleteTest.java b/transports/sse/deployment/src/test/java/io/quarkiverse/mcp/server/test/complete/ResourceTemplateCompleteTest.java new file mode 100644 index 0000000..276fde0 --- /dev/null +++ b/transports/sse/deployment/src/test/java/io/quarkiverse/mcp/server/test/complete/ResourceTemplateCompleteTest.java @@ -0,0 +1,78 @@ +package io.quarkiverse.mcp.server.test.complete; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkiverse.mcp.server.test.McpServerTest; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.http.ContentType; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +public class ResourceTemplateCompleteTest extends McpServerTest { + + @RegisterExtension + static final QuarkusUnitTest config = defaultConfig() + .withApplicationRoot( + root -> root.addClasses(MyResourceTemplates.class)); + + @Test + public void testCompletion() throws URISyntaxException { + URI endpoint = initClient(); + + JsonObject completeMessage = newMessage("completion/complete") + .put("params", new JsonObject() + .put("ref", new JsonObject() + .put("type", "ref/resource") + .put("name", "foo_template")) + .put("argument", new JsonObject() + .put("name", "foo") + .put("value", "Ja"))); + + given().contentType(ContentType.JSON) + .when() + .body(completeMessage.encode()) + .post(endpoint) + .then() + .statusCode(200); + + JsonObject completeResponse = waitForLastResponse(); + + JsonObject completeResult = assertResponseMessage(completeMessage, completeResponse); + assertNotNull(completeResult); + JsonArray values = completeResult.getJsonObject("completion").getJsonArray("values"); + assertEquals(1, values.size()); + assertEquals("Jachym", values.getString(0)); + + completeMessage = newMessage("completion/complete") + .put("params", new JsonObject() + .put("ref", new JsonObject() + .put("type", "ref/resource") + .put("name", "foo_template")) + .put("argument", new JsonObject() + .put("name", "bar") + .put("value", "Ja"))); + + given().contentType(ContentType.JSON) + .when() + .body(completeMessage.encode()) + .post(endpoint) + .then() + .statusCode(200); + + completeResponse = waitForLastResponse(); + + completeResult = assertResponseMessage(completeMessage, completeResponse); + assertNotNull(completeResult); + values = completeResult.getJsonObject("completion").getJsonArray("values"); + assertEquals(1, values.size()); + assertEquals("_bar", values.getString(0)); + } +} diff --git a/transports/sse/runtime/src/main/java/io/quarkiverse/mcp/server/sse/runtime/SseMcpMessageHandler.java b/transports/sse/runtime/src/main/java/io/quarkiverse/mcp/server/sse/runtime/SseMcpMessageHandler.java index 5940173..41b1428 100644 --- a/transports/sse/runtime/src/main/java/io/quarkiverse/mcp/server/sse/runtime/SseMcpMessageHandler.java +++ b/transports/sse/runtime/src/main/java/io/quarkiverse/mcp/server/sse/runtime/SseMcpMessageHandler.java @@ -9,6 +9,7 @@ import io.quarkiverse.mcp.server.runtime.PromptCompleteManager; import io.quarkiverse.mcp.server.runtime.PromptManager; import io.quarkiverse.mcp.server.runtime.ResourceManager; +import io.quarkiverse.mcp.server.runtime.ResourceTemplateCompleteManager; import io.quarkiverse.mcp.server.runtime.ResourceTemplateManager; import io.quarkiverse.mcp.server.runtime.Responder; import io.quarkiverse.mcp.server.runtime.ToolManager; @@ -29,9 +30,9 @@ class SseMcpMessageHandler extends McpMessageHandler implements Handler createMessagesEndpointHandler() { return new SseMcpMessageHandler(config, container.instance(ConnectionManager.class).get(), container.instance(PromptManager.class).get(), container.instance(ToolManager.class).get(), container.instance(ResourceManager.class).get(), container.instance(PromptCompleteManager.class).get(), - container.instance(ResourceTemplateManager.class).get()); + container.instance(ResourceTemplateManager.class).get(), + container.instance(ResourceTemplateCompleteManager.class).get()); } } diff --git a/transports/stdio/runtime/src/main/java/io/quarkiverse/mcp/server/stdio/runtime/StdioMcpMessageHandler.java b/transports/stdio/runtime/src/main/java/io/quarkiverse/mcp/server/stdio/runtime/StdioMcpMessageHandler.java index d24b5a4..d414ce4 100644 --- a/transports/stdio/runtime/src/main/java/io/quarkiverse/mcp/server/stdio/runtime/StdioMcpMessageHandler.java +++ b/transports/stdio/runtime/src/main/java/io/quarkiverse/mcp/server/stdio/runtime/StdioMcpMessageHandler.java @@ -18,6 +18,7 @@ import io.quarkiverse.mcp.server.runtime.PromptCompleteManager; import io.quarkiverse.mcp.server.runtime.PromptManager; import io.quarkiverse.mcp.server.runtime.ResourceManager; +import io.quarkiverse.mcp.server.runtime.ResourceTemplateCompleteManager; import io.quarkiverse.mcp.server.runtime.ResourceTemplateManager; import io.quarkiverse.mcp.server.runtime.Responder; import io.quarkiverse.mcp.server.runtime.ToolManager; @@ -36,9 +37,9 @@ class StdioMcpMessageHandler extends McpMessageHandler { protected StdioMcpMessageHandler(McpRuntimeConfig config, ConnectionManager connectionManager, PromptManager promptManager, ToolManager toolManager, ResourceManager resourceManager, PromptCompleteManager promptCompleteManager, - ResourceTemplateManager resourceTemplateManager) { + ResourceTemplateManager resourceTemplateManager, ResourceTemplateCompleteManager resourceTemplateCompleteManager) { super(config, connectionManager, promptManager, toolManager, resourceManager, promptCompleteManager, - resourceTemplateManager); + resourceTemplateManager, resourceTemplateCompleteManager); this.executor = Executors.newSingleThreadExecutor(); this.trafficLogger = config.trafficLogging().enabled() ? new TrafficLogger(config.trafficLogging().textLimit()) : null; diff --git a/transports/stdio/runtime/src/main/java/io/quarkiverse/mcp/server/stdio/runtime/StdioMcpServerRecorder.java b/transports/stdio/runtime/src/main/java/io/quarkiverse/mcp/server/stdio/runtime/StdioMcpServerRecorder.java index c500c71..7ba80f6 100644 --- a/transports/stdio/runtime/src/main/java/io/quarkiverse/mcp/server/stdio/runtime/StdioMcpServerRecorder.java +++ b/transports/stdio/runtime/src/main/java/io/quarkiverse/mcp/server/stdio/runtime/StdioMcpServerRecorder.java @@ -7,6 +7,7 @@ import io.quarkiverse.mcp.server.runtime.PromptCompleteManager; import io.quarkiverse.mcp.server.runtime.PromptManager; import io.quarkiverse.mcp.server.runtime.ResourceManager; +import io.quarkiverse.mcp.server.runtime.ResourceTemplateCompleteManager; import io.quarkiverse.mcp.server.runtime.ResourceTemplateManager; import io.quarkiverse.mcp.server.runtime.ToolManager; import io.quarkiverse.mcp.server.runtime.config.McpRuntimeConfig; @@ -38,7 +39,8 @@ public void initialize() { container.instance(ConnectionManager.class).get(), container.instance(PromptManager.class).get(), container.instance(ToolManager.class).get(), container.instance(ResourceManager.class).get(), container.instance(PromptCompleteManager.class).get(), - container.instance(ResourceTemplateManager.class).get()); + container.instance(ResourceTemplateManager.class).get(), + container.instance(ResourceTemplateCompleteManager.class).get()); messageHandler.initialize(stdout, config); }