Skip to content

Commit a59bad8

Browse files
committed
Refactor Codegen
1 parent 36104e3 commit a59bad8

File tree

14 files changed

+1402
-1000
lines changed

14 files changed

+1402
-1000
lines changed

codegen/pom.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,6 @@
4343
<groupId>io.helidon.service</groupId>
4444
<artifactId>helidon-service-codegen</artifactId>
4545
</dependency>
46-
<dependency>
47-
<groupId>io.helidon.json.schema</groupId>
48-
<artifactId>helidon-json-schema</artifactId>
49-
</dependency>
5046
</dependencies>
5147

5248
<build>

codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCodegen.java

Lines changed: 36 additions & 963 deletions
Large diffs are not rendered by default.
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.extensions.mcp.codegen;
17+
18+
import java.util.List;
19+
import java.util.Optional;
20+
import java.util.regex.Pattern;
21+
import java.util.stream.Collectors;
22+
23+
import io.helidon.codegen.classmodel.ClassModel;
24+
import io.helidon.codegen.classmodel.Method;
25+
import io.helidon.codegen.classmodel.Parameter;
26+
import io.helidon.common.types.AccessModifier;
27+
import io.helidon.common.types.Annotated;
28+
import io.helidon.common.types.Annotation;
29+
import io.helidon.common.types.ResolvedType;
30+
import io.helidon.common.types.TypeInfo;
31+
import io.helidon.common.types.TypeName;
32+
import io.helidon.common.types.TypeNames;
33+
import io.helidon.common.types.TypedElementInfo;
34+
35+
import static io.helidon.common.types.TypeNames.LIST;
36+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_CANCELLATION;
37+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_DESCRIPTION;
38+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_FEATURES;
39+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_LOGGER;
40+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_PARAMETERS;
41+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_PROGRESS;
42+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_REQUEST;
43+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_ROOTS;
44+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_SAMPLING;
45+
46+
/**
47+
* Utility class for methods used by several MCP code generator.
48+
*/
49+
class McpCodegenUtil {
50+
private static final Pattern PATTERN = Pattern.compile("^.");
51+
52+
static final List<String> MCP_TYPES = List.of(MCP_REQUEST.classNameWithEnclosingNames(),
53+
MCP_FEATURES.classNameWithEnclosingNames(),
54+
MCP_LOGGER.classNameWithEnclosingNames(),
55+
MCP_PROGRESS.classNameWithEnclosingNames(),
56+
MCP_CANCELLATION.classNameWithEnclosingNames(),
57+
MCP_SAMPLING.classNameWithEnclosingNames(),
58+
MCP_PARAMETERS.classNameWithEnclosingNames());
59+
60+
private McpCodegenUtil() {
61+
}
62+
63+
static boolean isBoolean(TypeName type) {
64+
return TypeNames.PRIMITIVE_BOOLEAN.equals(type)
65+
|| TypeNames.BOXED_BOOLEAN.equals(type);
66+
}
67+
68+
static boolean isNumber(TypeName type) {
69+
return TypeNames.BOXED_INT.equals(type)
70+
|| TypeNames.BOXED_BYTE.equals(type)
71+
|| TypeNames.BOXED_LONG.equals(type)
72+
|| TypeNames.BOXED_FLOAT.equals(type)
73+
|| TypeNames.BOXED_SHORT.equals(type)
74+
|| TypeNames.BOXED_DOUBLE.equals(type)
75+
|| TypeNames.PRIMITIVE_INT.equals(type)
76+
|| TypeNames.PRIMITIVE_BYTE.equals(type)
77+
|| TypeNames.PRIMITIVE_LONG.equals(type)
78+
|| TypeNames.PRIMITIVE_FLOAT.equals(type)
79+
|| TypeNames.PRIMITIVE_SHORT.equals(type)
80+
|| TypeNames.PRIMITIVE_DOUBLE.equals(type);
81+
}
82+
83+
static boolean isList(TypeName type) {
84+
return type.equals(TypeNames.LIST) && type.typeArguments().size() == 1;
85+
}
86+
87+
/**
88+
* Create a class name from the provided element and suffix.
89+
* The first character is change to upper case.
90+
*
91+
* @param element name as prefix
92+
* @param suffix the suffix
93+
* @return class name as TypeName
94+
*/
95+
static TypeName createClassName(TypedElementInfo element, String suffix) {
96+
String uppercaseElement = PATTERN.matcher(element.elementName()).replaceFirst(m -> m.group().toUpperCase());
97+
return TypeName.builder()
98+
.className(uppercaseElement + suffix)
99+
.build();
100+
}
101+
102+
static List<TypedElementInfo> getElementsWithAnnotation(TypeInfo type, TypeName target) {
103+
return type.elementInfo().stream()
104+
.filter(element -> element.hasAnnotation(target))
105+
.collect(Collectors.toList());
106+
}
107+
108+
static TypeName generatedTypeName(TypeName factoryTypeName, String suffix) {
109+
return TypeName.builder()
110+
.packageName(factoryTypeName.packageName())
111+
.className(factoryTypeName.classNameWithEnclosingNames().replace('.', '_') + "__" + suffix)
112+
.build();
113+
}
114+
115+
static boolean isIgnoredSchemaElement(TypeName typeName) {
116+
return MCP_REQUEST.equals(typeName)
117+
|| MCP_ROOTS.equals(typeName)
118+
|| MCP_LOGGER.equals(typeName)
119+
|| MCP_FEATURES.equals(typeName)
120+
|| MCP_PROGRESS.equals(typeName)
121+
|| MCP_SAMPLING.equals(typeName)
122+
|| MCP_CANCELLATION.equals(typeName);
123+
}
124+
125+
static boolean isResourceTemplate(String uri) {
126+
return uri.contains("{") || uri.contains("}");
127+
}
128+
129+
/**
130+
* Add a new method to the generated class that convert a {@code List<McpParameters>} to
131+
* a list of the provided type.
132+
*
133+
* @param classModel generated class
134+
* @param type generic type
135+
*/
136+
static void addToListMethod(ClassModel.Builder classModel, TypeName type) {
137+
TypeName typeList = ResolvedType.create(TypeName.builder(LIST)
138+
.addTypeArgument(type)
139+
.build()).type();
140+
TypeName parameterList = ResolvedType.create(TypeName.builder(LIST)
141+
.addTypeArgument(MCP_PARAMETERS)
142+
.build()).type();
143+
Method.Builder method = Method.builder()
144+
.name("toList")
145+
.isStatic(true)
146+
.accessModifier(AccessModifier.PRIVATE)
147+
.returnType(typeList)
148+
.addParameter(Parameter.builder().name("list").type(parameterList).build())
149+
.addContentLine("return list == null ? List.of()")
150+
.increaseContentPadding()
151+
.addContentLine(": list.stream().map(p -> p.as(" + type + ".class))")
152+
.increaseContentPadding()
153+
.addContentLine(".map(p -> p.get()).toList();");
154+
classModel.addMethod(method.build());
155+
}
156+
157+
/**
158+
* Returns {@code true} if the provided type is an MCP type and create request getter for that type,
159+
* otherwise nothing is created and return {@code false}.
160+
*
161+
* @param parameters list of parameters
162+
* @param type the tested type
163+
* @return {@code true} if an MCP type, {@code false} otherwise.
164+
*/
165+
static boolean isMcpType(List<String> parameters, TypedElementInfo type) {
166+
if (MCP_REQUEST.equals(type.typeName())) {
167+
parameters.add("request");
168+
return true;
169+
}
170+
if (MCP_FEATURES.equals(type.typeName())) {
171+
parameters.add("request.features()");
172+
return true;
173+
}
174+
if (MCP_LOGGER.equals(type.typeName())) {
175+
parameters.add("request.features().logger()");
176+
return true;
177+
}
178+
if (MCP_PROGRESS.equals(type.typeName())) {
179+
parameters.add("request.features().progress()");
180+
return true;
181+
}
182+
if (MCP_CANCELLATION.equals(type.typeName())) {
183+
parameters.add("request.features().cancellation()");
184+
return true;
185+
}
186+
if (MCP_SAMPLING.equals(type.typeName())) {
187+
parameters.add("request.features().sampling()");
188+
return true;
189+
}
190+
if (MCP_ROOTS.equals(type.typeName())) {
191+
parameters.add("request.features().roots()");
192+
return true;
193+
}
194+
if (MCP_PARAMETERS.equals(type.typeName())) {
195+
parameters.add("request.parameters()");
196+
return true;
197+
}
198+
return false;
199+
}
200+
201+
static Optional<String> getDescription(Annotated element) {
202+
if (element.hasAnnotation(MCP_DESCRIPTION)) {
203+
Annotation description = element.annotation(MCP_DESCRIPTION);
204+
return description.stringValue();
205+
}
206+
return Optional.empty();
207+
}
208+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.extensions.mcp.codegen;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
21+
import io.helidon.codegen.CodegenException;
22+
import io.helidon.codegen.classmodel.ClassModel;
23+
import io.helidon.codegen.classmodel.Method;
24+
import io.helidon.common.types.AccessModifier;
25+
import io.helidon.common.types.Annotation;
26+
import io.helidon.common.types.Annotations;
27+
import io.helidon.common.types.EnumValue;
28+
import io.helidon.common.types.TypeInfo;
29+
import io.helidon.common.types.TypeName;
30+
import io.helidon.common.types.TypeNames;
31+
import io.helidon.common.types.TypedElementInfo;
32+
33+
import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.MCP_TYPES;
34+
import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.createClassName;
35+
import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.getElementsWithAnnotation;
36+
import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.isMcpType;
37+
import static io.helidon.extensions.mcp.codegen.McpTypes.FUNCTION_REQUEST_COMPLETION_CONTENT;
38+
import static io.helidon.extensions.mcp.codegen.McpTypes.LIST_STRING;
39+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_COMPLETION;
40+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_COMPLETION_CONTENT;
41+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_COMPLETION_CONTENTS;
42+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_COMPLETION_INTERFACE;
43+
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_COMPLETION_TYPE;
44+
45+
class McpCompletionCodegen {
46+
private final McpRecorder recorder;
47+
48+
McpCompletionCodegen(McpRecorder recorder) {
49+
this.recorder = recorder;
50+
}
51+
52+
void generate(ClassModel.Builder classModel, TypeInfo type) {
53+
getElementsWithAnnotation(type, MCP_COMPLETION).forEach(element -> {
54+
TypeName innerTypeName = createClassName(element, "__Completion");
55+
Annotation mcpCompletion = element.annotation(MCP_COMPLETION);
56+
String reference = mcpCompletion.value().orElse("");
57+
EnumValue referenceType = (EnumValue) mcpCompletion.objectValue("type").orElse(null);
58+
59+
recorder.completion(innerTypeName);
60+
classModel.addInnerClass(clazz -> clazz
61+
.name(innerTypeName.className())
62+
.addInterface(MCP_COMPLETION_INTERFACE)
63+
.accessModifier(AccessModifier.PRIVATE)
64+
.addMethod(method -> addCompletionReferenceMethod(method, reference))
65+
.addMethod(method -> addCompletionReferenceTypeMethod(method, referenceType))
66+
.addMethod(method -> addCompletionMethod(method, classModel, element)));
67+
});
68+
}
69+
70+
private void addCompletionReferenceMethod(Method.Builder builder, String reference) {
71+
builder.name("reference")
72+
.addAnnotation(Annotations.OVERRIDE)
73+
.returnType(TypeNames.STRING)
74+
.addContent("return \"")
75+
.addContent(reference)
76+
.addContentLine("\";");
77+
}
78+
79+
private void addCompletionReferenceTypeMethod(Method.Builder builder, EnumValue referenceType) {
80+
String enumValue = referenceType != null ? referenceType.name() : "PROMPT";
81+
builder.name("referenceType")
82+
.addAnnotation(Annotations.OVERRIDE)
83+
.returnType(McpTypes.MCP_COMPLETION_TYPE)
84+
.addContent("return ")
85+
.addContent(MCP_COMPLETION_TYPE)
86+
.addContent(".")
87+
.addContent(enumValue)
88+
.addContentLine(";");
89+
}
90+
91+
private void addCompletionMethod(Method.Builder builder, ClassModel.Builder classModel, TypedElementInfo element) {
92+
List<String> parameters = new ArrayList<>();
93+
94+
builder.name("completion")
95+
.returnType(returned -> returned.type(FUNCTION_REQUEST_COMPLETION_CONTENT))
96+
.addAnnotation(Annotations.OVERRIDE);
97+
builder.addContentLine("return request -> {");
98+
99+
for (TypedElementInfo parameter : element.parameterArguments()) {
100+
if (isMcpType(parameters, parameter)) {
101+
continue;
102+
}
103+
if (parameter.typeName().equals(TypeNames.STRING)) {
104+
parameters.add(parameter.elementName());
105+
builder.addContent("var ")
106+
.addContent(parameter.elementName())
107+
.addContentLine(" = request.parameters().get(\"value\").asString().orElse(\"\");");
108+
continue;
109+
}
110+
throw new CodegenException(String.format("Wrong parameter type for method: %s. Supported types are: %s, or String.",
111+
parameter.elementName(),
112+
String.join(", ", MCP_TYPES)));
113+
}
114+
115+
String params = String.join(", ", parameters);
116+
if (element.typeName().equals(LIST_STRING)) {
117+
builder.addContent("return ")
118+
.addContent(MCP_COMPLETION_CONTENTS)
119+
.addContent(".completion(delegate.")
120+
.addContent(element.elementName())
121+
.addContent("(")
122+
.addContent(params)
123+
.addContentLine("));")
124+
.decreaseContentPadding()
125+
.addContentLine("};");
126+
return;
127+
}
128+
if (element.typeName().equals(MCP_COMPLETION_CONTENT)) {
129+
builder.addContent("return delegate.")
130+
.addContent(element.elementName())
131+
.addContent("(")
132+
.addContent(params)
133+
.addContentLine(");")
134+
.decreaseContentPadding()
135+
.addContentLine("};");
136+
return;
137+
}
138+
throw new CodegenException(String.format("Wrong return type for method: %s. Supported types are: %s, or %s.",
139+
element.elementName(),
140+
LIST_STRING,
141+
MCP_COMPLETION_CONTENT.classNameWithTypes()));
142+
143+
}
144+
}

codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpJsonSchemaCodegen.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,11 @@
2121
import java.util.Optional;
2222

2323
import io.helidon.codegen.classmodel.Method;
24-
import io.helidon.common.types.Annotated;
25-
import io.helidon.common.types.Annotation;
2624
import io.helidon.common.types.TypeName;
2725
import io.helidon.common.types.TypeNames;
2826
import io.helidon.common.types.TypedElementInfo;
2927

30-
import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_DESCRIPTION;
28+
import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.getDescription;
3129
import static io.helidon.extensions.mcp.codegen.McpTypes.SERVICES;
3230

3331
/**
@@ -59,14 +57,6 @@ static void addSchemaMethodBody(Method.Builder method, List<TypedElementInfo> fi
5957
method.addContentLine("return builder.toString();");
6058
}
6159

62-
static Optional<String> getDescription(Annotated element) {
63-
if (element.hasAnnotation(MCP_DESCRIPTION)) {
64-
Annotation description = element.annotation(MCP_DESCRIPTION);
65-
return description.stringValue();
66-
}
67-
return Optional.empty();
68-
}
69-
7060
private static void addPropertySchema(Method.Builder method, TypedElementInfo element) {
7161
TypeName typeName = element.typeName();
7262
Optional<String> description = getDescription(element);

0 commit comments

Comments
 (0)