Skip to content

Commit 8993d29

Browse files
committed
Declarative works with examples
1 parent aeaa2e5 commit 8993d29

File tree

29 files changed

+1341
-387
lines changed

29 files changed

+1341
-387
lines changed

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

Lines changed: 305 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,49 +16,324 @@
1616

1717
package io.helidon.integrations.mcp.codegen;
1818

19+
import java.util.ArrayList;
1920
import java.util.Collection;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.Optional;
2024
import java.util.stream.Collectors;
2125

2226
import io.helidon.codegen.CodegenContext;
2327
import io.helidon.codegen.CodegenException;
28+
import io.helidon.codegen.CodegenUtil;
2429
import io.helidon.codegen.RoundContext;
30+
import io.helidon.codegen.classmodel.ClassModel;
31+
import io.helidon.codegen.classmodel.Field;
32+
import io.helidon.codegen.classmodel.Method;
2533
import io.helidon.codegen.spi.CodegenExtension;
34+
import io.helidon.common.types.AccessModifier;
35+
import io.helidon.common.types.Annotation;
36+
import io.helidon.common.types.Annotations;
2637
import io.helidon.common.types.ElementKind;
2738
import io.helidon.common.types.TypeInfo;
39+
import io.helidon.common.types.TypeName;
40+
import io.helidon.common.types.TypeNames;
41+
import io.helidon.common.types.TypedElementInfo;
2842

43+
import static io.helidon.integrations.mcp.codegen.McpTypes.MCPSERVER;
44+
import static io.helidon.integrations.mcp.codegen.McpTypes.MCP_NOTIFICATION;
45+
import static io.helidon.integrations.mcp.codegen.McpTypes.MCP_PROMPT;
46+
import static io.helidon.integrations.mcp.codegen.McpTypes.MCP_PROMPT_COMPONENT;
47+
import static io.helidon.integrations.mcp.codegen.McpTypes.MCP_PROMT_PARAM;
48+
import static io.helidon.integrations.mcp.codegen.McpTypes.MCP_RESOURCE;
49+
import static io.helidon.integrations.mcp.codegen.McpTypes.MCP_RESOURCE_COMPONENT;
2950
import static io.helidon.integrations.mcp.codegen.McpTypes.MCP_SERVER;
51+
import static io.helidon.integrations.mcp.codegen.McpTypes.MCP_SUBSCRIBE;
52+
import static io.helidon.integrations.mcp.codegen.McpTypes.MCP_TOOL;
53+
import static io.helidon.integrations.mcp.codegen.McpTypes.MCP_TOOL_COMPONENT;
54+
import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_ANNOTATION_SINGLETON;
3055

