diff --git a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCodegen.java b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCodegen.java
index c15c3c4f..23b7cb7d 100644
--- a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCodegen.java
+++ b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCodegen.java
@@ -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;
@@ -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;
@@ -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());
@@ -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);
@@ -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();");
}
}
}
@@ -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++;
@@ -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;
@@ -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);
}
@@ -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)
diff --git a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpJsonSchemaCodegen.java b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpJsonSchemaCodegen.java
index 429473e6..a0dc385a 100644
--- a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpJsonSchemaCodegen.java
+++ b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpJsonSchemaCodegen.java
@@ -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));
diff --git a/etc/checkstyle-suppressions.xml b/etc/checkstyle-suppressions.xml
index 57865c7c..e354aa28 100644
--- a/etc/checkstyle-suppressions.xml
+++ b/etc/checkstyle-suppressions.xml
@@ -21,6 +21,6 @@
"-//Puppy Crawl//DTD Suppressions 1.1//EN"
"http://checkstyle.sourceforge.net/dtds/suppressions_1_1.dtd">
-
+
diff --git a/examples/calendar/README.md b/examples/calendar-application/README.md
similarity index 68%
rename from examples/calendar/README.md
rename to examples/calendar-application/README.md
index f8af0016..7e8d2c86 100644
--- a/examples/calendar/README.md
+++ b/examples/calendar-application/README.md
@@ -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.
@@ -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
@@ -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
diff --git a/examples/calendar-application/calendar-declarative/pom.xml b/examples/calendar-application/calendar-declarative/pom.xml
new file mode 100644
index 00000000..a1e8d132
--- /dev/null
+++ b/examples/calendar-application/calendar-declarative/pom.xml
@@ -0,0 +1,153 @@
+
+
+
+ 4.0.0
+
+ io.helidon.applications
+ helidon-se
+ 4.3.1
+
+
+
+ io.helidon.extensions.mcp.examples
+ helidon4-extensions-mcp-examples-calendar-declarative
+ 1.0.0-SNAPSHOT
+ Helidon 4 Extensions MCP Calendar Example Declarative
+
+
+ io.helidon.extensions.mcp.examples.calendar.declarative.Main
+ 0.11.3
+
+
+
+
+ io.helidon.extensions.mcp
+ helidon4-extensions-mcp-server
+ ${project.version}
+
+
+ io.helidon.webclient
+ helidon-webclient
+
+
+ io.helidon.http.media
+ helidon-http-media-jsonb
+
+
+ io.helidon.config
+ helidon-config-yaml
+
+
+ org.eclipse
+ yasson
+
+
+ io.helidon.logging
+ helidon-logging-jul
+ runtime
+
+
+ io.helidon.webserver.testing.junit5
+ helidon-webserver-testing-junit5
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.hamcrest
+ hamcrest-all
+ test
+
+
+ io.modelcontextprotocol.sdk
+ mcp
+ ${version.lib.mcp-sdk}
+ test
+
+
+ org.slf4j
+ slf4j-jdk14
+ test
+
+
+
+
+ helidon-calendar-declarative
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ copy-libs
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+
+ io.helidon.codegen
+ helidon-codegen-apt
+ ${helidon.version}
+
+
+
+ io.helidon.service
+ helidon-service-codegen
+ ${helidon.version}
+
+
+ io.helidon.extensions.mcp
+ helidon4-extensions-mcp-codegen
+ ${project.version}
+
+
+
+
+
+
+ io.helidon.service
+ helidon-service-maven-plugin
+ ${helidon.version}
+
+
+ create-application
+
+ create-application
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/Calendar.java b/examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/Calendar.java
new file mode 100644
index 00000000..65343609
--- /dev/null
+++ b/examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/Calendar.java
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+
+package io.helidon.extensions.mcp.examples.calendar.declarative;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import io.helidon.extensions.mcp.server.McpException;
+import io.helidon.service.registry.Service;
+
+/**
+ * The calendar is a temporary file containing a list of created events.
+ */
+@Service.Singleton
+final class Calendar {
+
+ private final Path file;
+ private final String uri;
+
+ Calendar() {
+ try {
+ this.file = Files.createTempFile("calendar", "-calendar");
+ this.uri = file.toUri().toString();
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+ String uri() {
+ return uri;
+ }
+
+ String readContent() {
+ try {
+ return Files.readString(file);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ String readContentMatchesLine(Predicate lineMatcher) {
+ try {
+ return Files.readAllLines(file)
+ .stream()
+ .filter(lineMatcher)
+ .collect(Collectors.joining("\n"));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ List readEventNames() {
+ String prefix = "name: ";
+ List values = new ArrayList<>();
+ String contents = readContent();
+ for (int i = 0; i < contents.length();) {
+ int k = contents.indexOf(prefix, i);
+ if (k == -1) {
+ break;
+ }
+ int j = contents.indexOf(", ", k);
+ values.add(contents.substring(k + prefix.length(), j));
+ i = j + 1;
+ }
+ return values;
+ }
+
+ void createNewEvent(String name, String date, List attendees) {
+ try {
+ String content = String.format("Event: { name: %s, date: %s, attendees: %s }\n", name, date, attendees);
+ Files.writeString(file, content, StandardOpenOption.APPEND);
+ } catch (IOException e) {
+ throw new McpException("Error happened when writing to event registry", e);
+ }
+ }
+}
diff --git a/examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/Main.java b/examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/Main.java
new file mode 100644
index 00000000..06dafc69
--- /dev/null
+++ b/examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/Main.java
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+package io.helidon.extensions.mcp.examples.calendar.declarative;
+
+import io.helidon.logging.common.LogConfig;
+import io.helidon.service.registry.Service;
+import io.helidon.service.registry.ServiceRegistryManager;
+
+/**
+ * Main class to start the application using service registry.
+ */
+@Service.GenerateBinding
+public class Main {
+
+ static {
+ // initialize logging at build time (for native-image build)
+ LogConfig.initClass();
+ }
+
+ private Main() {
+ }
+
+ /**
+ * Start the application.
+ *
+ * @param args command line arguments, currently ignored
+ */
+ public static void main(String[] args) {
+ // initialize logging at runtime
+ // (if in GraalVM native image, this will re-configure logging with runtime configuration)
+ LogConfig.configureRuntime();
+
+ // start the service registry - uses generated application binding to avoid reflection and runtime lookup
+ ServiceRegistryManager.start(ApplicationBinding.create());
+ }
+}
diff --git a/examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/McpCalendarServer.java b/examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/McpCalendarServer.java
new file mode 100644
index 00000000..91268d32
--- /dev/null
+++ b/examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/McpCalendarServer.java
@@ -0,0 +1,204 @@
+/*
+ * 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.
+ */
+
+package io.helidon.extensions.mcp.examples.calendar.declarative;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+import io.helidon.common.media.type.MediaTypes;
+import io.helidon.extensions.mcp.server.Mcp;
+import io.helidon.extensions.mcp.server.McpCompletionContent;
+import io.helidon.extensions.mcp.server.McpCompletionContents;
+import io.helidon.extensions.mcp.server.McpCompletionType;
+import io.helidon.extensions.mcp.server.McpException;
+import io.helidon.extensions.mcp.server.McpFeatures;
+import io.helidon.extensions.mcp.server.McpLogger;
+import io.helidon.extensions.mcp.server.McpParameters;
+import io.helidon.extensions.mcp.server.McpProgress;
+import io.helidon.extensions.mcp.server.McpPromptContent;
+import io.helidon.extensions.mcp.server.McpPromptContents;
+import io.helidon.extensions.mcp.server.McpResourceContent;
+import io.helidon.extensions.mcp.server.McpResourceContents;
+import io.helidon.extensions.mcp.server.McpRole;
+import io.helidon.extensions.mcp.server.McpToolContent;
+import io.helidon.extensions.mcp.server.McpToolContents;
+import io.helidon.service.registry.Service;
+
+@Mcp.Path("/calendar")
+@Mcp.Server("helidon-mcp-calendar-manager")
+class McpCalendarServer {
+ static final String[] FRIENDS = new String[] {
+ "Frank, Tweety", "Frank, Daffy", "Frank, Tweety, Daffy"
+ };
+ static final String EVENTS_URI = "file://events";
+ static final String EVENTS_URI_TEMPLATE = EVENTS_URI + "/{name}";
+ static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+ @Service.Inject
+ Calendar calendar;
+
+ // -- Tools ---------------------------------------------------------------
+
+ /**
+ * Tool that returns all calendar events on a certain date or all events
+ * if no date is provided.
+ *
+ * @param date the date or {@code ""} if not provided
+ * @return list of calendar events
+ */
+ @Mcp.Tool("List calendar events")
+ List listCalendarEvent(String date) {
+ String entries = calendar.readContentMatchesLine(
+ line -> date.isEmpty() || line.contains("date: " + date));
+ return List.of(McpToolContents.textContent(entries));
+ }
+
+ /**
+ * Tool that adds a new calendar event with a name, date and list of
+ * attendees.
+ *
+ * @param features the MCP features
+ * @param name the event's name
+ * @param date the event's date
+ * @param attendees the list of attendees
+ * @return text confirming event being created
+ */
+ @Mcp.Tool("Adds a new event to the calendar")
+ List addCalendarEvent(McpFeatures features,
+ String name,
+ String date,
+ List attendees) {
+ if (name.isEmpty() || date.isEmpty() || attendees.isEmpty()) {
+ throw new McpException("Missing required arguments name, date or attendees");
+ }
+
+ McpLogger logger = features.logger();
+ McpProgress progress = features.progress();
+ progress.total(100);
+ logger.info("Request to add new event");
+ progress.send(0);
+ calendar.createNewEvent(name, date, attendees);
+ progress.send(50);
+ features.subscriptions().sendUpdate(calendar.uri());
+ progress.send(100);
+
+ return List.of(McpToolContents.textContent("New event added to the calendar"));
+ }
+
+ // -- Resources -----------------------------------------------------------
+
+ /**
+ * Resource whose representation is a list of all calendar events created.
+ *
+ * @param logger the MCP logger
+ * @return text with a list of calendar events
+ */
+ @Mcp.Resource(uri = EVENTS_URI,
+ mediaType = MediaTypes.TEXT_PLAIN_VALUE,
+ description = "List of calendar events created")
+ List eventsResource(McpLogger logger) {
+ logger.debug("Reading calendar events from registry...");
+ String content = calendar.readContent();
+ return List.of(McpResourceContents.textContent(content));
+ }
+
+ /**
+ * Resource whose representation is a single calendar event given its name.
+ *
+ * @param logger the MCP logger
+ * @param name the event's name
+ * @return text with calendar event lines or empty line if not found
+ */
+ @Mcp.Resource(uri = EVENTS_URI_TEMPLATE,
+ mediaType = MediaTypes.TEXT_PLAIN_VALUE,
+ description = "List single calendar event by name")
+ List eventResourceTemplate(McpLogger logger, String name) {
+ logger.debug("Reading calendar events from registry...");
+ String content = calendar.readContentMatchesLine(line -> line.contains("name: " + name));
+ return List.of(McpResourceContents.textContent(content));
+ }
+
+ /**
+ * Completion for event resource template that returns possible event names
+ * containing {@code nameValue} as a substring.
+ *
+ * @param nameValue the value for the name template argument
+ * @return list of possible values or empty
+ */
+ @Mcp.Completion(value = EVENTS_URI_TEMPLATE,
+ type = McpCompletionType.RESOURCE)
+ McpCompletionContent eventResourceTemplateCompletion(String nameValue) {
+ List values = calendar.readEventNames()
+ .stream()
+ .filter(name -> name.contains(nameValue))
+ .toList();
+ return McpCompletionContents.completion(values.toArray(new String[0]));
+ }
+
+ // -- Prompts -------------------------------------------------------------
+
+ /**
+ * Prompt to create a new event given a name, date and attendees.
+ *
+ * @param logger the MCP logger
+ * @param name the event's name
+ * @param date the event's date
+ * @param attendees the list of attendees
+ * @return text with prompt
+ */
+ @Mcp.Prompt("Prompt to create a new event given a name, date and attendees")
+ List createEventPrompt(McpLogger logger,
+ @Mcp.Description("event's name") String name,
+ @Mcp.Description("event's date") String date,
+ @Mcp.Description("event's attendees") String attendees) {
+ logger.debug("Creating calendar event prompt...");
+ return List.of(McpPromptContents.textContent(
+ """
+ Create a new calendar event with name %s, on %s with attendees %s. Make
+ sure all attendees are free to attend the event.
+ """.formatted(name, date, attendees), McpRole.USER));
+ }
+
+ /**
+ * Completion for prompt.
+ *
+ * @param parameters the MCP parameters
+ * @return list of possible values or empty
+ */
+ @Mcp.Completion(value = "createEventPrompt",
+ type = McpCompletionType.PROMPT)
+ McpCompletionContent createEventPromptCompletion(McpParameters parameters) {
+ String promptName = parameters.get("name").asString().orElse(null);
+ if ("name".equals(promptName)) {
+ return McpCompletionContents.completion("Frank & Friends");
+ }
+ if ("date".equals(promptName)) {
+ LocalDate today = LocalDate.now();
+ String[] dates = new String[3];
+ for (int i = 0; i < dates.length; i++) {
+ dates[i] = today.plusDays(i).format(FORMATTER);
+ }
+ return McpCompletionContents.completion(dates);
+ }
+ if ("attendees".equals(promptName)) {
+ return McpCompletionContents.completion(FRIENDS);
+ }
+ // no completion
+ return McpCompletionContents.completion();
+ }
+}
diff --git a/examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/package-info.java b/examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/package-info.java
new file mode 100644
index 00000000..77b1047c
--- /dev/null
+++ b/examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * Helidon MCP Calendar application declarative example.
+ */
+package io.helidon.extensions.mcp.examples.calendar.declarative;
diff --git a/examples/calendar-application/calendar-declarative/src/main/resources/application.yaml b/examples/calendar-application/calendar-declarative/src/main/resources/application.yaml
new file mode 100644
index 00000000..45b4606a
--- /dev/null
+++ b/examples/calendar-application/calendar-declarative/src/main/resources/application.yaml
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+
+server:
+ port: 8081
+ host: 0.0.0.0
diff --git a/examples/calendar/src/main/resources/logging.properties b/examples/calendar-application/calendar-declarative/src/main/resources/logging.properties
similarity index 100%
rename from examples/calendar/src/main/resources/logging.properties
rename to examples/calendar-application/calendar-declarative/src/main/resources/logging.properties
diff --git a/examples/calendar/pom.xml b/examples/calendar-application/calendar/pom.xml
similarity index 100%
rename from examples/calendar/pom.xml
rename to examples/calendar-application/calendar/pom.xml
diff --git a/examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/AddCalendarEventTool.java b/examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/AddCalendarEventTool.java
similarity index 100%
rename from examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/AddCalendarEventTool.java
rename to examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/AddCalendarEventTool.java
diff --git a/examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/Calendar.java b/examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/Calendar.java
similarity index 100%
rename from examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/Calendar.java
rename to examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/Calendar.java
diff --git a/examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CalendarEventResource.java b/examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CalendarEventResource.java
similarity index 100%
rename from examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CalendarEventResource.java
rename to examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CalendarEventResource.java
diff --git a/examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CalendarEventResourceCompletion.java b/examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CalendarEventResourceCompletion.java
similarity index 100%
rename from examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CalendarEventResourceCompletion.java
rename to examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CalendarEventResourceCompletion.java
diff --git a/examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CalendarEventResourceTemplate.java b/examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CalendarEventResourceTemplate.java
similarity index 100%
rename from examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CalendarEventResourceTemplate.java
rename to examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CalendarEventResourceTemplate.java
diff --git a/examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CreateCalendarEventPrompt.java b/examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CreateCalendarEventPrompt.java
similarity index 100%
rename from examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CreateCalendarEventPrompt.java
rename to examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CreateCalendarEventPrompt.java
diff --git a/examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CreateCalendarEventPromptCompletion.java b/examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CreateCalendarEventPromptCompletion.java
similarity index 100%
rename from examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CreateCalendarEventPromptCompletion.java
rename to examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CreateCalendarEventPromptCompletion.java
diff --git a/examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/ListCalendarEventTool.java b/examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/ListCalendarEventTool.java
similarity index 100%
rename from examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/ListCalendarEventTool.java
rename to examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/ListCalendarEventTool.java
diff --git a/examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/Main.java b/examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/Main.java
similarity index 100%
rename from examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/Main.java
rename to examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/Main.java
diff --git a/examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/package-info.java b/examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/package-info.java
similarity index 100%
rename from examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/package-info.java
rename to examples/calendar-application/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/package-info.java
diff --git a/examples/calendar/src/main/resources/application.yaml b/examples/calendar-application/calendar/src/main/resources/application.yaml
similarity index 100%
rename from examples/calendar/src/main/resources/application.yaml
rename to examples/calendar-application/calendar/src/main/resources/application.yaml
diff --git a/examples/calendar-application/calendar/src/main/resources/logging.properties b/examples/calendar-application/calendar/src/main/resources/logging.properties
new file mode 100644
index 00000000..9b6a7a7b
--- /dev/null
+++ b/examples/calendar-application/calendar/src/main/resources/logging.properties
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+handlers=io.helidon.logging.jul.HelidonConsoleHandler
+
+# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread
+java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n
+
+.level=INFO
+io.helidon.extensions.mcp.server.McpServerFeature.level=FINEST
diff --git a/examples/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/BaseTest.java b/examples/calendar-application/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/BaseTest.java
similarity index 100%
rename from examples/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/BaseTest.java
rename to examples/calendar-application/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/BaseTest.java
diff --git a/examples/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/SseClientTest.java b/examples/calendar-application/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/SseClientTest.java
similarity index 100%
rename from examples/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/SseClientTest.java
rename to examples/calendar-application/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/SseClientTest.java
diff --git a/examples/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/StreamableHttpClientTest.java b/examples/calendar-application/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/StreamableHttpClientTest.java
similarity index 100%
rename from examples/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/StreamableHttpClientTest.java
rename to examples/calendar-application/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/StreamableHttpClientTest.java
diff --git a/examples/calendar-application/pom.xml b/examples/calendar-application/pom.xml
new file mode 100644
index 00000000..16d02fc7
--- /dev/null
+++ b/examples/calendar-application/pom.xml
@@ -0,0 +1,39 @@
+
+
+
+ 4.0.0
+
+
+ io.helidon.extensions.mcp.examples
+ helidon4-extensions-mcp-examples-project
+ 1.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ Helidon 4 Extensions MCP Calendar Application Project
+ helidon4-extensions-mcp-examples-calendar-application-project
+ pom
+
+
+ calendar
+ calendar-declarative
+
+
\ No newline at end of file
diff --git a/examples/pom.xml b/examples/pom.xml
index bc9d3101..94f63bd0 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -41,7 +41,7 @@
weather-application
secured-server
- calendar
+ calendar-application
diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpParameters.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpParameters.java
index df35245a..c27da2c9 100644
--- a/server/src/main/java/io/helidon/extensions/mcp/server/McpParameters.java
+++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpParameters.java
@@ -286,10 +286,14 @@ public OptionalValue as(Function function) {
* @param class type
* @return optional value
*/
+ @SuppressWarnings("unchecked")
public OptionalValue as(Class clazz) {
if (value == JsonValue.NULL) {
return empty();
}
+ if (clazz == String.class) {
+ return (OptionalValue) asString();
+ }
var value = OptionalValue.create(MAPPERS, key, clazz);
return value.isEmpty()
? OptionalValue.create(MAPPERS, key, params.as(clazz))