Skip to content

Commit cafe8b3

Browse files
committed
- 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 6216bb5 commit cafe8b3

File tree

12 files changed

+315
-52
lines changed

12 files changed

+315
-52
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-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

examples/calendar-application/calendar-declarative/pom.xml

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,39 @@
11
<?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+
-->
219
<project xmlns="http://maven.apache.org/POM/4.0.0"
320
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
421
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
522
<modelVersion>4.0.0</modelVersion>
623
<parent>
7-
<groupId>io.helidon.extensions.mcp</groupId>
8-
<artifactId>helidon4-extensions-mcp-project</artifactId>
9-
<version>1.0.0-SNAPSHOT</version>
24+
<groupId>io.helidon.applications</groupId>
25+
<artifactId>helidon-se</artifactId>
26+
<version>4.3.1</version>
1027
<relativePath/>
1128
</parent>
1229

1330
<groupId>io.helidon.extensions.mcp.examples</groupId>
1431
<artifactId>helidon4-extensions-mcp-examples-calendar-declarative</artifactId>
32+
<version>1.0.0-SNAPSHOT</version>
1533
<name>Helidon 4 Extensions MCP Calendar Example Declarative</name>
1634

17-
1835
<properties>
19-
<mainClass>io.helidon.extensions.mcp.examples.calendar.Main</mainClass>
36+
<mainClass>io.helidon.extensions.mcp.examples.calendar.declarative.Main</mainClass>
2037
<version.lib.mcp-sdk>0.11.3</version.lib.mcp-sdk>
2138
</properties>
2239

examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/Calendar.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.nio.file.Files;
2222
import java.nio.file.Path;
2323
import java.nio.file.StandardOpenOption;
24+
import java.util.ArrayList;
2425
import java.util.List;
2526
import java.util.function.Predicate;
2627
import java.util.stream.Collectors;
@@ -33,17 +34,14 @@
3334
*/
3435
@Service.Singleton
3536
final class Calendar {
36-
static final String URI_TEMPLATE = "file://events/{name}";
3737

3838
private final Path file;
3939
private final String uri;
40-
private final String uriTemplate;
4140

4241
Calendar() {
4342
try {
4443
this.file = Files.createTempFile("calendar", "-calendar");
4544
this.uri = file.toUri().toString();
46-
this.uriTemplate = URI_TEMPLATE;
4745
} catch (IOException ex) {
4846
throw new UncheckedIOException(ex);
4947
}
@@ -53,10 +51,6 @@ String uri() {
5351
return uri;
5452
}
5553

56-
String uriTemplate() {
57-
return uriTemplate;
58-
}
59-
6054
String readContent() {
6155
try {
6256
return Files.readString(file);
@@ -76,6 +70,22 @@ String readContentMatchesLine(Predicate<String> lineMatcher) {
7670
}
7771
}
7872

73+
List<String> readEventNames() {
74+
String prefix = "name: ";
75+
List<String> values = new ArrayList<>();
76+
String contents = readContent();
77+
for (int i = 0; i < contents.length();) {
78+
int k = contents.indexOf(prefix, i);
79+
if (k == -1) {
80+
break;
81+
}
82+
int j = contents.indexOf(", ", k);
83+
values.add(contents.substring(k + prefix.length(), j));
84+
i = j + 1;
85+
}
86+
return values;
87+
}
88+
7989
void createNewEvent(String name, String date, List<String> attendees) {
8090
try {
8191
String content = String.format("Event: { name: %s, date: %s, attendees: %s }\n", name, date, attendees);

0 commit comments

Comments
 (0)