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))