Skip to content

Commit c1c51da

Browse files
authored
New calendar declarative example (#78)
- New version of Calendar app using declarative - Add support for service injection in MCP server class - Fix problem binding lists to method params - Few other minor fixes - Update README for new app
1 parent 0b68ac6 commit c1c51da

File tree

30 files changed

+692
-24
lines changed

30 files changed

+692
-24
lines changed

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

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import io.helidon.codegen.RoundContext;
3333
import io.helidon.codegen.classmodel.ClassModel;
3434
import io.helidon.codegen.classmodel.Method;
35+
import io.helidon.codegen.classmodel.Parameter;
3536
import io.helidon.codegen.spi.CodegenExtension;
3637
import io.helidon.common.types.AccessModifier;
3738
import io.helidon.common.types.Annotation;
@@ -44,6 +45,7 @@
4445
import io.helidon.common.types.TypeNames;
4546
import io.helidon.common.types.TypedElementInfo;
4647

48+
import static io.helidon.common.types.TypeNames.LIST;
4749
import static io.helidon.extensions.mcp.codegen.McpJsonSchemaCodegen.addSchemaMethodBody;
4850
import static io.helidon.extensions.mcp.codegen.McpJsonSchemaCodegen.getDescription;
4951
import static io.helidon.extensions.mcp.codegen.McpTypes.CONSUMER_REQUEST;
@@ -96,7 +98,7 @@
9698

9799
final class McpCodegen implements CodegenExtension {
98100
private static final TypeName GENERATOR = TypeName.create(McpCodegen.class);
99-
private static final ResolvedType STRING_LIST = ResolvedType.create(TypeName.builder(TypeNames.LIST)
101+
private static final ResolvedType STRING_LIST = ResolvedType.create(TypeName.builder(LIST)
100102
.addTypeArgument(TypeNames.STRING)
101103
.build());
102104

@@ -148,12 +150,25 @@ private void process(RoundContext roundCtx, TypeInfo type) {
148150

149151
serverClassModel.addField(delegate -> delegate
150152
.accessModifier(AccessModifier.PRIVATE)
151-
.isFinal(true)
152-
.name("delegate")
153153
.type(type.typeName())
154-
.addContent("new ")
155-
.addContent(type.typeName())
156-
.addContent("()"));
154+
.name("delegate"));
155+
156+
serverClassModel.addImport("io.helidon.service.registry.GlobalServiceRegistry");
157+
158+
serverClassModel.addConstructor(constructor -> {
159+
constructor.accessModifier(AccessModifier.PUBLIC);
160+
constructor.addContentLine("try {")
161+
.addContent("delegate = GlobalServiceRegistry.registry().get(")
162+
.addContent(type.typeName())
163+
.addContentLine(".class);")
164+
.decreaseContentPadding()
165+
.addContentLine("} catch (Exception e) {")
166+
.addContent("delegate = ")
167+
.addContent("new ")
168+
.addContent(type.typeName())
169+
.addContentLine("();")
170+
.addContentLine("}");
171+
});
157172

158173
generateTools(generatedType, serverClassModel, type);
159174
generatePrompts(generatedType, serverClassModel, type);
@@ -440,10 +455,15 @@ private void addResourceMethod(Method.Builder builder, String uri, ClassModel.Bu
440455
if (TypeNames.STRING.equals(parameter.typeName())) {
441456
parameters.add(parameter.elementName());
442457
builder.addContent("String ")
443-
.addContent(parameter.elementName())
458+
.addContent("encoded_" + parameter.elementName())
444459
.addContent(" = request.parameters().get(\"")
445460
.addContent(parameter.elementName())
446461
.addContentLine("\").asString().orElse(\"\");");
462+
builder.addContent("String ")
463+
.addContent(parameter.elementName())
464+
.addContent(" = io.helidon.common.uri.UriPath.create(")
465+
.addContent("encoded_" + parameter.elementName())
466+
.addContentLine(").path();");
447467
}
448468
}
449469
}
@@ -620,7 +640,7 @@ private void addPromptArgumentsMethod(Method.Builder builder, TypedElementInfo e
620640
.returnType(LIST_MCP_PROMPT_ARGUMENT);
621641

622642
for (TypedElementInfo param : element.parameterArguments()) {
623-
if (MCP_FEATURES.equals(param.typeName())) {
643+
if (isIgnoredSchemaElement(param.typeName())) {
624644
continue;
625645
}
626646
String builderName = "builder" + index++;
@@ -812,6 +832,22 @@ private void addToolMethod(Method.Builder builder, ClassModel.Builder classModel
812832
.addContentLine("().orElse(null);");
813833
continue;
814834
}
835+
if (isList(param.typeName())) {
836+
TypeName typeArg = param.typeName().typeArguments().getFirst();
837+
addToListMethod(classModel, typeArg);
838+
839+
if (!parametersLocalVar) {
840+
addParametersLocalVar(builder, classModel);
841+
parametersLocalVar = true;
842+
}
843+
parameters.add(param.elementName());
844+
builder.addContent("var ")
845+
.addContent(param.elementName())
846+
.addContent(" = toList(parameters.get(\"")
847+
.addContent(param.elementName())
848+
.addContentLine("\").asList().orElse(null));");
849+
continue;
850+
}
815851
if (!parametersLocalVar) {
816852
addParametersLocalVar(builder, classModel);
817853
parametersLocalVar = true;
@@ -975,6 +1011,23 @@ private void addSubscriberMethod(Method.Builder builder, TypedElementInfo elemen
9751011
.addContentLine("};");
9761012
}
9771013

1014+
private void addToListMethod(ClassModel.Builder classModel, TypeName type) {
1015+
Method.Builder method = Method.builder();
1016+
TypeName typeList = TypeName.create("java.util.List<" + type + ">");
1017+
TypeName parameterList = TypeName.create("java.util.List<" + MCP_PARAMETERS + ">");
1018+
method.name("toList")
1019+
.isStatic(true)
1020+
.accessModifier(AccessModifier.PRIVATE)
1021+
.returnType(typeList);
1022+
method.addParameter(Parameter.builder().name("list").type(parameterList).build());
1023+
method.addContentLine("return list == null ? List.of()");
1024+
method.increaseContentPadding();
1025+
method.addContentLine(": list.stream().map(p -> p.as(" + type + ".class))");
1026+
method.increaseContentPadding();
1027+
method.addContentLine(".map(p -> p.get()).toList();");
1028+
classModel.addMethod(method.build());
1029+
}
1030+
9781031
private boolean isBoolean(TypeName type) {
9791032
return TypeNames.PRIMITIVE_BOOLEAN.equals(type) || TypeNames.BOXED_BOOLEAN.equals(type);
9801033
}
@@ -994,6 +1047,10 @@ private boolean isNumber(TypeName type) {
9941047
|| TypeNames.PRIMITIVE_DOUBLE.equals(type);
9951048
}
9961049

1050+
private boolean isList(TypeName type) {
1051+
return type.equals(TypeNames.LIST) && type.typeArguments().size() == 1;
1052+
}
1053+
9971054
private TypeName createClassName(TypeName generatedType, TypedElementInfo element, String suffix) {
9981055
return TypeName.builder()
9991056
.className(element.findAnnotation(MCP_NAME)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ private static void addPropertySchema(Method.Builder method, TypedElementInfo el
8787
TypeName argument = typeName.boxed().typeArguments().getFirst();
8888
method.addContent("builder.append(\"\\\"")
8989
.addContent(element.elementName())
90-
.addContentLine("\\\"\": {\");");
90+
.addContentLine("\\\": {\");");
9191

9292
description.ifPresent(desc -> addDescription(method, desc));
9393

etc/checkstyle-suppressions.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
"-//Puppy Crawl//DTD Suppressions 1.1//EN"
2222
"http://checkstyle.sourceforge.net/dtds/suppressions_1_1.dtd">
2323
<suppressions>
24-
<!-- Suppress visibility checks for @Mcp.JsonSchema annotated classes -->
24+
<suppress files="examples/calendar-application/calendar-declarative/src/main/java/.*" checks="VisibilityModifier"/>
2525
<suppress files="tests/declarative/src/main/java/.*" checks="VisibilityModifier"/>
2626
</suppressions>

examples/calendar/README.md renamed to examples/calendar-application/README.md

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Helidon MCP Calendar Application
22

3-
This application serves as a calendar manager and demonstrates how to utilize the MCP Inspector for debugging an MCP server.
3+
This application serves as a calendar manager and demonstrates how to use the MCP Inspector for debugging an MCP server.
44
It allows you to create events and view them through a single registered resource. The calendar is basic and has a single usage:
55
record created events.
66

@@ -18,7 +18,7 @@ Build and launch the calendar application using the following commands:
1818

1919
```shell
2020
mvn clean package
21-
java -jar target/helidon-mcp-calendar-server.jar
21+
java -jar target/helidon-calendar-declarative.jar
2222
```
2323

2424
## Using the MCP Inspector
@@ -34,39 +34,42 @@ MCP server:
3434
### Testing the Tool
3535

3636
1. Navigate to the **Tool** tab.
37-
2. Click **List Tools** and select the first tool from the list.
37+
2. Click **List Tools** and select the `addCalendarEventTool` tool from the list.
3838
3. Enter the following parameters on the right panel:
3939

40-
* **Name**: Franck-birthday
40+
* **Name**: Frank birthday
4141
* **Date**: 2021-04-20
42-
* **Attendees**: CLick `switch to JSON` and enter `["Franck"]`.
42+
* **Attendees**: Click `switch to JSON` and enter `["Frank"]`.
4343
4. Click **Run Tool**.
44-
5. You should see the message: `New event added to the calendar.`
44+
5. You should see the message: `New event added to the calendar`
4545

4646
### Testing the Resource
4747

4848
1. Navigate to the **Resource** tab.
4949
2. Click **List Resources**.
5050
3. Select the first resource in the list.
51-
4. Verify that the result displayed includes Franck's birthday event.
51+
4. Verify that the result displayed includes Frank's birthday event.
5252

5353
### Testing the Resource Template
5454

5555
1. Navigate to the **Resource** tab.
5656
2. Click **List Resources Template**.
57-
3. Enter `calendar` as path.
58-
4. Verify that the result displayed includes Franck's birthday event.
57+
3. Select `eventResourceTemplate`.
58+
3. Type `F` for the name to trigger completion.
59+
4. Select `Frank birthday` from pop-up menu.
60+
5. Click `Read Resource`.
61+
4. Verify that the result displayed includes Frank's birthday event.
5962

6063
### Testing the Prompt
6164

62-
1. Navigate to the **Prompt** tab.
63-
2. Click **List Prompt**.
65+
1. Navigate to the **Prompts** tab.
66+
2. Click **List Prompts**.
6467
3. Select the first Prompt in the list.
6568
4. Enter the following parameters on the right panel:
6669

67-
* **Name**: Franck-birthday
70+
* **Name**: Frank birthday
6871
* **Date**: 2021-04-20
69-
* **Attendees**: Franck
72+
* **Attendees**: Frank
7073
5. Click **Get Prompt**
7174

7275
## References
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
4+
Copyright (c) 2025 Oracle and/or its affiliates.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
18+
-->
19+
<project xmlns="http://maven.apache.org/POM/4.0.0"
20+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
21+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
22+
<modelVersion>4.0.0</modelVersion>
23+
<parent>
24+
<groupId>io.helidon.applications</groupId>
25+
<artifactId>helidon-se</artifactId>
26+
<version>4.3.1</version>
27+
<relativePath/>
28+
</parent>
29+
30+
<groupId>io.helidon.extensions.mcp.examples</groupId>
31+
<artifactId>helidon4-extensions-mcp-examples-calendar-declarative</artifactId>
32+
<version>1.0.0-SNAPSHOT</version>
33+
<name>Helidon 4 Extensions MCP Calendar Example Declarative</name>
34+
35+
<properties>
36+
<mainClass>io.helidon.extensions.mcp.examples.calendar.declarative.Main</mainClass>
37+
<version.lib.mcp-sdk>0.11.3</version.lib.mcp-sdk>
38+
</properties>
39+
40+
<dependencies>
41+
<dependency>
42+
<groupId>io.helidon.extensions.mcp</groupId>
43+
<artifactId>helidon4-extensions-mcp-server</artifactId>
44+
<version>${project.version}</version>
45+
</dependency>
46+
<dependency>
47+
<groupId>io.helidon.webclient</groupId>
48+
<artifactId>helidon-webclient</artifactId>
49+
</dependency>
50+
<dependency>
51+
<groupId>io.helidon.http.media</groupId>
52+
<artifactId>helidon-http-media-jsonb</artifactId>
53+
</dependency>
54+
<dependency>
55+
<groupId>io.helidon.config</groupId>
56+
<artifactId>helidon-config-yaml</artifactId>
57+
</dependency>
58+
<dependency>
59+
<groupId>org.eclipse</groupId>
60+
<artifactId>yasson</artifactId>
61+
</dependency>
62+
<dependency>
63+
<groupId>io.helidon.logging</groupId>
64+
<artifactId>helidon-logging-jul</artifactId>
65+
<scope>runtime</scope>
66+
</dependency>
67+
<dependency>
68+
<groupId>io.helidon.webserver.testing.junit5</groupId>
69+
<artifactId>helidon-webserver-testing-junit5</artifactId>
70+
<scope>test</scope>
71+
</dependency>
72+
<dependency>
73+
<groupId>org.junit.jupiter</groupId>
74+
<artifactId>junit-jupiter-api</artifactId>
75+
<scope>test</scope>
76+
</dependency>
77+
<dependency>
78+
<groupId>org.junit.jupiter</groupId>
79+
<artifactId>junit-jupiter-engine</artifactId>
80+
<scope>test</scope>
81+
</dependency>
82+
<dependency>
83+
<groupId>org.hamcrest</groupId>
84+
<artifactId>hamcrest-all</artifactId>
85+
<scope>test</scope>
86+
</dependency>
87+
<dependency>
88+
<groupId>io.modelcontextprotocol.sdk</groupId>
89+
<artifactId>mcp</artifactId>
90+
<version>${version.lib.mcp-sdk}</version>
91+
<scope>test</scope>
92+
</dependency>
93+
<dependency>
94+
<groupId>org.slf4j</groupId>
95+
<artifactId>slf4j-jdk14</artifactId>
96+
<scope>test</scope>
97+
</dependency>
98+
</dependencies>
99+
100+
<build>
101+
<finalName>helidon-calendar-declarative</finalName>
102+
<plugins>
103+
<plugin>
104+
<groupId>org.apache.maven.plugins</groupId>
105+
<artifactId>maven-dependency-plugin</artifactId>
106+
<executions>
107+
<execution>
108+
<id>copy-libs</id>
109+
</execution>
110+
</executions>
111+
</plugin>
112+
<plugin>
113+
<groupId>org.apache.maven.plugins</groupId>
114+
<artifactId>maven-compiler-plugin</artifactId>
115+
<configuration>
116+
<annotationProcessorPaths>
117+
<path>
118+
<!-- Annotation processor extension for Helidon Codegen -->
119+
<groupId>io.helidon.codegen</groupId>
120+
<artifactId>helidon-codegen-apt</artifactId>
121+
<version>${helidon.version}</version>
122+
</path>
123+
<path>
124+
<!-- Code generator for Helidon service registry -->
125+
<groupId>io.helidon.service</groupId>
126+
<artifactId>helidon-service-codegen</artifactId>
127+
<version>${helidon.version}</version>
128+
</path>
129+
<path>
130+
<groupId>io.helidon.extensions.mcp</groupId>
131+
<artifactId>helidon4-extensions-mcp-codegen</artifactId>
132+
<version>${project.version}</version>
133+
</path>
134+
</annotationProcessorPaths>
135+
</configuration>
136+
</plugin>
137+
<plugin>
138+
<!-- Maven plugin that generates the application binding -->
139+
<groupId>io.helidon.service</groupId>
140+
<artifactId>helidon-service-maven-plugin</artifactId>
141+
<version>${helidon.version}</version>
142+
<executions>
143+
<execution>
144+
<id>create-application</id>
145+
<goals>
146+
<goal>create-application</goal>
147+
</goals>
148+
</execution>
149+
</executions>
150+
</plugin>
151+
</plugins>
152+
</build>
153+
</project>

0 commit comments

Comments
 (0)