Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import io.helidon.codegen.RoundContext;
import io.helidon.codegen.classmodel.ClassModel;
import io.helidon.codegen.classmodel.Method;
import io.helidon.codegen.classmodel.Parameter;
import io.helidon.codegen.spi.CodegenExtension;
import io.helidon.common.types.AccessModifier;
import io.helidon.common.types.Annotation;
Expand All @@ -44,6 +45,7 @@
import io.helidon.common.types.TypeNames;
import io.helidon.common.types.TypedElementInfo;

import static io.helidon.common.types.TypeNames.LIST;
import static io.helidon.extensions.mcp.codegen.McpJsonSchemaCodegen.addSchemaMethodBody;
import static io.helidon.extensions.mcp.codegen.McpJsonSchemaCodegen.getDescription;
import static io.helidon.extensions.mcp.codegen.McpTypes.CONSUMER_REQUEST;
Expand Down Expand Up @@ -96,7 +98,7 @@

final class McpCodegen implements CodegenExtension {
private static final TypeName GENERATOR = TypeName.create(McpCodegen.class);
private static final ResolvedType STRING_LIST = ResolvedType.create(TypeName.builder(TypeNames.LIST)
private static final ResolvedType STRING_LIST = ResolvedType.create(TypeName.builder(LIST)
.addTypeArgument(TypeNames.STRING)
.build());

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

serverClassModel.addField(delegate -> delegate
.accessModifier(AccessModifier.PRIVATE)
.isFinal(true)
.name("delegate")
.type(type.typeName())
.addContent("new ")
.addContent(type.typeName())
.addContent("()"));
.name("delegate"));

serverClassModel.addImport("io.helidon.service.registry.GlobalServiceRegistry");

serverClassModel.addConstructor(constructor -> {
constructor.accessModifier(AccessModifier.PUBLIC);
constructor.addContentLine("try {")
.addContent("delegate = GlobalServiceRegistry.registry().get(")
.addContent(type.typeName())
.addContentLine(".class);")
.decreaseContentPadding()
.addContentLine("} catch (Exception e) {")
.addContent("delegate = ")
.addContent("new ")
.addContent(type.typeName())
.addContentLine("();")
.addContentLine("}");
});

generateTools(generatedType, serverClassModel, type);
generatePrompts(generatedType, serverClassModel, type);
Expand Down Expand Up @@ -440,10 +455,15 @@ private void addResourceMethod(Method.Builder builder, String uri, ClassModel.Bu
if (TypeNames.STRING.equals(parameter.typeName())) {
parameters.add(parameter.elementName());
builder.addContent("String ")
.addContent(parameter.elementName())
.addContent("encoded_" + parameter.elementName())
.addContent(" = request.parameters().get(\"")
.addContent(parameter.elementName())
.addContentLine("\").asString().orElse(\"\");");
builder.addContent("String ")
.addContent(parameter.elementName())
.addContent(" = io.helidon.common.uri.UriPath.create(")
.addContent("encoded_" + parameter.elementName())
.addContentLine(").path();");
}
}
}
Expand Down Expand Up @@ -620,7 +640,7 @@ private void addPromptArgumentsMethod(Method.Builder builder, TypedElementInfo e
.returnType(LIST_MCP_PROMPT_ARGUMENT);

for (TypedElementInfo param : element.parameterArguments()) {
if (MCP_FEATURES.equals(param.typeName())) {
if (isIgnoredSchemaElement(param.typeName())) {
continue;
}
String builderName = "builder" + index++;
Expand Down Expand Up @@ -812,6 +832,22 @@ private void addToolMethod(Method.Builder builder, ClassModel.Builder classModel
.addContentLine("().orElse(null);");
continue;
}
if (isList(param.typeName())) {
TypeName typeArg = param.typeName().typeArguments().getFirst();
addToListMethod(classModel, typeArg);

if (!parametersLocalVar) {
addParametersLocalVar(builder, classModel);
parametersLocalVar = true;
}
parameters.add(param.elementName());
builder.addContent("var ")
.addContent(param.elementName())
.addContent(" = toList(parameters.get(\"")
.addContent(param.elementName())
.addContentLine("\").asList().orElse(null));");
continue;
}
if (!parametersLocalVar) {
addParametersLocalVar(builder, classModel);
parametersLocalVar = true;
Expand Down Expand Up @@ -975,6 +1011,23 @@ private void addSubscriberMethod(Method.Builder builder, TypedElementInfo elemen
.addContentLine("};");
}

private void addToListMethod(ClassModel.Builder classModel, TypeName type) {
Method.Builder method = Method.builder();
TypeName typeList = TypeName.create("java.util.List<" + type + ">");
TypeName parameterList = TypeName.create("java.util.List<" + MCP_PARAMETERS + ">");
method.name("toList")
.isStatic(true)
.accessModifier(AccessModifier.PRIVATE)
.returnType(typeList);
method.addParameter(Parameter.builder().name("list").type(parameterList).build());
method.addContentLine("return list == null ? List.of()");
method.increaseContentPadding();
method.addContentLine(": list.stream().map(p -> p.as(" + type + ".class))");
method.increaseContentPadding();
method.addContentLine(".map(p -> p.get()).toList();");
classModel.addMethod(method.build());
}

private boolean isBoolean(TypeName type) {
return TypeNames.PRIMITIVE_BOOLEAN.equals(type) || TypeNames.BOXED_BOOLEAN.equals(type);
}
Expand All @@ -994,6 +1047,10 @@ private boolean isNumber(TypeName type) {
|| TypeNames.PRIMITIVE_DOUBLE.equals(type);
}

private boolean isList(TypeName type) {
return type.equals(TypeNames.LIST) && type.typeArguments().size() == 1;
}

private TypeName createClassName(TypeName generatedType, TypedElementInfo element, String suffix) {
return TypeName.builder()
.className(element.findAnnotation(MCP_NAME)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ private static void addPropertySchema(Method.Builder method, TypedElementInfo el
TypeName argument = typeName.boxed().typeArguments().getFirst();
method.addContent("builder.append(\"\\\"")
.addContent(element.elementName())
.addContentLine("\\\"\": {\");");
.addContentLine("\\\": {\");");

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

Expand Down
2 changes: 1 addition & 1 deletion etc/checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
"-//Puppy Crawl//DTD Suppressions 1.1//EN"
"http://checkstyle.sourceforge.net/dtds/suppressions_1_1.dtd">
<suppressions>
<!-- Suppress visibility checks for @Mcp.JsonSchema annotated classes -->
<suppress files="examples/calendar-application/calendar-declarative/src/main/java/.*" checks="VisibilityModifier"/>
<suppress files="tests/declarative/src/main/java/.*" checks="VisibilityModifier"/>
</suppressions>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Helidon MCP Calendar Application

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

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

```shell
mvn clean package
java -jar target/helidon-mcp-calendar-server.jar
java -jar target/helidon-calendar-declarative.jar
```

## Using the MCP Inspector
Expand All @@ -34,39 +34,42 @@ MCP server:
### Testing the Tool

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

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

### Testing the Resource

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

### Testing the Resource Template

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

### Testing the Prompt

1. Navigate to the **Prompt** tab.
2. Click **List Prompt**.
1. Navigate to the **Prompts** tab.
2. Click **List Prompts**.
3. Select the first Prompt in the list.
4. Enter the following parameters on the right panel:

* **Name**: Franck-birthday
* **Name**: Frank birthday
* **Date**: 2021-04-20
* **Attendees**: Franck
* **Attendees**: Frank
5. Click **Get Prompt**

## References
Expand Down
153 changes: 153 additions & 0 deletions examples/calendar-application/calendar-declarative/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2025 Oracle and/or its affiliates.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.helidon.applications</groupId>
<artifactId>helidon-se</artifactId>
<version>4.3.1</version>
<relativePath/>
</parent>

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

<properties>
<mainClass>io.helidon.extensions.mcp.examples.calendar.declarative.Main</mainClass>
<version.lib.mcp-sdk>0.11.3</version.lib.mcp-sdk>
</properties>

<dependencies>
<dependency>
<groupId>io.helidon.extensions.mcp</groupId>
<artifactId>helidon4-extensions-mcp-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.webclient</groupId>
<artifactId>helidon-webclient</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.http.media</groupId>
<artifactId>helidon-http-media-jsonb</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-yaml</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse</groupId>
<artifactId>yasson</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.logging</groupId>
<artifactId>helidon-logging-jul</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.helidon.webserver.testing.junit5</groupId>
<artifactId>helidon-webserver-testing-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>${version.lib.mcp-sdk}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<finalName>helidon-calendar-declarative</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<!-- Annotation processor extension for Helidon Codegen -->
<groupId>io.helidon.codegen</groupId>
<artifactId>helidon-codegen-apt</artifactId>
<version>${helidon.version}</version>
</path>
<path>
<!-- Code generator for Helidon service registry -->
<groupId>io.helidon.service</groupId>
<artifactId>helidon-service-codegen</artifactId>
<version>${helidon.version}</version>
</path>
<path>
<groupId>io.helidon.extensions.mcp</groupId>
<artifactId>helidon4-extensions-mcp-codegen</artifactId>
<version>${project.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<!-- Maven plugin that generates the application binding -->
<groupId>io.helidon.service</groupId>
<artifactId>helidon-service-maven-plugin</artifactId>
<version>${helidon.version}</version>
<executions>
<execution>
<id>create-application</id>
<goals>
<goal>create-application</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Loading