Skip to content

Commit 1d9cb2f

Browse files
authored
Merge pull request #166 from iocanel/mcp-cli-adapter
Introduce mcp server cli adapter
2 parents 2fa9278 + 537d5b5 commit 1d9cb2f

File tree

20 files changed

+1342
-2
lines changed

20 files changed

+1342
-2
lines changed

Diff for: cli-adapter/deployment/pom.xml

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<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">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<parent>
6+
<groupId>io.quarkiverse.mcp</groupId>
7+
<artifactId>quarkus-mcp-server-cli-adapter-parent</artifactId>
8+
<version>999-SNAPSHOT</version>
9+
</parent>
10+
<artifactId>quarkus-mcp-server-cli-adapter-deployment</artifactId>
11+
<name>Quarkus MCP Server - CLI Adapter - Deployment</name>
12+
13+
<dependencies>
14+
<dependency>
15+
<groupId>io.quarkus</groupId>
16+
<artifactId>quarkus-picocli-deployment</artifactId>
17+
</dependency>
18+
<dependency>
19+
<groupId>io.quarkiverse.mcp</groupId>
20+
<artifactId>quarkus-mcp-server-stdio-deployment</artifactId>
21+
<version>${project.version}</version>
22+
</dependency>
23+
<dependency>
24+
<groupId>io.quarkiverse.mcp</groupId>
25+
<artifactId>quarkus-mcp-server-cli-adapter</artifactId>
26+
<version>${project.version}</version>
27+
</dependency>
28+
<dependency>
29+
<groupId>io.quarkus</groupId>
30+
<artifactId>quarkus-junit5-internal</artifactId>
31+
<scope>test</scope>
32+
</dependency>
33+
</dependencies>
34+
35+
<build>
36+
<plugins>
37+
<plugin>
38+
<artifactId>maven-compiler-plugin</artifactId>
39+
<executions>
40+
<execution>
41+
<id>default-compile</id>
42+
<configuration>
43+
<annotationProcessorPaths>
44+
<path>
45+
<groupId>io.quarkus</groupId>
46+
<artifactId>quarkus-extension-processor</artifactId>
47+
<version>${quarkus.version}</version>
48+
</path>
49+
</annotationProcessorPaths>
50+
</configuration>
51+
</execution>
52+
</executions>
53+
</plugin>
54+
</plugins>
55+
</build>
56+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package io.quarkiverse.mcp.server.cli.adapter.deployment;
2+
3+
import static io.quarkiverse.mcp.server.cli.adapter.deployment.DotNames.QUALIFIER;
4+
import static io.quarkus.arc.processor.DotNames.INJECT;
5+
import static io.quarkus.arc.processor.DotNames.OBJECT;
6+
7+
import java.util.ArrayList;
8+
import java.util.HashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.Set;
12+
13+
import org.jboss.jandex.AnnotationInstance;
14+
import org.jboss.jandex.ClassInfo;
15+
import org.jboss.jandex.DotName;
16+
import org.jboss.jandex.FieldInfo;
17+
import org.jboss.jandex.IndexView;
18+
19+
class CommandUtil {
20+
21+
/**
22+
* Check if the given class should be instantiated.
23+
* The class can be instantiated if it has a default constructor and does not require container services.
24+
*
25+
* @param clazz the given class
26+
* @return {@code true} if the class can be instantiated, {@code false} otherwise
27+
*/
28+
public static boolean canBeInstantiated(ClassInfo clazz, IndexView index) {
29+
return hasNoArgConstructor(clazz) && !requiresContainerServices(clazz, index);
30+
}
31+
32+
/**
33+
* List all the qualifiers for the given class.
34+
*
35+
* @param clazz the given class
36+
* @param index the index
37+
* @return a list of {@link AnnotationInstance}
38+
*/
39+
public static List<AnnotationInstance> listQualifiers(ClassInfo clazz, IndexView index) {
40+
return clazz.annotations().stream()
41+
.filter(a -> isQualifier(a, index))
42+
.toList();
43+
}
44+
45+
/**
46+
* List all Fields annotated with {@link picocli.CommandLine.Parameters}.
47+
* The search is done recursively on the super classes.
48+
*
49+
* @param clazz the given class
50+
* @param index the index
51+
* @return a list of {@link FieldInfo}
52+
*/
53+
public static List<FieldInfo> listParameters(ClassInfo clazz, IndexView index) {
54+
List<FieldInfo> parameters = new ArrayList<>();
55+
clazz.fields().stream().filter(f -> f.annotations(DotNames.PARAMETERS).size() > 0).forEach(parameters::add);
56+
DotName superClassName = clazz.superClassType().name();
57+
if (superClassName != null) {
58+
ClassInfo superClassInfo = index.getClassByName(superClassName);
59+
if (superClassInfo != null) {
60+
parameters.addAll(listParameters(superClassInfo, index));
61+
}
62+
}
63+
return parameters;
64+
}
65+
66+
/**
67+
* List all Fields annotated with {@link picocli.CommandLine.Option}.
68+
* The search is done recursively on the super classes.
69+
*
70+
* @param clazz the given class
71+
* @param index the index
72+
* @return a map of option names to {@link FieldInfo}
73+
*/
74+
public static Map<String, FieldInfo> listOptions(ClassInfo clazz, IndexView index) {
75+
Map<String, FieldInfo> options = new HashMap<>();
76+
clazz.fields().stream().filter(f -> f.annotations(DotNames.OPTION).size() > 0).forEach(a -> {
77+
a.annotations(DotNames.OPTION).forEach(o -> {
78+
String[] names = o.value("names").asStringArray();
79+
if (names.length != 0) {
80+
options.put(names[0], a);
81+
}
82+
});
83+
});
84+
DotName superClassName = clazz.superClassType().name();
85+
if (superClassName != null) {
86+
ClassInfo superClassInfo = index.getClassByName(superClassName);
87+
if (superClassInfo != null) {
88+
options.putAll(listOptions(superClassInfo, index));
89+
}
90+
}
91+
return options;
92+
}
93+
94+
/**
95+
* Check if the given class is a qualifier.
96+
*
97+
* @param clazz the given class
98+
* @return {@code true} if the class is a qualifier, {@code false} otherwise
99+
*/
100+
public static boolean isQualifier(AnnotationInstance annotationInstance, IndexView index) {
101+
return isQualifier(index.getClassByName(annotationInstance.name()));
102+
}
103+
104+
/**
105+
* Check if the given class is a qualifier.
106+
*
107+
* @param clazz the given class
108+
* @return {@code true} if the class is a qualifier, {@code false} otherwise
109+
*/
110+
public static boolean isQualifier(ClassInfo clazz) {
111+
return clazz != null && clazz.annotations().stream().anyMatch(a -> QUALIFIER.equals(a.name()));
112+
}
113+
114+
private static boolean hasNoArgConstructor(ClassInfo clazz) {
115+
return !clazz.constructors().stream().anyMatch(c -> !c.parameters().isEmpty());
116+
}
117+
118+
private static boolean requiresContainerServices(ClassInfo clazz, IndexView index) {
119+
return requiresContainerServices(clazz, Set.of(INJECT), index);
120+
}
121+
122+
private static boolean requiresContainerServices(ClassInfo clazz, Set<DotName> containerAnnotationNames, IndexView index) {
123+
if (hasContainerAnnotation(clazz, containerAnnotationNames)) {
124+
return true;
125+
}
126+
if (index != null) {
127+
DotName superName = clazz.superName();
128+
while (superName != null && !superName.equals(OBJECT)) {
129+
final ClassInfo superClass = index.getClassByName(superName);
130+
if (superClass != null) {
131+
if (hasContainerAnnotation(superClass, containerAnnotationNames)) {
132+
return true;
133+
}
134+
superName = superClass.superName();
135+
} else {
136+
superName = null;
137+
}
138+
}
139+
}
140+
return false;
141+
}
142+
143+
private static boolean hasContainerAnnotation(ClassInfo clazz, Set<DotName> containerAnnotationNames) {
144+
if (clazz.annotationsMap().isEmpty() || containerAnnotationNames.isEmpty()) {
145+
return false;
146+
}
147+
return containsAny(clazz, containerAnnotationNames);
148+
}
149+
150+
private static boolean containsAny(ClassInfo clazz, Set<DotName> annotationNames) {
151+
return clazz.annotationsMap().keySet().stream().anyMatch(annotationNames::contains);
152+
}
153+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.quarkiverse.mcp.server.cli.adapter.deployment;
2+
3+
import java.util.Optional;
4+
5+
import jakarta.inject.Qualifier;
6+
7+
import org.jboss.jandex.DotName;
8+
9+
import io.quarkiverse.mcp.server.Tool;
10+
import io.quarkiverse.mcp.server.ToolArg;
11+
import io.quarkiverse.mcp.server.cli.adapter.runtime.AbstractMcpCommand;
12+
import io.quarkiverse.mcp.server.cli.adapter.runtime.McpAdapter;
13+
import io.quarkus.picocli.runtime.annotations.TopCommand;
14+
import picocli.CommandLine;
15+
import picocli.CommandLine.Command;
16+
17+
final class DotNames {
18+
static final DotName TOOL = DotName.createSimple(Tool.class);
19+
static final DotName TOOL_ARG = DotName.createSimple(ToolArg.class);
20+
static final DotName COMMAND = DotName.createSimple(Command.class);
21+
static final DotName QUALIFIER = DotName.createSimple(Qualifier.class);
22+
23+
static final DotName TOP_COMAMND = DotName.createSimple(TopCommand.class);
24+
static final DotName ABSTRACT_MCP_COMAMND = DotName.createSimple(AbstractMcpCommand.class);
25+
static final DotName MCP_ADAPTER = DotName.createSimple(McpAdapter.class);
26+
27+
static final DotName COMMANDLINE = DotName.createSimple(CommandLine.class);
28+
static final DotName OPTION = DotName.createSimple(CommandLine.Option.class);
29+
static final DotName PARAMETERS = DotName.createSimple(CommandLine.Parameters.class);
30+
31+
static final DotName OPTIONAL = DotName.createSimple(Optional.class);
32+
static final DotName STRING = DotName.createSimple(String.class);
33+
}

0 commit comments

Comments
 (0)