3156
class McpCodegen implements CodegenExtension {
3257

33-
private final System.Logger LOGGER = System.getLogger(this.getClass().getName());
34-
private final CodegenContext ctx;
35-
36-
McpCodegen(CodegenContext ctx) {
37-
this.ctx = ctx;
38-
}
39-
40-
@Override
41-
public void process(RoundContext roundContext) {
42-
LOGGER.log(System.Logger.Level.INFO, "Processing mcp codegen extension with context "
43-
+ roundContext.types().stream().map(Object::toString).collect(Collectors.joining()));
44-
Collection<TypeInfo> types = roundContext.annotatedTypes(MCP_SERVER);
45-
for (TypeInfo type : types) {
46-
process(roundContext, type);
47-
}
48-
}
49-
50-
private void process(RoundContext roundCtx, TypeInfo type) {
51-
if (type.kind() != ElementKind.CLASS) {
52-
throw new CodegenException("Type annotated with " + MCP_SERVER.fqName() + " must be a class.",
53-
type.originatingElementValue());
54-
}
55-
}
56-
57-
/**
58-
* Generate JSON-RPC schema out of method signature.
59-
*/
60-
private static class SchemaGenerator {
61-
62-
}
58+
private static final TypeName GENERATOR = TypeName.create(McpCodegen.class);
6359

60+
private final System.Logger LOGGER = System.getLogger(this.getClass().getName());
61+
private final CodegenContext context;
62+
63+
McpCodegen(CodegenContext context) {
64+
this.context = context;
65+
}
66+
67+
@Override
68+
public void process(RoundContext roundContext) {
69+
LOGGER.log(System.Logger.Level.INFO, "Processing mcp codegen extension with context "
70+
+ roundContext.types().stream().map(Object::toString).collect(Collectors.joining()));
71+
Collection<TypeInfo> types = roundContext.annotatedTypes(MCP_SERVER);
72+
for (TypeInfo type : types) {
73+
process(roundContext, type);
74+
}
75+
}
76+
77+
private void process(RoundContext roundCtx, TypeInfo type) {
78+
if (type.kind() != ElementKind.CLASS && type.kind() != ElementKind.INTERFACE) {
79+
throw new CodegenException("Type annotated with " + MCP_SERVER.fqName() + " must be a class or an interface.",
80+
type.originatingElementValue());
81+
}
82+
83+
TypeName mcpFactoryType = type.typeName();
84+
TypeName generatedType = generatedTypeName(mcpFactoryType, "McpFactory");
85+
86+
var classModel = ClassModel.builder()
87+
.type(generatedType)
88+
.copyright(CodegenUtil.copyright(GENERATOR,
89+
mcpFactoryType,
90+
generatedType))
91+
.addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR,
92+
mcpFactoryType,
93+
generatedType,
94+
"1",
95+
""))
96+
.accessModifier(AccessModifier.PACKAGE_PRIVATE)
97+
.addInterface(supplierType(MCPSERVER))
98+
.addAnnotation(Annotation.create(SERVICE_ANNOTATION_SINGLETON));
99+
100+
classModel.addImport("java.util.List");
101+
classModel.addImport(TypeName.create("io.helidon.integrations.mcp.server.InputSchema"));
102+
classModel.addImport(TypeName.create("io.modelcontextprotocol.spec.McpSchema"));
103+
104+
classModel.addField(Field.builder()
105+
.accessModifier(AccessModifier.PRIVATE)
106+
.isFinal(true)
107+
.name("delegate")
108+
.type(type.typeName())
109+
.addContent("new " + type.typeName().className() + "()")
110+
.build());
111+
112+
Method.Builder builder = Method.builder()
113+
.accessModifier(AccessModifier.PUBLIC)
114+
.addAnnotation(Annotations.OVERRIDE)
115+
.returnType(MCPSERVER)
116+
.name("get")
117+
.addContentLine("McpServer.Builder builder = McpServer.fluentBuilder();");
118+
119+
generateServerConfig(builder, type);
120+
generateTools(classModel, builder, type);
121+
generateResources(classModel, builder, type);
122+
generatePrompts(classModel, builder, type);
123+
124+
builder.addContentLine("return builder.build();");
125+
classModel.addMethod(builder);
126+
127+
roundCtx.addGeneratedType(generatedType, classModel, mcpFactoryType, type.originatingElementValue());
128+
}
129+
130+
private void generateServerConfig(Method.Builder builder, TypeInfo type) {
131+
if (type.hasAnnotation(MCP_SERVER)) {
132+
var map = type.annotation(MCP_SERVER).values();
133+
var name = map.get("name").toString();
134+
var version = map.get("version").toString();
135+
builder.addContentLine("builder.name(" + quoted(name) + ");");
136+
builder.addContentLine("builder.version(" + quoted(version) + ");");
137+
}
138+
139+
if (type.hasAnnotation(MCP_NOTIFICATION)) {
140+
var map = type.annotation(MCP_NOTIFICATION).values();
141+
var array = (List<String>) map.get("value");
142+
for (String value : array) {
143+
if ("tool".equals(value)) {
144+
builder.addContentLine("builder.toolChange(true);");
145+
}
146+
if ("resource".equals(value)) {
147+
builder.addContentLine("builder.resourceChange(true);");
148+
}
149+
if ("prompt".equals(value)) {
150+
builder.addContentLine("builder.promptChange(true);");
151+
}
152+
}
153+
}
154+
155+
if (type.hasAnnotation(MCP_SUBSCRIBE)) {
156+
var map = type.annotation(MCP_NOTIFICATION).values();
157+
var array = (List<String>) map.get("value");
158+
if ("resource".equals(array.getFirst())) {
159+
builder.addContentLine("builder.resourceSubscribe(true);");
160+
}
161+
}
162+
}
163+
164+
165+
private void generateTools(ClassModel.Builder classModel, Method.Builder method, TypeInfo type) {
166+
List<TypedElementInfo> elements = getElementsWithAnnotation(type, MCP_TOOL);
167+
for (TypedElementInfo element : elements) {
168+
generateToolMethod(classModel, element);
169+
method.addContentLine("builder.addTool(" + element.elementName() + "());");
170+
}
171+
}
172+
173+
private void generateToolMethod(ClassModel.Builder classModel, TypedElementInfo element) {
174+
String methodName = element.elementName();
175+
Method.Builder builder = Method.builder()
176+
.accessModifier(AccessModifier.PACKAGE_PRIVATE)
177+
.returnType(MCP_TOOL_COMPONENT)
178+
.name(methodName);
179+
Map<String, Object> annotation = element.annotations().getFirst().values();
180+
String name = quoted(annotation.get("name").toString());
181+
String description = quoted(annotation.get("description").toString());
182+
String schema = createSchema(element);
183+
String handler = createHandler(element);
184+
builder.content("""
185+
return ToolComponent.builder()
186+
.name(%s)
187+
.description(%s)
188+
.schema(%s)
189+
.handler(arguments -> {
190+
%s
191+
})
192+
.build();
193+
""".formatted(name, description, schema, handler));
194+
classModel.addMethod(builder);
195+
}
196+
197+
198+
private void generateResources(ClassModel.Builder classModel, Method.Builder method, TypeInfo type) {
199+
List<TypedElementInfo> elements = getElementsWithAnnotation(type, MCP_RESOURCE);
200+
for (TypedElementInfo element : elements) {
201+
generateResourceMethod(classModel, element);
202+
method.addContentLine("builder.addResource(" + element.elementName() + "());");
203+
}
204+
}
205+
206+
private void generateResourceMethod(ClassModel.Builder classModel, TypedElementInfo element) {
207+
String methodName = element.elementName();
208+
Method.Builder builder = Method.builder()
209+
.accessModifier(AccessModifier.PACKAGE_PRIVATE)
210+
.returnType(MCP_RESOURCE_COMPONENT)
211+
.name(methodName);
212+
Map<String, Object> annotation = element.annotations().getFirst().values();
213+
String uri = quoted(annotation.get("uri").toString());
214+
String name = quoted(annotation.get("name").toString());
215+
String description = quoted(annotation.get("description").toString());
216+
builder.content("""
217+
return ResourceComponent.builder()
218+
.uri(%s)
219+
.name(%s)
220+
.description(%s)
221+
.build();
222+
""".formatted(uri, name, description));
223+
classModel.addMethod(builder);
224+
}
225+
226+
private void generatePrompts(ClassModel.Builder classModel, Method.Builder method, TypeInfo type) {
227+
List<TypedElementInfo> elements = getElementsWithAnnotation(type, MCP_PROMPT);
228+
for (TypedElementInfo element : elements) {
229+
generatePromptMethod(classModel, element);
230+
method.addContentLine("builder.addPrompt(" + element.elementName() + "());");
231+
}
232+
}
233+
234+
private void generatePromptMethod(ClassModel.Builder classModel, TypedElementInfo element) {
235+
String methodName = element.elementName();
236+
Method.Builder builder = Method.builder()
237+
.accessModifier(AccessModifier.PACKAGE_PRIVATE)
238+
.returnType(MCP_PROMPT_COMPONENT)
239+
.name(methodName);
240+
Map<String, Object> annotation = element.annotations().getFirst().values();
241+
String name = quoted(annotation.get("name").toString());
242+
String description = quoted(annotation.get("description").toString());
243+
String handler = createHandler(element);
244+
builder.addContentLine("var builder = PromptComponent.builder();");
245+
createPromptArguments(builder, element.parameterArguments());
246+
builder.addContentLine("""
247+
return builder.name(%s)
248+
.description(%s)
249+
.handler(arguments -> {
250+
%s
251+
})
252+
.build();
253+
""".formatted(name, description, handler));
254+
classModel.addMethod(builder);
255+
}
256+
257+
private String createHandler(TypedElementInfo elementInfo) {
258+
List<TypedElementInfo> parameters = elementInfo.parameterArguments();
259+
List<String> required = parameters.stream()
260+
.map(param -> param.elementName())
261+
.toList();
262+
List<String> variables = new ArrayList<>();
263+
for (TypedElementInfo param : parameters) {
264+
variables.add("String " + param.elementName() + " = arguments.get(" + quoted(param.elementName()) + ").toString();");
265+
}
266+
variables.add(" return delegate." + elementInfo.elementName() + "(" + String.join(", ", required) + ");");
267+
return String.join(System.lineSeparator(), variables);
268+
}
269+
270+
private String createSchema(TypedElementInfo tool) {
271+
List<TypedElementInfo> parameters = tool.parameterArguments();
272+
List<String> required = parameters.stream()
273+
.map(param -> quoted(param.elementName()))
274+
.toList();
275+
List<String> properties = parameters.stream()
276+
.map(param -> quoted(param.elementName())
277+
+ ", "
278+
+ quoted(param.enclosingType().orElse(TypeNames.STRING).className().toLowerCase()))
279+
.toList();
280+
return String.format("InputSchema.builder().required(%s).properties(%s)",
281+
String.join(",", required),
282+
String.join(",", properties));
283+
}
284+
285+
private void createPromptArguments(Method.Builder it, List<TypedElementInfo> elements) {
286+
String description = "none";
287+
List<TypedElementInfo> promptParam = elements.stream()
288+
.filter(this::hasPromptAnnotation)
289+
.toList();
290+
for (TypedElementInfo param : promptParam) {
291+
if (param.hasAnnotation(MCP_PROMT_PARAM)) {
292+
Annotation annotation = param.annotation(MCP_PROMT_PARAM);
293+
description = annotation.value().orElse("none");
294+
}
295+
it.addContentLine("builder.promptArgument(new McpSchema.PromptArgument("
296+
+ quoted(param.elementName()) + ", " + quoted(description) + ", true));");
297+
}
298+
}
299+
300+
private boolean hasPromptAnnotation(TypedElementInfo element) {
301+
return element.annotations().stream()
302+
.anyMatch(annotation -> MCP_PROMT_PARAM.name().equals(annotation.typeName().name()));
303+
}
304+
305+
private Optional<TypedElementInfo> getElementWithAnnotation(TypeInfo type, TypeName target) {
306+
return type.elementInfo().stream()
307+
.filter(element -> element.hasAnnotation(target))
308+
.findFirst();
309+
}
310+
311+
private Optional<Annotation> annotation(TypeInfo type, String name) {
312+
return type.annotations().stream()
313+
.filter(annotation -> name.equals(annotation.typeName().name()))
314+
.findFirst();
315+
}
316+
317+
private List<TypedElementInfo> getElementsWithAnnotation(TypeInfo type, TypeName target) {
318+
return type.elementInfo().stream()
319+
.filter(element -> element.hasAnnotation(target))
320+
.collect(Collectors.toList());
321+
}
322+
323+
private TypeName generatedTypeName(TypeName factoryTypeName, String suffix) {
324+
return TypeName.builder()
325+
.packageName(factoryTypeName.packageName())
326+
.className(factoryTypeName.classNameWithEnclosingNames().replace('.', '_') + "__" + suffix)
327+
.build();
328+
}
329+
330+
private TypeName supplierType(TypeName suppliedType) {
331+
return TypeName.builder(TypeNames.SUPPLIER)
332+
.addTypeArgument(suppliedType)
333+
.build();
334+
}
335+
336+
private String quoted(String value) {
337+
return "\"" + value + "\"";
338+
}
64339
}

