Skip to content

Introduce mcp server cli adapter #166

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

Merged
merged 1 commit into from
Apr 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions cli-adapter/deployment/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.quarkiverse.mcp</groupId>
<artifactId>quarkus-mcp-server-cli-adapter-parent</artifactId>
<version>999-SNAPSHOT</version>
</parent>
<artifactId>quarkus-mcp-server-cli-adapter-deployment</artifactId>
<name>Quarkus MCP Server - CLI Adapter - Deployment</name>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-picocli-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.mcp</groupId>
<artifactId>quarkus-mcp-server-stdio-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkiverse.mcp</groupId>
<artifactId>quarkus-mcp-server-cli-adapter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${quarkus.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package io.quarkiverse.mcp.server.cli.adapter.deployment;

import static io.quarkiverse.mcp.server.cli.adapter.deployment.DotNames.QUALIFIER;
import static io.quarkus.arc.processor.DotNames.INJECT;
import static io.quarkus.arc.processor.DotNames.OBJECT;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.IndexView;

class CommandUtil {

/**
* Check if the given class should be instantiated.
* The class can be instantiated if it has a default constructor and does not require container services.
*
* @param clazz the given class
* @return {@code true} if the class can be instantiated, {@code false} otherwise
*/
public static boolean canBeInstantiated(ClassInfo clazz, IndexView index) {
return hasNoArgConstructor(clazz) && !requiresContainerServices(clazz, index);
}

/**
* List all the qualifiers for the given class.
*
* @param clazz the given class
* @param index the index
* @return a list of {@link AnnotationInstance}
*/
public static List<AnnotationInstance> listQualifiers(ClassInfo clazz, IndexView index) {
return clazz.annotations().stream()
.filter(a -> isQualifier(a, index))
.toList();
}

/**
* List all Fields annotated with {@link picocli.CommandLine.Parameters}.
* The search is done recursively on the super classes.
*
* @param clazz the given class
* @param index the index
* @return a list of {@link FieldInfo}
*/
public static List<FieldInfo> listParameters(ClassInfo clazz, IndexView index) {
List<FieldInfo> parameters = new ArrayList<>();
clazz.fields().stream().filter(f -> f.annotations(DotNames.PARAMETERS).size() > 0).forEach(parameters::add);
DotName superClassName = clazz.superClassType().name();
if (superClassName != null) {
ClassInfo superClassInfo = index.getClassByName(superClassName);
if (superClassInfo != null) {
parameters.addAll(listParameters(superClassInfo, index));
}
}
return parameters;
}

/**
* List all Fields annotated with {@link picocli.CommandLine.Option}.
* The search is done recursively on the super classes.
*
* @param clazz the given class
* @param index the index
* @return a map of option names to {@link FieldInfo}
*/
public static Map<String, FieldInfo> listOptions(ClassInfo clazz, IndexView index) {
Map<String, FieldInfo> options = new HashMap<>();
clazz.fields().stream().filter(f -> f.annotations(DotNames.OPTION).size() > 0).forEach(a -> {
a.annotations(DotNames.OPTION).forEach(o -> {
String[] names = o.value("names").asStringArray();
if (names.length != 0) {
options.put(names[0], a);
}
});
});
DotName superClassName = clazz.superClassType().name();
if (superClassName != null) {
ClassInfo superClassInfo = index.getClassByName(superClassName);
if (superClassInfo != null) {
options.putAll(listOptions(superClassInfo, index));
}
}
return options;
}

/**
* Check if the given class is a qualifier.
*
* @param clazz the given class
* @return {@code true} if the class is a qualifier, {@code false} otherwise
*/
public static boolean isQualifier(AnnotationInstance annotationInstance, IndexView index) {
return isQualifier(index.getClassByName(annotationInstance.name()));
}

/**
* Check if the given class is a qualifier.
*
* @param clazz the given class
* @return {@code true} if the class is a qualifier, {@code false} otherwise
*/
public static boolean isQualifier(ClassInfo clazz) {
return clazz != null && clazz.annotations().stream().anyMatch(a -> QUALIFIER.equals(a.name()));
}

private static boolean hasNoArgConstructor(ClassInfo clazz) {
return !clazz.constructors().stream().anyMatch(c -> !c.parameters().isEmpty());
}

private static boolean requiresContainerServices(ClassInfo clazz, IndexView index) {
return requiresContainerServices(clazz, Set.of(INJECT), index);
}

private static boolean requiresContainerServices(ClassInfo clazz, Set<DotName> containerAnnotationNames, IndexView index) {
if (hasContainerAnnotation(clazz, containerAnnotationNames)) {
return true;
}
if (index != null) {
DotName superName = clazz.superName();
while (superName != null && !superName.equals(OBJECT)) {
final ClassInfo superClass = index.getClassByName(superName);
if (superClass != null) {
if (hasContainerAnnotation(superClass, containerAnnotationNames)) {
return true;
}
superName = superClass.superName();
} else {
superName = null;
}
}
}
return false;
}

private static boolean hasContainerAnnotation(ClassInfo clazz, Set<DotName> containerAnnotationNames) {
if (clazz.annotationsMap().isEmpty() || containerAnnotationNames.isEmpty()) {
return false;
}
return containsAny(clazz, containerAnnotationNames);
}

private static boolean containsAny(ClassInfo clazz, Set<DotName> annotationNames) {
return clazz.annotationsMap().keySet().stream().anyMatch(annotationNames::contains);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.quarkiverse.mcp.server.cli.adapter.deployment;

import java.util.Optional;

import jakarta.inject.Qualifier;

import org.jboss.jandex.DotName;

import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import io.quarkiverse.mcp.server.cli.adapter.runtime.AbstractMcpCommand;
import io.quarkiverse.mcp.server.cli.adapter.runtime.McpAdapter;
import io.quarkus.picocli.runtime.annotations.TopCommand;
import picocli.CommandLine;
import picocli.CommandLine.Command;

final class DotNames {
static final DotName TOOL = DotName.createSimple(Tool.class);
static final DotName TOOL_ARG = DotName.createSimple(ToolArg.class);
static final DotName COMMAND = DotName.createSimple(Command.class);
static final DotName QUALIFIER = DotName.createSimple(Qualifier.class);

static final DotName TOP_COMAMND = DotName.createSimple(TopCommand.class);
static final DotName ABSTRACT_MCP_COMAMND = DotName.createSimple(AbstractMcpCommand.class);
static final DotName MCP_ADAPTER = DotName.createSimple(McpAdapter.class);

static final DotName COMMANDLINE = DotName.createSimple(CommandLine.class);
static final DotName OPTION = DotName.createSimple(CommandLine.Option.class);
static final DotName PARAMETERS = DotName.createSimple(CommandLine.Parameters.class);

static final DotName OPTIONAL = DotName.createSimple(Optional.class);
static final DotName STRING = DotName.createSimple(String.class);
}
Loading