Skip to content

Commit

Permalink
Introduce Prompt complete feature
Browse files Browse the repository at this point in the history
- also fixes #53
  • Loading branch information
mkouba committed Jan 14, 2025
1 parent 2a0acba commit 658b7b4
Show file tree
Hide file tree
Showing 32 changed files with 785 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import org.jboss.jandex.DotName;

import io.quarkiverse.mcp.server.BlobResourceContents;
import io.quarkiverse.mcp.server.CompleteArg;
import io.quarkiverse.mcp.server.CompletePrompt;
import io.quarkiverse.mcp.server.CompletionResponse;
import io.quarkiverse.mcp.server.Content;
import io.quarkiverse.mcp.server.ImageContent;
import io.quarkiverse.mcp.server.McpConnection;
Expand Down Expand Up @@ -56,5 +59,8 @@ class DotNames {
static final DotName TEXT_RESOURCE_CONTENS = DotName.createSimple(TextResourceContents.class);
static final DotName BLOB_RESOURCE_CONTENS = 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_ARG = DotName.createSimple(CompleteArg.class);
static final DotName COMPLETE_RESPONSE = DotName.createSimple(CompletionResponse.class);

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@

final class FeatureMethodBuildItem extends MultiBuildItem {

private final Feature feature;

// invocation info
private final BeanInfo bean;
private final InvokerInfo invoker;
private final MethodInfo method;

// The name of the feature
private final String name;
private final String description;

private final MethodInfo method;
private final Feature feature;
// Optional description
private final String description;

// Resource-only
private final String uri;
Expand Down Expand Up @@ -76,13 +81,18 @@ boolean isPrompt() {
return feature == Feature.PROMPT;
}

boolean isPromptComplete() {
return feature == Feature.PROMPT_COMPLETE;
}

boolean isResource() {
return feature == Feature.RESOURCE;
}

@Override
public String toString() {
return "FeatureMethodBuildItem [name=" + name + ", method=" + method.declaringClass() + "#" + method.name()
return "FeatureMethodBuildItem [name=" + name + ", method=" + method.declaringClass() + "#"
+ method.name()
+ "(), feature=" + feature + "]";
}

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkiverse.mcp.server;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
* Annotates a parameter of a "complete" method, such as {@link CompletePrompt}.
*/
@Retention(RUNTIME)
@Target(ElementType.PARAMETER)
public @interface CompleteArg {

/**
* Constant value for {@link #name()} indicating that the annotated element's name should be used as-is.
*/
String ELEMENT_NAME = "<<element name>>";

String name() default ELEMENT_NAME;

}
Original file line number Diff line number Diff line change
@@ -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 a prompt argument
* <p>
* 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.
* <ul>
* <li>If the method returns {@link String} then the reponse contains the single value.</li>
* <li>If the method returns a {@link List} of {@link String}s then the reponse contains the list of values.</li>
* <li>The method may return a {@link Uni} that wraps any of the type mentioned above.</li>
* </ul>
* In other words, the return type must be one of the following list:
* <ul>
* <li>{@code CompletionResponse}</li>
* <li>{@code String}</li>
* <li>{@code List<String>}</li>
* <li>{@code Uni<CompletionResponse>}</li>
* <li>{@code Uni<String>}</li>
* <li>{@code Uni<List<String>>}</li>
* </ul>
*
* @see Prompt#name()
*/
@Retention(RUNTIME)
@Target(METHOD)
public @interface CompletePrompt {

/**
* The name reference to a prompt. If not such {@link Prompt} exists then the build fails.
*/
String value();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkiverse.mcp.server;

import java.util.List;

public record CompletionResponse(List<String> values, Integer total, Boolean hasMore) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,10 @@
*/
String ELEMENT_NAME = "<<element name>>";

/**
*
*/
String name() default ELEMENT_NAME;

/**
*
*/
String description() default "";

/**
*
*/
boolean required() default true;

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,10 @@
*/
String ELEMENT_NAME = "<<element name>>";

/**
*
*/
String name() default ELEMENT_NAME;

/**
*
*/
String description() default "";

/**
*
*/
boolean required() default true;

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ protected FeatureManager(Vertx vertx, ObjectMapper mapper) {
this.mapper = mapper;
}

public Future<R> get(String id, ArgumentProviders argProviders) throws McpException {
public Future<R> execute(String id, ArgumentProviders argProviders) throws McpException {
FeatureMetadata<R> metadata = getMetadata(id);
if (metadata == null) {
throw notFound(id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public JsonObject asJson() {
public enum Feature {
PROMPT,
TOOL,
RESOURCE
RESOURCE,
PROMPT_COMPLETE
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,20 @@ public class McpMessageHandler {

private final PromptMessageHandler promptHandler;

private final PromptCompletionMessageHandler promptCompleteHandler;

private final ResourceMessageHandler resourceHandler;

protected final McpRuntimeConfig config;

private final Map<String, Object> serverInfo;

protected McpMessageHandler(McpRuntimeConfig config, ConnectionManager connectionManager, PromptManager promptManager,
ToolManager toolManager, ResourceManager resourceManager) {
ToolManager toolManager, ResourceManager resourceManager, PromptCompleteManager promptCompleteManager) {
this.connectionManager = connectionManager;
this.toolHandler = new ToolMessageHandler(toolManager);
this.promptHandler = new PromptMessageHandler(promptManager);
this.promptCompleteHandler = new PromptCompletionMessageHandler(promptCompleteManager);
this.resourceHandler = new ResourceMessageHandler(resourceManager);
this.config = config;
this.serverInfo = serverInfo(promptManager, toolManager, resourceManager);
Expand Down Expand Up @@ -98,6 +101,7 @@ private void initializing(JsonObject message, Responder responder, McpConnection
private static final String RESOURCES_LIST = "resources/list";
private static final String RESOURCES_READ = "resources/read";
private static final String PING = "ping";
private static final String COMPLETION_COMPLETE = "completion/complete";
// non-standard messages
private static final String Q_CLOSE = "q/close";

Expand All @@ -111,12 +115,39 @@ private void operation(JsonObject message, Responder responder, McpConnection co
case PING -> ping(message, responder);
case RESOURCES_LIST -> resourceHandler.resourcesList(message, responder);
case RESOURCES_READ -> resourceHandler.resourcesRead(message, responder, connection);
case COMPLETION_COMPLETE -> complete(message, responder, connection);
case Q_CLOSE -> close(message, responder, connection);
default -> responder.send(
Messages.newError(message.getValue("id"), JsonRPC.METHOD_NOT_FOUND, "Unsupported method: " + method));
}
}

private void complete(JsonObject message, Responder responder, McpConnection connection) {
Object id = message.getValue("id");
JsonObject params = message.getJsonObject("params");
JsonObject ref = params.getJsonObject("ref");
if (ref == null) {
responder.sendError(id, JsonRPC.INVALID_REQUEST, "Reference not found");
} else {
String referenceType = ref.getString("type");
if (referenceType == null) {
responder.sendError(id, JsonRPC.INVALID_REQUEST, "Reference type not found");
} else {
JsonObject argument = params.getJsonObject("argument");
if (argument == null) {
responder.sendError(id, JsonRPC.INVALID_REQUEST, "Argument not found");
} else {
if ("ref/prompt".equals(referenceType)) {
promptCompleteHandler.promptComplete(id, ref, argument, responder, connection);
} else {
responder.sendError(id, JsonRPC.INVALID_REQUEST,
"Unsupported reference found: " + ref.getString("type"));
}
}
}
}
}

private void ping(JsonObject message, Responder responder) {
// https://spec.modelcontextprotocol.io/specification/basic/utilities/ping/
Object id = message.getValue("id");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;

import io.quarkiverse.mcp.server.CompletionResponse;
import io.quarkiverse.mcp.server.PromptResponse;
import io.quarkiverse.mcp.server.ResourceResponse;
import io.quarkiverse.mcp.server.ToolResponse;
Expand All @@ -10,6 +11,8 @@ public interface McpMetadata {

List<FeatureMetadata<PromptResponse>> prompts();

List<FeatureMetadata<CompletionResponse>> promptCompletions();

List<FeatureMetadata<ToolResponse>> tools();

List<FeatureMetadata<ResourceResponse>> resources();
Expand Down
Original file line number Diff line number Diff line change
@@ -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 PromptCompleteManager extends FeatureManager<CompletionResponse> {

// key = prompt name + "_" + argument name
final Map<String, FeatureMetadata<CompletionResponse>> completions;

protected PromptCompleteManager(McpMetadata metadata, Vertx vertx, ObjectMapper mapper) {
super(vertx, mapper);
this.completions = metadata.promptCompletions().stream()
.collect(Collectors.toMap(
m -> m.info().name() + "_"
+ m.info().arguments().stream().filter(FeatureArgument::isParam).findFirst().orElseThrow()
.name(),
Function.identity()));
}

@Override
public List<FeatureMetadata<CompletionResponse>> list() {
return completions.values().stream().sorted().toList();
}

@Override
protected FeatureMetadata<CompletionResponse> getMetadata(String id) {
return completions.get(id);
}

@Override
protected McpException notFound(String id) {
return new McpException("Prompt completion does not exist: " + id, JsonRPC.INVALID_PARAMS);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.quarkiverse.mcp.server.runtime;

import java.util.Map;

import org.jboss.logging.Logger;

import io.quarkiverse.mcp.server.CompletionResponse;
import io.quarkiverse.mcp.server.McpConnection;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;

class PromptCompletionMessageHandler {

private static final Logger LOG = Logger.getLogger(PromptCompletionMessageHandler.class);

private final PromptCompleteManager manager;

PromptCompletionMessageHandler(PromptCompleteManager manager) {
this.manager = manager;
}

void promptComplete(Object id, JsonObject ref, JsonObject argument, Responder responder, McpConnection connection) {
String promptName = ref.getString("name");
String argumentName = argument.getString("name");

LOG.infof("Complete prompt %s for argument %s [id: %s]", promptName, argumentName, id);

String key = promptName + "_" + argumentName;

ArgumentProviders argProviders = new ArgumentProviders(
Map.of(argumentName, argument.getString("value")), connection, id);

try {
Future<CompletionResponse> fu = manager.execute(key, argProviders);
fu.onComplete(new Handler<AsyncResult<CompletionResponse>>() {
@Override
public void handle(AsyncResult<CompletionResponse> ar) {
if (ar.succeeded()) {
CompletionResponse completionResponse = ar.result();
JsonObject result = new JsonObject();
JsonObject completion = new JsonObject()
.put("values", completionResponse.values());
if (completionResponse.total() != null) {
completion.put("total", completionResponse.total());
}
if (completionResponse.hasMore() != null) {
completion.put("hasMore", completionResponse.hasMore());
}
result.put("completion", completion);
responder.sendResult(id, result);
} else {
LOG.errorf(ar.cause(), "Unable to complete prompt %s", promptName);
responder.sendInternalError(id);
}
}
});
} catch (McpException e) {
responder.sendError(id, e.getJsonRpcError(), e.getMessage());
}
}

}
Loading

0 comments on commit 658b7b4

Please sign in to comment.