integrations/mcp/codegen/src/main/java/io/helidon/integrations/mcp/codegen/McpTypes.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,22 @@
44

55
final class McpTypes {
66

7+
//Annotations
78
static final TypeName MCP_SERVER = TypeName.create("io.helidon.integrations.mcp.server.Mcp.Server");
89
static final TypeName MCP_TOOL = TypeName.create("io.helidon.integrations.mcp.server.Mcp.Tool");
910
static final TypeName MCP_TOOL_PARAM = TypeName.create("io.helidon.integrations.mcp.server.Mcp.ToolParam");
1011
static final TypeName MCP_RESOURCE = TypeName.create("io.helidon.integrations.mcp.server.Mcp.Resource");
1112
static final TypeName MCP_PROMPT = TypeName.create("io.helidon.integrations.mcp.server.Mcp.Prompt");
1213
static final TypeName MCP_PROMT_PARAM = TypeName.create("io.helidon.integrations.mcp.server.Mcp.PromptParam");
13-
static final TypeName MCP_RESOURCE_TEMPLATE = TypeName.create("io.helidon.integrations.mcp.server.Mcp.ResourceTemplate");
14+
static final TypeName MCP_NOTIFICATION = TypeName.create("io.helidon.integrations.mcp.server.Mcp.Notification");
15+
static final TypeName MCP_SUBSCRIBE = TypeName.create("io.helidon.integrations.mcp.server.Mcp.Subscribe");
16+
17+
//Implementations
18+
static final TypeName MCPSERVER = TypeName.create("io.helidon.integrations.mcp.server.McpServer");
19+
static final TypeName MCPIMPLEMENTATION = TypeName.create("io.helidon.integrations.mcp.server.Implementation");
20+
static final TypeName MCPCAPABILITIES = TypeName.create("io.helidon.integrations.mcp.server.Capabilities");
21+
static final TypeName MCP_TOOL_COMPONENT = TypeName.create("io.helidon.integrations.mcp.server.ToolComponent");
22+
static final TypeName MCP_RESOURCE_COMPONENT = TypeName.create("io.helidon.integrations.mcp.server.ResourceComponent");
23+
static final TypeName MCP_PROMPT_COMPONENT = TypeName.create("io.helidon.integrations.mcp.server.PromptComponent");
1424

1525
}

integrations/mcp/server/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
<groupId>io.helidon.webserver</groupId>
4444
<artifactId>helidon-webserver-sse</artifactId>
4545
</dependency>
46+
<dependency>
47+
<groupId>io.helidon.webclient</groupId>
48+
<artifactId>helidon-webclient</artifactId>
49+
</dependency>
4650
<dependency>
4751
<groupId>io.helidon.http.media</groupId>
4852
<artifactId>helidon-http-media-jackson</artifactId>

0 commit comments

Comments
 (0)