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 40a2fa04..229c7ca3 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -61,7 +61,7 @@ final class McpCodegen implements CodegenExtension { McpCodegen(CodegenContext context) { logger = context.logger(); recorder = new McpRecorder(); - toolCodegen = new McpToolCodegen(recorder); + toolCodegen = new McpToolCodegen(recorder, context); promptCodegen = new McpPromptCodegen(recorder); resourceCodegen = new McpResourceCodegen(recorder); completionCodegen = new McpCompletionCodegen(recorder); diff --git a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCodegenUtil.java b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCodegenUtil.java index 0b5ce511..d8f010b5 100644 --- a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCodegenUtil.java +++ b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCodegenUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -21,6 +21,7 @@ import java.util.stream.Collectors; import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.classmodel.Executable; import io.helidon.codegen.classmodel.Method; import io.helidon.codegen.classmodel.Parameter; import io.helidon.common.types.AccessModifier; @@ -34,19 +35,28 @@ import static io.helidon.common.types.TypeNames.LIST; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_CANCELLATION; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_COMPLETION_REQUEST; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_DESCRIPTION; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_FEATURES; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_LOGGER; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_PARAMETERS; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_PROGRESS; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_PROMPT_REQUEST; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_REQUEST; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_RESOURCE_REQUEST; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_ROOTS; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_SAMPLING; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_SUBSCRIBE_REQUEST; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_TOOL_REQUEST; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_UNSUBSCRIBE_REQUEST; /** * Utility class for methods used by several MCP code generator. */ class McpCodegenUtil { + /** + * Pattern to match the first character of a string. + */ private static final Pattern PATTERN = Pattern.compile("^."); static final List MCP_TYPES = List.of(MCP_REQUEST.classNameWithEnclosingNames(), @@ -119,7 +129,12 @@ static boolean isIgnoredSchemaElement(TypeName typeName) { || MCP_FEATURES.equals(typeName) || MCP_PROGRESS.equals(typeName) || MCP_SAMPLING.equals(typeName) - || MCP_CANCELLATION.equals(typeName); + || MCP_PARAMETERS.equals(typeName) + || MCP_CANCELLATION.equals(typeName) + || MCP_TOOL_REQUEST.equals(typeName) + || MCP_PROMPT_REQUEST.equals(typeName) + || MCP_RESOURCE_REQUEST.equals(typeName) + || MCP_COMPLETION_REQUEST.equals(typeName); } static boolean isResourceTemplate(String uri) { @@ -154,6 +169,23 @@ static void addToListMethod(ClassModel.Builder classModel, TypeName type) { classModel.addMethod(method.build()); } + /** + * Add text content to the builder as literal. The text can be multi line. + * + * @param builder executable builder + * @param text text content + */ + static void generateSafeMultiLine(Executable.Builder builder, String text) { + if (text.contains("\n")) { + builder.addContentLine("\"\"\"") + .increaseContentPadding() + .addContent(text.replace("\\\"", "\"")) + .addContent("\"\"\""); + } else { + builder.addContentLiteral(text); + } + } + /** * Returns {@code true} if the provided type is an MCP type and create request getter for that type, * otherwise nothing is created and return {@code false}. @@ -195,6 +227,14 @@ static boolean isMcpType(List parameters, TypedElementInfo type) { parameters.add("request.parameters()"); return true; } + if (MCP_UNSUBSCRIBE_REQUEST.equals(type.typeName())) { + parameters.add("request"); + return true; + } + if (MCP_SUBSCRIBE_REQUEST.equals(type.typeName())) { + parameters.add("request"); + return true; + } return false; } diff --git a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCompletionCodegen.java b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCompletionCodegen.java index 800ebc3e..dddda368 100644 --- a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCompletionCodegen.java +++ b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCompletionCodegen.java @@ -34,12 +34,11 @@ import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.createClassName; import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.getElementsWithAnnotation; import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.isMcpType; -import static io.helidon.extensions.mcp.codegen.McpTypes.FUNCTION_COMPLETION_REQUEST_COMPLETION_CONTENT; import static io.helidon.extensions.mcp.codegen.McpTypes.LIST_STRING; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_COMPLETION; -import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_COMPLETION_CONTENT; -import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_COMPLETION_CONTENTS; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_COMPLETION_INTERFACE; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_COMPLETION_REQUEST; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_COMPLETION_RESULT; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_COMPLETION_TYPE; class McpCompletionCodegen { @@ -92,14 +91,18 @@ private void addCompletionMethod(Method.Builder builder, ClassModel.Builder clas List parameters = new ArrayList<>(); builder.name("completion") - .returnType(returned -> returned.type(FUNCTION_COMPLETION_REQUEST_COMPLETION_CONTENT)) + .returnType(returned -> returned.type(MCP_COMPLETION_RESULT)) + .addParameter(parameter -> parameter.type(MCP_COMPLETION_REQUEST).name("request")) .addAnnotation(Annotations.OVERRIDE); - builder.addContentLine("return request -> {"); for (TypedElementInfo parameter : element.parameterArguments()) { if (isMcpType(parameters, parameter)) { continue; } + if (parameter.typeName().equals(MCP_COMPLETION_REQUEST)) { + parameters.add("request"); + continue; + } if (parameter.typeName().equals(TypeNames.STRING)) { parameters.add(parameter.elementName()); builder.addContent("var ") @@ -114,31 +117,30 @@ private void addCompletionMethod(Method.Builder builder, ClassModel.Builder clas String params = String.join(", ", parameters); if (element.typeName().equals(LIST_STRING)) { - builder.addContent("return ") - .addContent(MCP_COMPLETION_CONTENTS) - .addContent(".completion(delegate.") + // Create local variable for delegate result called "list". + builder.addContent(LIST_STRING) + .addContent(" list = delegate.") .addContent(element.elementName()) .addContent("(") .addContent(params) - .addContentLine("));") - .decreaseContentPadding() - .addContentLine("};"); + .addContentLine(");"); + builder.addContent("return ") + .addContent(MCP_COMPLETION_RESULT) + .addContentLine(".create(list);"); return; } - if (element.typeName().equals(MCP_COMPLETION_CONTENT)) { + if (element.typeName().equals(MCP_COMPLETION_RESULT)) { builder.addContent("return delegate.") .addContent(element.elementName()) .addContent("(") .addContent(params) - .addContentLine(");") - .decreaseContentPadding() - .addContentLine("};"); + .addContentLine(");"); return; } throw new CodegenException(String.format("Wrong return type for method: %s. Supported types are: %s, or %s.", element.elementName(), LIST_STRING, - MCP_COMPLETION_CONTENT.classNameWithTypes())); + MCP_COMPLETION_RESULT.classNameWithTypes())); } } diff --git a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpPromptCodegen.java b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpPromptCodegen.java index 489cff3c..95f5ef8f 100644 --- a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpPromptCodegen.java +++ b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpPromptCodegen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -17,7 +17,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Optional; import io.helidon.codegen.CodegenException; import io.helidon.codegen.classmodel.ClassModel; @@ -35,15 +34,14 @@ import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.getElementsWithAnnotation; import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.isIgnoredSchemaElement; import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.isMcpType; -import static io.helidon.extensions.mcp.codegen.McpTypes.FUNCTION_REQUEST_LIST_PROMPT_CONTENT; import static io.helidon.extensions.mcp.codegen.McpTypes.LIST_MCP_PROMPT_ARGUMENT; -import static io.helidon.extensions.mcp.codegen.McpTypes.LIST_MCP_PROMPT_CONTENT; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_DESCRIPTION; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_NAME; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_PROMPT; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_PROMPT_ARGUMENT; -import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_PROMPT_CONTENTS; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_PROMPT_INTERFACE; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_PROMPT_REQUEST; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_PROMPT_RESULT; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_ROLE; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_ROLE_ENUM; @@ -95,24 +93,29 @@ private void addPromptDescriptionMethod(Method.Builder builder, String descripti private void addPromptMethod(Method.Builder builder, ClassModel.Builder classModel, TypedElementInfo element) { List parameters = new ArrayList<>(); TypeName returnType = element.signature().type(); - Optional role = element.findAnnotation(MCP_ROLE) - .flatMap(annotation -> annotation.value()); + String role = element.findAnnotation(MCP_ROLE) + .flatMap(annotation -> annotation.value()) + .orElse("ASSISTANT"); builder.name("prompt") - .returnType(returned -> returned.type(FUNCTION_REQUEST_LIST_PROMPT_CONTENT)) + .returnType(returned -> returned.type(MCP_PROMPT_RESULT)) + .addParameter(parameter -> parameter.type(MCP_PROMPT_REQUEST).name("request")) .addAnnotation(Annotations.OVERRIDE); - builder.addContentLine("return request -> {"); for (TypedElementInfo param : element.parameterArguments()) { if (isMcpType(parameters, param)) { continue; } + if (param.typeName().equals(MCP_PROMPT_REQUEST)) { + parameters.add("request"); + continue; + } if (param.typeName().equals(TypeNames.STRING)) { parameters.add(param.elementName()); builder.addContent(param.typeName().classNameWithEnclosingNames()) .addContent(" ") .addContent(param.elementName()) - .addContent(" = request.parameters().get(\"") + .addContent(" = request.arguments().get(\"") .addContent(param.elementName()) .addContentLine("\").asString().orElse(\"\");"); continue; @@ -123,37 +126,35 @@ private void addPromptMethod(Method.Builder builder, ClassModel.Builder classMod } String params = String.join(", ", parameters); - if (returnType.equals(TypeNames.STRING)) { - builder.addContent("return ") - .addContent(List.class) - .addContent(".of(") - .addContent(MCP_PROMPT_CONTENTS) - .addContent(".textContent(delegate.") + if (returnType.equals(MCP_PROMPT_RESULT)) { + builder.addContent("return delegate.") .addContent(element.elementName()) .addContent("(") .addContent(params) - .addContent("), ") - .addContent(MCP_ROLE_ENUM) - .addContent(".") - .addContent(role.orElse("ASSISTANT")) - .addContentLine("));") - .decreaseContentPadding() - .addContentLine("};"); + .addContentLine(");"); return; } - if (returnType.equals(LIST_MCP_PROMPT_CONTENT)) { - builder.addContent("return delegate.") + if (returnType.equals(TypeNames.STRING)) { + builder.addContent("return ") + .addContent(MCP_PROMPT_RESULT) + .addContentLine(".builder()") + .increaseContentPadding() + .addContent(".addTextContent(t -> t.text(") + .addContent("delegate.") .addContent(element.elementName()) .addContent("(") .addContent(params) - .addContentLine(");") - .decreaseContentPadding() - .addContentLine("};"); + .addContent(")).role(") + .addContent(MCP_ROLE_ENUM) + .addContent(".") + .addContent(role) + .addContentLine("))") + .addContentLine(".build();"); return; } throw new CodegenException(String.format("Wrong return type for method: %s. Supported types are: %s, or String.", element.elementName(), - LIST_MCP_PROMPT_CONTENT.classNameWithTypes())); + MCP_PROMPT_RESULT.classNameWithTypes())); } private void addPromptArgumentsMethod(Method.Builder builder, TypedElementInfo element) { diff --git a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpResourceCodegen.java b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpResourceCodegen.java index de4fa8dc..b9f5e8e2 100644 --- a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpResourceCodegen.java +++ b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpResourceCodegen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -34,22 +34,23 @@ import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.getElementsWithAnnotation; import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.isMcpType; import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.isResourceTemplate; -import static io.helidon.extensions.mcp.codegen.McpTypes.CONSUMER_REQUEST; -import static io.helidon.extensions.mcp.codegen.McpTypes.FUNCTION_REQUEST_LIST_RESOURCE_CONTENT; import static io.helidon.extensions.mcp.codegen.McpTypes.HELIDON_MEDIA_TYPE; import static io.helidon.extensions.mcp.codegen.McpTypes.HELIDON_MEDIA_TYPES; -import static io.helidon.extensions.mcp.codegen.McpTypes.LIST_MCP_RESOURCE_CONTENT; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_NAME; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_RESOURCE; -import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_RESOURCE_CONTENTS; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_RESOURCE_INTERFACE; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_RESOURCE_REQUEST; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_RESOURCE_RESULT; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_RESOURCE_SUBSCRIBER; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_RESOURCE_SUBSCRIBER_INTERFACE; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_RESOURCE_UNSUBSCRIBER; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_RESOURCE_UNSUBSCRIBER_INTERFACE; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_SUBSCRIBE_REQUEST; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_UNSUBSCRIBE_REQUEST; import static io.helidon.extensions.mcp.codegen.McpTypes.URI_PATH; class McpResourceCodegen { + private static final List SUPPORTED_TYPES = List.of("String", "McpResourceResult"); private final McpRecorder recorder; McpResourceCodegen(McpRecorder recorder) { @@ -102,7 +103,7 @@ private void generateSubscribers(ClassModel.Builder classModel, TypeInfo type) { .addInterface(MCP_RESOURCE_SUBSCRIBER_INTERFACE) .accessModifier(AccessModifier.PRIVATE) .addMethod(method -> addSubscriberUriMethod(method, uri)) - .addMethod(method -> addSubscriberMethod(method, element, "subscribe"))); + .addMethod(method -> addSubscriberMethod(method, element))); }); } @@ -118,7 +119,7 @@ private void generateUnsubscribers(ClassModel.Builder classModel, TypeInfo type) .addInterface(MCP_RESOURCE_UNSUBSCRIBER_INTERFACE) .accessModifier(AccessModifier.PRIVATE) .addMethod(method -> addSubscriberUriMethod(method, uri)) - .addMethod(method -> addSubscriberMethod(method, element, "unsubscribe"))); + .addMethod(method -> addUnsubscriberMethod(method, element))); }); } @@ -169,13 +170,17 @@ private void addResourceMethod(Method.Builder builder, String uri, ClassModel.Bu builder.name("resource") .addAnnotation(Annotations.OVERRIDE) - .returnType(returned -> returned.type(FUNCTION_REQUEST_LIST_RESOURCE_CONTENT)); - builder.addContentLine("return request -> {"); + .returnType(returned -> returned.type(MCP_RESOURCE_RESULT)) + .addParameter(parameter -> parameter.type(MCP_RESOURCE_REQUEST).name("request")); for (TypedElementInfo parameter : element.parameterArguments()) { if (isMcpType(parameters, parameter)) { continue; } + if (parameter.typeName().equals(MCP_RESOURCE_REQUEST)) { + parameters.add("request"); + continue; + } if (isResourceTemplate(uri)) { if (TypeNames.STRING.equals(parameter.typeName())) { parameters.add(parameter.elementName()); @@ -199,33 +204,30 @@ private void addResourceMethod(Method.Builder builder, String uri, ClassModel.Bu String.join(", ", MCP_TYPES))); } String params = String.join(", ", parameters); - if (returnType.equals(TypeNames.STRING)) { - builder.addContent("return ") - .addContent(List.class) - .addContent(".of(") - .addContent(MCP_RESOURCE_CONTENTS) - .addContent(".textContent(delegate.") + if (returnType.equals(MCP_RESOURCE_RESULT)) { + builder.addContent("return delegate.") .addContent(element.elementName()) .addContent("(") .addContent(params) - .addContentLine(")));") - .decreaseContentPadding() - .addContentLine("};"); + .addContentLine(");"); return; } - if (returnType.equals(LIST_MCP_RESOURCE_CONTENT)) { - builder.addContent("return delegate.") + if (returnType.equals(TypeNames.STRING)) { + builder.addContent("return ") + .addContent(MCP_RESOURCE_RESULT) + .addContentLine(".builder()") + .increaseContentPadding() + .addContent(".addTextContent(delegate.") .addContent(element.elementName()) .addContent("(") .addContent(params) - .addContentLine(");") - .addContentLine("};"); + .addContentLine("))") + .addContentLine(".build();"); return; } throw new CodegenException(String.format("Method %s must return one the following return type: %s", element.elementName(), - String.join(", ", List.of("String", - LIST_MCP_RESOURCE_CONTENT.classNameWithTypes())))); + String.join(", ", SUPPORTED_TYPES))); } private void addSubscriberUriMethod(Method.Builder builder, String uri) { @@ -237,13 +239,24 @@ private void addSubscriberUriMethod(Method.Builder builder, String uri) { .addContentLine("\";"); } - private void addSubscriberMethod(Method.Builder builder, TypedElementInfo element, String methodName) { + private void addSubscriberMethod(Method.Builder builder, TypedElementInfo element) { + addSubscriberMethod(builder, element, "subscribe", MCP_SUBSCRIBE_REQUEST); + } + + private void addUnsubscriberMethod(Method.Builder builder, TypedElementInfo element) { + addSubscriberMethod(builder, element, "unsubscribe", MCP_UNSUBSCRIBE_REQUEST); + } + + private void addSubscriberMethod(Method.Builder builder, + TypedElementInfo element, + String methodName, + TypeName parameterType) { List parameters = new ArrayList<>(); builder.name(methodName) .addAnnotation(Annotations.OVERRIDE) - .returnType(returned -> returned.type(CONSUMER_REQUEST)); - builder.addContentLine("return request -> {"); + .returnType(TypeNames.PRIMITIVE_VOID) + .addParameter(param -> param.type(parameterType).name("request")); for (TypedElementInfo parameter : element.parameterArguments()) { if (isMcpType(parameters, parameter)) { @@ -257,9 +270,7 @@ private void addSubscriberMethod(Method.Builder builder, TypedElementInfo elemen .addContent(element.elementName()) .addContent("(") .addContent(params) - .addContentLine(");") - .decreaseContentPadding() - .addContentLine("};"); + .addContentLine(");"); return; } throw new CodegenException("Method " + element.elementName() + " must return void"); diff --git a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpToolCodegen.java b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpToolCodegen.java index 5d304bb0..3dba19f1 100644 --- a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpToolCodegen.java +++ b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpToolCodegen.java @@ -19,7 +19,9 @@ import java.util.List; import java.util.Optional; +import io.helidon.codegen.CodegenContext; import io.helidon.codegen.CodegenException; +import io.helidon.codegen.CodegenLogger; import io.helidon.codegen.classmodel.ClassModel; import io.helidon.codegen.classmodel.Method; import io.helidon.common.types.AccessModifier; @@ -33,6 +35,7 @@ import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.addToListMethod; import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.createClassName; +import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.generateSafeMultiLine; import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.getDescription; import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.getElementsWithAnnotation; import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.isBoolean; @@ -41,21 +44,24 @@ import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.isMcpType; import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.isNumber; import static io.helidon.extensions.mcp.codegen.McpJsonSchemaCodegen.addSchemaMethodBody; -import static io.helidon.extensions.mcp.codegen.McpTypes.FUNCTION_REQUEST_TOOL_RESULT; -import static io.helidon.extensions.mcp.codegen.McpTypes.LIST_MCP_TOOL_CONTENT; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_DESCRIPTION; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_NAME; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_TOOL; -import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_TOOL_CONTENTS; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_TOOL_INTERFACE; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_TOOL_OUTPUT_SCHEMA; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_TOOL_OUTPUT_SCHEMA_TEXT; +import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_TOOL_REQUEST; import static io.helidon.extensions.mcp.codegen.McpTypes.MCP_TOOL_RESULT; import static io.helidon.extensions.mcp.codegen.McpTypes.OPTIONAL_STRING; +import static io.helidon.extensions.mcp.codegen.McpTypes.OPTIONAL_TOOL_ANNOTATIONS; class McpToolCodegen { private final McpRecorder recorder; + private final CodegenLogger logger; - McpToolCodegen(McpRecorder recorder) { + McpToolCodegen(McpRecorder recorder, CodegenContext context) { this.recorder = recorder; + this.logger = context.logger(); } void generate(ClassModel.Builder classModel, TypeInfo type) { @@ -74,25 +80,45 @@ void generate(ClassModel.Builder classModel, TypeInfo type) { .addMethod(method -> addToolSchemaMethod(method, element)) .addMethod(method -> addToolMethod(method, classModel, element)) .addMethod(method -> addToolAnnotationsMethod(method, toolAnnotation)) - .addMethod(method -> addToolOutputSchema(method, toolAnnotation))); + .addMethod(method -> addToolOutputSchema(method, element))); }); } - private void addToolOutputSchema(Method.Builder builder, Annotation toolAnnotation) { - var outputSchema = toolAnnotation.getValue("outputSchema"); + private void addToolOutputSchema(Method.Builder builder, TypedElementInfo element) { + Optional schema = element.findAnnotation(MCP_TOOL_OUTPUT_SCHEMA); + Optional textSchema = element.findAnnotation(MCP_TOOL_OUTPUT_SCHEMA_TEXT); builder.name("outputSchema") .returnType(OPTIONAL_STRING) .addAnnotation(Annotations.OVERRIDE); - if (outputSchema.isPresent() && !outputSchema.get().isBlank()) { - String schema = toolAnnotation.getValue("outputSchema") - .orElse("\"\"") - .replace("\"", "\\\""); - builder.addContent("return Optional.of(\"") - .addContent(schema) - .addContent("\");"); + + if (schema.isEmpty() && textSchema.isEmpty()) { + builder.addContent("return Optional.empty();"); return; } - builder.addContent("return Optional.empty();"); + + if (schema.isPresent() && textSchema.isPresent()) { + String message = String.format("Annotation %s will be ignored.", + MCP_TOOL_OUTPUT_SCHEMA_TEXT.classNameWithEnclosingNames()); + logger.log(System.Logger.Level.WARNING, message); + } + + if (schema.isPresent()) { + String outputSchema = schema.flatMap(Annotation::typeValue) + .map(TypeName::classNameWithTypes) + .map(value -> value + "__JsonSchema") + .orElseThrow(() -> new CodegenException("Cannot parse output schema")); + builder.addContent("return Optional.of(Services.get(") + .addContent(outputSchema) + .addContent(".class).jsonSchema());"); + return; + } + + String outputShema = textSchema.flatMap(t -> t.stringValue()) + .orElseThrow(() -> new CodegenException("Cannot parse output text schema")) + .replace("\"", "\\\""); + builder.addContent("return Optional.of("); + generateSafeMultiLine(builder, outputShema); + builder.addContentLine(");"); } private void addToolSchemaMethod(Method.Builder builder, TypedElementInfo element) { @@ -127,19 +153,23 @@ private void addToolMethod(Method.Builder builder, ClassModel.Builder classModel TypeName returnType = element.signature().type(); builder.name("tool") - .returnType(returned -> returned.type(FUNCTION_REQUEST_TOOL_RESULT)) - .addAnnotation(Annotations.OVERRIDE); - builder.addContentLine("return request -> {"); + .addAnnotation(Annotations.OVERRIDE) + .returnType(returned -> returned.type(MCP_TOOL_RESULT)) + .addParameter(parameter -> parameter.type(MCP_TOOL_REQUEST).name("request")); for (TypedElementInfo param : element.parameterArguments()) { if (isMcpType(parameters, param)) { continue; } + if (param.typeName().equals(MCP_TOOL_REQUEST)) { + parameters.add("request"); + continue; + } if (TypeNames.STRING.equals(param.typeName())) { parameters.add(param.elementName()); builder.addContent("var ") .addContent(param.elementName()) - .addContent(" = request.parameters().get(\"") + .addContent(" = request.arguments().get(\"") .addContent(param.elementName()) .addContentLine("\").asString().orElse(\"\");"); continue; @@ -148,7 +178,7 @@ private void addToolMethod(Method.Builder builder, ClassModel.Builder classModel parameters.add(param.elementName()); builder.addContent("boolean ") .addContent(param.elementName()) - .addContent(" = request.parameters().get(\"") + .addContent(" = request.arguments().get(\"") .addContent(param.elementName()) .addContentLine("\").asBoolean().orElse(false);"); continue; @@ -157,7 +187,7 @@ private void addToolMethod(Method.Builder builder, ClassModel.Builder classModel parameters.add(param.elementName()); builder.addContent("var ") .addContent(param.elementName()) - .addContent(" = request.parameters().get(\"") + .addContent(" = request.arguments().get(\"") .addContent(param.elementName()) .addContent("\").as") .addContent(param.typeName().className()) @@ -170,7 +200,7 @@ private void addToolMethod(Method.Builder builder, ClassModel.Builder classModel parameters.add(param.elementName()); builder.addContent("var ") .addContent(param.elementName()) - .addContent(" = toList(request.parameters().get(\"") + .addContent(" = toList(request.arguments().get(\"") .addContent(param.elementName()) .addContentLine("\").asList().orElse(null));"); continue; @@ -179,7 +209,7 @@ private void addToolMethod(Method.Builder builder, ClassModel.Builder classModel builder.addContent(param.typeName().classNameWithEnclosingNames()) .addContent(" ") .addContent(param.elementName()) - .addContent(" = request.parameters().get(\"") + .addContent(" = request.arguments().get(\"") .addContent(param.elementName()) .addContent("\").as(") .addContent(param.typeName()) @@ -190,29 +220,14 @@ private void addToolMethod(Method.Builder builder, ClassModel.Builder classModel if (returnType.equals(TypeNames.STRING)) { builder.addContent("return ") .addContent(MCP_TOOL_RESULT) - .addContent(".builder().contents(") - .addContent(List.class) - .addContent(".of(") - .addContent(MCP_TOOL_CONTENTS) - .addContent(".textContent(delegate.") - .addContent(element.elementName()) - .addContent("(") - .addContent(params) - .addContentLine(")))).build();") - .decreaseContentPadding() - .addContentLine("};"); - return; - } - if (returnType.equals(LIST_MCP_TOOL_CONTENT)) { - builder.addContent("return ") - .addContent(MCP_TOOL_RESULT) - .addContent(".builder().contents(delegate.") + .addContentLine(".builder()") + .increaseContentPadding() + .addContent(".addTextContent(delegate.") .addContent(element.elementName()) .addContent("(") .addContent(params) - .addContentLine(")).build();") - .decreaseContentPadding() - .addContentLine("};"); + .addContentLine("))") + .addContentLine(".build();"); return; } if (returnType.equals(MCP_TOOL_RESULT)) { @@ -220,9 +235,7 @@ private void addToolMethod(Method.Builder builder, ClassModel.Builder classModel .addContent(element.elementName()) .addContent("(") .addContent(params) - .addContentLine(");") - .decreaseContentPadding() - .addContentLine("};"); + .addContentLine(");"); return; } throw new CodegenException(String.format("Method %s must return one the following return type: %s", @@ -253,7 +266,7 @@ private void addToolDescriptionMethod(Method.Builder builder, String description private void addToolAnnotationsMethod(Method.Builder builder, Annotation toolAnnotation) { builder.name("annotations") .addAnnotation(Annotations.OVERRIDE) - .returnType(McpTypes.MCP_TOOL_ANNOTATIONS) + .returnType(OPTIONAL_TOOL_ANNOTATIONS) .addContentLine("var builder = McpToolAnnotations.builder();") .addContent("builder.title(\"") .addContent(toolAnnotation.stringValue("title").orElse("")) @@ -272,6 +285,8 @@ private void addToolAnnotationsMethod(Method.Builder builder, Annotation toolAnn .addContent(toolAnnotation.booleanValue("openWorldHint").orElse(true).toString()) .addContentLine(");") .decreaseContentPadding() - .addContentLine("return builder.build();"); + .addContent("return ") + .addContent(Optional.class) + .addContentLine(".of(builder.build());"); } } diff --git a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpTypes.java b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpTypes.java index cc83a656..49d7933d 100644 --- a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpTypes.java +++ b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpTypes.java @@ -16,15 +16,14 @@ package io.helidon.extensions.mcp.codegen; -import java.util.List; import java.util.function.Consumer; import java.util.function.Function; -import io.helidon.common.types.ResolvedType; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNames; import static io.helidon.common.types.TypeNames.LIST; +import static io.helidon.common.types.TypeNames.OPTIONAL; final class McpTypes { private McpTypes() { @@ -43,8 +42,11 @@ private McpTypes() { static final TypeName MCP_DESCRIPTION = TypeName.create("io.helidon.extensions.mcp.server.Mcp.Description"); static final TypeName MCP_TOOLS_PAGE_SIZE = TypeName.create("io.helidon.extensions.mcp.server.Mcp.ToolsPageSize"); static final TypeName MCP_PROMPTS_PAGE_SIZE = TypeName.create("io.helidon.extensions.mcp.server.Mcp.PromptsPageSize"); + static final TypeName MCP_TOOL_OUTPUT_SCHEMA = TypeName.create("io.helidon.extensions.mcp.server.Mcp.ToolOutputSchema"); static final TypeName MCP_RESOURCES_PAGE_SIZE = TypeName.create("io.helidon.extensions.mcp.server.Mcp.ResourcesPageSize"); static final TypeName MCP_RESOURCE_SUBSCRIBER = TypeName.create("io.helidon.extensions.mcp.server.Mcp.ResourceSubscriber"); + static final TypeName MCP_TOOL_OUTPUT_SCHEMA_TEXT = + TypeName.create("io.helidon.extensions.mcp.server.Mcp.ToolOutputSchemaText"); static final TypeName MCP_RESOURCE_UNSUBSCRIBER = TypeName.create("io.helidon.extensions.mcp.server.Mcp.ResourceUnsubscriber"); static final TypeName MCP_RESOURCE_TEMPLATES_PAGE_SIZE = @@ -61,22 +63,22 @@ private McpTypes() { static final TypeName MCP_PARAMETERS = TypeName.create("io.helidon.extensions.mcp.server.McpParameters"); static final TypeName MCP_TOOL_RESULT = TypeName.create("io.helidon.extensions.mcp.server.McpToolResult"); static final TypeName MCP_PROMPT_INTERFACE = TypeName.create("io.helidon.extensions.mcp.server.McpPrompt"); - static final TypeName MCP_TOOL_CONTENT = TypeName.create("io.helidon.extensions.mcp.server.McpToolContent"); + static final TypeName MCP_TOOL_REQUEST = TypeName.create("io.helidon.extensions.mcp.server.McpToolRequest"); static final TypeName MCP_CANCELLATION = TypeName.create("io.helidon.extensions.mcp.server.McpCancellation"); static final TypeName MCP_SERVER_CONFIG = TypeName.create("io.helidon.extensions.mcp.server.McpServerConfig"); - static final TypeName MCP_TOOL_CONTENTS = TypeName.create("io.helidon.extensions.mcp.server.McpToolContents"); + static final TypeName MCP_PROMPT_RESULT = TypeName.create("io.helidon.extensions.mcp.server.McpPromptResult"); static final TypeName MCP_RESOURCE_INTERFACE = TypeName.create("io.helidon.extensions.mcp.server.McpResource"); - static final TypeName MCP_PROMPT_CONTENT = TypeName.create("io.helidon.extensions.mcp.server.McpPromptContent"); - static final TypeName MCP_PROMPT_CONTENTS = TypeName.create("io.helidon.extensions.mcp.server.McpPromptContents"); + static final TypeName MCP_PROMPT_REQUEST = TypeName.create("io.helidon.extensions.mcp.server.McpPromptRequest"); static final TypeName MCP_PROMPT_ARGUMENT = TypeName.create("io.helidon.extensions.mcp.server.McpPromptArgument"); static final TypeName MCP_COMPLETION_TYPE = TypeName.create("io.helidon.extensions.mcp.server.McpCompletionType"); + static final TypeName MCP_RESOURCE_RESULT = TypeName.create("io.helidon.extensions.mcp.server.McpResourceResult"); static final TypeName MCP_COMPLETION_INTERFACE = TypeName.create("io.helidon.extensions.mcp.server.McpCompletion"); static final TypeName MCP_TOOL_ANNOTATIONS = TypeName.create("io.helidon.extensions.mcp.server.McpToolAnnotations"); - static final TypeName MCP_RESOURCE_CONTENT = TypeName.create("io.helidon.extensions.mcp.server.McpResourceContent"); - static final TypeName MCP_RESOURCE_CONTENTS = TypeName.create("io.helidon.extensions.mcp.server.McpResourceContents"); + static final TypeName MCP_RESOURCE_REQUEST = TypeName.create("io.helidon.extensions.mcp.server.McpResourceRequest"); + static final TypeName MCP_COMPLETION_RESULT = TypeName.create("io.helidon.extensions.mcp.server.McpCompletionResult"); + static final TypeName MCP_SUBSCRIBE_REQUEST = TypeName.create("io.helidon.extensions.mcp.server.McpSubscribeRequest"); static final TypeName MCP_COMPLETION_REQUEST = TypeName.create("io.helidon.extensions.mcp.server.McpCompletionRequest"); - static final TypeName MCP_COMPLETION_CONTENT = TypeName.create("io.helidon.extensions.mcp.server.McpCompletionContent"); - static final TypeName MCP_COMPLETION_CONTENTS = TypeName.create("io.helidon.extensions.mcp.server.McpCompletionContents"); + static final TypeName MCP_UNSUBSCRIBE_REQUEST = TypeName.create("io.helidon.extensions.mcp.server.McpUnsubscribeRequest"); static final TypeName MCP_RESOURCE_SUBSCRIBER_INTERFACE = TypeName.create("io.helidon.extensions.mcp.server.McpResourceSubscriber"); static final TypeName MCP_RESOURCE_UNSUBSCRIBER_INTERFACE = @@ -91,43 +93,9 @@ private McpTypes() { static final TypeName HELIDON_MEDIA_TYPES = TypeName.create("io.helidon.common.media.type.MediaTypes"); static final TypeName HTTP_ROUTING_BUILDER = TypeName.create("io.helidon.webserver.http.HttpRouting.Builder"); static final TypeName GLOBAL_SERVICE_REGISTRY = TypeName.create("io.helidon.service.registry.GlobalServiceRegistry"); - static final TypeName OPTIONAL_STRING = TypeName.builder(TypeNames.OPTIONAL).addTypeArgument(TypeNames.STRING).build(); - static final TypeName LIST_MCP_PROMPT_ARGUMENT = TypeName.builder(LIST) - .addTypeArgument(MCP_PROMPT_ARGUMENT) - .build(); - static final TypeName FUNCTION_COMPLETION_REQUEST_COMPLETION_CONTENT = TypeName.builder(FUNCTION) - .addTypeArgument(MCP_COMPLETION_REQUEST) - .addTypeArgument(MCP_COMPLETION_CONTENT) - .build(); - static final TypeName LIST_MCP_RESOURCE_CONTENT = ResolvedType.create(TypeName.builder(LIST) - .addTypeArgument(MCP_RESOURCE_CONTENT) - .build()).type(); - static final TypeName FUNCTION_REQUEST_LIST_RESOURCE_CONTENT = - ResolvedType.create(TypeName.builder(FUNCTION) - .addTypeArguments(List.of( - MCP_REQUEST, - LIST_MCP_RESOURCE_CONTENT)) - .build()).type(); - static final TypeName LIST_MCP_TOOL_CONTENT = ResolvedType.create(TypeName.builder(LIST) - .addTypeArgument(MCP_TOOL_CONTENT) - .build()).type(); - static final TypeName FUNCTION_REQUEST_TOOL_RESULT = - ResolvedType.create(TypeName.builder(FUNCTION) - .addTypeArgument(MCP_REQUEST) - .addTypeArgument(MCP_TOOL_RESULT) - .build()).type(); - static final TypeName LIST_MCP_PROMPT_CONTENT = ResolvedType.create(TypeName.builder(LIST) - .addTypeArgument(MCP_PROMPT_CONTENT) - .build()).type(); - static final TypeName FUNCTION_REQUEST_LIST_PROMPT_CONTENT = - ResolvedType.create(TypeName.builder(FUNCTION) - .addTypeArguments(List.of(MCP_REQUEST, - LIST_MCP_PROMPT_CONTENT)) - .build()).type(); - static final TypeName CONSUMER_REQUEST = ResolvedType.create(TypeName.builder(CONSUMER) - .addTypeArgument(MCP_REQUEST) - .build()).type(); - static final TypeName LIST_STRING = ResolvedType.create(TypeName.builder(LIST) - .addTypeArgument(TypeNames.STRING) - .build()).type(); + static final TypeName LIST_STRING = TypeName.builder(LIST).addTypeArgument(TypeNames.STRING).build(); + static final TypeName CONSUMER_REQUEST = TypeName.builder(CONSUMER).addTypeArgument(MCP_REQUEST).build(); + static final TypeName OPTIONAL_STRING = TypeName.builder(OPTIONAL).addTypeArgument(TypeNames.STRING).build(); + static final TypeName LIST_MCP_PROMPT_ARGUMENT = TypeName.builder(LIST).addTypeArgument(MCP_PROMPT_ARGUMENT).build(); + static final TypeName OPTIONAL_TOOL_ANNOTATIONS = TypeName.builder(OPTIONAL).addTypeArgument(MCP_TOOL_ANNOTATIONS).build(); } 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 index 9f1552c5..1733edec 100644 --- 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 @@ -22,21 +22,17 @@ 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.McpCompletionResult; 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.McpPromptResult; +import io.helidon.extensions.mcp.server.McpResourceResult; import io.helidon.extensions.mcp.server.McpRole; -import io.helidon.extensions.mcp.server.McpToolContent; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.service.registry.Service; @Mcp.Path("/calendar") @@ -62,22 +58,22 @@ class McpCalendarServer { * @return list of calendar events */ @Mcp.Tool("List calendar events") - List listCalendarEvents(String date) { + McpToolResult listCalendarEvents(String date) { String entries = calendar.readContentMatchesLine( line -> date.isEmpty() || line.contains("date: " + date)); - return List.of(McpToolContents.textContent(entries)); + return McpToolResult.create(entries); } /** * Tool that adds a new calendar event with a name, date and list of * attendees. * - * @param features the MCP features - * @param event the event + * @param features the MCP features + * @param event the event * @return text confirming event being created */ @Mcp.Tool("Adds a new event to the calendar") - List addCalendarEvent(McpFeatures features, CalendarEvent event) { + McpToolResult addCalendarEvent(McpFeatures features, CalendarEvent event) { if (event.getName().isEmpty() || event.getDate().isEmpty() || event.getAttendees().isEmpty()) { throw new McpException("Missing required arguments name, date or attendees"); } @@ -92,7 +88,7 @@ List addCalendarEvent(McpFeatures features, CalendarEvent event) features.subscriptions().sendUpdate(EVENTS_URI); progress.send(100); - return List.of(McpToolContents.textContent("New event added to the calendar")); + return McpToolResult.create("New event added to the calendar"); } // -- Resources ----------------------------------------------------------- @@ -106,10 +102,10 @@ List addCalendarEvent(McpFeatures features, CalendarEvent event) @Mcp.Resource(uri = EVENTS_URI, mediaType = MediaTypes.TEXT_PLAIN_VALUE, description = "List of calendar events created") - List eventsResource(McpLogger logger) { + McpResourceResult eventsResource(McpLogger logger) { logger.debug("Reading calendar events from registry..."); String content = calendar.readContent(); - return List.of(McpResourceContents.textContent(content)); + return McpResourceResult.create(content); } /** @@ -122,10 +118,10 @@ List eventsResource(McpLogger logger) { @Mcp.Resource(uri = EVENTS_URI_TEMPLATE, mediaType = MediaTypes.TEXT_PLAIN_VALUE, description = "List single calendar event by name") - List eventResourceTemplate(McpLogger logger, String name) { + McpResourceResult 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)); + return McpResourceResult.builder().addTextContent(content).build(); } /** @@ -137,12 +133,12 @@ List eventResourceTemplate(McpLogger logger, String name) { */ @Mcp.Completion(value = EVENTS_URI_TEMPLATE, type = McpCompletionType.RESOURCE) - McpCompletionContent eventResourceTemplateCompletion(String nameValue) { + McpCompletionResult eventResourceTemplateCompletion(String nameValue) { List values = calendar.readEventNames() .stream() .filter(name -> name.contains(nameValue)) .toList(); - return McpCompletionContents.completion(values); + return McpCompletionResult.create(values); } // -- Prompts ------------------------------------------------------------- @@ -157,16 +153,19 @@ McpCompletionContent eventResourceTemplateCompletion(String nameValue) { * @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) { + McpPromptResult 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)); + return McpPromptResult.builder() + .addTextContent(t -> t + .text(""" + 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)) + .role(McpRole.USER)) + .build(); } /** @@ -177,10 +176,10 @@ List createEventPrompt(McpLogger logger, */ @Mcp.Completion(value = "createEventPrompt", type = McpCompletionType.PROMPT) - McpCompletionContent createEventPromptCompletion(McpParameters parameters) { + McpCompletionResult createEventPromptCompletion(McpParameters parameters) { String promptName = parameters.get("argument").get("name").asString().orElse(null); if ("name".equals(promptName)) { - return McpCompletionContents.completion("Frank & Friends"); + return McpCompletionResult.create("Frank & Friends"); } if ("date".equals(promptName)) { LocalDate today = LocalDate.now(); @@ -188,12 +187,12 @@ McpCompletionContent createEventPromptCompletion(McpParameters parameters) { for (int i = 0; i < dates.length; i++) { dates[i] = today.plusDays(i).format(FORMATTER); } - return McpCompletionContents.completion(dates); + return McpCompletionResult.create(dates); } if ("attendees".equals(promptName)) { - return McpCompletionContents.completion(FRIENDS); + return McpCompletionResult.create(FRIENDS); } // no completion - return McpCompletionContents.completion(); + return McpCompletionResult.create(); } } diff --git a/examples/calendar-application/calendar-declarative/src/test/java/io/helidon/extensions/mcp/examples/calendar/declarative/BaseTest.java b/examples/calendar-application/calendar-declarative/src/test/java/io/helidon/extensions/mcp/examples/calendar/declarative/BaseTest.java index 44fbf9de..c576c658 100644 --- a/examples/calendar-application/calendar-declarative/src/test/java/io/helidon/extensions/mcp/examples/calendar/declarative/BaseTest.java +++ b/examples/calendar-application/calendar-declarative/src/test/java/io/helidon/extensions/mcp/examples/calendar/declarative/BaseTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -33,6 +33,7 @@ import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; @TestMethodOrder(OrderAnnotation.class) @@ -141,7 +142,7 @@ void testPromptCall() { Map arguments = Map.of("name", "Frank-birthday", "date", "2021-04-20", "attendees", "Frank"); McpSchema.GetPromptRequest request = new McpSchema.GetPromptRequest("createEventPrompt", arguments); McpSchema.GetPromptResult promptResult = client().getPrompt(request); - assertThat(promptResult.description(), is("Prompt to create a new event given a name, date and attendees")); + assertThat(promptResult.description(), is(nullValue())); List messages = promptResult.messages(); assertThat(messages.size(), is(1)); diff --git a/examples/calendar-application/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 index 09191552..54f8149e 100644 --- a/examples/calendar-application/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 @@ -17,7 +17,6 @@ package io.helidon.extensions.mcp.examples.calendar; import java.util.List; -import java.util.function.Function; import io.helidon.common.mapper.OptionalValue; import io.helidon.extensions.mcp.server.McpException; @@ -25,9 +24,8 @@ 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.McpRequest; import io.helidon.extensions.mcp.server.McpTool; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; import static io.helidon.extensions.mcp.examples.calendar.Calendar.EVENTS_URI; @@ -83,14 +81,10 @@ public String schema() { } @Override - public Function tool() { - return this::addCalendarEvent; - } - - private McpToolResult addCalendarEvent(McpRequest request) { + public McpToolResult tool(McpToolRequest request) { McpFeatures features = request.features(); McpLogger logger = features.logger(); - McpParameters mcpParameters = request.parameters(); + McpParameters mcpParameters = request.arguments(); McpProgress progress = features.progress(); progress.total(100); @@ -117,7 +111,7 @@ private McpToolResult addCalendarEvent(McpRequest request) { progress.send(100); return McpToolResult.builder() - .addContent(McpToolContents.textContent("New event added to the calendar")) + .addTextContent("New event added to the calendar") .build(); } diff --git a/examples/calendar-application/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 index 174729c0..95166cba 100644 --- a/examples/calendar-application/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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -16,15 +16,11 @@ package io.helidon.extensions.mcp.examples.calendar; -import java.util.List; -import java.util.function.Function; - import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpResource; -import io.helidon.extensions.mcp.server.McpResourceContent; -import io.helidon.extensions.mcp.server.McpResourceContents; +import io.helidon.extensions.mcp.server.McpResourceRequest; +import io.helidon.extensions.mcp.server.McpResourceResult; /** * Resource that represents a calendar event registry. @@ -57,13 +53,11 @@ public MediaType mediaType() { } @Override - public Function> resource() { - return this::readRegistry; - } - - private List readRegistry(McpRequest request) { + public McpResourceResult resource(McpResourceRequest request) { request.features().logger().debug("Reading calendar events from registry..."); String content = calendar.readContent(); - return List.of(McpResourceContents.textContent(content)); + return McpResourceResult.builder() + .addTextContent(content) + .build(); } } diff --git a/examples/calendar-application/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 index a309fe16..85244c35 100644 --- a/examples/calendar-application/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 @@ -17,12 +17,10 @@ package io.helidon.extensions.mcp.examples.calendar; import java.util.List; -import java.util.function.Function; import io.helidon.extensions.mcp.server.McpCompletion; -import io.helidon.extensions.mcp.server.McpCompletionContent; -import io.helidon.extensions.mcp.server.McpCompletionContents; import io.helidon.extensions.mcp.server.McpCompletionRequest; +import io.helidon.extensions.mcp.server.McpCompletionResult; import io.helidon.extensions.mcp.server.McpCompletionType; /** @@ -46,15 +44,11 @@ public McpCompletionType referenceType() { } @Override - public Function completion() { - return this::complete; - } - - private McpCompletionContent complete(McpCompletionRequest request) { + public McpCompletionResult completion(McpCompletionRequest request) { List values = calendar.readEventNames() .stream() .filter(name -> name.contains(request.value())) .toList(); - return McpCompletionContents.completion(values); + return McpCompletionResult.create(values); } } diff --git a/examples/calendar-application/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 index 25e42db7..3173b1ae 100644 --- a/examples/calendar-application/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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -16,15 +16,11 @@ package io.helidon.extensions.mcp.examples.calendar; -import java.util.List; -import java.util.function.Function; - import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpResource; -import io.helidon.extensions.mcp.server.McpResourceContent; -import io.helidon.extensions.mcp.server.McpResourceContents; +import io.helidon.extensions.mcp.server.McpResourceRequest; +import io.helidon.extensions.mcp.server.McpResourceResult; /** * Resource template to help accessing to the event registry. @@ -57,14 +53,12 @@ public MediaType mediaType() { } @Override - public Function> resource() { - return request -> { - String name = request.parameters() - .get("name") - .asString() - .orElse("Unknown"); - String content = calendar.readContentMatchesLine(line -> line.contains("name: " + name)); - return List.of(McpResourceContents.textContent(content)); - }; + public McpResourceResult resource(McpResourceRequest request) { + String name = request.parameters() + .get("name") + .asString() + .orElse("Unknown"); + String content = calendar.readContentMatchesLine(line -> line.contains("name: " + name)); + return McpResourceResult.create(content); } } diff --git a/examples/calendar-application/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 index 06a36d5d..9ac10a00 100644 --- a/examples/calendar-application/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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -17,16 +17,14 @@ package io.helidon.extensions.mcp.examples.calendar; import java.util.List; -import java.util.function.Function; import io.helidon.extensions.mcp.server.McpException; import io.helidon.extensions.mcp.server.McpLogger; import io.helidon.extensions.mcp.server.McpParameters; import io.helidon.extensions.mcp.server.McpPrompt; import io.helidon.extensions.mcp.server.McpPromptArgument; -import io.helidon.extensions.mcp.server.McpPromptContent; -import io.helidon.extensions.mcp.server.McpPromptContents; -import io.helidon.extensions.mcp.server.McpRequest; +import io.helidon.extensions.mcp.server.McpPromptRequest; +import io.helidon.extensions.mcp.server.McpPromptResult; import io.helidon.extensions.mcp.server.McpRole; /** @@ -67,13 +65,9 @@ public List arguments() { } @Override - public Function> prompt() { - return this::createCalendarEvent; - } - - private List createCalendarEvent(McpRequest request) { + public McpPromptResult prompt(McpPromptRequest request) { McpLogger logger = request.features().logger(); - McpParameters mcpParameters = request.parameters(); + McpParameters mcpParameters = request.arguments(); logger.debug("Creating calendar event prompt..."); @@ -89,11 +83,15 @@ private List createCalendarEvent(McpRequest request) { logger.debug("Argument successfully parsed from client request"); - 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)); + return McpPromptResult.builder() + .description("New event created") + .addTextContent(content -> content + .text(""" + 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)) + .role(McpRole.USER)) + .build(); } private RuntimeException requiredArgument(String argument) { diff --git a/examples/calendar-application/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 index a2f40f89..c184aa2f 100644 --- a/examples/calendar-application/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 @@ -18,12 +18,12 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.function.Function; +import java.util.ArrayList; +import java.util.List; import io.helidon.extensions.mcp.server.McpCompletion; -import io.helidon.extensions.mcp.server.McpCompletionContent; -import io.helidon.extensions.mcp.server.McpCompletionContents; import io.helidon.extensions.mcp.server.McpCompletionRequest; +import io.helidon.extensions.mcp.server.McpCompletionResult; import io.helidon.extensions.mcp.server.McpCompletionType; /** @@ -32,9 +32,7 @@ final class CreateCalendarEventPromptCompletion implements McpCompletion { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - private static final String[] FRIENDS = new String[] { - "Frank, Tweety", "Frank, Daffy", "Frank, Tweety, Daffy" - }; + private static final List FRIENDS = List.of("Frank, Tweety", "Frank, Daffy", "Frank, Tweety, Daffy"); @Override public String reference() { @@ -47,27 +45,23 @@ public McpCompletionType referenceType() { } @Override - public Function completion() { - return this::complete; - } - - private McpCompletionContent complete(McpCompletionRequest request) { + public McpCompletionResult completion(McpCompletionRequest request) { String promptName = request.name(); if ("name".equals(promptName)) { - return McpCompletionContents.completion("Frank & Friends"); + return McpCompletionResult.create("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); + List dates = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + dates.add(today.plusDays(i).format(FORMATTER)); } - return McpCompletionContents.completion(dates); + return McpCompletionResult.create(dates); } if ("attendees".equals(promptName)) { - return McpCompletionContents.completion(FRIENDS); + return McpCompletionResult.create(FRIENDS); } // no completion - return McpCompletionContents.completion(); + return McpCompletionResult.create(); } } diff --git a/examples/calendar-application/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 index 2cc605db..5cea26bb 100644 --- a/examples/calendar-application/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 @@ -16,12 +16,8 @@ package io.helidon.extensions.mcp.examples.calendar; -import java.util.function.Function; - -import io.helidon.extensions.mcp.server.McpParameters; -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpTool; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; /** @@ -64,17 +60,15 @@ public String schema() { } @Override - public Function tool() { - return request -> { - McpParameters mcpParameters = request.parameters(); - String date = mcpParameters.get("date") - .asString() - .orElse(null); - String entries = calendar.readContentMatchesLine(line -> date == null || line.contains(date)); + public McpToolResult tool(McpToolRequest request) { + String date = request.arguments() + .get("date") + .asString() + .orElse(null); + String entries = calendar.readContentMatchesLine(line -> date == null || line.contains(date)); - return McpToolResult.builder() - .addContent(McpToolContents.textContent(entries)) - .build(); - }; + return McpToolResult.builder() + .addTextContent(entries) + .build(); } } diff --git a/examples/calendar-application/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 index b927adad..0202776c 100644 --- a/examples/calendar-application/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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -148,7 +148,7 @@ void testPromptCall() { Map arguments = Map.of("name", "Frank-birthday", "date", "2021-04-20", "attendees", "Frank"); McpSchema.GetPromptRequest request = new McpSchema.GetPromptRequest("create-event", arguments); McpSchema.GetPromptResult promptResult = client().getPrompt(request); - assertThat(promptResult.description(), is("Create a new event and add it to the calendar")); + assertThat(promptResult.description(), is("New event created")); List messages = promptResult.messages(); assertThat(messages.size(), is(1)); diff --git a/examples/secured-server/src/main/java/io/helidon/extensions/mcp/examples/secured/SecuredTool.java b/examples/secured-server/src/main/java/io/helidon/extensions/mcp/examples/secured/SecuredTool.java index 92fb643e..f17b0900 100644 --- a/examples/secured-server/src/main/java/io/helidon/extensions/mcp/examples/secured/SecuredTool.java +++ b/examples/secured-server/src/main/java/io/helidon/extensions/mcp/examples/secured/SecuredTool.java @@ -16,11 +16,8 @@ package io.helidon.extensions.mcp.examples.secured; -import java.util.function.Function; - -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpTool; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.security.SecurityContext; @@ -45,15 +42,11 @@ public String schema() { } @Override - public Function tool() { - return request -> { - String username = request.requestContext() - .get(SecurityContext.class) - .map(SecurityContext::userName) - .orElse("Unknown"); - return McpToolResult.builder() - .addContent(McpToolContents.textContent("Username: " + username)) - .build(); - }; + public McpToolResult tool(McpToolRequest request) { + String username = request.requestContext() + .get(SecurityContext.class) + .map(SecurityContext::userName) + .orElse("Unknown"); + return McpToolResult.create("Username: " + username); } } diff --git a/examples/weather-application/mcp-server-declarative/src/main/java/io/helidon/extensions/mcp/weather/server/declarative/McpServer.java b/examples/weather-application/mcp-server-declarative/src/main/java/io/helidon/extensions/mcp/weather/server/declarative/McpServer.java index afd888a6..d762e833 100644 --- a/examples/weather-application/mcp-server-declarative/src/main/java/io/helidon/extensions/mcp/weather/server/declarative/McpServer.java +++ b/examples/weather-application/mcp-server-declarative/src/main/java/io/helidon/extensions/mcp/weather/server/declarative/McpServer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -20,8 +20,7 @@ import java.util.stream.Collectors; import io.helidon.extensions.mcp.server.Mcp; -import io.helidon.extensions.mcp.server.McpToolContent; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.webclient.api.HttpClientResponse; import io.helidon.webclient.api.WebClient; @@ -39,7 +38,7 @@ class McpServer { .build(); @Mcp.Tool("Get weather alert per US state") - List getWeatherAlertFromState(String state) { + McpToolResult getWeatherAlertFromState(String state) { try (HttpClientResponse response = WEBCLIENT.get() .path("/alerts/active/area/" + state) .request()) { @@ -58,9 +57,9 @@ List getWeatherAlertFromState(String state) { .collect(Collectors.joining("\n")); if (content.isEmpty()) { - return List.of(McpToolContents.textContent("There is no alert for this state")); + return McpToolResult.create("There is no alert for this state"); } - return List.of(McpToolContents.textContent(content)); + return McpToolResult.create(content); } } diff --git a/examples/weather-application/mcp-server/src/main/java/io/helidon/extensions/mcp/weather/server/Main.java b/examples/weather-application/mcp-server/src/main/java/io/helidon/extensions/mcp/weather/server/Main.java index 1045da50..39be2c71 100644 --- a/examples/weather-application/mcp-server/src/main/java/io/helidon/extensions/mcp/weather/server/Main.java +++ b/examples/weather-application/mcp-server/src/main/java/io/helidon/extensions/mcp/weather/server/Main.java @@ -21,9 +21,8 @@ import io.helidon.config.Config; import io.helidon.extensions.mcp.server.McpParameters; -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpServerConfig; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.json.schema.Schema; import io.helidon.webclient.api.HttpClientResponse; @@ -74,7 +73,7 @@ private static String createWeatherSchema() { .generate(); } - private static McpToolResult getWeatherAlertFromState(McpRequest request) { + private static McpToolResult getWeatherAlertFromState(McpToolRequest request) { McpParameters mcpParameters = request.parameters(); String state = mcpParameters.get("state").asString().orElse("NY"); @@ -96,13 +95,9 @@ private static McpToolResult getWeatherAlertFromState(McpRequest request) { .collect(Collectors.joining("\n")); if (content.isEmpty()) { - return McpToolResult.builder() - .addContent(McpToolContents.textContent("There is no alert for this state")) - .build(); + return McpToolResult.create("There is no alert for this state"); } - return McpToolResult.builder() - .addContent(McpToolContents.textContent(content)) - .build(); + return McpToolResult.create(content); } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/Mcp.java b/server/src/main/java/io/helidon/extensions/mcp/server/Mcp.java index 5ceb0746..f49044f8 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/Mcp.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/Mcp.java @@ -186,7 +186,27 @@ public final class Mcp { * @return the hint */ boolean openWorldHint() default true; + } + + /** + * Tool output schema. + */ + public @interface ToolOutputSchema { + /** + * The tool output schema is a JSON schema that defines the tool content output. + * The string must be compliant with + * JSON Schema Version 2020-12. + * If the output schema is defined, the tool must set {@link McpToolResult.Builder#structuredContent(Object)}. + * + * @return the tool output schema + */ + Class value(); + } + /** + * Tool output schema. + */ + public @interface ToolOutputSchemaText { /** * The tool output schema is a JSON schema that defines the tool content output. * The string must be compliant with @@ -195,7 +215,7 @@ public final class Mcp { * * @return the tool output schema */ - String outputSchema() default ""; + String value(); } /** diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpAudioContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpAudioContent.java index bc4f121d..f3a1db61 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpAudioContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpAudioContent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -19,5 +19,9 @@ /** * Audio content. */ -sealed interface McpAudioContent extends McpMediaContent permits McpAudioContentImpl { +interface McpAudioContent extends McpMediaContent { + @Override + default McpContentType type() { + return McpContentType.AUDIO; + } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpAudioContentImpl.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpAudioContentImpl.java index 44433d42..f0106826 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpAudioContentImpl.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpAudioContentImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; import java.util.Base64; @@ -45,7 +44,7 @@ public MediaType mediaType() { } @Override - public ContentType type() { - return ContentType.AUDIO; + public McpContentType type() { + return McpContentType.AUDIO; } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletion.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletion.java new file mode 100644 index 00000000..fb14c978 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletion.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025, 2026 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.server; + +/** + * Configuration of an MCP Completion. + */ +public interface McpCompletion { + /** + * Completion reference must be a {@link McpPromptArgument} name or a {@link McpResource} uri template. + * + * @return completion reference + */ + String reference(); + + /** + * The reference type of this completion. + * + * @return reference type + */ + default McpCompletionType referenceType() { + return McpCompletionType.PROMPT; + } + + /** + * Completion request handler. + * + * @param request completion request + * @return completion suggestion + */ + McpCompletionResult completion(McpCompletionRequest request); +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionConfigBlueprint.java similarity index 72% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionBlueprint.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionConfigBlueprint.java index 83e02758..3b85a624 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionBlueprint.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, 2026 Oracle and/or its affiliates. + * Copyright (c) 2026 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; import java.util.function.Function; @@ -22,11 +21,10 @@ import io.helidon.builder.api.Prototype; /** - * Configuration of an MCP Completion. + * Completion configuration. */ @Prototype.Blueprint -@Prototype.IncludeDefaultMethods -interface McpCompletionBlueprint { +interface McpCompletionConfigBlueprint { /** * Completion reference must be a {@link McpPromptArgument} name or a {@link McpResource} uri template. * @@ -40,15 +38,13 @@ interface McpCompletionBlueprint { * @return reference type */ @Option.Default("PROMPT") - default McpCompletionType referenceType() { - return McpCompletionType.PROMPT; - } + McpCompletionType referenceType(); /** - * Complete the client argument accessible from parameters. The returned {@link McpCompletionContent} - * can be instantiated using the {@link McpCompletionContents} factory. + * Complete the client argument accessible from parameters. The returned {@link McpCompletionResult} + * can be instantiated using the {@link McpCompletionResult} factory. * * @return completion suggestion */ - Function completion(); + Function completion(); } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionContents.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionContents.java deleted file mode 100644 index aafa49aa..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionContents.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.server; - -import java.util.List; -import java.util.Objects; - -/** - * {@link McpCompletionContent} factory. - */ -public final class McpCompletionContents { - private McpCompletionContents() { - } - - /** - * Create a completion content from the provided list of string. The maximum - * number of suggestion cannot exceed 100. - * - * @param values completion values - * @return completion content - */ - public static McpCompletionContent completion(String... values) { - return new McpCompletionContentImpl(List.of(values)); - } - - /** - * Create a completion content from provided list of string. The maximum - * number of suggestion cannot exceed 100. - * - * @param values completion values - * @return completion content - */ - public static McpCompletionContent completion(List values) { - return new McpCompletionContentImpl(values); - } - - /** - * Completion content default implementation. - */ - static final class McpCompletionContentImpl implements McpCompletionContent { - private final List values; - - private McpCompletionContentImpl(List values) { - Objects.requireNonNull(values, "values must not be null"); - if (values.size() > 100) { - throw new McpInternalException("Cannot contain more than 100 values"); - } - this.values = values; - } - - @Override - public List values() { - return values; - } - - @Override - public int total() { - return values.size(); - } - - @Override - public boolean hasMore() { - return false; - } - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingTextMessageImpl.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionImpl.java similarity index 56% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingTextMessageImpl.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionImpl.java index 9c9bc987..55306323 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingTextMessageImpl.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2026 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. @@ -13,33 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; -/** - * MCP sampling text content. - */ -final class McpSamplingTextMessageImpl implements McpSamplingTextMessage { - private final String text; - private final McpRole role; +final class McpCompletionImpl implements McpCompletion { + private final McpCompletionConfig config; - McpSamplingTextMessageImpl(String text, McpRole role) { - this.text = text; - this.role = role; + McpCompletionImpl(McpCompletionConfig config) { + this.config = config; } @Override - public McpSamplingMessageType type() { - return McpSamplingMessageType.TEXT; + public String reference() { + return config.reference(); } @Override - public McpRole role() { - return role; + public McpCompletionType referenceType() { + return config.referenceType(); } @Override - public String text() { - return text; + public McpCompletionResult completion(McpCompletionRequest request) { + return config.completion().apply(request); } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionResultBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionResultBlueprint.java new file mode 100644 index 00000000..0221143f --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionResultBlueprint.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.List; +import java.util.Optional; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; + +@Prototype.Blueprint +@Prototype.CustomMethods(McpCompletionSupport.class) +interface McpCompletionResultBlueprint { + /** + * Completion suggestions returned for the current request. + *

+ * The list is capped at 100 elements by the MCP protocol. If you have more than 100 + * suggestions available, return the first 100 items, set {@link #total()} to the total + * number of available suggestions, and set {@link #hasMore()} to {@code true}. + * + * @return completion suggestion values (max 100 items) + */ + @Option.Singular + @Option.Decorator(McpDecorators.CompletionValuesDecorator.class) + List values(); + + /** + * Total number of completion suggestions available. + *

+ * This is typically set when {@link #values()} is truncated to the MCP limit (100 items). + * For example, if you return the first 100 suggestions out of 250 available, set this to 250. + *

+ * If the total number of suggestions is unknown or not applicable, leave this empty. + * + * @return total number of available suggestions, if known + */ + Optional total(); + + /** + * Indicates whether additional completion suggestions exist beyond {@link #values()}. + *

+ * Set this to {@code true} when you have more suggestions than were returned in {@link #values()}. + * This is typically used together with {@link #total()}. + * + * @return {@code true} if more suggestions exist beyond {@link #values()}, {@code false} otherwise + */ + Optional hasMore(); +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionSupport.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionSupport.java new file mode 100644 index 00000000..0c1b762d --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionSupport.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.Arrays; +import java.util.List; + +import io.helidon.builder.api.Prototype; + +final class McpCompletionSupport { + private McpCompletionSupport() { + } + + /** + * Create a {@link io.helidon.extensions.mcp.server.McpCompletionResult} instance + * from the list of suggestion values. + * + * @param values suggestions + * @return completion result + */ + @Prototype.FactoryMethod + static McpCompletionResult create(List values) { + return McpCompletionResult.builder() + .values(values) + .total(values.size()) + .hasMore(false) + .build(); + } + + /** + * Create a {@link io.helidon.extensions.mcp.server.McpCompletionResult} instance + * from the list of suggestion values. + * + * @param values suggestions + * @return completion result + */ + @Prototype.FactoryMethod + static McpCompletionResult create(String... values) { + return McpCompletionResult.builder() + .values(Arrays.asList(values)) + .total(values.length) + .hasMore(false) + .build(); + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpContent.java index 999e99ce..13930128 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpContent.java @@ -13,60 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; /** * General content type for all MCP component contents. */ -public sealed interface McpContent permits McpTextContent, - McpMediaContent, - McpResourceContent, - McpResourceLinkContent, - McpEmbeddedResource { +interface McpContent { /** * Content type. * * @return type */ - ContentType type(); - - /** - * Content types. - */ - enum ContentType { - /** - * Text. - */ - TEXT, - - /** - * Image. - */ - IMAGE, - - /** - * Resource. - */ - RESOURCE, - - /** - * Resource link. - */ - RESOURCE_LINK, - - /** - * Audio. - */ - AUDIO; - - /** - * Return text representation. - * - * @return text representation - */ - public String text() { - return this.name().toLowerCase(); - } - } + McpContentType type(); } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpContentType.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpContentType.java new file mode 100644 index 00000000..3ff1d096 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpContentType.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 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.server; + +/** + * Content types. + */ +public enum McpContentType { + /** + * Text. + */ + TEXT, + + /** + * Image. + */ + IMAGE, + + /** + * Resource. + */ + RESOURCE, + + /** + * Resource link. + */ + RESOURCE_LINK, + + /** + * Audio. + */ + AUDIO; + + /** + * Return text representation. + * + * @return text representation + */ + String text() { + return this.name().toLowerCase(); + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpDecorators.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpDecorators.java index 17d8b049..84db88f3 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpDecorators.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpDecorators.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -16,7 +16,10 @@ package io.helidon.extensions.mcp.server; import java.net.URI; +import java.util.Collection; +import java.util.List; import java.util.Optional; +import java.util.Set; import io.helidon.builder.api.Prototype; @@ -94,6 +97,41 @@ public void decorate(McpRoot.BuilderBase builder, URI uri) { } } + /** + * Number of suggestions must not exceed 100 items. + */ + static class CompletionValuesDecorator implements Prototype.OptionDecorator, String> { + @Override + public void decorate(McpCompletionResult.BuilderBase builder, String value) { + } + + @Override + public void decorateSetList(McpCompletionResult.BuilderBase builder, List values) { + lessThan100Items(values); + } + + @Override + public void decorateAddList(McpCompletionResult.BuilderBase builder, List values) { + lessThan100Items(values); + } + + @Override + public void decorateSetSet(McpCompletionResult.BuilderBase builder, Set values) { + lessThan100Items(values); + } + + @Override + public void decorateAddSet(McpCompletionResult.BuilderBase builder, Set values) { + lessThan100Items(values); + } + + private void lessThan100Items(Collection values) { + if (values.size() > 100) { + throw new IllegalArgumentException("Completion values must be less than 100"); + } + } + } + static boolean isPositiveAndLessThanOne(Double value) { return 0 <= value && value <= 1.0; } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpEmbeddedBinaryResourceContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpEmbeddedBinaryResourceContent.java new file mode 100644 index 00000000..567e9703 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpEmbeddedBinaryResourceContent.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.Base64; + +interface McpEmbeddedBinaryResourceContent extends McpEmbeddedResourceContent { + /** + * Resource content. + * + * @return content + */ + byte[] data(); + + default String base64Data() { + return Base64.getEncoder().encodeToString(data()); + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpEmbeddedResource.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpEmbeddedResourceContent.java similarity index 83% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpEmbeddedResource.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpEmbeddedResourceContent.java index 3857d6c8..2e221014 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpEmbeddedResource.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpEmbeddedResourceContent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -21,7 +21,7 @@ /** * Reference a resource, used by {@link McpTool} and {@link McpPrompt}. */ -sealed interface McpEmbeddedResource extends McpContent permits McpPromptResourceContent, McpToolResourceContent { +interface McpEmbeddedResourceContent extends McpResourceContent { /** * A valid resource URI that points to a resource registered on this MCP server. diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingTextMessage.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpEmbeddedTextResourceContent.java similarity index 77% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingTextMessage.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpEmbeddedTextResourceContent.java index a6c4ff85..6ba8457a 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingTextMessage.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpEmbeddedTextResourceContent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2026 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. @@ -15,10 +15,7 @@ */ package io.helidon.extensions.mcp.server; -/** - * MCP sampling text content. - */ -public sealed interface McpSamplingTextMessage extends McpSamplingMessage permits McpSamplingTextMessageImpl { +interface McpEmbeddedTextResourceContent extends McpEmbeddedResourceContent { /** * Text content as string. * diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpImageContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpImageContent.java index 61aa7ffd..ea1d8d20 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpImageContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpImageContent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -19,5 +19,10 @@ /** * Image content. */ -sealed interface McpImageContent extends McpMediaContent permits McpImageContentImpl { +interface McpImageContent extends McpMediaContent { + + @Override + default McpContentType type() { + return McpContentType.IMAGE; + } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpImageContentImpl.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpImageContentImpl.java index f0d7b8c4..f67567e7 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpImageContentImpl.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpImageContentImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; import java.util.Base64; @@ -46,7 +45,7 @@ public MediaType mediaType() { } @Override - public ContentType type() { - return ContentType.IMAGE; + public McpContentType type() { + return McpContentType.IMAGE; } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializer.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializer.java index f62a25ae..79d1b67b 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializer.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializer.java @@ -160,11 +160,9 @@ static boolean isResponse(JsonObject payload) { return !payload.containsKey("method") && payload.containsKey("id"); } - JsonObjectBuilder toJson(Set capabilities, McpServerConfig config); + JsonObjectBuilder createJsonInitializeResponse(Set capabilities, McpServerConfig config); - JsonObjectBuilder toJson(McpTool tool); - - JsonObjectBuilder toolCall(McpTool tool, McpToolResult result); + // ---------- LIST RESPONSE ---------- JsonObject listResources(McpPage page); @@ -174,7 +172,9 @@ static boolean isResponse(JsonObject payload) { JsonObject listPrompts(McpPage page); - Optional toJson(McpToolResourceContent content); + // ---------- LIST RESPONSE COMPONENT MAPPING ---------- + + JsonObjectBuilder toJson(McpTool tool); JsonObjectBuilder toJson(McpPrompt prompt); @@ -184,51 +184,77 @@ static boolean isResponse(JsonObject payload) { JsonObjectBuilder resourceTemplates(McpResource resource); - JsonObject readResource(String uri, List contents); + // ---------- COMPONENT EXECUTION RESULT ---------- - JsonObject toJson(List contents, String description); + JsonObject toolCall(McpTool tool, McpToolResult result); - Optional toJson(McpPromptContent content); + JsonObject resourceRead(String uri, McpResourceResult result); + + JsonObject promptGet(McpPromptResult result); + + JsonObject completionComplete(McpCompletionResult result); + + // ---------- CONTENTS ---------- Optional toJson(McpContent content); - JsonObjectBuilder toJson(McpSamplingMessage message); + JsonObjectBuilder toJson(McpTextContent content); - JsonObjectBuilder toJson(McpResourceContent content); + JsonObjectBuilder toJson(McpImageContent content); - Optional toJson(McpPromptResourceContent resource); + JsonObjectBuilder toJson(McpEmbeddedTextResourceContent content); - Optional toJson(McpPromptImageContent image); + JsonObjectBuilder toJson(McpEmbeddedBinaryResourceContent content); - Optional toJson(McpPromptTextContent content); + Optional toJson(McpAudioContent content); - Optional toJson(McpPromptAudioContent audio); + // ---------- PROMPT CONTENTS ---------- - JsonObjectBuilder toJson(McpSamplingImageMessage image); + Optional toJson(McpPromptContent content); - JsonObjectBuilder toJson(McpSamplingTextMessage text); + JsonObjectBuilder toJson(McpPromptImageContent image); - JsonObjectBuilder toJson(McpSamplingAudioMessage audio); + JsonObjectBuilder toJson(McpPromptTextResourceContent text); - JsonObjectBuilder toJson(McpTextContent content); + JsonObjectBuilder toJson(McpPromptBinaryResourceContent binary); - JsonObjectBuilder toJson(McpImageContent content); + Optional toJson(McpPromptAudioContent audio); - Optional toJson(McpAudioContent content); + JsonObjectBuilder toJson(McpPromptTextContent content); + + // ---------- RESOURCE CONTENTS ---------- + + Optional toJson(McpResourceContent content); JsonObjectBuilder toJson(McpResourceBinaryContent content); JsonObjectBuilder toJson(McpResourceTextContent content); - JsonObject toJson(McpProgress progress, int newProgress, String message); + // ---------- SAMPLING ---------- + + JsonObjectBuilder toJson(McpSamplingRequest request); + + JsonObjectBuilder toJson(McpSamplingMessage message); + + JsonObjectBuilder toJson(McpSamplingImageMessage image); + + JsonObjectBuilder toJson(McpSamplingTextMessage text); + + JsonObjectBuilder toJson(McpSamplingAudioMessage audio); + + JsonObject createSamplingRequest(long id, McpSamplingRequest request); + + McpSamplingResponse createSamplingResponse(JsonObject object) throws McpSamplingException; + + // ---------- NOTIFICATIONS ---------- + + JsonObject progressNotification(McpProgress progress, int newProgress, String message); JsonObject createLoggingNotification(McpLogger.Level level, String name, String message); JsonObject createUpdateNotification(String uri); - JsonObject toJson(McpCompletionContent content); - - JsonObjectBuilder toJson(McpSamplingRequest request); + // ---------- JSON-RPC ---------- JsonObject createJsonRpcNotification(String method, JsonObjectBuilder params); @@ -240,11 +266,9 @@ static boolean isResponse(JsonObject payload) { JsonObject createJsonRpcResultResponse(long id, JsonValue params); - JsonObject timeoutResponse(long requestId); + JsonObject jsonrpcErrorTimeoutResponse(long requestId); - List parseRoots(JsonObject response); - - JsonObject createSamplingRequest(long id, McpSamplingRequest request); + // ---------- ROOTS ---------- - McpSamplingResponse createSamplingResponse(JsonObject object) throws McpSamplingException; + List parseRoots(JsonObject response); } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializerV1.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializerV1.java index 3e2419d5..3fab4eba 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializerV1.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializerV1.java @@ -56,7 +56,7 @@ class McpJsonSerializerV1 implements McpJsonSerializer { private final ReentrantLock lock = new ReentrantLock(); @Override - public JsonObjectBuilder toJson(Set capabilities, McpServerConfig config) { + public JsonObjectBuilder createJsonInitializeResponse(Set capabilities, McpServerConfig config) { return JSON_BUILDER_FACTORY.createObjectBuilder() .add("protocolVersion", McpProtocolVersion.VERSION_2024_11_05.text()) .add("capabilities", JSON_BUILDER_FACTORY.createObjectBuilder() @@ -108,18 +108,15 @@ public JsonObjectBuilder toJson(McpTool tool) { } @Override - public JsonObjectBuilder toolCall(McpTool tool, McpToolResult result) { + public JsonObject toolCall(McpTool tool, McpToolResult result) { JsonArrayBuilder array = JSON_BUILDER_FACTORY.createArrayBuilder(); - for (McpToolContent content : result.contents()) { - if (content instanceof McpToolResourceContent trc) { - toJson(trc).ifPresent(array::add); - } else { - toJson(content.content()).ifPresent(array::add); - } + for (McpToolContent content : McpToolSupport.aggregateContent(result)) { + toJson(content).ifPresent(array::add); } return JSON_BUILDER_FACTORY.createObjectBuilder() .add("content", array) - .add("isError", result.error()); + .add("isError", result.error()) + .build(); } @Override @@ -179,11 +176,25 @@ public JsonObject listPrompts(McpPage page) { } @Override - public Optional toJson(McpToolResourceContent content) { - return toJson(content.content()) - .map(resource -> JSON_BUILDER_FACTORY.createObjectBuilder() - .add("type", content.type().text()) - .add("resource", resource.add("uri", content.uri().toASCIIString()))); + public JsonObjectBuilder toJson(McpEmbeddedTextResourceContent content) { + var resource = JSON_BUILDER_FACTORY.createObjectBuilder() + .add("uri", content.uri().toASCIIString()) + .add("mimeType", content.mediaType().text()) + .add("text", content.text()); + return JSON_BUILDER_FACTORY.createObjectBuilder() + .add("type", content.type().text()) + .add("resource", resource); + } + + @Override + public JsonObjectBuilder toJson(McpEmbeddedBinaryResourceContent content) { + var resource = JSON_BUILDER_FACTORY.createObjectBuilder() + .add("uri", content.uri().toASCIIString()) + .add("mimeType", content.mediaType().text()) + .add("blob", content.base64Data()); + return JSON_BUILDER_FACTORY.createObjectBuilder() + .add("type", content.type().text()) + .add("resource", resource); } @Override @@ -225,38 +236,38 @@ public JsonObjectBuilder resourceTemplates(McpResource resource) { } @Override - public JsonObject readResource(String uri, List contents) { + public JsonObject resourceRead(String uri, McpResourceResult result) { JsonArrayBuilder array = JSON_BUILDER_FACTORY.createArrayBuilder(); - for (McpResourceContent content : contents) { - JsonObjectBuilder builder = toJson(content); - builder.add("uri", uri); - array.add(builder); + for (McpResourceContent content : McpResourceSupport.aggregateContent(result)) { + toJson(content).map(builder -> builder.add("uri", uri)).ifPresent(array::add); } return JSON_BUILDER_FACTORY.createObjectBuilder().add("contents", array).build(); } @Override - public JsonObject toJson(List contents, String description) { + public JsonObject promptGet(McpPromptResult result) { JsonArrayBuilder array = JSON_BUILDER_FACTORY.createArrayBuilder(); - for (McpPromptContent prompt : contents) { + JsonObjectBuilder object = JSON_BUILDER_FACTORY.createObjectBuilder(); + for (McpPromptContent prompt : McpPromptSupport.aggregateContent(result)) { toJson(prompt).ifPresent(array::add); } - return JSON_BUILDER_FACTORY.createObjectBuilder() - .add("description", description) - .add("messages", array) - .build(); + result.description().ifPresent(description -> object.add("description", description)); + return object.add("messages", array).build(); } @Override public Optional toJson(McpPromptContent content) { + if (content instanceof McpPromptTextContent text) { + return Optional.of(toJson(text)); + } if (content instanceof McpPromptImageContent image) { - return toJson(image); + return Optional.of(toJson(image)); } - if (content instanceof McpPromptTextContent text) { - return toJson(text); + if (content instanceof McpPromptTextResourceContent text) { + return Optional.of(toJson(text)); } - if (content instanceof McpPromptResourceContent resource) { - return toJson(resource); + if (content instanceof McpPromptBinaryResourceContent binary) { + return Optional.of(toJson(binary)); } return Optional.empty(); } @@ -269,70 +280,71 @@ public Optional toJson(McpContent content) { if (content instanceof McpImageContent image) { return Optional.of(toJson(image)); } - if (content instanceof McpResourceContent resource) { - return Optional.of(toJson(resource)); + if (content instanceof McpEmbeddedTextResourceContent text) { + return Optional.of(toJson(text)); + } + if (content instanceof McpEmbeddedBinaryResourceContent binary) { + return Optional.of(toJson(binary)); } return Optional.empty(); } @Override public JsonObjectBuilder toJson(McpSamplingMessage message) { - if (message instanceof McpSamplingTextMessageImpl text) { + if (message instanceof McpSamplingTextMessage text) { return toJson(text); } - if (message instanceof McpSamplingImageMessageImpl image) { + if (message instanceof McpSamplingImageMessage image) { return toJson(image); } - if (message instanceof McpSamplingAudioMessageImpl resource) { + if (message instanceof McpSamplingAudioMessage resource) { return toJson(resource); } throw new IllegalArgumentException("Unsupported content type: " + message.getClass().getName()); } @Override - public JsonObjectBuilder toJson(McpResourceContent content) { + public Optional toJson(McpResourceContent content) { if (content instanceof McpResourceTextContent text) { - return toJson(text); + return Optional.of(toJson(text)); } if (content instanceof McpResourceBinaryContent binary) { - return toJson(binary); + return Optional.of(toJson(binary)); } - throw new IllegalArgumentException("Unsupported content type: " + content.getClass().getName()); + return Optional.empty(); } @Override - public Optional toJson(McpPromptResourceContent resource) { - return toJson(resource.content()) - .map(contentBuilder -> JSON_BUILDER_FACTORY.createObjectBuilder() - .add("role", resource.role().text()) - .add("content", JSON_BUILDER_FACTORY.createObjectBuilder() - .add("type", resource.type().text()) - .add("resource", contentBuilder - .add("uri", resource.uri().toASCIIString())))); + public JsonObjectBuilder toJson(McpPromptImageContent image) { + return JSON_BUILDER_FACTORY.createObjectBuilder() + .add("role", image.role().text()) + .add("content", toJson((McpImageContent) image)); + } + + @Override + public JsonObjectBuilder toJson(McpPromptTextResourceContent text) { + var builder = JSON_BUILDER_FACTORY.createObjectBuilder(); + var content = toJson((McpEmbeddedTextResourceContent) text); + return builder.add("role", text.role().text()).add("content", content); } @Override - public Optional toJson(McpPromptImageContent image) { - return toJson(image.content()) - .map(content -> JSON_BUILDER_FACTORY.createObjectBuilder() - .add("role", image.role().text()) - .add("content", content)); + public JsonObjectBuilder toJson(McpPromptBinaryResourceContent binary) { + var builder = JSON_BUILDER_FACTORY.createObjectBuilder(); + var content = toJson((McpEmbeddedBinaryResourceContent) binary); + return builder.add("role", binary.role().text()).add("content", content); } @Override - public Optional toJson(McpPromptTextContent content) { - return toJson(content.content()) - .map(contentBuilder -> JSON_BUILDER_FACTORY.createObjectBuilder() - .add("role", content.role().text()) - .add("content", contentBuilder)); + public JsonObjectBuilder toJson(McpPromptTextContent content) { + return JSON_BUILDER_FACTORY.createObjectBuilder() + .add("role", content.role().text()) + .add("content", toJson((McpTextContent) content)); } @Override public Optional toJson(McpPromptAudioContent audio) { - return toJson(audio.content()) - .map(content -> JSON_BUILDER_FACTORY.createObjectBuilder() - .add("role", audio.role().text()) - .add("content", content)); + return Optional.empty(); } @Override @@ -387,19 +399,19 @@ public Optional toJson(McpAudioContent content) { @Override public JsonObjectBuilder toJson(McpResourceBinaryContent content) { return JSON_BUILDER_FACTORY.createObjectBuilder() - .add("mimeType", content.mimeType().text()) + .add("mimeType", content.mediaType().text()) .add("blob", content.base64Data()); } @Override public JsonObjectBuilder toJson(McpResourceTextContent content) { return JSON_BUILDER_FACTORY.createObjectBuilder() - .add("mimeType", content.mimeType().text()) + .add("mimeType", content.mediaType().text()) .add("text", content.text()); } @Override - public JsonObject toJson(McpProgress progress, int newProgress, String message) { + public JsonObject progressNotification(McpProgress progress, int newProgress, String message) { JsonObjectBuilder params = JSON_BUILDER_FACTORY.createObjectBuilder() .add("progress", newProgress) .add("total", progress.total()); @@ -430,12 +442,13 @@ public JsonObject createUpdateNotification(String uri) { } @Override - public JsonObject toJson(McpCompletionContent content) { + public JsonObject completionComplete(McpCompletionResult result) { + var builder = JSON_BUILDER_FACTORY.createObjectBuilder() + .add("values", JSON_BUILDER_FACTORY.createArrayBuilder(result.values())); + result.hasMore().ifPresent(hasMore -> builder.add("hasMore", hasMore)); + result.total().ifPresent(total -> builder.add("total", total)); return JSON_BUILDER_FACTORY.createObjectBuilder() - .add("completion", JSON_BUILDER_FACTORY.createObjectBuilder() - .add("values", JSON_BUILDER_FACTORY.createArrayBuilder(content.values())) - .add("total", content.total()) - .add("hasMore", content.hasMore())) + .add("completion", builder) .build(); } @@ -458,7 +471,7 @@ public JsonObjectBuilder toJson(McpSamplingRequest request) { request.intelligencePriority().map(intelligence -> modelPreference.add("intelligencePriority", intelligence)); params.add("modelPreference", modelPreference); - request.messages().stream() + McpSamplingSupport.aggregate(request).stream() .map(this::toJson) .forEach(messages::add); params.add("messages", messages); @@ -522,7 +535,7 @@ public JsonObject createJsonRpcResultResponse(long id, JsonValue params) { } @Override - public JsonObject timeoutResponse(long requestId) { + public JsonObject jsonrpcErrorTimeoutResponse(long requestId) { var error = JSON_BUILDER_FACTORY.createObjectBuilder() .add("code", INTERNAL_ERROR) .add("message", "response timeout"); @@ -593,16 +606,16 @@ McpSamplingMessage parseMessage(McpRole role, JsonObject object) { String type = object.getString("type").toUpperCase(); McpSamplingMessageType messageType = McpSamplingMessageType.valueOf(type); return switch (messageType) { - case TEXT -> new McpSamplingTextMessageImpl(object.getString("text"), role); + case TEXT -> McpSamplingTextMessage.builder().text(object.getString("text")).role(role).build(); case IMAGE -> { byte[] data = object.getString("data").getBytes(StandardCharsets.UTF_8); MediaType mediaType = MediaTypes.create(object.getString("mimeType")); - yield new McpSamplingImageMessageImpl(data, mediaType, role); + yield McpSamplingImageMessage.builder().data(data).mediaType(mediaType).role(role).build(); } case AUDIO -> { byte[] data = object.getString("data").getBytes(StandardCharsets.UTF_8); MediaType mediaType = MediaTypes.create(object.getString("mimeType")); - yield new McpSamplingAudioMessageImpl(data, mediaType, role); + yield McpSamplingAudioMessage.builder().data(data).mediaType(mediaType).role(role).build(); } }; } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializerV2.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializerV2.java index 9446670f..9a326ba9 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializerV2.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializerV2.java @@ -30,8 +30,8 @@ class McpJsonSerializerV2 extends McpJsonSerializerV1 { private static final JsonBuilderFactory JSON_BUILDER_FACTORY = Json.createBuilderFactory(Map.of()); @Override - public JsonObjectBuilder toJson(Set capabilities, McpServerConfig config) { - return super.toJson(capabilities, config) + public JsonObjectBuilder createJsonInitializeResponse(Set capabilities, McpServerConfig config) { + return super.createJsonInitializeResponse(capabilities, config) .add("protocolVersion", McpProtocolVersion.VERSION_2025_03_26.text()); } @@ -59,17 +59,26 @@ public Optional toJson(McpAudioContent content) { .add("mimeType", content.mediaType().text())); } + @Override + public Optional toJson(McpPromptAudioContent audio) { + return toJson((McpAudioContent) audio) + .map(content -> JSON_BUILDER_FACTORY.createObjectBuilder() + .add("role", audio.role().text()) + .add("content", content)); + } + @Override public JsonObjectBuilder toJson(McpTool tool) { var builder = super.toJson(tool); - McpToolAnnotations annotations = tool.annotations(); - JsonObjectBuilder annotBuilder = JSON_BUILDER_FACTORY.createObjectBuilder(); - annotBuilder.add("title", annotations.title()); - annotBuilder.add("destructiveHint", annotations.destructiveHint()); - annotBuilder.add("idempotentHint", annotations.idempotentHint()); - annotBuilder.add("openWorldHint", annotations.openWorldHint()); - annotBuilder.add("readOnlyHint", annotations.readOnlyHint()); - builder.add("annotations", annotBuilder.build()); + tool.annotations().ifPresent(annotations -> { + JsonObjectBuilder annotBuilder = JSON_BUILDER_FACTORY.createObjectBuilder(); + annotBuilder.add("title", annotations.title()); + annotBuilder.add("destructiveHint", annotations.destructiveHint()); + annotBuilder.add("idempotentHint", annotations.idempotentHint()); + annotBuilder.add("openWorldHint", annotations.openWorldHint()); + annotBuilder.add("readOnlyHint", annotations.readOnlyHint()); + builder.add("annotations", annotBuilder.build()); + }); return builder; } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializerV3.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializerV3.java index 3da12fce..5b02ac7d 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializerV3.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpJsonSerializerV3.java @@ -43,13 +43,13 @@ class McpJsonSerializerV3 extends McpJsonSerializerV2 { private final ReentrantLock lock = new ReentrantLock(); @Override - public JsonObjectBuilder toJson(Set capabilities, McpServerConfig config) { - return super.toJson(capabilities, config) + public JsonObjectBuilder createJsonInitializeResponse(Set capabilities, McpServerConfig config) { + return super.createJsonInitializeResponse(capabilities, config) .add("protocolVersion", McpProtocolVersion.VERSION_2025_06_18.text()); } @Override - public JsonObjectBuilder toolCall(McpTool tool, McpToolResult result) { + public JsonObject toolCall(McpTool tool, McpToolResult result) { if (tool.outputSchema().isEmpty() && result.structuredContent().isPresent()) { if (LOGGER.isLoggable(System.Logger.Level.WARNING)) { LOGGER.log(System.Logger.Level.WARNING, "Output schema must be specified for tool '" @@ -57,18 +57,17 @@ public JsonObjectBuilder toolCall(McpTool tool, McpToolResult result) { } } - JsonObjectBuilder builder = super.toolCall(tool, result); + JsonObjectBuilder builder = JSON_BUILDER_FACTORY.createObjectBuilder(super.toolCall(tool, result)); result.structuredContent().ifPresent((content) -> { String json = JSON_B.toJson(content); JsonObject sc = JSON_B.fromJson(json, JsonObject.class); builder.add("structuredContent", sc); - if (result.contents().isEmpty()) { - var text = McpToolContents.textContent(json); - toJson(text.content()) - .ifPresent(it -> builder.add("content", JSON_BUILDER_FACTORY.createArrayBuilder().add(it))); + if (result.textContents().isEmpty()) { + McpToolContent text = McpToolTextContent.builder().text(json).build(); + toJson(text).ifPresent(it -> builder.add("content", JSON_BUILDER_FACTORY.createArrayBuilder().add(it))); } }); - return builder; + return builder.build(); } @Override diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpMediaContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpMediaContent.java index 00577285..1d36aa16 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpMediaContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpMediaContent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -15,9 +15,11 @@ */ package io.helidon.extensions.mcp.server; +import java.util.Base64; + import io.helidon.common.media.type.MediaType; -sealed interface McpMediaContent extends McpContent permits McpImageContent, McpAudioContent { +interface McpMediaContent extends McpContent { /** * Image content data. * @@ -26,17 +28,19 @@ sealed interface McpMediaContent extends McpContent permits McpImageContent, Mcp byte[] data(); /** - * Media content data encoded in base64. + * Image content MIME type. * - * @return content in base64. + * @return MIME type */ - String base64Data(); + MediaType mediaType(); /** - * Image content MIME type. + * Media content data encoded in base64. * - * @return MIME type + * @return content in base64. */ - MediaType mediaType(); + default String base64Data() { + return Base64.getEncoder().encodeToString(data()); + } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpProgress.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpProgress.java index 1d97787c..0c16e110 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpProgress.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpProgress.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; import java.util.Objects; +import io.helidon.common.LazyValue; + /** * Progress notification to the client. */ @@ -67,7 +68,7 @@ public void send(int progress, String message) { return; } if (isSending) { - var notification = session.serializer().toJson(this, progress, message); + var notification = session.serializer().progressNotification(this, progress, message); transport().send(notification); } if (progress >= total) { @@ -102,7 +103,23 @@ void stopSending() { isSending = false; } + /** + * The progress listener look for progress token inside request + * and set it in the associate progress feature instance. + */ static class McpProgressListener implements McpFeatureLifecycle { + /** + * The listener being static and used for every session, a singleton + * avoid creation of unnecessary instance per session. + */ + private static final LazyValue INSTANCE = LazyValue.create(McpProgressListener::new); + + private McpProgressListener() { + } + + static McpProgressListener create() { + return INSTANCE.get(); + } @Override public void beforeRequest(McpParameters parameters, McpFeatures features) { diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPrompt.java similarity index 80% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpPromptBlueprint.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpPrompt.java index 0d4c0d57..39033c4b 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptBlueprint.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpPrompt.java @@ -13,23 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; import java.util.List; import java.util.Optional; -import java.util.function.Function; - -import io.helidon.builder.api.Option; -import io.helidon.builder.api.Prototype; /** - * Configuration of an MCP Prompt. + * MCP Prompt. */ -@Prototype.Blueprint -@Prototype.IncludeDefaultMethods("title") -interface McpPromptBlueprint { - +public interface McpPrompt { /** * Prompt name. * @@ -37,15 +29,6 @@ interface McpPromptBlueprint { */ String name(); - /** - * Human-readable prompt title. - * - * @return the prompt title - */ - default Optional title() { - return Optional.empty(); - } - /** * Prompt description. * @@ -58,13 +41,22 @@ default Optional title() { * * @return {@link List} of arguments */ - @Option.Singular List arguments(); /** * Prompt template processing. * + * @param request prompt request * @return Prompt content */ - Function> prompt(); + McpPromptResult prompt(McpPromptRequest request); + + /** + * Human-readable prompt title. + * + * @return the prompt title + */ + default Optional title() { + return Optional.empty(); + } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptAudioContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptAudioContent.java deleted file mode 100644 index 6496f18c..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptAudioContent.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.server; - -import io.helidon.common.media.type.MediaType; - -/** - * Prompt audio content. - */ -final class McpPromptAudioContent implements McpPromptContent { - private final McpRole role; - private final McpAudioContent audio; - - McpPromptAudioContent(byte[] data, MediaType type, McpRole role) { - this.role = role; - this.audio = new McpAudioContentImpl(data, type); - } - - @Override - public McpRole role() { - return role; - } - - @Override - public McpContent content() { - return audio; - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingImageMessage.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptAudioContentBlueprint.java similarity index 69% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingImageMessage.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpPromptAudioContentBlueprint.java index 635dd184..8bd6d9b0 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingImageMessage.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptAudioContentBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -15,10 +15,11 @@ */ package io.helidon.extensions.mcp.server; +import io.helidon.builder.api.Prototype; + /** - * MCP sampling image content. + * Prompt audio content. */ -public sealed interface McpSamplingImageMessage extends McpSamplingMessage, - McpSamplingMediaMessage permits McpSamplingImageMessageImpl { - +@Prototype.Blueprint +interface McpPromptAudioContentBlueprint extends McpAudioContent, McpPromptContent { } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolResourceLinkContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptBinaryResourceContentBlueprint.java similarity index 70% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpToolResourceLinkContent.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpPromptBinaryResourceContentBlueprint.java index ed4e209f..e8e75406 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolResourceLinkContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptBinaryResourceContentBlueprint.java @@ -15,15 +15,11 @@ */ package io.helidon.extensions.mcp.server; -final class McpToolResourceLinkContent implements McpToolContent { - private final McpResourceLinkContent link; +import io.helidon.builder.api.Prototype; - McpToolResourceLinkContent(McpResourceLinkContent link) { - this.link = link; - } - - @Override - public McpContent content() { - return link; - } +/** + * Prompt binary resource content. + */ +@Prototype.Blueprint +interface McpPromptBinaryResourceContentBlueprint extends McpEmbeddedBinaryResourceContent, McpPromptContent { } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptConfigBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptConfigBlueprint.java new file mode 100644 index 00000000..5e236a4d --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptConfigBlueprint.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; + +/** + * Prompt configuration. + */ +@Prototype.Blueprint +interface McpPromptConfigBlueprint { + /** + * Prompt name. + * + * @return name + */ + String name(); + + /** + * Prompt description. + * + * @return description + */ + String description(); + + /** + * A {@link java.util.List} of prompt arguments. + * + * @return {@link java.util.List} of arguments + */ + @Option.Singular + List arguments(); + + /** + * Prompt template processing. + * + * @return Prompt content + */ + Function prompt(); + + /** + * Human-readable prompt title. + * + * @return the prompt title + */ + Optional title(); +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptContent.java index 8a3955f7..bd9df40c 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptContent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -19,21 +19,11 @@ /** * Prompt contents that can be returned by the prompt execution. */ -public sealed interface McpPromptContent permits McpPromptImageContent, - McpPromptResourceContent, - McpPromptTextContent, - McpPromptAudioContent { +interface McpPromptContent extends McpContent { /** * Prompt role. * * @return role */ McpRole role(); - - /** - * Prompt content. - * - * @return content - */ - McpContent content(); } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptContents.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptContents.java deleted file mode 100644 index 0fa42016..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptContents.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.server; - -import java.net.URI; -import java.util.Objects; - -import io.helidon.common.media.type.MediaType; - -/** - * {@link McpPromptContent} factory. - */ -public final class McpPromptContents { - private McpPromptContents() { - } - - /** - * Create a prompt text content. - * - * @param prompt content - * @param role role - * @return text prompt content - */ - public static McpPromptContent textContent(String prompt, McpRole role) { - Objects.requireNonNull(prompt, "Prompt text content must not be null"); - Objects.requireNonNull(role, "Prompt Role must not be null"); - return new McpPromptTextContent(prompt, role); - } - - /** - * Create a prompt image content. - * - * @param data content - * @param type media type - * @param role role - * @return image prompt content instance - */ - public static McpPromptContent imageContent(byte[] data, MediaType type, McpRole role) { - Objects.requireNonNull(data, "Prompt image data must not be null"); - Objects.requireNonNull(type, "Prompt image MIME type must not be null"); - Objects.requireNonNull(role, "Prompt role must not be null"); - return new McpPromptImageContent(data, type, role); - } - - /** - * Create a prompt resource content. - * - * @param uri resource uri - * @param role role - * @param content resource content - * @return prompt resource content instance - */ - public static McpPromptContent resourceContent(URI uri, McpResourceContent content, McpRole role) { - Objects.requireNonNull(role, "Prompt role must not be null"); - Objects.requireNonNull(uri, "Prompt resource URI must not be null"); - Objects.requireNonNull(content, "Prompt resource content must not be null"); - return new McpPromptResourceContent(uri, content, role); - } - - /** - * Create a prompt audio content. - * - * @param data content - * @param type media type - * @param role role - * @return image prompt content instance - */ - public static McpPromptContent audioContent(byte[] data, MediaType type, McpRole role) { - Objects.requireNonNull(data, "Prompt audio data must not be null"); - Objects.requireNonNull(type, "Prompt audio MIME type must not be null"); - Objects.requireNonNull(role, "Prompt role must not be null"); - return new McpPromptAudioContent(data, type, role); - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptImageContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptImageContent.java deleted file mode 100644 index 4e8ee181..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptImageContent.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.server; - -import io.helidon.common.media.type.MediaType; - -/** - * Prompt image content. - */ -final class McpPromptImageContent implements McpPromptContent { - private final McpRole role; - private final McpImageContent image; - - McpPromptImageContent(byte[] data, MediaType type, McpRole role) { - this.role = role; - this.image = new McpImageContentImpl(data, type); - } - - @Override - public McpRole role() { - return role; - } - - @Override - public McpContent content() { - return image; - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingAudioMessage.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptImageContentBlueprint.java similarity index 69% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingAudioMessage.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpPromptImageContentBlueprint.java index fc92bba4..649ca258 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingAudioMessage.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptImageContentBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -15,9 +15,11 @@ */ package io.helidon.extensions.mcp.server; +import io.helidon.builder.api.Prototype; + /** - * MCP sampling audio content. + * Prompt image content. */ -public sealed interface McpSamplingAudioMessage extends McpSamplingMessage, - McpSamplingMediaMessage permits McpSamplingAudioMessageImpl { +@Prototype.Blueprint +interface McpPromptImageContentBlueprint extends McpImageContent, McpPromptContent { } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptResourceContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptImpl.java similarity index 50% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpPromptResourceContent.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpPromptImpl.java index 84ae141d..eafce71e 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptResourceContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2026 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. @@ -13,39 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; -import java.net.URI; +import java.util.List; +import java.util.Optional; + +final class McpPromptImpl implements McpPrompt { + private final McpPromptConfig config; -final class McpPromptResourceContent implements McpPromptContent, McpEmbeddedResource { - private final McpRole role; - private final URI uri; - private final McpResourceContent content; + McpPromptImpl(McpPromptConfig config) { + this.config = config; + } - McpPromptResourceContent(URI uri, McpResourceContent content, McpRole role) { - this.uri = uri; - this.role = role; - this.content = content; + @Override + public String name() { + return config.name(); } @Override - public URI uri() { - return uri; + public String description() { + return config.description(); } @Override - public McpRole role() { - return role; + public List arguments() { + return config.arguments(); } @Override - public McpContent content() { - return content; + public McpPromptResult prompt(McpPromptRequest request) { + return config.prompt().apply(request); } @Override - public ContentType type() { - return ContentType.RESOURCE; + public Optional title() { + return config.title(); } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptRequest.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptRequest.java new file mode 100644 index 00000000..e8eae3b0 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptRequest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 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.server; + +/** + * MCP prompt request. + */ +public sealed interface McpPromptRequest extends McpRequest permits McpPromptRequestImpl { + /** + * Get prompt request arguments. + * + * @return arguments + */ + McpParameters arguments(); + + /** + * Get prompt request name. + * + * @return name + * @throws java.lang.IllegalStateException if name is missing from the request + */ + String name(); +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptRequestImpl.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptRequestImpl.java new file mode 100644 index 00000000..b52dbcc4 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptRequestImpl.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026 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.server; + +import io.helidon.common.context.Context; + +final class McpPromptRequestImpl implements McpPromptRequest { + private final McpRequest request; + + McpPromptRequestImpl(McpRequest request) { + this.request = request; + } + + @Override + public McpParameters arguments() { + return request.parameters().get("arguments"); + } + + @Override + public String name() { + return request.parameters() + .get("name") + .asString() + .orElseThrow(() -> new IllegalStateException("Prompt request name is missing")); + } + + @Override + public McpParameters parameters() { + return request.parameters(); + } + + @Override + public McpParameters meta() { + return request.meta(); + } + + @Override + public McpFeatures features() { + return request.features(); + } + + @Override + public String protocolVersion() { + return request.protocolVersion(); + } + + @Override + public Context sessionContext() { + return request.sessionContext(); + } + + @Override + public Context requestContext() { + return request.requestContext(); + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptResultBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptResultBlueprint.java new file mode 100644 index 00000000..bd99b8c8 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptResultBlueprint.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.List; +import java.util.Optional; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; + +/** + * MCP prompt result. + */ +@Prototype.Blueprint +@Prototype.CustomMethods(McpPromptSupport.class) +interface McpPromptResultBlueprint { + /** + * Prompt text content. + * + * @return list of text content + */ + @Option.Singular + List textContents(); + + /** + * Prompt image content. + * + * @return list of image content + */ + @Option.Singular + List imageContents(); + + /** + * Prompt audio content. + * + * @return list of audio content + */ + @Option.Singular + List audioContents(); + + /** + * Prompt embedded text resource content. + * + * @return list of embedded text resource content + */ + @Option.Singular + List textResourceContents(); + + /** + * Prompt embedded binary resource content. + * + * @return list of embedded binary resource content + */ + @Option.Singular + List binaryResourceContents(); + + /** + * Prompt result description. + * + * @return description + */ + Optional description(); +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptSupport.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptSupport.java new file mode 100644 index 00000000..c4d758ec --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptSupport.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import io.helidon.builder.api.Prototype; +import io.helidon.common.media.type.MediaType; + +class McpPromptSupport { + private McpPromptSupport() { + } + + /** + * Create a {@link io.helidon.extensions.mcp.server.McpPromptResult} instance + * with a prompt text content based on provided text. + * + * @param text text content + * @return a prompt result instance + */ + @Prototype.FactoryMethod + static McpPromptResult create(String text) { + return McpPromptResult.builder().addTextContent(text).build(); + } + + /** + * Add a prompt text content to the prompt result from the provided text. + * + * @param builder prompt result builder + * @param text text content + */ + @Prototype.BuilderMethod + static void addTextContent(McpPromptResult.BuilderBase builder, String text) { + Objects.requireNonNull(text, "text is null"); + builder.addTextContent(b -> b.text(text).role(McpRole.USER)); + } + + /** + * Add a prompt image content to the prompt result from the provided data and media type. + * + * @param builder prompt result builder + * @param data prompt image data + * @param type prompt image media type + */ + @Prototype.BuilderMethod + static void addImageContent(McpPromptResult.BuilderBase builder, byte[] data, MediaType type) { + Objects.requireNonNull(data, "data is null"); + Objects.requireNonNull(type, "media type is null"); + builder.addImageContent(image -> image.data(data).mediaType(type).role(McpRole.USER)); + } + + /** + * Add a prompt audio content to the prompt result from the provided data and media type. + * + * @param builder prompt result builder + * @param data prompt audio data + * @param type prompt audio media type + */ + @Prototype.BuilderMethod + static void addAudioContent(McpPromptResult.BuilderBase builder, byte[] data, MediaType type) { + Objects.requireNonNull(data, "data is null"); + Objects.requireNonNull(type, "media type is null"); + builder.addAudioContent(audio -> audio.data(data).mediaType(type).role(McpRole.USER)); + } + + /** + * Aggregate contents of a prompt result into a single list. + * + * @param result prompt result + * @return list of prompt content + */ + static List aggregateContent(McpPromptResult result) { + List contents = new LinkedList<>(); + contents.addAll(result.textContents()); + contents.addAll(result.imageContents()); + contents.addAll(result.audioContents()); + contents.addAll(result.textResourceContents()); + contents.addAll(result.binaryResourceContents()); + return contents; + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptTextContentBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptTextContentBlueprint.java new file mode 100644 index 00000000..9c219fb3 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptTextContentBlueprint.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 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.server; + +import io.helidon.builder.api.Prototype; + +/** + * Prompt text content. + */ +@Prototype.Blueprint +interface McpPromptTextContentBlueprint extends McpTextContent, McpPromptContent { +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptTextResourceContentBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptTextResourceContentBlueprint.java new file mode 100644 index 00000000..86baf395 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptTextResourceContentBlueprint.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 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.server; + +import io.helidon.builder.api.Prototype; + +/** + * Prompt text resource content. + */ +@Prototype.Blueprint +interface McpPromptTextResourceContentBlueprint extends McpEmbeddedTextResourceContent, McpPromptContent { +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResource.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResource.java new file mode 100644 index 00000000..a10c22e2 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResource.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025, 2026 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.server; + +import java.util.Optional; + +import io.helidon.common.media.type.MediaType; + +/** + * MCP Resource. + */ +public interface McpResource { + /** + * Resource URI. + * + * @return uri + */ + String uri(); + + /** + * Resource name. + * + * @return name + */ + String name(); + + /** + * Resource description. + * + * @return description + */ + String description(); + + /** + * Resource mime type. + * + * @return type + */ + MediaType mediaType(); + + /** + * Resource reader. + * + * @param request resource read request + * @return result + */ + McpResourceResult resource(McpResourceRequest request); + + /** + * Human-readable resource title. + * + * @return the resource title + */ + default Optional title() { + return Optional.empty(); + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceBinaryContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceBinaryContent.java deleted file mode 100644 index caa3f914..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceBinaryContent.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.server; - -import java.util.Base64; - -import io.helidon.common.media.type.MediaType; - -final class McpResourceBinaryContent implements McpResourceContent { - private final MediaType type; - private final byte[] data; - - McpResourceBinaryContent(MediaType type, byte[] data) { - this.type = type; - this.data = data; - } - - @Override - public byte[] data() { - return data; - } - - @Override - public MediaType mimeType() { - return type; - } - - @Override - public ContentType type() { - return ContentType.RESOURCE; - } - - public String base64Data() { - return Base64.getEncoder().encodeToString(data); - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceBinaryContentBlueprint.java similarity index 56% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionContent.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpResourceBinaryContentBlueprint.java index 61fce548..84a97f6f 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpCompletionContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceBinaryContentBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -13,34 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; -import java.util.List; +import java.util.Base64; + +import io.helidon.builder.api.Prototype; /** - * Completion result content. + * Resource binary content. */ -public sealed interface McpCompletionContent permits McpCompletionContents.McpCompletionContentImpl { - - /** - * List of completion values. - * - * @return values - */ - List values(); +@Prototype.Blueprint +interface McpResourceBinaryContentBlueprint extends McpResourceContent { /** - * Total number of values. + * Resource content. * - * @return total + * @return content */ - int total(); + byte[] data(); /** - * Whether there is more values. + * Resource content data base64 encoded. * - * @return {code true} if there is more values, {code false} otherwise + * @return encoded data */ - boolean hasMore(); + default String base64Data() { + return Base64.getEncoder().encodeToString(data()); + } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceConfigBlueprint.java similarity index 81% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpResourceBlueprint.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpResourceConfigBlueprint.java index 6cd7926f..e8b63721 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceBlueprint.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, 2026 Oracle and/or its affiliates. + * Copyright (c) 2026 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. @@ -13,10 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; -import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -25,12 +23,10 @@ import io.helidon.common.media.type.MediaType; /** - * Configuration of an MCP Resource. + * Resource configuration. */ @Prototype.Blueprint -@Prototype.IncludeDefaultMethods("title") -interface McpResourceBlueprint { - +interface McpResourceConfigBlueprint { /** * Resource URI. * @@ -45,15 +41,6 @@ interface McpResourceBlueprint { */ String name(); - /** - * Human-readable resource title. - * - * @return the resource title - */ - default Optional title() { - return Optional.empty(); - } - /** * Resource description. * @@ -74,5 +61,12 @@ default Optional title() { * * @return resource content as a {@link McpResourceContent} */ - Function> resource(); + Function resource(); + + /** + * Human-readable resource title. + * + * @return the resource title + */ + Optional title(); } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceContent.java index 87d3c6d5..cd5feb50 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceContent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; import io.helidon.common.media.type.MediaType; @@ -21,18 +20,17 @@ /** * Resource contents that can be returned during resource reading. */ -public sealed interface McpResourceContent extends McpContent permits McpResourceBinaryContent, McpResourceTextContent { - /** - * Resource content. - * - * @return content - */ - byte[] data(); +interface McpResourceContent extends McpContent { /** * Resource content MIME type. * * @return MIME type */ - MediaType mimeType(); + MediaType mediaType(); + + @Override + default McpContentType type() { + return McpContentType.RESOURCE; + } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceContents.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceContents.java deleted file mode 100644 index 0cd1a597..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceContents.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.server; - -import java.util.Objects; - -import io.helidon.common.media.type.MediaType; - -/** - * {@link McpResourceContent} factory. - */ -public final class McpResourceContents { - private McpResourceContents() { - } - - /** - * Create a new resource text content. - * - * @param text text - * @return text content - */ - public static McpResourceContent textContent(String text) { - Objects.requireNonNull(text, "Resource text content must not be null"); - return new McpResourceTextContent(text); - } - - /** - * Create a new resource binary content. - * - * @param data binary data - * @param type media type - * @return binary content - */ - public static McpResourceContent binaryContent(byte[] data, MediaType type) { - Objects.requireNonNull(data, "Resource data must not be null"); - Objects.requireNonNull(type, "Resource MIME type must not be null"); - return new McpResourceBinaryContent(type, data); - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceImpl.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceImpl.java new file mode 100644 index 00000000..59aaaa72 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceImpl.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.Optional; + +import io.helidon.common.media.type.MediaType; + +final class McpResourceImpl implements McpResource { + private final McpResourceConfig config; + + McpResourceImpl(McpResourceConfig config) { + this.config = config; + } + + @Override + public String uri() { + return config.uri(); + } + + @Override + public String name() { + return config.name(); + } + + @Override + public String description() { + return config.description(); + } + + @Override + public MediaType mediaType() { + return config.mediaType(); + } + + @Override + public McpResourceResult resource(McpResourceRequest request) { + return config.resource().apply(request); + } + + @Override + public Optional title() { + return config.title(); + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceLinkContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceLinkContent.java index 773b7ace..4c66fe77 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceLinkContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceLinkContent.java @@ -22,7 +22,7 @@ /** * MCP resource link content. */ -public sealed interface McpResourceLinkContent extends McpContent permits McpResourceLinkContentImpl { +interface McpResourceLinkContent extends McpContent { /** * Resource URI. * @@ -65,153 +65,8 @@ public sealed interface McpResourceLinkContent extends McpContent permits McpRes */ Optional size(); - /** - * Create an MCP resource link content builder instance. - * - * @return resource link content builder instance - */ - static McpResourceLinkContent.Builder builder() { - return new McpResourceLinkContent.Builder(); - } - - /** - * MCP resource link content builder. - */ - final class Builder { - private Long size; - private String uri; - private String name; - private String title; - private String description; - private MediaType mediaType; - - /** - * Set resource link uri. - * - * @param uri the uri - * @return this builder - */ - public Builder uri(String uri) { - this.uri = uri; - return this; - } - - /** - * Set resource link name. - * - * @param name the name - * @return this builder - */ - public Builder name(String name) { - this.name = name; - return this; - } - - /** - * Set resource link size. - * - * @param size the size - * @return this builder - */ - public Builder size(long size) { - this.size = size; - return this; - } - - /** - * Set resource link title. - * - * @param title the title - * @return this builder - */ - public Builder title(String title) { - this.title = title; - return this; - } - - /** - * Set resource link description. - * - * @param description the description - * @return this builder - */ - public Builder description(String description) { - this.description = description; - return this; - } - - /** - * Set resource link media type. - * - * @param mediaType the media type - * @return this builder - */ - public Builder mediaType(MediaType mediaType) { - this.mediaType = mediaType; - return this; - } - - /** - * Get resource link uri. - * - * @return the uri - */ - public String uri() { - return uri; - } - - /** - * Get resource link name. - * - * @return the name - */ - public String name() { - return name; - } - - /** - * Get resource link size. - * - * @return the size - */ - public Long size() { - return size; - } - - /** - * Get resource link title. - * - * @return the title - */ - public String title() { - return title; - } - - /** - * Get resource link description. - * - * @return the description - */ - public String description() { - return description; - } - - /** - * Get resource link media type. - * - * @return the media type - */ - public MediaType mediaType() { - return mediaType; - } - - /** - * Build an instance of {@link io.helidon.extensions.mcp.server.McpResourceLinkContent}. - * - * @return the instance - */ - public McpResourceLinkContent build() { - return new McpResourceLinkContentImpl(this); - } + @Override + default McpContentType type() { + return McpContentType.RESOURCE_LINK; } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceLinkContentImpl.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceLinkContentImpl.java deleted file mode 100644 index 4888a0cd..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceLinkContentImpl.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2026 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.server; - -import java.util.Optional; - -import io.helidon.common.media.type.MediaType; - -final class McpResourceLinkContentImpl implements McpResourceLinkContent { - private final Long size; - private final String uri; - private final String name; - private final String title; - private final String description; - private final MediaType mediaType; - - McpResourceLinkContentImpl(Builder builder) { - this.uri = builder.uri(); - this.name = builder.name(); - this.size = builder.size(); - this.title = builder.title(); - this.mediaType = builder.mediaType(); - this.description = builder.description(); - } - - @Override - public ContentType type() { - return ContentType.RESOURCE_LINK; - } - - @Override - public String uri() { - return uri; - } - - @Override - public String name() { - return name; - } - - @Override - public Optional size() { - return Optional.ofNullable(size); - } - - @Override - public Optional title() { - return Optional.ofNullable(title); - } - - @Override - public Optional description() { - return Optional.ofNullable(description); - } - - @Override - public Optional mediaType() { - return Optional.ofNullable(mediaType); - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceRequest.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceRequest.java new file mode 100644 index 00000000..babf7157 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceRequest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 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.server; + +import java.net.URI; + +/** + * Resource read request. + */ +public sealed interface McpResourceRequest extends McpRequest permits McpResourceRequestImpl { + /** + * Resource URI. + * + * @return uri + */ + URI uri(); +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceRequestImpl.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceRequestImpl.java new file mode 100644 index 00000000..109a335a --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceRequestImpl.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 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.server; + +import java.net.URI; + +import io.helidon.common.context.Context; + +final class McpResourceRequestImpl implements McpResourceRequest { + private final McpRequest request; + + McpResourceRequestImpl(McpRequest request) { + this.request = request; + } + + @Override + public McpParameters parameters() { + return request.parameters(); + } + + @Override + public McpParameters meta() { + return request.meta(); + } + + @Override + public McpFeatures features() { + return request.features(); + } + + @Override + public String protocolVersion() { + return request.protocolVersion(); + } + + @Override + public Context sessionContext() { + return request.sessionContext(); + } + + @Override + public Context requestContext() { + return request.requestContext(); + } + + @Override + public URI uri() { + return request.parameters() + .get("uri") + .asString() + .map(URI::create) + .orElseThrow(() -> new McpInternalException("'uri' parameter is missing from read request")); + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceResultBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceResultBlueprint.java new file mode 100644 index 00000000..a5ccb506 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceResultBlueprint.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.List; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; + +/** + * Read resource result. + */ +@Prototype.Blueprint +@Prototype.CustomMethods(McpResourceSupport.class) +interface McpResourceResultBlueprint { + /** + * Resource text content. + * + * @return list of resource text content + */ + @Option.Singular + List textContents(); + + /** + * Resource binary content. + * + * @return list of resource binary content + */ + @Option.Singular + List binaryContents(); +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSubscriber.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSubscriber.java new file mode 100644 index 00000000..6e20ce98 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSubscriber.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; + +/** + * Resource subscriber. + */ +@RuntimeType.PrototypedBy(McpResourceSubscriberConfig.class) +public interface McpResourceSubscriber extends RuntimeType.Api { + /** + * Create a resource subscriber configuration builder. + * + * @return builder + */ + static McpResourceSubscriberConfig.Builder builder() { + return McpResourceSubscriberConfig.builder(); + } + + /** + * Create a resource subscriber from its configuration. + * + * @param configuration resource subscriber configuration + * @return tool instance + */ + static McpResourceSubscriber create(McpResourceSubscriberConfig configuration) { + return new McpResourceSubscriberImpl(configuration); + } + + /** + * Create a resource subscriber from its configuration builder. + * + * @param consumer resource subscriber configuration + * @return resource subscriber instance + */ + static McpResourceSubscriber create(Consumer consumer) { + return builder().update(consumer).build(); + } + + /** + * Resource URI. + * + * @return uri + */ + String uri(); + + /** + * Resource subscriber. + * + * @param request subscribe request + */ + void subscribe(McpSubscribeRequest request); + + @Override + default McpResourceSubscriberConfig prototype() { + return McpResourceSubscriberConfig.builder() + .uri(uri()) + .subscribe(this::subscribe) + .buildPrototype(); + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSubscriberBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSubscriberConfigBlueprint.java similarity index 81% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSubscriberBlueprint.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSubscriberConfigBlueprint.java index e8c95d23..8e3e62c1 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSubscriberBlueprint.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSubscriberConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; import java.util.function.Consumer; @@ -24,8 +23,7 @@ * Configuration of an MCP Resource Subscriber. */ @Prototype.Blueprint -interface McpResourceSubscriberBlueprint { - +interface McpResourceSubscriberConfigBlueprint extends Prototype.Factory { /** * Resource URI. * @@ -36,5 +34,5 @@ interface McpResourceSubscriberBlueprint { /** * Resource subscriber. */ - Consumer subscribe(); + Consumer subscribe(); } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptTextContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSubscriberImpl.java similarity index 55% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpPromptTextContent.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSubscriberImpl.java index 8f746301..b4328204 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpPromptTextContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSubscriberImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2026 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. @@ -13,25 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; -final class McpPromptTextContent implements McpPromptContent { - private final McpRole role; - private final McpTextContent text; +class McpResourceSubscriberImpl implements McpResourceSubscriber { + private final McpResourceSubscriberConfig config; + + McpResourceSubscriberImpl(McpResourceSubscriberConfig config) { + this.config = config; + } - McpPromptTextContent(String text, McpRole role) { - this.role = role; - this.text = new McpTextContent.McpTextContentImpl(text); + @Override + public String uri() { + return config.uri(); } @Override - public McpRole role() { - return role; + public void subscribe(McpSubscribeRequest request) { + config.subscribe().accept(request); } @Override - public McpContent content() { - return text; + public McpResourceSubscriberConfig prototype() { + return config; } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSupport.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSupport.java new file mode 100644 index 00000000..f1fedf95 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceSupport.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import io.helidon.builder.api.Prototype; +import io.helidon.common.media.type.MediaType; + +class McpResourceSupport { + private McpResourceSupport() { + } + + /** + * Create a {@link io.helidon.extensions.mcp.server.McpResourceResult} instance + * with a resource text content based on provided text. + * + * @param text text content + * @return a resource result instance + */ + @Prototype.FactoryMethod + static McpResourceResult create(String text) { + return McpResourceResult.builder().addTextContent(text).build(); + } + + /** + * Add a resource text content to the resource result from the provided text. + * + * @param builder resource builder result + * @param text text content + */ + @Prototype.BuilderMethod + static void addTextContent(McpResourceResult.BuilderBase builder, String text) { + Objects.requireNonNull(text, "text is null"); + builder.addTextContent(b -> b.text(text)); + } + + /** + * Add a resource binary content to the resource result from the provided data and media type. + * + * @param builder resource builder result + * @param data resource binary data + * @param type resource media type + */ + @Prototype.BuilderMethod + static void addBinaryContent(McpResourceResult.BuilderBase builder, byte[] data, MediaType type) { + Objects.requireNonNull(data, "data is null"); + Objects.requireNonNull(type, "media type is null"); + builder.addBinaryContent(binary -> binary.data(data).mediaType(type)); + } + + /** + * Aggregate contents of a resource result into a single list. + * + * @param result resource result + * @return list of resource content + */ + static List aggregateContent(McpResourceResult result) { + List contents = new LinkedList<>(); + contents.addAll(result.textContents()); + contents.addAll(result.binaryContents()); + return contents; + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceTemplate.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceTemplate.java index 35647f8a..d51866b0 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceTemplate.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceTemplate.java @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import io.helidon.common.LazyValue; import io.helidon.common.media.type.MediaType; import io.helidon.jsonrpc.core.JsonRpcParams; @@ -31,14 +31,14 @@ import static io.helidon.extensions.mcp.server.McpJsonSerializer.JSON_BUILDER_FACTORY; class McpResourceTemplate implements McpResource { - private final Pattern pattern; private final McpResource delegate; private final List variables; + private final LazyValue pattern; McpResourceTemplate(McpResource resource) { this.delegate = resource; this.variables = new ArrayList<>(); - this.pattern = createPattern(resource.uri()); + this.pattern = LazyValue.create(() -> createPattern(resource.uri())); } @Override @@ -62,17 +62,22 @@ public MediaType mediaType() { } @Override - public Function> resource() { - return delegate.resource(); + public McpResourceResult resource(McpResourceRequest request) { + return delegate.resource(request); + } + + @Override + public Optional title() { + return delegate.title(); } boolean matches(String uri) { - return pattern.matcher(uri).matches(); + return pattern.get().matcher(uri).matches(); } McpParameters parameters(JsonRpcParams params, String uri) { JsonObjectBuilder builder = JSON_BUILDER_FACTORY.createObjectBuilder(); - Matcher matcher = pattern.matcher(uri); + Matcher matcher = pattern.get().matcher(uri); if (matcher.matches()) { for (int i = 0; i < matcher.groupCount(); i++) { builder.add(variables.get(i), matcher.group(i + 1)); diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceTextContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceTextContentBlueprint.java similarity index 59% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpResourceTextContent.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpResourceTextContentBlueprint.java index 8c66ae48..1e5a4f4e 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceTextContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceTextContentBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -13,38 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; -import java.nio.charset.StandardCharsets; - +import io.helidon.builder.api.Prototype; import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; -final class McpResourceTextContent implements McpResourceContent { - - private final String text; - - McpResourceTextContent(String text) { - this.text = text; - } - - @Override - public byte[] data() { - return text.getBytes(StandardCharsets.UTF_8); - } +/** + * Resource text content. + */ +@Prototype.Blueprint +@Prototype.IncludeDefaultMethods({"mimeType"}) +interface McpResourceTextContentBlueprint extends McpTextContent, McpResourceContent { @Override - public MediaType mimeType() { + default MediaType mediaType() { return MediaTypes.TEXT_PLAIN; } @Override - public ContentType type() { - return ContentType.RESOURCE; - } - - public String text() { - return text; + default McpContentType type() { + return McpContentType.RESOURCE; } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceUnsubscriber.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceUnsubscriber.java new file mode 100644 index 00000000..a55567a0 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceUnsubscriber.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025, 2026 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.server; + +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; + +/** + * MCP Resource Unsubscriber. + */ +@RuntimeType.PrototypedBy(McpResourceUnsubscriberConfig.class) +public interface McpResourceUnsubscriber extends RuntimeType.Api { + /** + * Create a resource unsubscriber configuration builder. + * + * @return builder + */ + static McpResourceUnsubscriberConfig.Builder builder() { + return McpResourceUnsubscriberConfig.builder(); + } + + /** + * Create a resource unsubscriber from its configuration. + * + * @param configuration resource unsubscriber configuration + * @return resource unsubscriber instance + */ + static McpResourceUnsubscriber create(McpResourceUnsubscriberConfig configuration) { + return new McpResourceUnsubscriberImpl(configuration); + } + + /** + * Create a resource unsubscriber from its configuration builder. + * + * @param consumer resource unsubscriber configuration + * @return resource unsubscriber instance + */ + static McpResourceUnsubscriber create(Consumer consumer) { + return builder().update(consumer).build(); + } + + /** + * Resource URI. + * + * @return uri + */ + String uri(); + + /** + * Resource unsubscriber. + * + * @param request unsubscribe request + */ + void unsubscribe(McpUnsubscribeRequest request); + + @Override + default McpResourceUnsubscriberConfig prototype() { + return McpResourceUnsubscriberConfig.builder() + .uri(uri()) + .unsubscribe(this::unsubscribe) + .buildPrototype(); + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceUnsubscriberBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceUnsubscriberConfigBlueprint.java similarity index 81% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpResourceUnsubscriberBlueprint.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpResourceUnsubscriberConfigBlueprint.java index a9818682..a733e638 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceUnsubscriberBlueprint.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceUnsubscriberConfigBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2026 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; import java.util.function.Consumer; @@ -24,8 +23,7 @@ * Configuration of an MCP Resource Unsubscriber. */ @Prototype.Blueprint -interface McpResourceUnsubscriberBlueprint { - +interface McpResourceUnsubscriberConfigBlueprint extends Prototype.Factory { /** * Resource URI. * @@ -36,5 +34,5 @@ interface McpResourceUnsubscriberBlueprint { /** * Resource unsubscriber. */ - Consumer unsubscribe(); + Consumer unsubscribe(); } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolResourceContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceUnsubscriberImpl.java similarity index 54% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpToolResourceContent.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpResourceUnsubscriberImpl.java index 04752169..0d7b5987 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolResourceContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpResourceUnsubscriberImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2026 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. @@ -13,35 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; -import java.net.URI; - -/** - * Tool resource content. - */ -final class McpToolResourceContent implements McpToolContent, McpEmbeddedResource { - private final URI uri; - private final McpResourceContent content; +class McpResourceUnsubscriberImpl implements McpResourceUnsubscriber { + private final McpResourceUnsubscriberConfig config; - McpToolResourceContent(URI uri, McpResourceContent content) { - this.uri = uri; - this.content = content; + McpResourceUnsubscriberImpl(McpResourceUnsubscriberConfig configuration) { + this.config = configuration; } @Override - public URI uri() { - return this.uri; + public String uri() { + return config.uri(); } @Override - public McpContent content() { - return content; + public void unsubscribe(McpUnsubscribeRequest request) { + config.unsubscribe().accept(request); } @Override - public ContentType type() { - return ContentType.RESOURCE; + public McpResourceUnsubscriberConfig prototype() { + return config; } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolTextContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingAudioMessageBlueprint.java similarity index 66% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpToolTextContent.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingAudioMessageBlueprint.java index 72a7ebe3..248c6dda 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolTextContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingAudioMessageBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -13,19 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; -final class McpToolTextContent implements McpToolContent { - - private final McpTextContent text; +import io.helidon.builder.api.Prototype; - McpToolTextContent(String text) { - this.text = new McpTextContent.McpTextContentImpl(text); - } +/** + * MCP sampling audio message. + */ +@Prototype.Blueprint +interface McpSamplingAudioMessageBlueprint extends McpSamplingMediaMessage { @Override - public McpContent content() { - return text; + default McpSamplingMessageType type() { + return McpSamplingMessageType.AUDIO; } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingAudioMessageImpl.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingAudioMessageImpl.java deleted file mode 100644 index a65a19e0..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingAudioMessageImpl.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.server; - -import java.util.Base64; - -import io.helidon.common.media.type.MediaType; - -/** - * MCP sampling audio content. - */ -final class McpSamplingAudioMessageImpl implements McpSamplingAudioMessage { - private final byte[] data; - private final McpRole role; - private final MediaType type; - - McpSamplingAudioMessageImpl(byte[] data, MediaType type, McpRole role) { - this.data = data; - this.role = role; - this.type = type; - } - - @Override - public McpSamplingMessageType type() { - return McpSamplingMessageType.AUDIO; - } - - @Override - public McpRole role() { - return role; - } - - @Override - public MediaType mediaType() { - return type; - } - - @Override - public byte[] data() { - return data; - } - - @Override - public byte[] decodeBase64Data() { - return Base64.getDecoder().decode(data); - } - - @Override - public String encodeBase64Data() { - return Base64.getEncoder().encodeToString(data); - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingImageMessageBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingImageMessageBlueprint.java new file mode 100644 index 00000000..e124450d --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingImageMessageBlueprint.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025, 2026 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.server; + +import io.helidon.builder.api.Prototype; + +/** + * MCP sampling image message. + */ +@Prototype.Blueprint +interface McpSamplingImageMessageBlueprint extends McpSamplingMediaMessage { + + @Override + default McpSamplingMessageType type() { + return McpSamplingMessageType.IMAGE; + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingImageMessageImpl.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingImageMessageImpl.java deleted file mode 100644 index 1ae8fd68..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingImageMessageImpl.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.server; - -import java.util.Base64; - -import io.helidon.common.media.type.MediaType; - -/** - * MCP sampling image content. - */ -final class McpSamplingImageMessageImpl implements McpSamplingImageMessage { - private final byte[] data; - private final McpRole role; - private final MediaType type; - - McpSamplingImageMessageImpl(byte[] data, MediaType mediaType, McpRole role) { - this.data = data; - this.role = role; - this.type = mediaType; - } - - @Override - public McpSamplingMessageType type() { - return McpSamplingMessageType.IMAGE; - } - - @Override - public McpRole role() { - return role; - } - - @Override - public MediaType mediaType() { - return type; - } - - @Override - public byte[] data() { - return data; - } - - @Override - public byte[] decodeBase64Data() { - return Base64.getDecoder().decode(data); - } - - @Override - public String encodeBase64Data() { - return Base64.getEncoder().encodeToString(data); - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingMediaMessage.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingMediaMessage.java index 131dfbc2..0cc5b09c 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingMediaMessage.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingMediaMessage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -15,12 +15,14 @@ */ package io.helidon.extensions.mcp.server; +import java.util.Base64; + import io.helidon.common.media.type.MediaType; /** * MCP sampling media content. */ -public sealed interface McpSamplingMediaMessage permits McpSamplingAudioMessage, McpSamplingImageMessage { +public interface McpSamplingMediaMessage extends McpSamplingMessage { /** * Image content raw data. * @@ -29,23 +31,27 @@ public sealed interface McpSamplingMediaMessage permits McpSamplingAudioMessage, byte[] data(); /** - * Returns the decoded image data using base64 decoder. + * Image content MIME type. * - * @return decoded content. + * @return MIME type */ - byte[] decodeBase64Data(); + MediaType mediaType(); /** - * Returns the encoded image data using base64 encoder. + * Returns the decoded image data using base64 decoder. * - * @return content in base64. + * @return decoded content. */ - String encodeBase64Data(); + default byte[] decodeBase64Data() { + return Base64.getDecoder().decode(data()); + } /** - * Image content MIME type. + * Returns the encoded image data using base64 encoder. * - * @return MIME type + * @return content in base64. */ - MediaType mediaType(); + default String encodeBase64Data() { + return Base64.getEncoder().encodeToString(data()); + } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingMessage.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingMessage.java index f274a7b0..5148b0c3 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingMessage.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingMessage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; /** * MCP sampling message. */ -public sealed interface McpSamplingMessage permits McpSamplingTextMessage, McpSamplingImageMessage, McpSamplingAudioMessage { +public interface McpSamplingMessage { /** * Sampling message role. * diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingMessages.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingMessages.java deleted file mode 100644 index cc1cc8df..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingMessages.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.server; - -import java.util.Objects; - -import io.helidon.common.media.type.MediaType; - -/** - * {@link io.helidon.extensions.mcp.server.McpSamplingMessage} factory class. - */ -public final class McpSamplingMessages { - private McpSamplingMessages() { - } - - /** - * Create a sampling text message. - * - * @param text text - * @param role role - * @return a sampling text message - */ - public static McpSamplingMessage textMessage(String text, McpRole role) { - Objects.requireNonNull(role, "role must not be null"); - Objects.requireNonNull(text, "text must not be null"); - return new McpSamplingTextMessageImpl(text, role); - } - - /** - * Create a sampling image message. - * - * @param data data - * @param mediaType media type - * @param role role - * @return a sampling image message - */ - public static McpSamplingMessage imageMessage(byte[] data, MediaType mediaType, McpRole role) { - Objects.requireNonNull(role, "role must not be null"); - Objects.requireNonNull(data, "data must not be null"); - Objects.requireNonNull(mediaType, "media type must not be null"); - return new McpSamplingImageMessageImpl(data, mediaType, role); - } - - /** - * Create a sampling audio message. - * - * @param data data - * @param mediaType media type - * @param role role - * @return a sampling audio message - */ - public static McpSamplingMessage audioMessage(byte[] data, MediaType mediaType, McpRole role) { - Objects.requireNonNull(data, "data must not be null"); - Objects.requireNonNull(role, "role must not be null"); - Objects.requireNonNull(mediaType, "media type must not be null"); - return new McpSamplingAudioMessageImpl(data, mediaType, role); - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingRequestBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingRequestBlueprint.java index 46d5ce0a..32521d73 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingRequestBlueprint.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingRequestBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; import java.time.Duration; @@ -29,14 +28,31 @@ * Configuration of an MCP sampling request. */ @Prototype.Blueprint +@Prototype.CustomMethods(McpSamplingSupport.class) interface McpSamplingRequestBlueprint { /** - * Sampling messages sent to the client. + * Sampling text messages sent to the client. + * + * @return messages + */ + @Option.Singular + List textMessages(); + + /** + * Sampling image messages sent to the client. + * + * @return messages + */ + @Option.Singular + List imageMessages(); + + /** + * Sampling audio messages sent to the client. * * @return messages */ @Option.Singular - List messages(); + List audioMessages(); /** * Sampling model hints. diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingResponse.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingResponse.java index c9210dc7..9a980d5d 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingResponse.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; import java.util.Optional; diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingSupport.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingSupport.java new file mode 100644 index 00000000..9dd7ac38 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingSupport.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.helidon.builder.api.Prototype; + +class McpSamplingSupport { + private McpSamplingSupport() { + } + + /** + * Add a sampling text message to the sampling request from the provided text. + * + * @param builder sampling request builder + * @param text text message + */ + @Prototype.BuilderMethod + static void addTextMessage(McpSamplingRequest.BuilderBase builder, String text) { + Objects.requireNonNull(text, "text is null"); + builder.addTextMessage(b -> b.text(text).role(McpRole.ASSISTANT)); + } + + static List aggregate(McpSamplingRequest request) { + List messages = new ArrayList<>(); + messages.addAll(request.textMessages()); + messages.addAll(request.imageMessages()); + messages.addAll(request.audioMessages()); + return messages; + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingTextMessageBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingTextMessageBlueprint.java new file mode 100644 index 00000000..d1ac217a --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingTextMessageBlueprint.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025, 2026 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.server; + +import io.helidon.builder.api.Prototype; + +/** + * MCP sampling text message. + */ +@Prototype.Blueprint +@Prototype.CustomMethods(McpSamplingTextMessageSupport.class) +interface McpSamplingTextMessageBlueprint extends McpSamplingMessage { + /** + * Text content as string. + * + * @return text + */ + String text(); + + @Override + default McpSamplingMessageType type() { + return McpSamplingMessageType.TEXT; + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingTextMessageSupport.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingTextMessageSupport.java new file mode 100644 index 00000000..61eafa40 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpSamplingTextMessageSupport.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026 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.server; + +import io.helidon.builder.api.Prototype; + +class McpSamplingTextMessageSupport { + private McpSamplingTextMessageSupport() { + } + + /** + * Create a {@link io.helidon.extensions.mcp.server.McpSamplingTextMessage} instance from the provided text. + * + * @param text text message + * @return sampling text message instance + */ + @Prototype.FactoryMethod + static McpSamplingTextMessage create(String text) { + return McpSamplingTextMessage.builder().text(text).role(McpRole.ASSISTANT).build(); + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpServerConfigBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpServerConfigBlueprint.java index 9d54a3fc..dc064317 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpServerConfigBlueprint.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpServerConfigBlueprint.java @@ -30,6 +30,7 @@ */ @Prototype.Blueprint @Prototype.Configured(McpServerConfigBlueprint.CONFIG_ROOT) +@Prototype.CustomMethods(McpServerFeatureSupport.class) interface McpServerConfigBlueprint extends Prototype.Factory { String CONFIG_ROOT = "mcp.server"; @@ -60,15 +61,6 @@ interface McpServerConfigBlueprint extends Prototype.Factory { @Option.Default("0.0.1") String version(); - /** - * Server logging configuration. - * - * @return logging - */ - @Option.Configured - @Option.DefaultBoolean(true) - boolean logging(); - /** * Server tools page size configuration. * diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpServerFeature.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpServerFeature.java index fabfbae2..91c400fb 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpServerFeature.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpServerFeature.java @@ -178,9 +178,7 @@ static McpServerFeature create(McpServerConfig config) { } static McpServerFeature create(Consumer consumer) { - McpServerConfig.Builder builder = McpServerConfig.builder(); - consumer.accept(builder); - return builder.build(); + return McpServerConfig.builder().update(consumer).build(); } /** @@ -313,7 +311,7 @@ private void initializeRpc(JsonRpcRequest req, JsonRpcResponse res) { .filter(Boolean::booleanValue) .ifPresent(it -> session.capability(McpCapability.ROOTS)); session.state(INITIALIZING); - var payload = session.serializer().toJson(capabilities, config); + var payload = session.serializer().createJsonInitializeResponse(capabilities, config); session.onRequest(requestId, req, res); res.result(payload.build()); session.send(requestId, res); @@ -412,19 +410,18 @@ private void toolsCallRpc(JsonRpcRequest req, JsonRpcResponse res) { McpFeatures features = session.createFeatures(requestId, req, res); session.beforeFeatureRequest(parameters, requestId); - McpToolResult result = tool.get() - .tool() - .apply(McpRequest.builder() - .parameters(parameters.get("arguments")) - .meta(parameters.get("_meta")) - .features(features) - .protocolVersion(session.protocolVersion().text()) - .sessionContext(session.context()) - .requestContext(req.context()) - .build()); + McpRequest request = McpRequest.builder() + .parameters(parameters) + .meta(parameters.get("_meta")) + .features(features) + .protocolVersion(session.protocolVersion().text()) + .sessionContext(session.context()) + .requestContext(req.context()) + .build(); + McpToolResult result = tool.get().tool(new McpToolRequestImpl(request)); session.afterFeatureRequest(parameters, requestId); - var toolCall = session.serializer().toolCall(tool.get(), result).build(); + var toolCall = session.serializer().toolCall(tool.get(), result); res.result(toolCall); session.send(requestId, res); } @@ -476,18 +473,17 @@ private void resourcesReadRpc(JsonRpcRequest req, JsonRpcResponse res) { McpFeatures features = session.createFeatures(requestId, req, res); session.beforeFeatureRequest(parameters, requestId); - List contents = resource.get() - .resource() - .apply(McpRequest.builder() - .parameters(parameters) - .meta(parameters.get("_meta")) - .features(features) - .protocolVersion(session.protocolVersion().text()) - .sessionContext(session.context()) - .requestContext(req.context()) - .build()); + McpRequest request = McpRequest.builder() + .parameters(parameters) + .meta(parameters.get("_meta")) + .features(features) + .protocolVersion(session.protocolVersion().text()) + .sessionContext(session.context()) + .requestContext(req.context()) + .build(); + McpResourceResult result = resource.get().resource(new McpResourceRequestImpl(request)); session.afterFeatureRequest(parameters, requestId); - var readResource = session.serializer().readResource(resourceUri, contents); + var readResource = session.serializer().resourceRead(resourceUri, result); res.result(readResource); session.send(requestId, res); } @@ -526,16 +522,14 @@ private void resourceSubscribeRpc(JsonRpcRequest req, JsonRpcResponse res) { if (subscriber.isPresent()) { McpFeatures features = session.createFeatures(requestId, req, res); session.beforeFeatureRequest(parameters, requestId); - subscriber.get() - .subscribe() - .accept(McpRequest.builder() - .parameters(parameters) - .meta(parameters.get("_meta")) - .features(features) - .protocolVersion(session.protocolVersion().text()) - .sessionContext(session.context()) - .requestContext(req.context()) - .build()); + subscriber.get().subscribe(McpSubscribeRequest.builder() + .parameters(parameters) + .meta(parameters.get("_meta")) + .features(features) + .protocolVersion(session.protocolVersion().text()) + .sessionContext(session.context()) + .requestContext(req.context()) + .build()); session.afterFeatureRequest(parameters, requestId); // send final response using active SSE sink res.result(JsonValue.EMPTY_JSON_OBJECT); @@ -571,16 +565,14 @@ private void resourceUnsubscribeRpc(JsonRpcRequest req, JsonRpcResponse res) { if (unsubscriber.isPresent()) { McpFeatures features = session.createFeatures(requestId, req, res); session.beforeFeatureRequest(parameters, requestId); - unsubscriber.get() - .unsubscribe() - .accept(McpRequest.builder() - .parameters(parameters) - .meta(parameters.get("_meta")) - .features(features) - .protocolVersion(session.protocolVersion().text()) - .sessionContext(session.context()) - .requestContext(req.context()) - .build()); + unsubscriber.get().unsubscribe(McpUnsubscribeRequest.builder() + .parameters(parameters) + .meta(parameters.get("_meta")) + .features(features) + .protocolVersion(session.protocolVersion().text()) + .sessionContext(session.context()) + .requestContext(req.context()) + .build()); session.afterFeatureRequest(parameters, requestId); } @@ -647,18 +639,17 @@ private void promptsGetRpc(JsonRpcRequest req, JsonRpcResponse res) { McpFeatures features = session.createFeatures(requestId, req, res); session.beforeFeatureRequest(parameters, requestId); - List contents = prompt.get() - .prompt() - .apply(McpRequest.builder() - .parameters(parameters.get("arguments")) - .meta(parameters.get("_meta")) - .features(features) - .protocolVersion(session.protocolVersion().text()) - .sessionContext(session.context()) - .requestContext(req.context()) - .build()); + McpRequest request = McpRequest.builder() + .parameters(parameters) + .meta(parameters.get("_meta")) + .features(features) + .protocolVersion(session.protocolVersion().text()) + .sessionContext(session.context()) + .requestContext(req.context()) + .build(); + McpPromptResult result = prompt.get().prompt(new McpPromptRequestImpl(request)); session.afterFeatureRequest(parameters, requestId); - var payload = session.serializer().toJson(contents, prompt.get().description()); + var payload = session.serializer().promptGet(result); res.result(payload); session.send(requestId, res); } @@ -723,9 +714,9 @@ private void completionRpc(JsonRpcRequest req, JsonRpcResponse res) { .sessionContext(session.context()) .requestContext(req.context()) .build(); - McpCompletionContent result = completion.completion().apply(new McpCompletionRequestImpl(request)); + McpCompletionResult result = completion.completion(new McpCompletionRequestImpl(request)); session.afterFeatureRequest(parameters, requestId); - var payload = session.serializer().toJson(result); + var payload = session.serializer().completionComplete(result); res.result(payload); session.send(requestId, res); return; diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpServerFeatureSupport.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpServerFeatureSupport.java new file mode 100644 index 00000000..7f2f93df --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpServerFeatureSupport.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.Objects; +import java.util.function.Consumer; + +import io.helidon.builder.api.Prototype; + +final class McpServerFeatureSupport { + private McpServerFeatureSupport() { + } + + /** + * Create a new {@link io.helidon.extensions.mcp.server.McpTool} from its configuration + * and register it to this server. + * + * @param builder server configuration builder + * @param config tool configuration + */ + @Prototype.BuilderMethod + static void addTool(McpServerConfig.BuilderBase builder, McpToolConfig config) { + Objects.requireNonNull(config, "config is null"); + builder.addTool(new McpToolImpl(config)); + } + + /** + * Create a new {@link io.helidon.extensions.mcp.server.McpTool} from its configuration builder + * and register it to this server. + * + * @param builder server configuration builder + * @param consumer tool configuration builder consumer + */ + @Prototype.BuilderMethod + static void addTool(McpServerConfig.BuilderBase builder, Consumer consumer) { + Objects.requireNonNull(consumer, "consumer is null"); + McpToolConfig config = McpToolConfig.builder().update(consumer).build(); + builder.addTool(new McpToolImpl(config)); + } + + /** + * Create a new {@link io.helidon.extensions.mcp.server.McpPrompt} from its configuration + * and register it to this server. + * + * @param builder server configuration builder + * @param config prompt configuration + */ + @Prototype.BuilderMethod + static void addPrompt(McpServerConfig.BuilderBase builder, McpPromptConfig config) { + Objects.requireNonNull(config, "config is null"); + builder.addPrompt(new McpPromptImpl(config)); + } + + /** + * Create a new {@link io.helidon.extensions.mcp.server.McpPrompt} from its configuration builder + * and register it to this server. + * + * @param builder server configuration builder + * @param consumer prompt configuration builder consumer + */ + @Prototype.BuilderMethod + static void addPrompt(McpServerConfig.BuilderBase builder, Consumer consumer) { + Objects.requireNonNull(consumer, "consumer is null"); + McpPromptConfig config = McpPromptConfig.builder().update(consumer).build(); + builder.addPrompt(new McpPromptImpl(config)); + } + + /** + * Create a new {@link io.helidon.extensions.mcp.server.McpResource} from its configuration + * and register it to this server. + * + * @param builder server configuration builder + * @param config resource configuration + */ + @Prototype.BuilderMethod + static void addResource(McpServerConfig.BuilderBase builder, McpResourceConfig config) { + Objects.requireNonNull(config, "config is null"); + builder.addResource(new McpResourceImpl(config)); + } + + /** + * Create a new {@link io.helidon.extensions.mcp.server.McpResource} from its configuration builder + * and register it to this server. + * + * @param builder server configuration builder + * @param consumer resource configuration builder consumer + */ + @Prototype.BuilderMethod + static void addResource(McpServerConfig.BuilderBase builder, Consumer consumer) { + Objects.requireNonNull(consumer, "consumer is null"); + McpResourceConfig config = McpResourceConfig.builder().update(consumer).build(); + builder.addResource(new McpResourceImpl(config)); + } + + /** + * Create a new {@link io.helidon.extensions.mcp.server.McpCompletion} from its configuration + * and register it to this server. + * + * @param builder server configuration builder + * @param config completion configuration + */ + @Prototype.BuilderMethod + static void addCompletion(McpServerConfig.BuilderBase builder, McpCompletionConfig config) { + Objects.requireNonNull(config, "config is null"); + builder.addCompletion(new McpCompletionImpl(config)); + } + + /** + * Create a new {@link io.helidon.extensions.mcp.server.McpCompletion} from its configuration builder + * and register it to this server. + * + * @param builder server configuration builder + * @param consumer completion configuration builder consumer + */ + @Prototype.BuilderMethod + static void addCompletion(McpServerConfig.BuilderBase builder, Consumer consumer) { + Objects.requireNonNull(consumer, "consumer is null"); + McpCompletionConfig config = McpCompletionConfig.builder().update(consumer).build(); + builder.addCompletion(new McpCompletionImpl(config)); + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSession.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSession.java index a7486510..bc78a10c 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpSession.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpSession.java @@ -68,9 +68,9 @@ class McpSession { this.sessions = sessions; this.clientCapabilities = new HashSet<>(); this.featureListeners = new CopyOnWriteArrayList<>(); - this.featureListeners.add(new McpProgress.McpProgressListener()); + this.featureListeners.add(McpProgress.McpProgressListener.create()); this.sessionFeatures = LazyValue.create(() -> new McpSessionFeatures(this)); - context.register(McpServerConfigBlueprint.class, config); + this.context.register(McpServerConfigBlueprint.class, config); } void send(JsonValue id, JsonRpcResponse response) { @@ -116,7 +116,7 @@ void beforeFeatureRequest(McpParameters parameters, JsonValue requestId) { void afterFeatureRequest(McpParameters parameters, JsonValue requestId) { features.get(requestId).ifPresent(feature -> { for (McpFeatureLifecycle listener : featureListeners) { - listener.beforeRequest(parameters, feature); + listener.afterRequest(parameters, feature); } }); } @@ -150,7 +150,7 @@ JsonObject pollResponse(long requestId, Duration timeout) { return response; } } else { - return serializer.timeoutResponse(requestId); + return serializer.jsonrpcErrorTimeoutResponse(requestId); } } catch (ClassCastException e) { if (LOGGER.isLoggable(Level.TRACE)) { diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpSubscribeRequestBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpSubscribeRequestBlueprint.java new file mode 100644 index 00000000..a290b8ba --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpSubscribeRequestBlueprint.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2026 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.server; + +import io.helidon.builder.api.Prototype; + +@Prototype.Blueprint +interface McpSubscribeRequestBlueprint extends McpRequestBlueprint { +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpTextContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpTextContent.java index 66aa39f8..f3ddfc4b 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpTextContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpTextContent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -19,7 +19,7 @@ /** * Text content. */ -sealed interface McpTextContent extends McpContent permits McpTextContent.McpTextContentImpl { +interface McpTextContent extends McpContent { /** * Text content as string. * @@ -27,21 +27,8 @@ sealed interface McpTextContent extends McpContent permits McpTextContent.McpTex */ String text(); - final class McpTextContentImpl implements McpTextContent { - private final String text; - - McpTextContentImpl(String text) { - this.text = text; - } - - @Override - public String text() { - return text; - } - - @Override - public ContentType type() { - return ContentType.TEXT; - } + @Override + default McpContentType type() { + return McpContentType.TEXT; } } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpTool.java similarity index 77% rename from server/src/main/java/io/helidon/extensions/mcp/server/McpToolBlueprint.java rename to server/src/main/java/io/helidon/extensions/mcp/server/McpTool.java index 2746b08f..bbda3c2c 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolBlueprint.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpTool.java @@ -13,21 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; import java.util.Optional; -import java.util.function.Function; - -import io.helidon.builder.api.Option; -import io.helidon.builder.api.Prototype; /** - * An MCP Tool. + * MCP Tool. */ -@Prototype.Blueprint -@Prototype.IncludeDefaultMethods -interface McpToolBlueprint { +public interface McpTool { /** * Tool name. * @@ -35,21 +28,11 @@ interface McpToolBlueprint { */ String name(); - /** - * Human-readable tool title. - * - * @return the tool title - */ - default Optional title() { - return Optional.empty(); - } - /** * Tool description. * * @return description */ - @Option.Default("No description available") String description(); /** @@ -61,14 +44,30 @@ default Optional title() { */ String schema(); + /** + * Tool execution. + * + * @param request tool request + * @return tool execution result + */ + McpToolResult tool(McpToolRequest request); + + /** + * Human-readable tool title. + * + * @return the tool title + */ + default Optional title() { + return Optional.empty(); + } + /** * Annotations for this tool. * * @return set of annotations */ - @Option.DefaultMethod("create") - default McpToolAnnotations annotations() { - return McpToolAnnotations.create(); + default Optional annotations() { + return Optional.empty(); } /** @@ -79,11 +78,4 @@ default McpToolAnnotations annotations() { default Optional outputSchema() { return Optional.empty(); } - - /** - * Tool execution function. - * - * @return function - */ - Function tool(); } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolAudioContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolAudioContent.java deleted file mode 100644 index 4d712334..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolAudioContent.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.server; - -import io.helidon.common.media.type.MediaType; - -final class McpToolAudioContent implements McpToolContent { - private final McpAudioContent audio; - - McpToolAudioContent(byte[] data, MediaType mediaType) { - this.audio = new McpAudioContentImpl(data, mediaType); - } - - @Override - public McpContent content() { - return audio; - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolAudioContentBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolAudioContentBlueprint.java new file mode 100644 index 00000000..8e8577bb --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolAudioContentBlueprint.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025, 2026 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.server; + +import io.helidon.builder.api.Prototype; + +/** + * Tool audio content. + */ +@Prototype.Blueprint +interface McpToolAudioContentBlueprint extends McpAudioContent, McpToolContent { +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolBinaryResourceContentBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolBinaryResourceContentBlueprint.java new file mode 100644 index 00000000..129cef0e --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolBinaryResourceContentBlueprint.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 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.server; + +import io.helidon.builder.api.Prototype; + +/** + * Tool embedded binary resource content. + */ +@Prototype.Blueprint +interface McpToolBinaryResourceContentBlueprint extends McpEmbeddedBinaryResourceContent, McpToolContent { +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolConfigBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolConfigBlueprint.java new file mode 100644 index 00000000..18e1c344 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolConfigBlueprint.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.Optional; +import java.util.function.Function; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; + +/** + * Tool configuration. + */ +@Prototype.Blueprint +interface McpToolConfigBlueprint { + /** + * Tool name. + * + * @return name + */ + String name(); + + /** + * Tool description. + * + * @return description + */ + @Option.Default("No description available") + String description(); + + /** + * JSON schema describing tool inputs. String must be compliant with + * JSON Schema Version 2020-12. + * An empty string is mapped to a schema of type object without any properties. + * + * @return JSON schema as a string + */ + String schema(); + + /** + * Human-readable tool title. + * + * @return the tool title + */ + Optional title(); + + /** + * Annotations for this tool. + * + * @return set of annotations + */ + Optional annotations(); + + /** + * Tool output schema. Describes this tool response format. + * + * @return the tool output schema + */ + Optional outputSchema(); + + /** + * Tool execution. + * + * @return tool execution result + */ + Function tool(); +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolContent.java index ba014207..421402a2 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolContent.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolContent.java @@ -13,21 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.extensions.mcp.server; /** * Tool contents that can be returned as part of the tool execution. */ -public sealed interface McpToolContent permits McpToolTextContent, - McpToolImageContent, - McpToolResourceContent, - McpToolResourceLinkContent, - McpToolAudioContent { - /** - * Get the content of this {@code ToolContent}. - * - * @return content - */ - McpContent content(); +interface McpToolContent extends McpContent { } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolContents.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolContents.java deleted file mode 100644 index ba51a3fe..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolContents.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (c) 2025, 2026 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.server; - -import java.net.URI; -import java.util.Objects; -import java.util.function.Consumer; - -import io.helidon.common.media.type.MediaType; - -/** - * {@link McpToolContent} factory. - */ -public final class McpToolContents { - private McpToolContents() { - } - - /** - * Create a text tool content instance. - * - * @param text text - * @return instance - */ - public static McpToolContent textContent(String text) { - Objects.requireNonNull(text, "Tool content text must not be null"); - return new McpToolTextContent(text); - } - - /** - * Create a image tool content instance. - * - * @param data data - * @param type media type - * @return instance - */ - public static McpToolContent imageContent(byte[] data, MediaType type) { - Objects.requireNonNull(data, "Tool image content data must not be null"); - Objects.requireNonNull(type, "Tool image content MIME type must not be null"); - return new McpToolImageContent(data, type); - } - - /** - * Create a resource tool content instance. - * - * @param uri resource uri - * @param content resource content - * @return instance - */ - public static McpToolContent resourceContent(URI uri, McpResourceContent content) { - Objects.requireNonNull(uri, "Tool resource URI must not be null"); - Objects.requireNonNull(content, "Tool resource content must not be null"); - return new McpToolResourceContent(uri, content); - } - - /** - * Create a resource link tool content instance with required name and URI. - * - * @param name resource link name - * @param uri resource link uri - * @return instance - */ - public static McpToolContent resourceLinkContent(String name, String uri) { - Objects.requireNonNull(uri, "Tool resource link uri must not be null"); - Objects.requireNonNull(name, "Tool resource link name must not be null"); - McpResourceLinkContent.Builder builder = McpResourceLinkContent.builder() - .name(name) - .uri(uri); - return resourceLinkContent(builder.build()); - } - - /** - * Create a resource link tool content instance. - * - * @param consumer the consumer of the resource link builder - * @return instance - */ - public static McpToolContent resourceLinkContent(Consumer consumer) { - Objects.requireNonNull(consumer, "Tool resource consumer must not be null"); - McpResourceLinkContent.Builder builder = McpResourceLinkContent.builder(); - consumer.accept(builder); - return resourceLinkContent(builder.build()); - } - - /** - * Create a resource link tool content instance. - * - * @param content the consumer of the resource link builder - * @return instance - */ - public static McpToolContent resourceLinkContent(McpResourceLinkContent content) { - Objects.requireNonNull(content, "Tool resource link content must not be null"); - return new McpToolResourceLinkContent(content); - } - - /** - * Create an audio tool content instance. - * - * @param data data - * @param type media type - * @return instance - */ - public static McpToolContent audioContent(byte[] data, MediaType type) { - Objects.requireNonNull(data, "Tool audio content data must not be null"); - Objects.requireNonNull(type, "Tool audio content MIME type must not be null"); - return new McpToolAudioContent(data, type); - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolErrorException.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolErrorException.java deleted file mode 100644 index 46e57eed..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolErrorException.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.server; - -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - -/** - * Tool error exception are sending a tool response to the client with - * the provided content and an error flag. - */ -public final class McpToolErrorException extends RuntimeException { - private final List contents; - - /** - * Creates a tool error exception with provided content. - * - * @param contents tool content - */ - public McpToolErrorException(List contents) { - this.contents = contents; - } - - /** - * Creates a tool error exception with provided content. - * - * @param contents tool content - */ - public McpToolErrorException(McpToolContent... contents) { - this.contents = Arrays.asList(contents); - } - - /** - * Creates a tool error exception with provided message. - * - * @param messages error messages - */ - public McpToolErrorException(String... messages) { - this.contents = Arrays.stream(messages) - .filter(Objects::nonNull) - .map(McpToolContents::textContent) - .toList(); - } - - List contents() { - return contents; - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolImageContent.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolImageContent.java deleted file mode 100644 index 21bdc544..00000000 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolImageContent.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.server; - -import io.helidon.common.media.type.MediaType; - -final class McpToolImageContent implements McpToolContent { - private final McpImageContent image; - - McpToolImageContent(byte[] data, MediaType mediaType) { - this.image = new McpImageContentImpl(data, mediaType); - } - - @Override - public McpContent content() { - return image; - } -} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolImageContentBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolImageContentBlueprint.java new file mode 100644 index 00000000..4ab81988 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolImageContentBlueprint.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025, 2026 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.server; + +import io.helidon.builder.api.Prototype; + +/** + * Tool image content. + */ +@Prototype.Blueprint +interface McpToolImageContentBlueprint extends McpImageContent, McpToolContent { +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolImpl.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolImpl.java new file mode 100644 index 00000000..22dee11f --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolImpl.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.Optional; + +final class McpToolImpl implements McpTool { + private final McpToolConfig config; + + McpToolImpl(McpToolConfig config) { + this.config = config; + } + + @Override + public String name() { + return config.name(); + } + + @Override + public String description() { + return config.description(); + } + + @Override + public String schema() { + return config.schema(); + } + + @Override + public McpToolResult tool(McpToolRequest request) { + return config.tool().apply(request); + } + + @Override + public Optional title() { + return config.title(); + } + + @Override + public Optional annotations() { + return config.annotations(); + } + + @Override + public Optional outputSchema() { + return config.outputSchema(); + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolRequest.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolRequest.java new file mode 100644 index 00000000..267c3dbf --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolRequest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 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.server; + +/** + * Tool call request. + */ +public sealed interface McpToolRequest extends McpRequest permits McpToolRequestImpl { + /** + * Get prompt request arguments. + * + * @return arguments + */ + McpParameters arguments(); + + /** + * Get prompt request name. + * + * @return name + * @throws java.lang.IllegalStateException if name is missing from the request + */ + String name(); +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolRequestImpl.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolRequestImpl.java new file mode 100644 index 00000000..c5f869e9 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolRequestImpl.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026 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.server; + +import io.helidon.common.context.Context; + +final class McpToolRequestImpl implements McpToolRequest { + private final McpRequest request; + + McpToolRequestImpl(McpRequest request) { + this.request = request; + } + + @Override + public McpParameters arguments() { + return request.parameters().get("arguments"); + } + + @Override + public String name() { + return request.parameters() + .get("name") + .asString() + .orElseThrow(() -> new IllegalStateException("Tool request name is missing")); + } + + @Override + public McpParameters parameters() { + return request.parameters(); + } + + @Override + public McpParameters meta() { + return request.meta(); + } + + @Override + public McpFeatures features() { + return request.features(); + } + + @Override + public String protocolVersion() { + return request.protocolVersion(); + } + + @Override + public Context sessionContext() { + return request.sessionContext(); + } + + @Override + public Context requestContext() { + return request.requestContext(); + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolResourceLinkContentBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolResourceLinkContentBlueprint.java new file mode 100644 index 00000000..7ec1ea92 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolResourceLinkContentBlueprint.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 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.server; + +import io.helidon.builder.api.Prototype; + +/** + * Tool resource link content. + */ +@Prototype.Blueprint +interface McpToolResourceLinkContentBlueprint extends McpResourceLinkContent, McpToolContent { +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolResultBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolResultBlueprint.java index c83bd7a8..d55ffc37 100644 --- a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolResultBlueprint.java +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolResultBlueprint.java @@ -25,14 +25,55 @@ * MCP tool result. */ @Prototype.Blueprint +@Prototype.CustomMethods(McpToolSupport.class) interface McpToolResultBlueprint { /** - * Tool result contents. + * Text content. * - * @return contents + * @return list of tool text content */ @Option.Singular - List contents(); + List textContents(); + + /** + * Image content. + * + * @return list of tool image content + */ + @Option.Singular + List imageContents(); + + /** + * Audio content. + * + * @return list of tool audio content + */ + @Option.Singular + List audioContents(); + + /** + * Embedded resource text content. + * + * @return list of tool embedded resource text content + */ + @Option.Singular + List textResourceContents(); + + /** + * Embedded resource binary content. + * + * @return list of tool embedded resource binary content + */ + @Option.Singular + List binaryResourceContents(); + + /** + * Resource link content. + * + * @return list of tool resource link content + */ + @Option.Singular + List resourceLinkContents(); /** * Structured tool result content. If specified, the tool definition @@ -47,6 +88,6 @@ interface McpToolResultBlueprint { * * @return error */ - @Option.DefaultBoolean({false}) + @Option.DefaultBoolean(false) boolean error(); } diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolSupport.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolSupport.java new file mode 100644 index 00000000..0c6bf1ce --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolSupport.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.helidon.builder.api.Prototype; +import io.helidon.common.media.type.MediaType; + +class McpToolSupport { + private McpToolSupport() { + } + + /** + * Create a {@link io.helidon.extensions.mcp.server.McpToolResult} instance with + * a text content based on the provided string. + * + * @param text text content + * @return tool result instance + */ + @Prototype.FactoryMethod + static McpToolResult create(String text) { + return McpToolResult.builder().addTextContent(text).build(); + } + + /** + * Add a tool text content to the tool result from the provided text. + * + * @param builder tool result builder + * @param text text content + */ + @Prototype.BuilderMethod + static void addTextContent(McpToolResult.BuilderBase builder, String text) { + Objects.requireNonNull(text, "text is null"); + builder.addTextContent(b -> b.text(text)); + } + + /** + * Add a tool image content to the tool result from the provided data and media type. + * + * @param builder tool result builder + * @param data tool image data + * @param type tool image media type + */ + @Prototype.BuilderMethod + static void addImageContent(McpToolResult.BuilderBase builder, byte[] data, MediaType type) { + Objects.requireNonNull(data, "data is null"); + Objects.requireNonNull(type, "media type is null"); + builder.addImageContent(image -> image.data(data).mediaType(type)); + } + + /** + * Add a tool resource link content to the tool result from the provided name and uri. + * + * @param builder tool result builder + * @param name tool resource link name + * @param uri tool resource link uri + */ + @Prototype.BuilderMethod + static void addResourceLinkContent(McpToolResult.BuilderBase builder, String name, String uri) { + Objects.requireNonNull(uri, "uri is null"); + Objects.requireNonNull(name, "name is null"); + builder.addResourceLinkContent(link -> link.name(name).uri(uri)); + } + + /** + * Add a tool audio content to the tool result from the provided data and media type. + * + * @param builder tool result builder + * @param data tool audio data + * @param type tool audio media type + */ + @Prototype.BuilderMethod + static void addAudioContent(McpToolResult.BuilderBase builder, byte[] data, MediaType type) { + Objects.requireNonNull(data, "data is null"); + Objects.requireNonNull(type, "media type is null"); + builder.addAudioContent(audio -> audio.data(data).mediaType(type)); + } + + /** + * Aggregate contents of a tool result into a single list. + * + * @param result tool result + * @return list of tool content + */ + static List aggregateContent(McpToolResult result) { + List contents = new ArrayList<>(); + contents.addAll(result.textContents()); + contents.addAll(result.imageContents()); + contents.addAll(result.audioContents()); + contents.addAll(result.textResourceContents()); + contents.addAll(result.binaryResourceContents()); + contents.addAll(result.resourceLinkContents()); + return contents; + } +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolTextContentBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolTextContentBlueprint.java new file mode 100644 index 00000000..49775dd2 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolTextContentBlueprint.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025, 2026 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.server; + +import io.helidon.builder.api.Prototype; + +/** + * Tool text content. + */ +@Prototype.Blueprint +interface McpToolTextContentBlueprint extends McpTextContent, McpToolContent { +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpToolTextResourceContentBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolTextResourceContentBlueprint.java new file mode 100644 index 00000000..152423ac --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpToolTextResourceContentBlueprint.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 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.server; + +import io.helidon.builder.api.Prototype; + +/** + * Tool embedded text resource content. + */ +@Prototype.Blueprint +interface McpToolTextResourceContentBlueprint extends McpEmbeddedTextResourceContent, McpToolContent { +} diff --git a/server/src/main/java/io/helidon/extensions/mcp/server/McpUnsubscribeRequestBlueprint.java b/server/src/main/java/io/helidon/extensions/mcp/server/McpUnsubscribeRequestBlueprint.java new file mode 100644 index 00000000..651768e4 --- /dev/null +++ b/server/src/main/java/io/helidon/extensions/mcp/server/McpUnsubscribeRequestBlueprint.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 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.server; + +import io.helidon.builder.api.Prototype; + +/** + * MCP unsubscribe request. + */ +@Prototype.Blueprint +interface McpUnsubscribeRequestBlueprint extends McpRequestBlueprint { +} diff --git a/server/src/test/java/io/helidon/extensions/mcp/server/McpCompletionContentTest.java b/server/src/test/java/io/helidon/extensions/mcp/server/McpCompletionContentTest.java deleted file mode 100644 index 32616b7b..00000000 --- a/server/src/test/java/io/helidon/extensions/mcp/server/McpCompletionContentTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.server; - -import java.util.List; - -import org.junit.jupiter.api.Test; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -class McpCompletionContentTest { - - @Test - void testDefaultMcpCompletion() { - var content = McpCompletionContents.completion(List.of()); - assertThat(content.total(), is(0)); - assertThat(content.hasMore(), is(false)); - assertThat(content.values(), is(List.of())); - } - - @Test - void testDefaultArrayMcpCompletion() { - var content = McpCompletionContents.completion(); - assertThat(content.total(), is(0)); - assertThat(content.hasMore(), is(false)); - assertThat(content.values(), is(List.of())); - } - - @Test - void testMcpCompletion() { - var content = McpCompletionContents.completion(List.of("foo")); - assertThat(content.total(), is(1)); - assertThat(content.hasMore(), is(false)); - assertThat(content.values(), is(List.of("foo"))); - } - - @Test - void testArrayMcpCompletion() { - var content = McpCompletionContents.completion("foo"); - assertThat(content.total(), is(1)); - assertThat(content.hasMore(), is(false)); - assertThat(content.values(), is(List.of("foo"))); - } -} diff --git a/server/src/test/java/io/helidon/extensions/mcp/server/McpCompletionTest.java b/server/src/test/java/io/helidon/extensions/mcp/server/McpCompletionTest.java index 3bc5ea66..eae221cb 100644 --- a/server/src/test/java/io/helidon/extensions/mcp/server/McpCompletionTest.java +++ b/server/src/test/java/io/helidon/extensions/mcp/server/McpCompletionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -15,20 +15,52 @@ */ package io.helidon.extensions.mcp.server; +import java.util.Collections; +import java.util.List; + import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.fail; class McpCompletionTest { @Test void testDefaultMcpCompletion() { - McpCompletion completion = McpCompletion.builder() + McpCompletionConfig config = McpCompletionConfig.builder() .reference("acompletion") - .completion(r -> McpCompletionContents.completion("")) + .completion(r -> McpCompletionResult.create()) .build(); + McpCompletion completion = new McpCompletionImpl(config); assertThat(completion.reference(), is("acompletion")); assertThat(completion.referenceType(), is(McpCompletionType.PROMPT)); // default } + + @Test + void testDefaultMcpCompletionResult() { + McpCompletionResult result = McpCompletionResult.create(); + assertThat(result.values(), is(List.of())); + assertThat(result.total().isEmpty(), is(true)); + assertThat(result.hasMore().isEmpty(), is(true)); + } + + @Test + void testCustomMcpCompletionResult() { + McpCompletionResult result = McpCompletionResult.create(List.of("foo")); + assertThat(result.values(), is(List.of("foo"))); + assertThat(result.total().orElse(-1), is(1)); + assertThat(result.hasMore().orElse(true), is(false)); + } + + @Test + void testTooManySuggestions() { + try { + McpCompletionResult result = McpCompletionResult.create(Collections.nCopies(101, "x")); + fail("Should have thrown exception"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), is("Completion values must be less than 100")); + } + } + } diff --git a/server/src/test/java/io/helidon/extensions/mcp/server/McpJsonSerializerV3Test.java b/server/src/test/java/io/helidon/extensions/mcp/server/McpJsonSerializerV3Test.java index 53dc9dc2..c238dd54 100644 --- a/server/src/test/java/io/helidon/extensions/mcp/server/McpJsonSerializerV3Test.java +++ b/server/src/test/java/io/helidon/extensions/mcp/server/McpJsonSerializerV3Test.java @@ -24,7 +24,6 @@ import jakarta.json.spi.JsonProvider; import org.junit.jupiter.api.Test; -import static io.helidon.extensions.mcp.server.McpToolContents.resourceLinkContent; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -35,7 +34,7 @@ class McpJsonSerializerV3Test { @Test void testSerializeTool() { - McpTool tool = McpTool.builder() + McpToolConfig config = McpToolConfig.builder() .name("name") .title("title") .schema("") @@ -43,6 +42,7 @@ void testSerializeTool() { .outputSchema("") .tool(request -> null) .build(); + McpTool tool = new McpToolImpl(config); JsonObject payload = MJS.toJson(tool).build(); assertThat(payload.getString("name"), is("name")); @@ -58,7 +58,7 @@ void testSerializeTool() { @Test void testSerializeToolOutputSchema() { - McpTool tool = McpTool.builder() + McpToolConfig config = McpToolConfig.builder() .name("name") .title("title") .schema("") @@ -69,6 +69,7 @@ void testSerializeToolOutputSchema() { .generate()) .tool(request -> null) .build(); + McpTool tool = new McpToolImpl(config); JsonObject payload = MJS.toJson(tool).build(); assertThat(payload.getString("name"), is("name")); @@ -86,7 +87,7 @@ void testSerializeToolOutputSchema() { @Test void testSerializeResource() { - McpResource resource = McpResource.builder() + McpResourceConfig config = McpResourceConfig.builder() .uri("https://foo") .name("name") .title("title") @@ -94,6 +95,7 @@ void testSerializeResource() { .mediaType(MediaTypes.APPLICATION_JSON) .resource(request -> null) .build(); + McpResource resource = new McpResourceImpl(config); JsonObject payload = MJS.toJson(resource).build(); assertThat(payload.getString("name"), is("name")); @@ -105,7 +107,7 @@ void testSerializeResource() { @Test void testSerializePrompt() { - McpPrompt prompt = McpPrompt.builder() + McpPromptConfig config = McpPromptConfig.builder() .name("name") .title("title") .description("description") @@ -115,7 +117,7 @@ void testSerializePrompt() { .required(true)) .prompt(request -> null) .build(); - + McpPrompt prompt = new McpPromptImpl(config); JsonObject payload = MJS.toJson(prompt).build(); assertThat(payload.getString("name"), is("name")); assertThat(payload.getString("title"), is("title")); @@ -149,13 +151,15 @@ void testStructuredContent() { McpToolResult result = McpToolResult.builder() .structuredContent(new StructuredContent("bar")) .build(); - McpTool tool = McpTool.builder() + McpToolConfig config = McpToolConfig.builder() .schema("") .name("name") .description("description") .tool((request) -> null) .build(); - JsonObject object = MJS.toolCall(tool, result).build(); + McpTool tool = new McpToolImpl(config); + + JsonObject object = MJS.toolCall(tool, result); assertThat(object, is(notNullValue())); assertThat(object.get("content"), is(notNullValue())); assertThat(object.get("structuredContent"), is(notNullValue())); @@ -174,16 +178,18 @@ void testStructuredContent() { @Test void testStructuredContentWithContent() { McpToolResult result = McpToolResult.builder() - .addContent(McpToolContents.textContent("foo")) + .addTextContent("foo") .structuredContent(new StructuredContent("bar")) .build(); - McpTool tool = McpTool.builder() + McpToolConfig config = McpToolConfig.builder() .schema("") .name("name") .description("description") .tool((request) -> null) .build(); - JsonObject object = MJS.toolCall(tool, result).build(); + McpTool tool = new McpToolImpl(config); + + JsonObject object = MJS.toolCall(tool, result); assertThat(object, is(notNullValue())); assertThat(object.get("content"), is(notNullValue())); assertThat(object.get("structuredContent"), is(notNullValue())); @@ -201,26 +207,30 @@ void testStructuredContentWithContent() { @Test void testSerializeResourceLinkDefault() { - McpToolContent link = resourceLinkContent("name", "https://foo"); + McpToolContent link = McpToolResourceLinkContent.builder() + .name("name") + .uri("https://foo").build(); - JsonObject payload = MJS.toJson(link.content()).orElseGet(JSON_PROVIDER::createObjectBuilder).build(); - assertThat(payload.getString("type"), is(McpContent.ContentType.RESOURCE_LINK.text())); + JsonObject payload = MJS.toJson(link).orElseGet(JSON_PROVIDER::createObjectBuilder).build(); + assertThat(payload.getString("type"), is(McpContentType.RESOURCE_LINK.text())); assertThat(payload.getString("uri"), is("https://foo")); assertThat(payload.getString("name"), is("name")); } @Test void testSerializeResourceLinkCustom() { - McpToolContent link = resourceLinkContent(resource -> resource.uri("https://foo") + McpToolContent link = McpToolResourceLinkContent.builder() .size(10) .name("name") .title("title") + .uri("https://foo") .description("description") - .mediaType(MediaTypes.APPLICATION_JSON)); + .mediaType(MediaTypes.APPLICATION_JSON) + .build(); - JsonObject payload = MJS.toJson(link.content()).orElseGet(JSON_PROVIDER::createObjectBuilder).build(); + JsonObject payload = MJS.toJson(link).orElseGet(JSON_PROVIDER::createObjectBuilder).build(); assertThat(payload.getJsonNumber("size").longValue(), is(10L)); - assertThat(payload.getString("type"), is(McpContent.ContentType.RESOURCE_LINK.text())); + assertThat(payload.getString("type"), is(McpContentType.RESOURCE_LINK.text())); assertThat(payload.getString("name"), is("name")); assertThat(payload.getString("title"), is("title")); assertThat(payload.getString("uri"), is("https://foo")); @@ -231,15 +241,16 @@ void testSerializeResourceLinkCustom() { @Test void testSerializeToolCallDefault() { McpToolResult result = McpToolResult.builder() - .addContent(resourceLinkContent("name", "https://foo")) + .addResourceLinkContent("name", "https://foo") .build(); - McpTool tool = McpTool.builder() + McpToolConfig config = McpToolConfig.builder() .schema("") .name("name") .description("description") .tool((request) -> null) .build(); - JsonObject payload = MJS.toolCall(tool, result).build(); + McpTool tool = new McpToolImpl(config); + JsonObject payload = MJS.toolCall(tool, result); JsonArray content = payload.getJsonArray("content"); assertThat(content.size(), is(1)); diff --git a/server/src/test/java/io/helidon/extensions/mcp/server/McpPromptTest.java b/server/src/test/java/io/helidon/extensions/mcp/server/McpPromptTest.java new file mode 100644 index 00000000..e70c0135 --- /dev/null +++ b/server/src/test/java/io/helidon/extensions/mcp/server/McpPromptTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class McpPromptTest { + @Test + void testMcpPromptCustom() { + McpPromptConfig config = McpPromptConfig.builder() + .name("name") + .title("title") + .description("description") + .prompt(request -> McpPromptResult.create()) + .build(); + McpPrompt prompt = new McpPromptImpl(config); + assertThat(prompt.name(), is("name")); + assertThat(prompt.title().orElse(""), is("title")); + assertThat(prompt.description(), is("description")); + } + + @Test + void testMcpPromptDefault() { + McpPromptConfig config = McpPromptConfig.builder() + .name("name") + .description("description") + .prompt(request -> McpPromptResult.create()) + .build(); + McpPrompt prompt = new McpPromptImpl(config); + assertThat(prompt.name(), is("name")); + assertThat(prompt.title().isEmpty(), is(true)); + assertThat(prompt.description(), is("description")); + } + + @Test + void testMcpPromptImplementation() { + Foo foo = new Foo(); + assertThat(foo.name(), is("name")); + assertThat(foo.title().isEmpty(), is(false)); + assertThat(foo.description(), is("description")); + assertThat(foo.arguments().size(), is(1)); + } + + private static class Foo implements McpPrompt { + @Override + public String name() { + return "name"; + } + + @Override + public String description() { + return "description"; + } + + @Override + public List arguments() { + return List.of(McpPromptArgument.builder() + .name("name") + .description("description") + .build()); + } + + @Override + public McpPromptResult prompt(McpPromptRequest request) { + return McpPromptResult.create(); + } + + @Override + public Optional title() { + return Optional.of("title"); + } + } +} diff --git a/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceLinkContentTest.java b/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceLinkContentTest.java index 9f7d0eed..3dda16fa 100644 --- a/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceLinkContentTest.java +++ b/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceLinkContentTest.java @@ -26,7 +26,7 @@ class McpResourceLinkContentTest { @Test void testDefaultResourceLinkContent() { - var content = McpResourceLinkContent.builder() + var content = McpToolResourceLinkContent.builder() .name("name") .uri("https://foo") .build(); @@ -41,7 +41,7 @@ void testDefaultResourceLinkContent() { @Test void testCustomResourceLinkContent() { - var content = McpResourceLinkContent.builder() + var content = McpToolResourceLinkContent.builder() .size(10) .name("name") .title("title") diff --git a/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceTemplateParameterTest.java b/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceTemplateParameterTest.java index 7f70a89e..56d0e7fe 100644 --- a/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceTemplateParameterTest.java +++ b/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceTemplateParameterTest.java @@ -16,8 +16,6 @@ package io.helidon.extensions.mcp.server; -import java.util.List; - import io.helidon.common.media.type.MediaTypes; import io.helidon.jsonrpc.core.JsonRpcParams; @@ -28,17 +26,17 @@ import static org.hamcrest.Matchers.is; class McpResourceTemplateParameterTest { - private final McpResource.Builder builder = McpResource.builder() + private final McpResourceConfig.Builder builder = McpResourceConfig.builder() .name("name") .description("description") .mediaType(MediaTypes.TEXT_PLAIN) - .resource(request -> List.of()); + .resource(request -> McpResourceResult.create()); @Test void testSimpleParameter() { JsonRpcParams params = JsonRpcParams.create(JsonValue.EMPTY_JSON_OBJECT); - var resource = builder.uri("https://{path}").build(); - McpResourceTemplate template = new McpResourceTemplate(resource); + McpResourceConfig resource = builder.uri("https://{path}").build(); + McpResourceTemplate template = new McpResourceTemplate(new McpResourceImpl(resource)); McpParameters parameters = template.parameters(params, "https://foo"); assertThat(parameters.get("path").asString().get(), is("foo")); @@ -48,7 +46,7 @@ void testSimpleParameter() { void testTwoParameter() { JsonRpcParams params = JsonRpcParams.create(JsonValue.EMPTY_JSON_OBJECT); var resource = builder.uri("https://{foo}/{bar}").build(); - McpResourceTemplate template = new McpResourceTemplate(resource); + McpResourceTemplate template = new McpResourceTemplate(new McpResourceImpl(resource)); McpParameters parameters = template.parameters(params, "https://foo/bar"); assertThat(parameters.get("foo").asString().get(), is("foo")); @@ -59,7 +57,7 @@ void testTwoParameter() { void testSpaceParameter() { JsonRpcParams params = JsonRpcParams.create(JsonValue.EMPTY_JSON_OBJECT); var resource = builder.uri("https://{foo}/{bar}").build(); - McpResourceTemplate template = new McpResourceTemplate(resource); + McpResourceTemplate template = new McpResourceTemplate(new McpResourceImpl(resource)); McpParameters parameters = template.parameters(params, "https://foo foo/bar bar"); assertThat(parameters.get("foo").asString().get(), is("foo foo")); @@ -70,7 +68,7 @@ void testSpaceParameter() { void testMiddleParameter() { JsonRpcParams params = JsonRpcParams.create(JsonValue.EMPTY_JSON_OBJECT); var resource = builder.uri("https://{foo}/path").build(); - McpResourceTemplate template = new McpResourceTemplate(resource); + McpResourceTemplate template = new McpResourceTemplate(new McpResourceImpl(resource)); McpParameters parameters = template.parameters(params, "https://foo/path"); assertThat(parameters.get("foo").asString().get(), is("foo")); @@ -80,7 +78,7 @@ void testMiddleParameter() { void testProtocolParameter() { JsonRpcParams params = JsonRpcParams.create(JsonValue.EMPTY_JSON_OBJECT); var resource = builder.uri("{protocol}://{foo}").build(); - McpResourceTemplate template = new McpResourceTemplate(resource); + McpResourceTemplate template = new McpResourceTemplate(new McpResourceImpl(resource)); McpParameters parameters = template.parameters(params, "https://foo"); assertThat(parameters.get("foo").asString().get(), is("foo")); diff --git a/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceTemplatePathMatchingTest.java b/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceTemplatePathMatchingTest.java index 224fd421..ceabf06c 100644 --- a/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceTemplatePathMatchingTest.java +++ b/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceTemplatePathMatchingTest.java @@ -16,7 +16,6 @@ package io.helidon.extensions.mcp.server; -import java.util.List; import java.util.regex.PatternSyntaxException; import io.helidon.common.media.type.MediaTypes; @@ -28,16 +27,16 @@ import static org.hamcrest.Matchers.startsWith; class McpResourceTemplatePathMatchingTest { - private final McpResource.Builder builder = McpResource.builder() + private final McpResourceConfig.Builder builder = McpResourceConfig.builder() .name("name") .description("description") .mediaType(MediaTypes.TEXT_PLAIN) - .resource(this::resource); + .resource(request -> McpResourceResult.create()); @Test void testSingleVariablePath() { var resource = builder.uri("https://{path}").build(); - McpResourceTemplate template = new McpResourceTemplate(resource); + McpResourceTemplate template = new McpResourceTemplate(new McpResourceImpl(resource)); assertThat(template.matches("https://foo"), is(true)); assertThat(template.matches("https://foo/"), is(false)); @@ -55,7 +54,7 @@ void testSingleVariablePath() { @Test void testMultipleVariablePath() { var resource = builder.uri("https://{path}/{path1}").build(); - McpResourceTemplate template = new McpResourceTemplate(resource); + McpResourceTemplate template = new McpResourceTemplate(new McpResourceImpl(resource)); assertThat(template.matches("https://foo/bar"), is(true)); assertThat(template.matches("https://foo-bar/foo-bar"), is(true)); @@ -73,7 +72,7 @@ void testMultipleVariablePath() { void testWrongVariablePath() { var resource = builder.uri("https://{path/path1}").build(); try { - McpResourceTemplate template = new McpResourceTemplate(resource); + McpResourceTemplate template = new McpResourceTemplate(new McpResourceImpl(resource)); } catch (PatternSyntaxException e) { assertThat(e.getMessage(), startsWith("Illegal repetition near index 9")); } @@ -83,13 +82,9 @@ void testWrongVariablePath() { void testWrongPath() { var resource = builder.uri("https://{path/path1").build(); try { - McpResourceTemplate template = new McpResourceTemplate(resource); + McpResourceTemplate template = new McpResourceTemplate(new McpResourceImpl(resource)); } catch (PatternSyntaxException e) { assertThat(e.getMessage(), startsWith("Illegal repetition near index 9")); } } - - private List resource(McpRequest request) { - return List.of(); - } } diff --git a/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceTest.java b/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceTest.java new file mode 100644 index 00000000..9696cf43 --- /dev/null +++ b/server/src/test/java/io/helidon/extensions/mcp/server/McpResourceTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026 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.server; + +import java.util.Optional; + +import io.helidon.common.media.type.MediaType; +import io.helidon.common.media.type.MediaTypes; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class McpResourceTest { + + @Test + void testDefaultMcpResource() { + McpResourceConfig resource = McpResourceConfig.builder() + .uri("uri") + .name("name") + .mediaType(MediaTypes.TEXT_PLAIN) + .resource(request -> McpResourceResult.create()) + .build(); + + assertThat(resource.uri(), is("uri")); + assertThat(resource.name(), is("name")); + assertThat(resource.title().isEmpty(), is(true)); + assertThat(resource.mediaType(), is(MediaTypes.TEXT_PLAIN)); + assertThat(resource.description(), is("No description available")); + } + + @Test + void testCustomMcpResource() { + McpResourceConfig resource = McpResourceConfig.builder() + .uri("uri") + .name("name") + .title("title") + .description("description") + .mediaType(MediaTypes.TEXT_PLAIN) + .resource(request -> McpResourceResult.create()) + .build(); + + assertThat(resource.uri(), is("uri")); + assertThat(resource.name(), is("name")); + assertThat(resource.title().orElse(""), is("title")); + assertThat(resource.description(), is("description")); + assertThat(resource.mediaType(), is(MediaTypes.TEXT_PLAIN)); + } + + @Test + void testMcpResourceImplementation() { + Foo foo = new Foo(); + assertThat(foo.uri(), is("uri")); + assertThat(foo.name(), is("name")); + assertThat(foo.title().orElse(""), is("title")); + assertThat(foo.description(), is("description")); + assertThat(foo.mediaType(), is(MediaTypes.TEXT_PLAIN)); + } + + static class Foo implements McpResource { + @Override + public String uri() { + return "uri"; + } + + @Override + public String name() { + return "name"; + } + + @Override + public String description() { + return "description"; + } + + @Override + public MediaType mediaType() { + return MediaTypes.TEXT_PLAIN; + } + + @Override + public McpResourceResult resource(McpResourceRequest request) { + return McpResourceResult.create(); + } + + @Override + public Optional title() { + return Optional.of("title"); + } + } +} diff --git a/server/src/test/java/io/helidon/extensions/mcp/server/McpSamplingRequestTest.java b/server/src/test/java/io/helidon/extensions/mcp/server/McpSamplingRequestTest.java index a53bb3b2..30723954 100644 --- a/server/src/test/java/io/helidon/extensions/mcp/server/McpSamplingRequestTest.java +++ b/server/src/test/java/io/helidon/extensions/mcp/server/McpSamplingRequestTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -18,13 +18,14 @@ import java.time.Duration; import java.util.List; +import io.helidon.common.media.type.MediaTypes; + import jakarta.json.JsonValue; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; class McpSamplingRequestTest { @@ -35,11 +36,13 @@ void testDefaultValues() { assertThat(request.maxTokens(), is(100)); assertThat(request.hints().isEmpty(), is(true)); - assertThat(request.messages().isEmpty(), is(true)); assertThat(request.metadata().isEmpty(), is(true)); assertThat(request.temperature().isEmpty(), is(true)); + assertThat(request.textMessages().isEmpty(), is(true)); assertThat(request.costPriority().isEmpty(), is(true)); assertThat(request.systemPrompt().isEmpty(), is(true)); + assertThat(request.imageMessages().isEmpty(), is(true)); + assertThat(request.audioMessages().isEmpty(), is(true)); assertThat(request.stopSequences().isEmpty(), is(true)); assertThat(request.speedPriority().isEmpty(), is(true)); assertThat(request.includeContext().isEmpty(), is(true)); @@ -61,7 +64,9 @@ void testCustomValues() { .timeout(Duration.ofSeconds(10)) .stopSequences(List.of("stop1")) .includeContext(McpIncludeContext.NONE) - .addMessage(McpSamplingMessages.textMessage("text", McpRole.USER)) + .addTextMessage(message -> message.text("text").role(McpRole.USER)) + .addImageMessage(message -> message.data(new byte[0]).mediaType(MediaTypes.TEXT_PLAIN).role(McpRole.ASSISTANT)) + .addAudioMessage(message -> message.data(new byte[0]).mediaType(MediaTypes.TEXT_PLAIN).role(McpRole.ASSISTANT)) .build(); assertThat(request.maxTokens(), is(1)); @@ -70,13 +75,28 @@ void testCustomValues() { assertThat(request.hints().isEmpty(), is(false)); assertThat(request.hints().get(), is(List.of("hint1"))); - assertThat(request.messages().isEmpty(), is(false)); - assertThat(request.messages().size(), is(1)); + assertThat(request.textMessages().isEmpty(), is(false)); + assertThat(request.textMessages().size(), is(1)); - var message = request.messages().getFirst(); - assertThat(message, instanceOf(McpSamplingTextMessage.class)); + McpSamplingTextMessage message = request.textMessages().getFirst(); + assertThat(message.text(), is("text")); assertThat(message.role(), is(McpRole.USER)); - assertThat(((McpSamplingTextMessage) message).text(), is("text")); + + assertThat(request.imageMessages().isEmpty(), is(false)); + assertThat(request.imageMessages().size(), is(1)); + + McpSamplingImageMessage image = request.imageMessages().getFirst(); + assertThat(image.data(), is(new byte[0])); + assertThat(image.role(), is(McpRole.ASSISTANT)); + assertThat(image.mediaType(), is(MediaTypes.TEXT_PLAIN)); + + assertThat(request.audioMessages().isEmpty(), is(false)); + assertThat(request.audioMessages().size(), is(1)); + + McpSamplingAudioMessage audio = request.audioMessages().getFirst(); + assertThat(audio.data(), is(new byte[0])); + assertThat(audio.role(), is(McpRole.ASSISTANT)); + assertThat(audio.mediaType(), is(MediaTypes.TEXT_PLAIN)); assertThat(request.metadata().isEmpty(), is(false)); assertThat(request.metadata().get(), is(JsonValue.TRUE)); diff --git a/server/src/test/java/io/helidon/extensions/mcp/server/McpSamplingResponseTest.java b/server/src/test/java/io/helidon/extensions/mcp/server/McpSamplingResponseTest.java index f2f7eb65..91a7920b 100644 --- a/server/src/test/java/io/helidon/extensions/mcp/server/McpSamplingResponseTest.java +++ b/server/src/test/java/io/helidon/extensions/mcp/server/McpSamplingResponseTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * Copyright (c) 2025, 2026 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. @@ -21,9 +21,6 @@ import org.junit.jupiter.api.Test; -import static io.helidon.extensions.mcp.server.McpSamplingMessages.audioMessage; -import static io.helidon.extensions.mcp.server.McpSamplingMessages.imageMessage; -import static io.helidon.extensions.mcp.server.McpSamplingMessages.textMessage; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; @@ -33,7 +30,7 @@ class McpSamplingResponseTest { @Test void testSamplingResponseTextMessage() { - var message = textMessage("text", McpRole.USER); + var message = McpSamplingTextMessage.builder().text("text").role(McpRole.USER).build(); McpSamplingResponse response = new McpSamplingResponseImpl(message, "helidon-model", McpStopReason.END_TURN); assertThat(response.model(), is("helidon-model")); @@ -52,7 +49,11 @@ void testSamplingResponseTextMessage() { @Test void testSamplingResponseImageMessage() { var data = "data".getBytes(StandardCharsets.UTF_8); - var message = imageMessage(data, MediaTypes.TEXT_PLAIN, McpRole.USER); + var message = McpSamplingImageMessage.builder() + .data(data) + .mediaType(MediaTypes.TEXT_PLAIN) + .role(McpRole.USER) + .build(); McpSamplingResponse response = new McpSamplingResponseImpl(message, "helidon-model", McpStopReason.END_TURN); assertThat(response.model(), is("helidon-model")); @@ -71,7 +72,11 @@ void testSamplingResponseImageMessage() { @Test void testSamplingResponseAudioMessage() { var data = "data".getBytes(StandardCharsets.UTF_8); - var message = audioMessage(data, MediaTypes.TEXT_PLAIN, McpRole.USER); + var message = McpSamplingAudioMessage.builder() + .data(data) + .mediaType(MediaTypes.TEXT_PLAIN) + .role(McpRole.USER) + .build(); McpSamplingResponse response = new McpSamplingResponseImpl(message, "helidon-model", McpStopReason.END_TURN); assertThat(response.model(), is("helidon-model")); diff --git a/server/src/test/java/io/helidon/extensions/mcp/server/McpToolTest.java b/server/src/test/java/io/helidon/extensions/mcp/server/McpToolTest.java index 397fd713..614aa055 100644 --- a/server/src/test/java/io/helidon/extensions/mcp/server/McpToolTest.java +++ b/server/src/test/java/io/helidon/extensions/mcp/server/McpToolTest.java @@ -15,9 +15,6 @@ */ package io.helidon.extensions.mcp.server; -import java.util.List; -import java.util.function.Function; - import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -26,14 +23,15 @@ class McpToolTest { @Test void testMcpToolCustom() { - McpTool tool = McpTool.builder() + McpToolConfig config = McpToolConfig.builder() .name("name") .title("title") .schema("schema") .description("description") .outputSchema("outputSchema") - .tool((request) -> null) + .tool(request -> McpToolResult.create()) .build(); + McpTool tool = new McpToolImpl(config); assertThat(tool.name(), is("name")); assertThat(tool.schema(), is("schema")); assertThat(tool.title().orElse(""), is("title")); @@ -43,12 +41,13 @@ void testMcpToolCustom() { @Test void testMcpToolDefault() { - McpTool tool = McpTool.builder() + McpToolConfig config = McpToolConfig.builder() .name("name") .schema("schema") .description("description") - .tool((request) -> null) + .tool(request -> McpToolResult.create()) .build(); + McpTool tool = new McpToolImpl(config); assertThat(tool.name(), is("name")); assertThat(tool.schema(), is("schema")); assertThat(tool.title().isEmpty(), is(true)); @@ -67,22 +66,24 @@ void testMcpToolImplementation() { } static class Foo implements McpTool { + @Override public String name() { return "name"; } + @Override public String description() { return "description"; } + @Override public String schema() { return "schema"; } - public Function tool() { - return request -> McpToolResult.builder() - .contents(List.of()) - .build(); + @Override + public McpToolResult tool(McpToolRequest request) { + return McpToolResult.create(); } } } diff --git a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/Langchain4jSseClientTest.java b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/Langchain4jSseClientTest.java index 22e56414..4821a26d 100644 --- a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/Langchain4jSseClientTest.java +++ b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/Langchain4jSseClientTest.java @@ -51,6 +51,7 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; @ServerTest class Langchain4jSseClientTest { @@ -145,7 +146,7 @@ void testToolCall() { @Test void testPromptCall() { var result = client.getPrompt(PROMPT_NAME, Map.of(PROMPT_ARGUMENT_NAME, "Praha")); - assertThat(result.description(), is(PROMPT_DESCRIPTION)); + assertThat(result.description(), is(nullValue())); var messages = result.messages(); assertThat(messages.size(), is(1)); diff --git a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/Langchain4jSseMultiplePromptTest.java b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/Langchain4jSseMultiplePromptTest.java index ffc97da1..2da50d0a 100644 --- a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/Langchain4jSseMultiplePromptTest.java +++ b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/Langchain4jSseMultiplePromptTest.java @@ -44,6 +44,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; @ServerTest class Langchain4jSseMultiplePromptTest { @@ -80,7 +81,7 @@ void listPrompts() { @Test void testPrompt1() { McpGetPromptResult prompt = client.getPrompt("prompt1", Map.of()); - assertThat(prompt.description(), is("Prompt 1")); + assertThat(prompt.description(), is(nullValue())); List messages = prompt.messages(); assertThat(messages.size(), is(1)); @@ -95,7 +96,7 @@ void testPrompt1() { @Test void testPrompt2() { McpGetPromptResult prompt = client.getPrompt("prompt2", Map.of()); - assertThat(prompt.description(), is("Prompt 2")); + assertThat(prompt.description(), is(nullValue())); List messages = prompt.messages(); assertThat(messages.size(), is(1)); @@ -111,7 +112,7 @@ void testPrompt2() { @Test void testPrompt3() { McpGetPromptResult prompt = client.getPrompt("prompt3", Map.of()); - assertThat(prompt.description(), is("Prompt 3")); + assertThat(prompt.description(), is(nullValue())); List messages = prompt.messages(); assertThat(messages.size(), is(1)); @@ -128,7 +129,7 @@ void testPrompt3() { @Test void testPrompt4() { McpGetPromptResult prompt = client.getPrompt("prompt4", Map.of("argument1", "text")); - assertThat(prompt.description(), is("Prompt 4")); + assertThat(prompt.description(), is(nullValue())); List messages = prompt.messages(); assertThat(messages.size(), is(3)); @@ -140,13 +141,13 @@ void testPrompt4() { assertThat(second.role(), is(McpRole.USER)); assertThat(third.role(), is(McpRole.USER)); - McpImageContent image = (McpImageContent) first.content(); + McpTextContent text = (McpTextContent) first.content(); + assertThat(text.text(), is("text")); + + McpImageContent image = (McpImageContent) second.content(); assertThat(image.data(), is(McpMedia.base64Media("helidon.png"))); assertThat(image.mimeType(), is(McpMedia.IMAGE_PNG_VALUE)); - McpTextContent text = (McpTextContent) second.content(); - assertThat(text.text(), is("text")); - McpEmbeddedResource resource = (McpEmbeddedResource) third.content(); McpTextResourceContents content = (McpTextResourceContents) resource.resource(); assertThat(content.text(), is("resource")); diff --git a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/Langchain4jSseMultipleResourceTest.java b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/Langchain4jSseMultipleResourceTest.java index 34b962f0..b1b6a85a 100644 --- a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/Langchain4jSseMultipleResourceTest.java +++ b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/Langchain4jSseMultipleResourceTest.java @@ -72,10 +72,10 @@ static void closeClient() throws Exception { @Test void listResources() { List list = client.listResources(); - assertThat(list.size(), is(3)); + assertThat(list.size(), is(4)); List names = list.stream().map(McpResource::name).toList(); - assertThat(names, hasItems("resource1", "resource2", "resource3")); + assertThat(names, hasItems("resource1", "resource2", "resource3", "resource4")); } @Test @@ -121,4 +121,16 @@ void readResource3() { assertThat(second.blob(), is(Base64.getEncoder().encodeToString("binary".getBytes(StandardCharsets.UTF_8)))); assertThat(second.mimeType(), is(MediaTypes.APPLICATION_JSON_VALUE)); } + + @Test + void readResource4() { + McpReadResourceResult resource = client.readResource("http://resource4"); + List contents = resource.contents(); + assertThat(contents.size(), is(1)); + + McpTextResourceContents first = (McpTextResourceContents) contents.getFirst(); + assertThat(first.uri(), is("http://resource4")); + assertThat(first.text(), is("http://resource4")); + assertThat(first.mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE)); + } } diff --git a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseClientTest.java b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseClientTest.java index 96601526..e39ed94f 100644 --- a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseClientTest.java +++ b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseClientTest.java @@ -174,7 +174,7 @@ void testToolCall() { void testPromptCall() { var result = client().getPrompt( new McpSchema.GetPromptRequest(PROMPT_NAME, Map.of(PROMPT_ARGUMENT_NAME, "Praha"))); - assertThat(result.description(), is(PROMPT_DESCRIPTION)); + assertThat(result.description(), is(nullValue())); var messages = result.messages(); assertThat(messages.size(), is(1)); diff --git a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseMultiplePromptTest.java b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseMultiplePromptTest.java index b99280e1..d2b99d02 100644 --- a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseMultiplePromptTest.java +++ b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseMultiplePromptTest.java @@ -113,12 +113,12 @@ void testPrompt4() { McpSchema.Content first = prompt4.messages().getFirst().content(); McpSchema.Content second = prompt4.messages().get(1).content(); McpSchema.Content third = prompt4.messages().get(2).content(); - assertThat(first.type(), is("image")); - assertThat(second.type(), is("text")); + assertThat(first.type(), is("text")); + assertThat(second.type(), is("image")); assertThat(third.type(), is("resource")); - McpSchema.ImageContent image = (McpSchema.ImageContent) first; - McpSchema.TextContent text = (McpSchema.TextContent) second; + McpSchema.TextContent text = (McpSchema.TextContent) first; + McpSchema.ImageContent image = (McpSchema.ImageContent) second; McpSchema.EmbeddedResource resource = (McpSchema.EmbeddedResource) third; assertThat(text.text(), is("text")); assertThat(image.data(), is(McpMedia.base64Media("helidon.png"))); diff --git a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseMultipleResourceTest.java b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseMultipleResourceTest.java index 4edbe7eb..ad767365 100644 --- a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseMultipleResourceTest.java +++ b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseMultipleResourceTest.java @@ -58,10 +58,10 @@ static void routing(HttpRouting.Builder builder) { @Test void listResources() { McpSchema.ListResourcesResult list = client().listResources(); - assertThat(list.resources().size(), is(3)); + assertThat(list.resources().size(), is(4)); List names = list.resources().stream().map(McpSchema.Resource::name).toList(); - assertThat(names, hasItems("resource1", "resource2", "resource3")); + assertThat(names, hasItems("resource1", "resource2", "resource3", "resource4")); } @Test @@ -101,4 +101,15 @@ void testReadResource3() { assertThat(second.uri(), is("http://resource3")); assertThat(second.mimeType(), is(MediaTypes.APPLICATION_JSON_VALUE)); } + + @Test + void testReadResource4() { + McpSchema.ReadResourceResult resource = client().readResource(new McpSchema.ReadResourceRequest("http://resource4")); + assertThat(resource.contents().size(), is(1)); + + McpSchema.TextResourceContents first = (McpSchema.TextResourceContents) resource.contents().getFirst(); + assertThat(first.uri(), is("http://resource4")); + assertThat(first.text(), is("http://resource4")); + assertThat(first.mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE)); + } } diff --git a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseMultipleToolTest.java b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseMultipleToolTest.java index fa51db54..9779b163 100644 --- a/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseMultipleToolTest.java +++ b/tests/2024-11-05/src/test/java/io/helidon/extensions/mcp/tests/McpSdkSseMultipleToolTest.java @@ -95,20 +95,20 @@ void testTool3() { McpSchema.Content first = tool3.content().getFirst(); McpSchema.Content second = tool3.content().get(1); McpSchema.Content third = tool3.content().get(2); - assertThat(first.type(), is("image")); - assertThat(second.type(), is("resource")); - assertThat(third.type(), is("text")); + assertThat(first.type(), is("text")); + assertThat(second.type(), is("image")); + assertThat(third.type(), is("resource")); - McpSchema.ImageContent image = (McpSchema.ImageContent) first; + McpSchema.TextContent text = (McpSchema.TextContent) first; + assertThat(text.text(), is("text")); + + McpSchema.ImageContent image = (McpSchema.ImageContent) second; assertThat(image.data(), is(McpMedia.base64Media("helidon.png"))); assertThat(image.mimeType(), is(McpMedia.IMAGE_PNG_VALUE)); - McpSchema.EmbeddedResource resource = (McpSchema.EmbeddedResource) second; + McpSchema.EmbeddedResource resource = (McpSchema.EmbeddedResource) third; assertThat(resource.resource().uri(), is("http://resource")); assertThat(resource.resource().mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE)); - - McpSchema.TextContent text = (McpSchema.TextContent) third; - assertThat(text.text(), is("text")); } @Test diff --git a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jClientTest.java b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jClientTest.java index 930803f8..464ac3ae 100644 --- a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jClientTest.java +++ b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jClientTest.java @@ -46,6 +46,7 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; abstract class AbstractLangchain4jClientTest { protected static McpClient client; @@ -125,7 +126,7 @@ void testToolCall() { @Test void testPromptCall() { var result = client.getPrompt(PROMPT_NAME, Map.of(PROMPT_ARGUMENT_NAME, "Praha")); - assertThat(result.description(), is(PROMPT_DESCRIPTION)); + assertThat(result.description(), is(nullValue())); var messages = result.messages(); assertThat(messages.size(), is(1)); diff --git a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultiplePromptTest.java b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultiplePromptTest.java index 763aa0ed..c44d9b51 100644 --- a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultiplePromptTest.java +++ b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultiplePromptTest.java @@ -39,6 +39,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; abstract class AbstractLangchain4jMultiplePromptTest { protected static McpClient client; @@ -63,7 +64,7 @@ void listPrompts() { @Test void testPrompt1() { McpGetPromptResult prompt = client.getPrompt("prompt1", Map.of()); - assertThat(prompt.description(), is("Prompt 1")); + assertThat(prompt.description(), is(nullValue())); List messages = prompt.messages(); assertThat(messages.size(), is(1)); @@ -78,7 +79,7 @@ void testPrompt1() { @Test void testPrompt2() { McpGetPromptResult prompt = client.getPrompt("prompt2", Map.of()); - assertThat(prompt.description(), is("Prompt 2")); + assertThat(prompt.description(), is(nullValue())); List messages = prompt.messages(); assertThat(messages.size(), is(1)); @@ -94,7 +95,7 @@ void testPrompt2() { @Test void testPrompt3() { McpGetPromptResult prompt = client.getPrompt("prompt3", Map.of()); - assertThat(prompt.description(), is("Prompt 3")); + assertThat(prompt.description(), is(nullValue())); List messages = prompt.messages(); assertThat(messages.size(), is(1)); @@ -111,7 +112,7 @@ void testPrompt3() { @Test void testPrompt4() { McpGetPromptResult prompt = client.getPrompt("prompt4", Map.of("argument1", "text")); - assertThat(prompt.description(), is("Prompt 4")); + assertThat(prompt.description(), is(nullValue())); List messages = prompt.messages(); assertThat(messages.size(), is(3)); @@ -123,13 +124,13 @@ void testPrompt4() { assertThat(second.role(), is(McpRole.USER)); assertThat(third.role(), is(McpRole.USER)); - McpImageContent image = (McpImageContent) first.content(); + McpTextContent text = (McpTextContent) first.content(); + assertThat(text.text(), is("text")); + + McpImageContent image = (McpImageContent) second.content(); assertThat(image.data(), is(McpMedia.base64Media("helidon.png"))); assertThat(image.mimeType(), is(McpMedia.IMAGE_PNG_VALUE)); - McpTextContent text = (McpTextContent) second.content(); - assertThat(text.text(), is("text")); - McpEmbeddedResource resource = (McpEmbeddedResource) third.content(); McpTextResourceContents content = (McpTextResourceContents) resource.resource(); assertThat(content.text(), is("resource")); diff --git a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultipleResourceTest.java b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultipleResourceTest.java index 584b2216..0ccccdd9 100644 --- a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultipleResourceTest.java +++ b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultipleResourceTest.java @@ -54,10 +54,10 @@ static void closeClient() throws Exception { @Test void listResources() { List list = client.listResources(); - assertThat(list.size(), is(3)); + assertThat(list.size(), is(4)); List names = list.stream().map(McpResource::name).toList(); - assertThat(names, hasItems("resource1", "resource2", "resource3")); + assertThat(names, hasItems("resource1", "resource2", "resource3", "resource4")); } @Test @@ -103,4 +103,16 @@ void readResource3() { assertThat(second.blob(), is(Base64.getEncoder().encodeToString("binary".getBytes(StandardCharsets.UTF_8)))); assertThat(second.mimeType(), is(MediaTypes.APPLICATION_JSON_VALUE)); } + + @Test + void readResource4() { + McpReadResourceResult resource = client.readResource("http://resource4"); + List contents = resource.contents(); + assertThat(contents.size(), is(1)); + + McpTextResourceContents first = (McpTextResourceContents) contents.getFirst(); + assertThat(first.uri(), is("http://resource4")); + assertThat(first.text(), is("http://resource4")); + assertThat(first.mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE)); + } } diff --git a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableClientTest.java b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableClientTest.java index b390c40f..14c14abf 100644 --- a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableClientTest.java +++ b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableClientTest.java @@ -175,7 +175,7 @@ void testToolCall() { void testPromptCall() { var result = client().getPrompt( new McpSchema.GetPromptRequest(PROMPT_NAME, Map.of(PROMPT_ARGUMENT_NAME, "Praha"))); - assertThat(result.description(), is(PROMPT_DESCRIPTION)); + assertThat(result.description(), is(nullValue())); var messages = result.messages(); assertThat(messages.size(), is(1)); @@ -213,9 +213,9 @@ void testCompletion() { new McpSchema.CompleteRequest.CompleteArgument(PROMPT_ARGUMENT_NAME, "f"))); var completion = result.completion(); - assertThat(completion.hasMore(), is(false)); assertThat(completion.total(), is(1)); assertThat(completion.values().size(), is(1)); + assertThat(completion.hasMore(), is(nullValue())); assertThat(completion.values().getFirst(), is("foo")); } } diff --git a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableCompletionTest.java b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableCompletionTest.java index 6c455e2b..d6153810 100644 --- a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableCompletionTest.java +++ b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableCompletionTest.java @@ -31,6 +31,7 @@ import static io.helidon.jsonrpc.core.JsonRpcError.INVALID_PARAMS; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertThrows; @ServerTest @@ -60,7 +61,7 @@ void testMcpSdkCompletion() { new McpSchema.CompleteRequest.CompleteArgument("argument", "Hel")); McpSchema.CompleteResult.CompleteCompletion result = client().completeCompletion(request).completion(); assertThat(result.total(), is(1)); - assertThat(result.hasMore(), is(false)); + assertThat(result.hasMore(), is(nullValue())); var list = result.values(); assertThat(list.size(), is(1)); diff --git a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultiplePromptTest.java b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultiplePromptTest.java index 5bd601c1..c25bb345 100644 --- a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultiplePromptTest.java +++ b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultiplePromptTest.java @@ -113,12 +113,12 @@ void testPrompt4() { McpSchema.Content first = prompt4.messages().getFirst().content(); McpSchema.Content second = prompt4.messages().get(1).content(); McpSchema.Content third = prompt4.messages().get(2).content(); - assertThat(first.type(), is("image")); - assertThat(second.type(), is("text")); + assertThat(first.type(), is("text")); + assertThat(second.type(), is("image")); assertThat(third.type(), is("resource")); - McpSchema.ImageContent image = (McpSchema.ImageContent) first; - McpSchema.TextContent text = (McpSchema.TextContent) second; + McpSchema.TextContent text = (McpSchema.TextContent) first; + McpSchema.ImageContent image = (McpSchema.ImageContent) second; McpSchema.EmbeddedResource resource = (McpSchema.EmbeddedResource) third; assertThat(text.text(), is("text")); assertThat(image.data(), is(McpMedia.base64Media("helidon.png"))); diff --git a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleResourceTest.java b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleResourceTest.java index a86d5c56..ab9c792b 100644 --- a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleResourceTest.java +++ b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleResourceTest.java @@ -59,10 +59,10 @@ static void routing(HttpRouting.Builder builder) { @Test void listResources() { McpSchema.ListResourcesResult list = client().listResources(); - assertThat(list.resources().size(), is(3)); + assertThat(list.resources().size(), is(4)); List names = list.resources().stream().map(McpSchema.Resource::name).toList(); - assertThat(names, hasItems("resource1", "resource2", "resource3")); + assertThat(names, hasItems("resource1", "resource2", "resource3", "resource4")); } @Test diff --git a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleToolTest.java b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleToolTest.java index 2d7334ca..4ae76313 100644 --- a/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleToolTest.java +++ b/tests/2025-03-26/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleToolTest.java @@ -97,25 +97,25 @@ void testTool3() { McpSchema.Content second = tool3.content().get(1); McpSchema.Content third = tool3.content().get(2); McpSchema.Content fourth = tool3.content().get(3); - assertThat(first.type(), is("image")); - assertThat(second.type(), is("resource")); - assertThat(third.type(), is("text")); - assertThat(fourth.type(), is("audio")); + assertThat(first.type(), is("text")); + assertThat(second.type(), is("image")); + assertThat(third.type(), is("audio")); + assertThat(fourth.type(), is("resource")); - McpSchema.ImageContent image = (McpSchema.ImageContent) first; + McpSchema.TextContent text = (McpSchema.TextContent) first; + assertThat(text.text(), is("text")); + + McpSchema.ImageContent image = (McpSchema.ImageContent) second; assertThat(image.data(), is(McpMedia.base64Media("helidon.png"))); assertThat(image.mimeType(), is(McpMedia.IMAGE_PNG_VALUE)); - McpSchema.EmbeddedResource resource = (McpSchema.EmbeddedResource) second; - assertThat(resource.resource().uri(), is("http://resource")); - assertThat(resource.resource().mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE)); - - McpSchema.TextContent text = (McpSchema.TextContent) third; - assertThat(text.text(), is("text")); - - McpSchema.AudioContent audio = (McpSchema.AudioContent) fourth; + McpSchema.AudioContent audio = (McpSchema.AudioContent) third; assertThat(audio.data(), is(McpMedia.base64Media("helidon.wav"))); assertThat(audio.mimeType(), is(McpMedia.AUDIO_WAV_VALUE)); + + McpSchema.EmbeddedResource resource = (McpSchema.EmbeddedResource) fourth; + assertThat(resource.resource().uri(), is("http://resource")); + assertThat(resource.resource().mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE)); } @Test diff --git a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jClientTest.java b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jClientTest.java index 930803f8..464ac3ae 100644 --- a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jClientTest.java +++ b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jClientTest.java @@ -46,6 +46,7 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; abstract class AbstractLangchain4jClientTest { protected static McpClient client; @@ -125,7 +126,7 @@ void testToolCall() { @Test void testPromptCall() { var result = client.getPrompt(PROMPT_NAME, Map.of(PROMPT_ARGUMENT_NAME, "Praha")); - assertThat(result.description(), is(PROMPT_DESCRIPTION)); + assertThat(result.description(), is(nullValue())); var messages = result.messages(); assertThat(messages.size(), is(1)); diff --git a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultiplePromptTest.java b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultiplePromptTest.java index 763aa0ed..c44d9b51 100644 --- a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultiplePromptTest.java +++ b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultiplePromptTest.java @@ -39,6 +39,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; abstract class AbstractLangchain4jMultiplePromptTest { protected static McpClient client; @@ -63,7 +64,7 @@ void listPrompts() { @Test void testPrompt1() { McpGetPromptResult prompt = client.getPrompt("prompt1", Map.of()); - assertThat(prompt.description(), is("Prompt 1")); + assertThat(prompt.description(), is(nullValue())); List messages = prompt.messages(); assertThat(messages.size(), is(1)); @@ -78,7 +79,7 @@ void testPrompt1() { @Test void testPrompt2() { McpGetPromptResult prompt = client.getPrompt("prompt2", Map.of()); - assertThat(prompt.description(), is("Prompt 2")); + assertThat(prompt.description(), is(nullValue())); List messages = prompt.messages(); assertThat(messages.size(), is(1)); @@ -94,7 +95,7 @@ void testPrompt2() { @Test void testPrompt3() { McpGetPromptResult prompt = client.getPrompt("prompt3", Map.of()); - assertThat(prompt.description(), is("Prompt 3")); + assertThat(prompt.description(), is(nullValue())); List messages = prompt.messages(); assertThat(messages.size(), is(1)); @@ -111,7 +112,7 @@ void testPrompt3() { @Test void testPrompt4() { McpGetPromptResult prompt = client.getPrompt("prompt4", Map.of("argument1", "text")); - assertThat(prompt.description(), is("Prompt 4")); + assertThat(prompt.description(), is(nullValue())); List messages = prompt.messages(); assertThat(messages.size(), is(3)); @@ -123,13 +124,13 @@ void testPrompt4() { assertThat(second.role(), is(McpRole.USER)); assertThat(third.role(), is(McpRole.USER)); - McpImageContent image = (McpImageContent) first.content(); + McpTextContent text = (McpTextContent) first.content(); + assertThat(text.text(), is("text")); + + McpImageContent image = (McpImageContent) second.content(); assertThat(image.data(), is(McpMedia.base64Media("helidon.png"))); assertThat(image.mimeType(), is(McpMedia.IMAGE_PNG_VALUE)); - McpTextContent text = (McpTextContent) second.content(); - assertThat(text.text(), is("text")); - McpEmbeddedResource resource = (McpEmbeddedResource) third.content(); McpTextResourceContents content = (McpTextResourceContents) resource.resource(); assertThat(content.text(), is("resource")); diff --git a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultipleResourceTest.java b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultipleResourceTest.java index 584b2216..0ccccdd9 100644 --- a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultipleResourceTest.java +++ b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/AbstractLangchain4jMultipleResourceTest.java @@ -54,10 +54,10 @@ static void closeClient() throws Exception { @Test void listResources() { List list = client.listResources(); - assertThat(list.size(), is(3)); + assertThat(list.size(), is(4)); List names = list.stream().map(McpResource::name).toList(); - assertThat(names, hasItems("resource1", "resource2", "resource3")); + assertThat(names, hasItems("resource1", "resource2", "resource3", "resource4")); } @Test @@ -103,4 +103,16 @@ void readResource3() { assertThat(second.blob(), is(Base64.getEncoder().encodeToString("binary".getBytes(StandardCharsets.UTF_8)))); assertThat(second.mimeType(), is(MediaTypes.APPLICATION_JSON_VALUE)); } + + @Test + void readResource4() { + McpReadResourceResult resource = client.readResource("http://resource4"); + List contents = resource.contents(); + assertThat(contents.size(), is(1)); + + McpTextResourceContents first = (McpTextResourceContents) contents.getFirst(); + assertThat(first.uri(), is("http://resource4")); + assertThat(first.text(), is("http://resource4")); + assertThat(first.mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE)); + } } diff --git a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableClientTest.java b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableClientTest.java index 4d079391..80290531 100644 --- a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableClientTest.java +++ b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableClientTest.java @@ -176,7 +176,7 @@ void testToolCall() { void testPromptCall() { var result = client().getPrompt( new McpSchema.GetPromptRequest(PROMPT_NAME, Map.of(PROMPT_ARGUMENT_NAME, "Praha"))); - assertThat(result.description(), is(PROMPT_DESCRIPTION)); + assertThat(result.description(), is(nullValue())); var messages = result.messages(); assertThat(messages.size(), is(1)); @@ -214,9 +214,9 @@ void testCompletion() { new McpSchema.CompleteRequest.CompleteArgument(PROMPT_ARGUMENT_NAME, "f"))); var completion = result.completion(); - assertThat(completion.hasMore(), is(false)); assertThat(completion.total(), is(1)); assertThat(completion.values().size(), is(1)); + assertThat(completion.hasMore(), is(nullValue())); assertThat(completion.values().getFirst(), is("foo")); } } diff --git a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableCompletionTest.java b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableCompletionTest.java index 62baf19b..4440955c 100644 --- a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableCompletionTest.java +++ b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableCompletionTest.java @@ -33,6 +33,7 @@ import static io.helidon.jsonrpc.core.JsonRpcError.INVALID_PARAMS; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertThrows; @ServerTest @@ -62,7 +63,7 @@ void testMcpSdkCompletion() { new McpSchema.CompleteRequest.CompleteArgument("argument", "Hel")); McpSchema.CompleteResult.CompleteCompletion result = client().completeCompletion(request).completion(); assertThat(result.total(), is(1)); - assertThat(result.hasMore(), is(false)); + assertThat(result.hasMore(), is(nullValue())); var list = result.values(); assertThat(list.size(), is(1)); diff --git a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultiplePromptTest.java b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultiplePromptTest.java index 5bd601c1..c25bb345 100644 --- a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultiplePromptTest.java +++ b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultiplePromptTest.java @@ -113,12 +113,12 @@ void testPrompt4() { McpSchema.Content first = prompt4.messages().getFirst().content(); McpSchema.Content second = prompt4.messages().get(1).content(); McpSchema.Content third = prompt4.messages().get(2).content(); - assertThat(first.type(), is("image")); - assertThat(second.type(), is("text")); + assertThat(first.type(), is("text")); + assertThat(second.type(), is("image")); assertThat(third.type(), is("resource")); - McpSchema.ImageContent image = (McpSchema.ImageContent) first; - McpSchema.TextContent text = (McpSchema.TextContent) second; + McpSchema.TextContent text = (McpSchema.TextContent) first; + McpSchema.ImageContent image = (McpSchema.ImageContent) second; McpSchema.EmbeddedResource resource = (McpSchema.EmbeddedResource) third; assertThat(text.text(), is("text")); assertThat(image.data(), is(McpMedia.base64Media("helidon.png"))); diff --git a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleResourceTest.java b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleResourceTest.java index a86d5c56..ab9c792b 100644 --- a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleResourceTest.java +++ b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleResourceTest.java @@ -59,10 +59,10 @@ static void routing(HttpRouting.Builder builder) { @Test void listResources() { McpSchema.ListResourcesResult list = client().listResources(); - assertThat(list.resources().size(), is(3)); + assertThat(list.resources().size(), is(4)); List names = list.resources().stream().map(McpSchema.Resource::name).toList(); - assertThat(names, hasItems("resource1", "resource2", "resource3")); + assertThat(names, hasItems("resource1", "resource2", "resource3", "resource4")); } @Test diff --git a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleToolTest.java b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleToolTest.java index fe99b830..dc84f8a4 100644 --- a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleToolTest.java +++ b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableMultipleToolTest.java @@ -101,28 +101,28 @@ void testTool3() { McpSchema.Content fourth = tool3.content().get(3); McpSchema.Content fifth = tool3.content().get(4); McpSchema.Content sixth = tool3.content().get(5); - assertThat(first.type(), is("image")); - assertThat(second.type(), is("resource")); - assertThat(third.type(), is("text")); - assertThat(fourth.type(), is("audio")); + assertThat(first.type(), is("text")); + assertThat(second.type(), is("image")); + assertThat(third.type(), is("audio")); + assertThat(fourth.type(), is("resource")); assertThat(fifth.type(), is("resource_link")); assertThat(sixth.type(), is("resource_link")); - McpSchema.ImageContent image = (McpSchema.ImageContent) first; + McpSchema.TextContent text = (McpSchema.TextContent) first; + assertThat(text.text(), is("text")); + + McpSchema.ImageContent image = (McpSchema.ImageContent) second; assertThat(image.data(), is(McpMedia.base64Media("helidon.png"))); assertThat(image.mimeType(), is(McpMedia.IMAGE_PNG_VALUE)); - McpSchema.EmbeddedResource resource = (McpSchema.EmbeddedResource) second; - assertThat(resource.resource().uri(), is("http://resource")); - assertThat(resource.resource().mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE)); - - McpSchema.TextContent text = (McpSchema.TextContent) third; - assertThat(text.text(), is("text")); - - McpSchema.AudioContent audio = (McpSchema.AudioContent) fourth; + McpSchema.AudioContent audio = (McpSchema.AudioContent) third; assertThat(audio.data(), is(McpMedia.base64Media("helidon.wav"))); assertThat(audio.mimeType(), is(McpMedia.AUDIO_WAV_VALUE)); + McpSchema.EmbeddedResource resource = (McpSchema.EmbeddedResource) fourth; + assertThat(resource.resource().uri(), is("http://resource")); + assertThat(resource.resource().mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE)); + McpSchema.ResourceLink link = (McpSchema.ResourceLink) fifth; assertThat(link.uri(), is("https://foo")); assertThat(link.name(), is("resource-link-default")); diff --git a/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableOutputSchemaToolsTest.java b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableOutputSchemaToolsTest.java new file mode 100644 index 00000000..3ce75037 --- /dev/null +++ b/tests/2025-06-18/src/test/java/io/helidon/extensions/mcp/tests/McpSdkStreamableOutputSchemaToolsTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2026 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.tests; + +import java.util.List; +import java.util.Map; + +import io.helidon.extensions.mcp.tests.common.OutputSchemaTools; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import io.modelcontextprotocol.client.McpClient; +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.McpSchema; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +@ServerTest +class McpSdkStreamableOutputSchemaToolsTest extends AbstractMcpSdkTest { + private final McpSyncClient client; + + McpSdkStreamableOutputSchemaToolsTest(WebServer server) { + client = McpClient.sync(streamable(server.port())).build(); + client.initialize(); + } + + @Override + McpSyncClient client() { + return client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + OutputSchemaTools.setUpRoute(builder); + } + + @Test + void listTools() { + McpSchema.ListToolsResult list = client.listTools(); + List tools = list.tools(); + assertThat(tools.size(), is(4)); + + McpSchema.Tool tool = tools.getFirst(); + assertThat(tool.name(), is("empty-output-schema")); + assertThat(tool.description(), is("Tool with an empty output schema")); + assertThat(tool.outputSchema().size(), is(2)); + assertThat(tool.inputSchema().properties().size(), is(0)); + + McpSchema.Tool tool1 = tools.get(1); + assertThat(tool1.name(), is("valid-output-schema")); + assertThat(tool1.description(), is("Tool with a valid output schema")); + assertThat(tool1.outputSchema().size(), is(3)); + assertThat(tool1.inputSchema().properties().size(), is(1)); + + McpSchema.Tool tool2 = tools.get(2); + assertThat(tool2.name(), is("invalid-output-schema")); + assertThat(tool2.description(), is(""" + Tool should return a structured content. This scenario work + until schema validation is implemented. + """)); + assertThat(tool2.outputSchema().size(), is(3)); + assertThat(tool2.inputSchema().properties().size(), is(0)); + + McpSchema.Tool tool3 = tools.get(3); + assertThat(tool3.name(), is("output-schema-and-content")); + assertThat(tool3.description(), is("Tool returns text content and structured content")); + assertThat(tool3.outputSchema().size(), is(3)); + assertThat(tool3.inputSchema().properties().size(), is(0)); + } + + @Test + void testEmptyOutputSchema() { + McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder().name("empty-output-schema").build(); + McpSchema.CallToolResult result = client.callTool(request); + assertThat(result.isError(), is(false)); + assertThat(result.content().size(), is(0)); + assertThat(result.structuredContent(), is(nullValue())); + } + + @Test + void testInvalidOutputSchema() { + McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder().name("invalid-output-schema").build(); + McpSchema.CallToolResult result = client.callTool(request); + assertThat(result.isError(), is(false)); + assertThat(result.content().size(), is(0)); + assertThat(result.structuredContent(), is(nullValue())); + } + + @Test + @SuppressWarnings("unchecked") + void testValidOutputSchema() { + McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder() + .name("valid-output-schema") + .arguments(Map.of("foo", "bar")) + .build(); + McpSchema.CallToolResult result = client.callTool(request); + assertThat(result.isError(), is(false)); + assertThat(result.content().size(), is(1)); + + McpSchema.Content content = result.content().getFirst(); + assertThat(content, instanceOf(McpSchema.TextContent.class)); + + McpSchema.TextContent textContent = (McpSchema.TextContent) content; + assertThat(textContent.text(), is("{\"foo\":\"bar\"}")); + + Object structuredContent = result.structuredContent(); + assertThat(structuredContent, is(notNullValue())); + assertThat(structuredContent, instanceOf(Map.class)); + Map map = (Map) structuredContent; + assertThat(map.get("foo"), is("bar")); + } + + @Test + @SuppressWarnings("unchecked") + void testOutputSchemaAndContent() { + McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder().name("output-schema-and-content").build(); + McpSchema.CallToolResult result = client.callTool(request); + assertThat(result.isError(), is(false)); + assertThat(result.content().size(), is(1)); + + McpSchema.Content content = result.content().getFirst(); + assertThat(content, instanceOf(McpSchema.TextContent.class)); + + McpSchema.TextContent textContent = (McpSchema.TextContent) content; + assertThat(textContent.text(), is("content")); + + Object structuredContent = result.structuredContent(); + assertThat(structuredContent, is(notNullValue())); + assertThat(structuredContent, instanceOf(Map.class)); + Map map = (Map) structuredContent; + assertThat(map.get("foo"), is("bar")); + } +} diff --git a/tests/codegen/src/test/java/io/helidon/extensions/mcp/codegen/McpTypesTest.java b/tests/codegen/src/test/java/io/helidon/extensions/mcp/codegen/McpTypesTest.java index 6d59a6bc..79fc75e6 100644 --- a/tests/codegen/src/test/java/io/helidon/extensions/mcp/codegen/McpTypesTest.java +++ b/tests/codegen/src/test/java/io/helidon/extensions/mcp/codegen/McpTypesTest.java @@ -34,9 +34,8 @@ import io.helidon.extensions.mcp.server.Mcp; import io.helidon.extensions.mcp.server.McpCancellation; import io.helidon.extensions.mcp.server.McpCompletion; -import io.helidon.extensions.mcp.server.McpCompletionContent; -import io.helidon.extensions.mcp.server.McpCompletionContents; import io.helidon.extensions.mcp.server.McpCompletionRequest; +import io.helidon.extensions.mcp.server.McpCompletionResult; import io.helidon.extensions.mcp.server.McpCompletionType; import io.helidon.extensions.mcp.server.McpFeatures; import io.helidon.extensions.mcp.server.McpLogger; @@ -44,23 +43,24 @@ import io.helidon.extensions.mcp.server.McpProgress; import io.helidon.extensions.mcp.server.McpPrompt; import io.helidon.extensions.mcp.server.McpPromptArgument; -import io.helidon.extensions.mcp.server.McpPromptContent; -import io.helidon.extensions.mcp.server.McpPromptContents; +import io.helidon.extensions.mcp.server.McpPromptRequest; +import io.helidon.extensions.mcp.server.McpPromptResult; import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpResource; -import io.helidon.extensions.mcp.server.McpResourceContent; -import io.helidon.extensions.mcp.server.McpResourceContents; +import io.helidon.extensions.mcp.server.McpResourceRequest; +import io.helidon.extensions.mcp.server.McpResourceResult; import io.helidon.extensions.mcp.server.McpResourceSubscriber; import io.helidon.extensions.mcp.server.McpResourceUnsubscriber; import io.helidon.extensions.mcp.server.McpRole; import io.helidon.extensions.mcp.server.McpRoots; import io.helidon.extensions.mcp.server.McpSampling; import io.helidon.extensions.mcp.server.McpServerConfig; +import io.helidon.extensions.mcp.server.McpSubscribeRequest; import io.helidon.extensions.mcp.server.McpTool; import io.helidon.extensions.mcp.server.McpToolAnnotations; -import io.helidon.extensions.mcp.server.McpToolContent; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; +import io.helidon.extensions.mcp.server.McpUnsubscribeRequest; import io.helidon.service.registry.GlobalServiceRegistry; import io.helidon.service.registry.Services; import io.helidon.webserver.http.HttpFeature; @@ -117,6 +117,8 @@ void testTypes() { checkField(toCheck, checked, fields, "MCP_PROMPT", Mcp.Prompt.class); checkField(toCheck, checked, fields, "MCP_VERSION", Mcp.Version.class); checkField(toCheck, checked, fields, "MCP_RESOURCE", Mcp.Resource.class); + checkField(toCheck, checked, fields, "MCP_TOOL_OUTPUT_SCHEMA", Mcp.ToolOutputSchema.class); + checkField(toCheck, checked, fields, "MCP_TOOL_OUTPUT_SCHEMA_TEXT", Mcp.ToolOutputSchemaText.class); checkField(toCheck, checked, fields, "MCP_TOOLS_PAGE_SIZE", Mcp.ToolsPageSize.class); checkField(toCheck, checked, fields, "MCP_PROMPTS_PAGE_SIZE", Mcp.PromptsPageSize.class); checkField(toCheck, checked, fields, "MCP_RESOURCES_PAGE_SIZE", Mcp.ResourcesPageSize.class); @@ -136,14 +138,10 @@ void testTypes() { checkField(toCheck, checked, fields, "MCP_PROMPT_INTERFACE", McpPrompt.class); checkField(toCheck, checked, fields, "MCP_CANCELLATION", McpCancellation.class); checkField(toCheck, checked, fields, "MCP_SERVER_CONFIG", McpServerConfig.class); - checkField(toCheck, checked, fields, "MCP_TOOL_CONTENTS", McpToolContents.class); checkField(toCheck, checked, fields, "MCP_RESOURCE_INTERFACE", McpResource.class); - checkField(toCheck, checked, fields, "MCP_PROMPT_CONTENTS", McpPromptContents.class); checkField(toCheck, checked, fields, "MCP_PROMPT_ARGUMENT", McpPromptArgument.class); checkField(toCheck, checked, fields, "MCP_COMPLETION_INTERFACE", McpCompletion.class); - checkField(toCheck, checked, fields, "MCP_RESOURCE_CONTENTS", McpResourceContents.class); checkField(toCheck, checked, fields, "MCP_COMPLETION_TYPE", McpCompletionType.class); - checkField(toCheck, checked, fields, "MCP_COMPLETION_CONTENTS", McpCompletionContents.class); checkField(toCheck, checked, fields, "HTTP_FEATURE", HttpFeature.class); checkField(toCheck, checked, fields, "HELIDON_MEDIA_TYPE", MediaType.class); checkField(toCheck, checked, fields, "HELIDON_MEDIA_TYPES", MediaTypes.class); @@ -152,26 +150,24 @@ void testTypes() { checkField(toCheck, checked, fields, "MCP_COMPLETION_REQUEST", McpCompletionRequest.class); checkField(toCheck, checked, fields, "MCP_RESOURCE_SUBSCRIBER", Mcp.ResourceSubscriber.class); checkField(toCheck, checked, fields, "MCP_RESOURCE_UNSUBSCRIBER", Mcp.ResourceUnsubscriber.class); + checkField(toCheck, checked, fields, "MCP_SUBSCRIBE_REQUEST", McpSubscribeRequest.class); + checkField(toCheck, checked, fields, "MCP_UNSUBSCRIBE_REQUEST", McpUnsubscribeRequest.class); checkField(toCheck, checked, fields, "MCP_RESOURCE_SUBSCRIBER_INTERFACE", McpResourceSubscriber.class); checkField(toCheck, checked, fields, "MCP_RESOURCE_UNSUBSCRIBER_INTERFACE", McpResourceUnsubscriber.class); checkField(toCheck, checked, fields, "SERVICES", Services.class); checkField(toCheck, checked, fields, "OPTIONAL_STRING", Optional.class); + checkField(toCheck, checked, fields, "OPTIONAL_TOOL_ANNOTATIONS", Optional.class); checkField(toCheck, checked, fields, "CONSUMER_REQUEST", Consumer.class); checkField(toCheck, checked, fields, "GLOBAL_SERVICE_REGISTRY", GlobalServiceRegistry.class); checkField(toCheck, checked, fields, "LIST_STRING", List.class); checkField(toCheck, checked, fields, "LIST_MCP_PROMPT_ARGUMENT", List.class); - checkField(toCheck, checked, fields, "LIST_MCP_RESOURCE_CONTENT", List.class); - checkField(toCheck, checked, fields, "LIST_MCP_TOOL_CONTENT", List.class); - checkField(toCheck, checked, fields, "LIST_MCP_PROMPT_CONTENT", List.class); - checkField(toCheck, checked, fields, "MCP_COMPLETION_CONTENT", McpCompletionContent.class); - checkField(toCheck, checked, fields, "MCP_RESOURCE_CONTENT", McpResourceContent.class); - checkField(toCheck, checked, fields, "MCP_PROMPT_CONTENT", McpPromptContent.class); - checkField(toCheck, checked, fields, "MCP_TOOL_CONTENT", McpToolContent.class); - checkField(toCheck, checked, fields, "FUNCTION_REQUEST_TOOL_RESULT", Function.class); - checkField(toCheck, checked, fields, "FUNCTION_REQUEST_LIST_RESOURCE_CONTENT", Function.class); - checkField(toCheck, checked, fields, "FUNCTION_REQUEST_LIST_PROMPT_CONTENT", Function.class); - checkField(toCheck, checked, fields, "FUNCTION_COMPLETION_REQUEST_COMPLETION_CONTENT", Function.class); + checkField(toCheck, checked, fields, "MCP_COMPLETION_RESULT", McpCompletionResult.class); + checkField(toCheck, checked, fields, "MCP_TOOL_REQUEST", McpToolRequest.class); + checkField(toCheck, checked, fields, "MCP_PROMPT_RESULT", McpPromptResult.class); + checkField(toCheck, checked, fields, "MCP_PROMPT_REQUEST", McpPromptRequest.class); + checkField(toCheck, checked, fields, "MCP_RESOURCE_RESULT", McpResourceResult.class); + checkField(toCheck, checked, fields, "MCP_RESOURCE_REQUEST", McpResourceRequest.class); assertThat("All the types from McpTypes must be tested.", toCheck, IsEmptyCollection.empty()); } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/CancelableTools.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/CancelableTools.java index e6301cb4..17315349 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/CancelableTools.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/CancelableTools.java @@ -17,16 +17,12 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; import io.helidon.extensions.mcp.server.McpCancellation; import io.helidon.extensions.mcp.server.McpCancellationResult; -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpServerFeature; import io.helidon.extensions.mcp.server.McpTool; -import io.helidon.extensions.mcp.server.McpToolContent; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.webserver.http.HttpRouting; @@ -76,32 +72,30 @@ public String schema() { } @Override - public Function tool() { - return request -> { - long now = System.currentTimeMillis(); - long timeout = now + TimeUnit.SECONDS.toMillis(5); - McpToolContent content = McpToolContents.textContent("Failed"); - McpCancellation cancellation = request.features().cancellation(); - cancellation.registerCancellationHook(latch::countDown); - - while (now < timeout) { - try { - McpCancellationResult result = cancellation.result(); - if (result.isRequested()) { - content = McpToolContents.textContent(result.reason()); - latch.countDown(); - break; - } - TimeUnit.MILLISECONDS.sleep(500); - now = System.currentTimeMillis(); - } catch (InterruptedException e) { - throw new RuntimeException(e); + public McpToolResult tool(McpToolRequest request) { + long now = System.currentTimeMillis(); + long timeout = now + TimeUnit.SECONDS.toMillis(5); + String content = "Failed"; + McpCancellation cancellation = request.features().cancellation(); + cancellation.registerCancellationHook(latch::countDown); + + while (now < timeout) { + try { + McpCancellationResult result = cancellation.result(); + if (result.isRequested()) { + content = result.reason(); + latch.countDown(); + break; } + TimeUnit.MILLISECONDS.sleep(500); + now = System.currentTimeMillis(); + } catch (InterruptedException e) { + throw new RuntimeException(e); } - return McpToolResult.builder() - .addContent(content) - .build(); - }; + } + return McpToolResult.builder() + .addTextContent(content) + .build(); } } @@ -128,20 +122,17 @@ public String schema() { } @Override - public Function tool() { - return request -> { - AtomicReference content = new AtomicReference<>(McpToolContents.textContent("Failed")); - McpCancellation cancellation = request.features().cancellation(); - cancellation.registerCancellationHook(latch::countDown); - try { - TimeUnit.SECONDS.sleep(3); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - return McpToolResult.builder() - .addContent(content.get()) - .build(); - }; + public McpToolResult tool(McpToolRequest request) { + McpCancellation cancellation = request.features().cancellation(); + cancellation.registerCancellationHook(latch::countDown); + try { + TimeUnit.SECONDS.sleep(3); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return McpToolResult.builder() + .addTextContent("Failed") + .build(); } } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/CompletionNotifications.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/CompletionNotifications.java index bf7fa1f6..244801a2 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/CompletionNotifications.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/CompletionNotifications.java @@ -18,14 +18,12 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import io.helidon.extensions.mcp.server.McpCompletion; -import io.helidon.extensions.mcp.server.McpCompletionContent; -import io.helidon.extensions.mcp.server.McpCompletionContents; import io.helidon.extensions.mcp.server.McpCompletionContext; import io.helidon.extensions.mcp.server.McpCompletionRequest; +import io.helidon.extensions.mcp.server.McpCompletionResult; import io.helidon.extensions.mcp.server.McpCompletionType; import io.helidon.extensions.mcp.server.McpServerFeature; import io.helidon.webserver.http.HttpRouting; @@ -62,15 +60,14 @@ public McpCompletionType referenceType() { } @Override - public Function completion() { - return this::complete; - } - - McpCompletionContent complete(McpCompletionRequest request) { + public McpCompletionResult completion(McpCompletionRequest request) { if (Objects.equals(request.value(), "Hel")) { - return McpCompletionContents.completion("Helidon"); + return McpCompletionResult.builder() + .addValue("Helidon") + .total(1) + .build(); } - return McpCompletionContents.completion(); + return McpCompletionResult.create(); } } @@ -82,17 +79,15 @@ public String reference() { } @Override - public Function completion() { - return completion -> { - String content = completion.context() - .map(McpCompletionContext::arguments) - .map(Map::entrySet) - .stream() - .flatMap(Set::stream) - .map(entry -> entry.getKey() + "," + entry.getValue()) - .collect(Collectors.joining(",")); - return McpCompletionContents.completion(content); - }; + public McpCompletionResult completion(McpCompletionRequest request) { + String content = request.context() + .map(McpCompletionContext::arguments) + .map(Map::entrySet) + .stream() + .flatMap(Set::stream) + .map(entry -> entry.getKey() + "," + entry.getValue()) + .collect(Collectors.joining(",")); + return McpCompletionResult.create(content); } } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/LoggingNotifications.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/LoggingNotifications.java index 3d0c3e0c..c988423c 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/LoggingNotifications.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/LoggingNotifications.java @@ -16,8 +16,7 @@ package io.helidon.extensions.mcp.tests.common; import io.helidon.extensions.mcp.server.McpServerFeature; -import io.helidon.extensions.mcp.server.McpTool; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolConfig; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.webserver.http.HttpRouting; @@ -36,7 +35,7 @@ private LoggingNotifications() { public static void setUpRoute(HttpRouting.Builder builder) { builder.addFeature(McpServerFeature.builder() .path("/") - .addTool(McpTool.builder() + .addTool(McpToolConfig.builder() .description("A tool that uses logging") .name("logging") .schema("") @@ -44,7 +43,7 @@ public static void setUpRoute(HttpRouting.Builder builder) { request.features().logger().info("Logging data"); request.features().logger().debug("Logging data"); return McpToolResult.builder() - .addContent(McpToolContents.textContent("Dummy text")) + .addTextContent("Dummy text") .build(); }) .build())); diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/McpExceptionServer.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/McpExceptionServer.java index a973c806..1200c225 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/McpExceptionServer.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/McpExceptionServer.java @@ -16,22 +16,23 @@ package io.helidon.extensions.mcp.tests.common; import java.util.List; -import java.util.function.Function; import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; import io.helidon.extensions.mcp.server.McpCompletion; -import io.helidon.extensions.mcp.server.McpCompletionContent; import io.helidon.extensions.mcp.server.McpCompletionRequest; +import io.helidon.extensions.mcp.server.McpCompletionResult; import io.helidon.extensions.mcp.server.McpException; import io.helidon.extensions.mcp.server.McpPrompt; import io.helidon.extensions.mcp.server.McpPromptArgument; -import io.helidon.extensions.mcp.server.McpPromptContent; -import io.helidon.extensions.mcp.server.McpRequest; +import io.helidon.extensions.mcp.server.McpPromptRequest; +import io.helidon.extensions.mcp.server.McpPromptResult; import io.helidon.extensions.mcp.server.McpResource; -import io.helidon.extensions.mcp.server.McpResourceContent; +import io.helidon.extensions.mcp.server.McpResourceRequest; +import io.helidon.extensions.mcp.server.McpResourceResult; import io.helidon.extensions.mcp.server.McpServerFeature; import io.helidon.extensions.mcp.server.McpTool; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.webserver.http.HttpRouting; @@ -81,10 +82,8 @@ public String schema() { } @Override - public Function tool() { - return request -> { - throw new McpException(INTERNAL_ERROR, MESSAGE); - }; + public McpToolResult tool(McpToolRequest request) { + throw new McpException(INTERNAL_ERROR, MESSAGE); } } @@ -100,11 +99,9 @@ public String schema() { } @Override - public Function tool() { - return request -> { - request.features().logger().info("Switching to the SSE channel"); - throw new McpException(INTERNAL_ERROR, MESSAGE); - }; + public McpToolResult tool(McpToolRequest request) { + request.features().logger().info("Switching to the SSE channel"); + throw new McpException(INTERNAL_ERROR, MESSAGE); } } @@ -132,10 +129,8 @@ public MediaType mediaType() { } @Override - public Function> resource() { - return request -> { - throw new McpException(INTERNAL_ERROR, MESSAGE); - }; + public McpResourceResult resource(McpResourceRequest request) { + throw new McpException(INTERNAL_ERROR, MESSAGE); } } @@ -146,11 +141,9 @@ public String uri() { } @Override - public Function> resource() { - return request -> { - request.features().logger().info("Switching to the SSE channel"); - throw new McpException(INTERNAL_ERROR, MESSAGE); - }; + public McpResourceResult resource(McpResourceRequest request) { + request.features().logger().info("Switching to the SSE channel"); + throw new McpException(INTERNAL_ERROR, MESSAGE); } } @@ -173,10 +166,8 @@ public List arguments() { } @Override - public Function> prompt() { - return request -> { - throw new McpException(INTERNAL_ERROR, MESSAGE); - }; + public McpPromptResult prompt(McpPromptRequest request) { + throw new McpException(INTERNAL_ERROR, MESSAGE); } } @@ -187,11 +178,9 @@ public String name() { } @Override - public Function> prompt() { - return request -> { - request.features().logger().info("Switching to the SSE channel"); - throw new McpException(INTERNAL_ERROR, MESSAGE); - }; + public McpPromptResult prompt(McpPromptRequest request) { + request.features().logger().info("Switching to the SSE channel"); + throw new McpException(INTERNAL_ERROR, MESSAGE); } } @@ -204,10 +193,8 @@ public String reference() { } @Override - public Function completion() { - return request -> { - throw new McpException(INTERNAL_ERROR, MESSAGE); - }; + public McpCompletionResult completion(McpCompletionRequest request) { + throw new McpException(INTERNAL_ERROR, MESSAGE); } } @@ -218,11 +205,9 @@ public String reference() { } @Override - public Function completion() { - return request -> { - request.features().logger().info("Switching to the SSE channel"); - throw new McpException(INTERNAL_ERROR, MESSAGE); - }; + public McpCompletionResult completion(McpCompletionRequest request) { + request.features().logger().info("Switching to the SSE channel"); + throw new McpException(INTERNAL_ERROR, MESSAGE); } } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/McpWeather.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/McpWeather.java index 72c66fb3..e20c5a4d 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/McpWeather.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/McpWeather.java @@ -15,19 +15,15 @@ */ package io.helidon.extensions.mcp.tests.common; -import java.util.List; - import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; -import io.helidon.extensions.mcp.server.McpCompletionContents; -import io.helidon.extensions.mcp.server.McpPromptContent; -import io.helidon.extensions.mcp.server.McpPromptContents; -import io.helidon.extensions.mcp.server.McpRequest; -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.McpCompletionResult; +import io.helidon.extensions.mcp.server.McpPromptRequest; +import io.helidon.extensions.mcp.server.McpPromptResult; +import io.helidon.extensions.mcp.server.McpResourceRequest; +import io.helidon.extensions.mcp.server.McpResourceResult; import io.helidon.extensions.mcp.server.McpServerFeature; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.webserver.http.HttpRouting; @@ -130,23 +126,30 @@ public static void setUpRoute(HttpRouting.Builder builder) { .addCompletion(completion -> completion .reference(PROMPT_NAME) - .completion(request -> McpCompletionContents.completion("foo")))); + .completion(request -> McpCompletionResult.builder() + .addValue("foo") + .total(1) + .build()))); } - static McpToolResult process(McpRequest request) { - String town = request.parameters().get("town").asString().orElse("unknown"); + static McpToolResult process(McpToolRequest request) { + String town = request.arguments().get("town").asString().orElse("unknown"); return McpToolResult.builder() - .addContent(McpToolContents.textContent("There is a hurricane in " + town)) + .addTextContent("There is a hurricane in " + town) .build(); } - static List prompt(McpRequest request) { - String town = request.parameters().get("town").asString().orElse("unknown"); + static McpPromptResult prompt(McpPromptRequest request) { + String town = request.arguments().get("town").asString().orElse("unknown"); String content = "What is the weather like in %s ?".formatted(town); - return List.of(McpPromptContents.textContent(content, McpRole.USER)); + return McpPromptResult.builder() + .addTextContent(content) + .build(); } - static List read(McpRequest features) { - return List.of(McpResourceContents.textContent("There are severe weather alerts in Praha")); + static McpResourceResult read(McpResourceRequest features) { + return McpResourceResult.builder() + .addTextContent("There are severe weather alerts in Praha") + .build(); } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MetaServer.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MetaServer.java index 1649fe70..a6aab636 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MetaServer.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MetaServer.java @@ -15,16 +15,11 @@ */ package io.helidon.extensions.mcp.tests.common; -import java.util.List; - import io.helidon.common.media.type.MediaTypes; -import io.helidon.extensions.mcp.server.McpCompletionContents; -import io.helidon.extensions.mcp.server.McpPromptContents; -import io.helidon.extensions.mcp.server.McpResourceContents; -import io.helidon.extensions.mcp.server.McpRole; +import io.helidon.extensions.mcp.server.McpCompletionResult; +import io.helidon.extensions.mcp.server.McpPromptResult; +import io.helidon.extensions.mcp.server.McpResourceResult; import io.helidon.extensions.mcp.server.McpServerFeature; -import io.helidon.extensions.mcp.server.McpToolContent; -import io.helidon.extensions.mcp.server.McpToolContents; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.webserver.http.HttpRouting; @@ -49,9 +44,8 @@ public static void setUpRoute(HttpRouting.Builder builder) { .schema("") .tool(request -> { String meta = request.meta().get("foo").asString().orElse("Not found"); - McpToolContent content = McpToolContents.textContent(meta); return McpToolResult.builder() - .addContent(content) + .addTextContent(meta) .build(); })) @@ -59,7 +53,9 @@ public static void setUpRoute(HttpRouting.Builder builder) { .description("Meta Prompt") .prompt(request -> { String meta = request.meta().get("foo").asString().orElse("Not found"); - return List.of(McpPromptContents.textContent(meta, McpRole.USER)); + return McpPromptResult.builder() + .addTextContent(meta) + .build(); })) .addResource(resource -> resource.uri("https://foo") @@ -68,13 +64,17 @@ public static void setUpRoute(HttpRouting.Builder builder) { .mediaType(MediaTypes.TEXT_PLAIN) .resource(request -> { String meta = request.meta().get("foo").asString().orElse("Not found"); - return List.of(McpResourceContents.textContent(meta)); + return McpResourceResult.builder() + .addTextContent(meta) + .build(); })) .addCompletion(completion -> completion.reference("meta-completion") .completion(request -> { String meta = request.meta().get("foo").asString().orElse("Not found"); - return McpCompletionContents.completion(meta); + return McpCompletionResult.builder() + .addValue(meta) + .build(); }))); } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultiplePrompt.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultiplePrompt.java index 1d995cd0..24987fe7 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultiplePrompt.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultiplePrompt.java @@ -17,15 +17,13 @@ import java.net.URI; import java.util.List; -import java.util.function.Function; +import io.helidon.common.media.type.MediaTypes; import io.helidon.extensions.mcp.server.McpParameters; import io.helidon.extensions.mcp.server.McpPrompt; import io.helidon.extensions.mcp.server.McpPromptArgument; -import io.helidon.extensions.mcp.server.McpPromptContent; -import io.helidon.extensions.mcp.server.McpPromptContents; -import io.helidon.extensions.mcp.server.McpRequest; -import io.helidon.extensions.mcp.server.McpResourceContents; +import io.helidon.extensions.mcp.server.McpPromptRequest; +import io.helidon.extensions.mcp.server.McpPromptResult; import io.helidon.extensions.mcp.server.McpRole; import io.helidon.extensions.mcp.server.McpServerFeature; import io.helidon.webserver.http.HttpRouting; @@ -47,34 +45,36 @@ public static void setUpRoute(HttpRouting.Builder builder) { .path("/") .addPrompt(prompt -> prompt.name("prompt1") .description("Prompt 1") - .prompt(request -> - List.of(McpPromptContents.textContent("text", McpRole.USER)))) + .prompt(request -> McpPromptResult.builder() + .addTextContent("text") + .build())) .addPrompt(prompt -> prompt.name("prompt2") .description("Prompt 2") - .prompt(request -> - List.of(McpPromptContents.imageContent( - McpMedia.media("helidon.png"), - McpMedia.IMAGE_PNG, - McpRole.ASSISTANT)))) + .prompt(request -> McpPromptResult.builder() + .addImageContent(image -> image.data(McpMedia.media("helidon.png")) + .mediaType(McpMedia.IMAGE_PNG) + .role(McpRole.ASSISTANT)) + .build())) .addPrompt(prompt -> prompt.name("prompt3") .description("Prompt 3") - .prompt(request -> - List.of(McpPromptContents.resourceContent( - URI.create("http://resource"), - McpResourceContents.textContent("resource"), - McpRole.ASSISTANT)))) + .prompt(request -> McpPromptResult.builder() + .addTextResourceContent(resource -> resource.uri(URI.create("http://resource")) + .text("resource") + .mediaType(MediaTypes.TEXT_PLAIN) + .role(McpRole.ASSISTANT)) + .build())) .addPrompt(new MyPrompt()) .addPrompt(prompt -> prompt.name("prompt5") .description("Prompt 5") - .prompt(request -> - List.of(McpPromptContents.audioContent( - McpMedia.media("helidon.wav"), - McpMedia.AUDIO_WAV, - McpRole.ASSISTANT))))); + .prompt(request -> McpPromptResult.builder() + .addAudioContent(audio -> audio.data(McpMedia.media("helidon.wav")) + .mediaType(McpMedia.AUDIO_WAV) + .role(McpRole.ASSISTANT)) + .build()))); } private static class MyPrompt implements McpPrompt { @@ -92,27 +92,25 @@ public String description() { @Override public List arguments() { return List.of(McpPromptArgument.builder() - .name("argument1") - .description("Argument 1") - .required(true) - .build()); + .name("argument1") + .description("Argument 1") + .required(true) + .build()); } @Override - public Function> prompt() { - return this::prompts; - } - - public List prompts(McpRequest request) { - McpParameters parameters = request.parameters(); - return List.of( - McpPromptContents.imageContent(McpMedia.media("helidon.png"), - McpMedia.IMAGE_PNG, - McpRole.USER), - McpPromptContents.textContent(parameters.get("argument1").asString().orElse("missing"), McpRole.USER), - McpPromptContents.resourceContent(URI.create("http://resource"), - McpResourceContents.textContent("resource"), - McpRole.USER)); + public McpPromptResult prompt(McpPromptRequest request) { + McpParameters parameters = request.arguments(); + return McpPromptResult.builder() + .addImageContent(image -> image.data(McpMedia.media("helidon.png")) + .mediaType(McpMedia.IMAGE_PNG) + .role(McpRole.USER)) + .addTextContent(parameters.get("argument1").asString().orElse("missing")) + .addTextResourceContent(resource -> resource.text("resource") + .uri(URI.create("http://resource")) + .mediaType(MediaTypes.TEXT_PLAIN) + .role(McpRole.USER)) + .build(); } } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultipleResource.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultipleResource.java index 5a5cd1be..5c7bf1cb 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultipleResource.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultipleResource.java @@ -17,29 +17,24 @@ package io.helidon.extensions.mcp.tests.common; import java.nio.charset.StandardCharsets; -import java.util.List; import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.function.Function; import io.helidon.common.context.Context; import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; import io.helidon.extensions.mcp.server.McpFeatures; -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpResource; -import io.helidon.extensions.mcp.server.McpResourceContent; -import io.helidon.extensions.mcp.server.McpResourceContents; +import io.helidon.extensions.mcp.server.McpResourceRequest; +import io.helidon.extensions.mcp.server.McpResourceResult; import io.helidon.extensions.mcp.server.McpResourceSubscriber; import io.helidon.extensions.mcp.server.McpResourceUnsubscriber; import io.helidon.extensions.mcp.server.McpServerFeature; +import io.helidon.extensions.mcp.server.McpSubscribeRequest; +import io.helidon.extensions.mcp.server.McpUnsubscribeRequest; import io.helidon.webserver.http.HttpRouting; -import static io.helidon.extensions.mcp.server.McpResourceContents.binaryContent; -import static io.helidon.extensions.mcp.server.McpResourceContents.textContent; - /** * Resource server tests. */ @@ -61,7 +56,9 @@ public static void setUpRoute(HttpRouting.Builder builder) { .description("Resource 1") .uri("http://resource1") .mediaType(MediaTypes.TEXT_PLAIN) - .resource(param -> List.of(textContent("text")))) + .resource(param -> McpResourceResult.builder() + .addTextContent("text") + .build())) .addResource(resource -> resource .name("resource2") @@ -69,13 +66,15 @@ public static void setUpRoute(HttpRouting.Builder builder) { .description("Resource 2") .uri("http://resource2") .mediaType(MediaTypes.APPLICATION_JSON) - .resource(param -> List.of( - binaryContent("binary".getBytes(StandardCharsets.UTF_8), - MediaTypes.APPLICATION_JSON)))) + .resource(param -> McpResourceResult.builder() + .addBinaryContent("binary".getBytes(StandardCharsets.UTF_8), + MediaTypes.APPLICATION_JSON) + .build())) .addResource(myResource) .addResourceSubscriber(new MyResourceSubscriber(myResource)) - .addResourceUnsubscriber(new MyResourceUnsubscriber(myResource))); + .addResourceUnsubscriber(new MyResourceUnsubscriber(myResource)) + .addResource(new UriEchoResource())); } private static final Context CONTEXT = Context.create(); @@ -93,9 +92,9 @@ public static Context context() { * This global state is required until we support keeping state in subscribers * and unsubscribers. It must be stored in {@link #CONTEXT}. * - * @param readLatch resource read latch + * @param readLatch resource read latch * @param updateLatch resource update latch - * @param cancelled subscription cancellation + * @param cancelled subscription cancellation */ public record State(CountDownLatch readLatch, CountDownLatch updateLatch, AtomicBoolean cancelled) { } @@ -128,17 +127,16 @@ public MediaType mediaType() { } @Override - public Function> resource() { - return request -> { - context().get(State.class).ifPresent(state -> state.readLatch().countDown()); - return List.of(McpResourceContents.textContent("text"), - McpResourceContents.binaryContent("binary".getBytes(StandardCharsets.UTF_8), - MediaTypes.APPLICATION_JSON)); - }; + public McpResourceResult resource(McpResourceRequest request) { + context().get(State.class).ifPresent(state -> state.readLatch().countDown()); + return McpResourceResult.builder() + .addTextContent("text") + .addBinaryContent(binary -> binary.data("binary".getBytes(StandardCharsets.UTF_8)) + .mediaType(MediaTypes.APPLICATION_JSON)) + .build(); } } - private static final class MyResourceSubscriber implements McpResourceSubscriber { private final MyResource resource; @@ -153,20 +151,18 @@ public String uri() { } @Override - public Consumer subscribe() { - return request -> { - State state = context().get(State.class).orElseThrow(); - try { - McpFeatures features = request.features(); - while (!state.cancelled().get() && state.updateLatch().getCount() > 0) { - features.subscriptions().sendSessionUpdate(resource.uri()); - Thread.sleep(100); // simulate delay - state.updateLatch().countDown(); - } - } catch (InterruptedException e) { - throw new RuntimeException(e); + public void subscribe(McpSubscribeRequest request) { + State state = context().get(State.class).orElseThrow(); + try { + McpFeatures features = request.features(); + while (!state.cancelled().get() && state.updateLatch().getCount() > 0) { + features.subscriptions().sendSessionUpdate(resource.uri()); + Thread.sleep(100); // simulate delay + state.updateLatch().countDown(); } - }; + } catch (InterruptedException e) { + throw new RuntimeException(e); + } } } @@ -184,11 +180,37 @@ public String uri() { } @Override - public Consumer unsubscribe() { - return request -> { - State state = context().get(State.class).orElseThrow(); - state.cancelled.set(true); - }; + public void unsubscribe(McpUnsubscribeRequest request) { + State state = context().get(State.class).orElseThrow(); + state.cancelled.set(true); + } + } + + private static class UriEchoResource implements McpResource { + + @Override + public String uri() { + return "http://resource4"; + } + + @Override + public String name() { + return "resource4"; + } + + @Override + public String description() { + return "Uri Echo Resource"; + } + + @Override + public MediaType mediaType() { + return MediaTypes.TEXT_PLAIN; + } + + @Override + public McpResourceResult resource(McpResourceRequest request) { + return McpResourceResult.create(request.uri().toASCIIString()); } } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultipleResourceTemplate.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultipleResourceTemplate.java index 566d729f..dc431639 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultipleResourceTemplate.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultipleResourceTemplate.java @@ -15,15 +15,11 @@ */ package io.helidon.extensions.mcp.tests.common; -import java.util.List; -import java.util.function.Function; - import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpResource; -import io.helidon.extensions.mcp.server.McpResourceContent; -import io.helidon.extensions.mcp.server.McpResourceContents; +import io.helidon.extensions.mcp.server.McpResourceRequest; +import io.helidon.extensions.mcp.server.McpResourceResult; import io.helidon.extensions.mcp.server.McpServerFeature; import io.helidon.webserver.http.HttpRouting; @@ -71,12 +67,14 @@ public static void setUpRoute(HttpRouting.Builder builder) { .addResource(new MyResource())); } - private static List resource(McpRequest request) { + private static McpResourceResult resource(McpResourceRequest request) { String path = request.parameters() .get("path") .asString() .orElse("Unknown"); - return List.of(McpResourceContents.textContent(path)); + return McpResourceResult.builder() + .addTextContent(path) + .build(); } private static final class MyResource implements McpResource { @@ -101,11 +99,7 @@ public MediaType mediaType() { } @Override - public Function> resource() { - return this::read; - } - - List read(McpRequest request) { + public McpResourceResult resource(McpResourceRequest request) { String foo = request.parameters() .get("foo") .asString() @@ -114,9 +108,10 @@ List read(McpRequest request) { .get("bar") .asString() .orElse("Unknown"); - return List.of( - McpResourceContents.textContent(foo), - McpResourceContents.textContent(bar)); + return McpResourceResult.builder() + .addTextContent(foo) + .addTextContent(bar) + .build(); } } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultipleTool.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultipleTool.java index 4f5e9920..24af7537 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultipleTool.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/MultipleTool.java @@ -17,25 +17,18 @@ import java.net.URI; import java.util.Optional; -import java.util.function.Function; import io.helidon.common.media.type.MediaTypes; import io.helidon.extensions.mcp.server.McpParameters; -import io.helidon.extensions.mcp.server.McpRequest; -import io.helidon.extensions.mcp.server.McpResourceContents; import io.helidon.extensions.mcp.server.McpServerFeature; import io.helidon.extensions.mcp.server.McpTool; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.json.schema.Schema; import io.helidon.json.schema.SchemaNumber; import io.helidon.json.schema.SchemaString; import io.helidon.webserver.http.HttpRouting; -import static io.helidon.extensions.mcp.server.McpToolContents.audioContent; -import static io.helidon.extensions.mcp.server.McpToolContents.imageContent; -import static io.helidon.extensions.mcp.server.McpToolContents.resourceContent; -import static io.helidon.extensions.mcp.server.McpToolContents.resourceLinkContent; -import static io.helidon.extensions.mcp.server.McpToolContents.textContent; /** * Tool server test. @@ -65,36 +58,34 @@ public static void setUpRoute(HttpRouting.Builder builder) { .description("Tool 1") .schema(SIMPLE_SCHEMA) .tool(request -> McpToolResult.builder() - .addContent(imageContent(McpMedia.media("helidon.png"), - McpMedia.IMAGE_PNG)) + .addImageContent(McpMedia.media("helidon.png"), McpMedia.IMAGE_PNG) .build())) .addTool(tool -> tool.name("tool2") .description("Tool 2") .schema(SIMPLE_SCHEMA) .tool(request -> McpToolResult.builder() - .addContent(resourceContent( - URI.create("http://resource"), - McpResourceContents.textContent("resource"))) + .addTextResourceContent(resource -> resource.uri(URI.create("http://resource")) + .text("resource") + .mediaType(MediaTypes.TEXT_PLAIN)) .build())) .addTool(tool -> tool.name("tool3") .description("Tool 3") .schema(SIMPLE_SCHEMA) .title("Tool 3 Title") .tool(request -> McpToolResult.builder() - .addContent(imageContent(McpMedia.media("helidon.png"), - McpMedia.IMAGE_PNG)) - .addContent(resourceContent(URI.create("http://resource"), - McpResourceContents.textContent("resource"))) - .addContent(textContent("text")) - .addContent(audioContent(McpMedia.media("helidon.wav"), - McpMedia.AUDIO_WAV)) - .addContent(resourceLinkContent("resource-link-default", "https://foo")) - .addContent(resourceLinkContent(link -> link.name("resource-link-custom") + .addImageContent(McpMedia.media("helidon.png"), McpMedia.IMAGE_PNG) + .addTextResourceContent(resource -> resource.uri(URI.create("http://resource")) + .text("resource") + .mediaType(MediaTypes.TEXT_PLAIN)) + .addTextContent("text") + .addAudioContent(McpMedia.media("helidon.wav"), McpMedia.AUDIO_WAV) + .addResourceLinkContent("resource-link-default", "https://foo") + .addResourceLinkContent(link -> link.name("resource-link-custom") .size(10) .title("title") .uri("https://foo") .description("description") - .mediaType(MediaTypes.TEXT_PLAIN))) + .mediaType(MediaTypes.TEXT_PLAIN)) .build())) .addTool(new TownTool()) .addTool(tool -> tool.name("tool5") @@ -127,21 +118,19 @@ public String schema() { } @Override - public Optional title() { - return Optional.of("Tool 4 Title"); + public McpToolResult tool(McpToolRequest request) { + McpParameters parameters = request.arguments(); + String name = parameters.get("name").asString().orElse("unknown"); + int population = parameters.get("population").asInteger().orElse(-1); + String content = String.format("%s has a population of %d inhabitants", name, population); + return McpToolResult.builder() + .addTextContent(content) + .build(); } @Override - public Function tool() { - return request -> { - McpParameters parameters = request.parameters(); - String name = parameters.get("name").asString().orElse("unknown"); - int population = parameters.get("population").asInteger().orElse(-1); - String content = String.format("%s has a population of %d inhabitants", name, population); - return McpToolResult.builder() - .addContent(textContent(content)) - .build(); - }; + public Optional title() { + return Optional.of("Tool 4 Title"); } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/OutputSchemaTools.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/OutputSchemaTools.java new file mode 100644 index 00000000..f00030f3 --- /dev/null +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/OutputSchemaTools.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2026 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.tests.common; + +import java.util.Optional; + +import io.helidon.extensions.mcp.server.McpServerConfig; +import io.helidon.extensions.mcp.server.McpTool; +import io.helidon.extensions.mcp.server.McpToolRequest; +import io.helidon.extensions.mcp.server.McpToolResult; +import io.helidon.json.schema.Schema; +import io.helidon.json.schema.SchemaObject; +import io.helidon.json.schema.SchemaString; +import io.helidon.webserver.http.HttpRouting; + +/** + * Output schema tool testing. + */ +public class OutputSchemaTools { + private OutputSchemaTools() { + } + + /** + * Setup webserver routing. + * + * @param builder routing builder + */ + public static void setUpRoute(HttpRouting.Builder builder) { + builder.addFeature(McpServerConfig.builder() + .path("/") + .addTool(new EmptyOutputSchemaTool()) + .addTool(new ValidOutputSchemaTool()) + .addTool(new InvalidOutputSchemaTool()) + .addTool(new OutputSchemaAndContentTool())); + } + + private static class EmptyOutputSchemaTool implements McpTool { + @Override + public String name() { + return "empty-output-schema"; + } + + @Override + public String description() { + return "Tool with an empty output schema"; + } + + @Override + public String schema() { + return ""; + } + + @Override + public McpToolResult tool(McpToolRequest request) { + return McpToolResult.create(); + } + + @Override + public Optional outputSchema() { + return Optional.of(Schema.builder() + .rootObject(SchemaObject.Builder::build) + .build() + .generate()); + } + } + + private static class ValidOutputSchemaTool implements McpTool { + @Override + public String name() { + return "valid-output-schema"; + } + + @Override + public String description() { + return "Tool with a valid output schema"; + } + + @Override + public String schema() { + return Schema.builder() + .rootObject(object -> object.addStringProperty("foo", SchemaString.create())) + .build() + .generate(); + } + + @Override + public McpToolResult tool(McpToolRequest request) { + // When structured content is set without text content, Helidon must serialize it + // and add it as text content to preserve backward compatibility. + return request.arguments() + .get("foo") + .asString() + .map(StructuredContentPojo::new) + .flatMap(value -> Optional.of(McpToolResult.builder().structuredContent(value).build())) + .orElse(McpToolResult.create("Unknown")); + } + + @Override + public Optional outputSchema() { + return Optional.of(Schema.builder() + .rootObject(object -> object.addStringProperty("foo", SchemaString.create())) + .build() + .generate()); + } + } + + private static class InvalidOutputSchemaTool implements McpTool { + + @Override + public String name() { + return "invalid-output-schema"; + } + + @Override + public String description() { + return """ + Tool should return a structured content. This scenario work + until schema validation is implemented. + """; + } + + @Override + public String schema() { + return ""; + } + + @Override + public McpToolResult tool(McpToolRequest request) { + return McpToolResult.create(); + } + + @Override + public Optional outputSchema() { + return Optional.of(Schema.builder() + .rootObject(object -> object.addStringProperty("foo", SchemaString.create())) + .build() + .generate()); + } + } + + private static class OutputSchemaAndContentTool implements McpTool { + + @Override + public String name() { + return "output-schema-and-content"; + } + + @Override + public String description() { + return "Tool returns text content and structured content"; + } + + @Override + public String schema() { + return ""; + } + + @Override + public McpToolResult tool(McpToolRequest request) { + return McpToolResult.builder() + .addTextContent("content") + .structuredContent(new StructuredContentPojo("bar")) + .build(); + } + + @Override + public Optional outputSchema() { + return Optional.of(Schema.builder() + .rootObject(object -> object.addStringProperty("foo", SchemaString.create())) + .build() + .generate()); + } + } + + /** + * Structured output content. + * + * @param foo foo + */ + public record StructuredContentPojo(String foo) { + } +} diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/PaginationServer.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/PaginationServer.java index 1d5a99ca..8f61a20c 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/PaginationServer.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/PaginationServer.java @@ -16,22 +16,19 @@ package io.helidon.extensions.mcp.tests.common; import java.util.List; -import java.util.function.Function; import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; import io.helidon.extensions.mcp.server.McpPrompt; import io.helidon.extensions.mcp.server.McpPromptArgument; -import io.helidon.extensions.mcp.server.McpPromptContent; -import io.helidon.extensions.mcp.server.McpPromptContents; -import io.helidon.extensions.mcp.server.McpRequest; +import io.helidon.extensions.mcp.server.McpPromptRequest; +import io.helidon.extensions.mcp.server.McpPromptResult; import io.helidon.extensions.mcp.server.McpResource; -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.McpResourceRequest; +import io.helidon.extensions.mcp.server.McpResourceResult; import io.helidon.extensions.mcp.server.McpServerFeature; import io.helidon.extensions.mcp.server.McpTool; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.json.schema.Schema; import io.helidon.json.schema.SchemaString; @@ -89,9 +86,9 @@ public String schema() { } @Override - public Function tool() { - return request -> McpToolResult.builder() - .addContent(McpToolContents.textContent("text")) + public McpToolResult tool(McpToolRequest request) { + return McpToolResult.builder() + .addTextContent("text") .build(); } } @@ -108,8 +105,10 @@ public List arguments() { } @Override - public Function> prompt() { - return request -> List.of(McpPromptContents.textContent("text", McpRole.USER)); + public McpPromptResult prompt(McpPromptRequest request) { + return McpPromptResult.builder() + .addTextContent("text") + .build(); } } @@ -130,8 +129,10 @@ public MediaType mediaType() { } @Override - public Function> resource() { - return request -> List.of(McpResourceContents.textContent("text")); + public McpResourceResult resource(McpResourceRequest request) { + return McpResourceResult.builder() + .addTextContent("text") + .build(); } } @@ -157,8 +158,8 @@ public MediaType mediaType() { } @Override - public Function> resource() { - return null; + public McpResourceResult resource(McpResourceRequest request) { + return McpResourceResult.create(); } } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ProgressNotifications.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ProgressNotifications.java index 20d2178b..d6d4bbcb 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ProgressNotifications.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ProgressNotifications.java @@ -15,13 +15,10 @@ */ package io.helidon.extensions.mcp.tests.common; -import java.util.function.Function; - import io.helidon.extensions.mcp.server.McpFeatures; -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpServerFeature; import io.helidon.extensions.mcp.server.McpTool; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.webserver.http.HttpRouting; @@ -61,32 +58,30 @@ public String schema() { } @Override - public Function tool() { - return request -> { - McpFeatures features = request.features(); + public McpToolResult tool(McpToolRequest request) { + McpFeatures features = request.features(); - // add a message to notifications - boolean addMessage = !request.protocolVersion().startsWith("2024"); + // add a message to notifications + boolean addMessage = !request.protocolVersion().startsWith("2024"); - // send progress reports - var progress = features.progress(); - progress.total(100); - try { - for (int i = 1; i <= 10; i++) { - Thread.sleep(50); - if (addMessage) { - progress.send(i * 10, "Elapsed time is " + (50 * i)); - } else { - progress.send(i * 10); - } + // send progress reports + var progress = features.progress(); + progress.total(100); + try { + for (int i = 1; i <= 10; i++) { + Thread.sleep(50); + if (addMessage) { + progress.send(i * 10, "Elapsed time is " + (50 * i)); + } else { + progress.send(i * 10); } - } catch (InterruptedException e) { - throw new RuntimeException(e); } - return McpToolResult.builder() - .addContent(McpToolContents.textContent("Dummy text")) - .build(); - }; + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return McpToolResult.builder() + .addTextContent("Dummy text") + .build(); } } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ProtocolVersion.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ProtocolVersion.java index 25b3e127..f35d03ad 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ProtocolVersion.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ProtocolVersion.java @@ -16,8 +16,7 @@ package io.helidon.extensions.mcp.tests.common; import io.helidon.extensions.mcp.server.McpServerFeature; -import io.helidon.extensions.mcp.server.McpTool; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolConfig; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.webserver.http.HttpRouting; @@ -37,9 +36,9 @@ private ProtocolVersion() { public static void setUpRoute(HttpRouting.Builder builder) { builder.addFeature(McpServerFeature.builder() .path("/") - .addTool(McpTool.builder() + .addTool(McpToolConfig.builder() .tool(r -> McpToolResult.builder() - .addContent(McpToolContents.textContent(r.protocolVersion())) + .addTextContent(r.protocolVersion()) .build()) .description("A tool that returns the protocol version") .name("protocolVersion") diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ResourceSubscriptions.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ResourceSubscriptions.java index e0eca4e6..5a0079ae 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ResourceSubscriptions.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ResourceSubscriptions.java @@ -15,17 +15,14 @@ */ package io.helidon.extensions.mcp.tests.common; -import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.function.Function; import io.helidon.common.context.Context; import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpResource; -import io.helidon.extensions.mcp.server.McpResourceContent; -import io.helidon.extensions.mcp.server.McpResourceContents; +import io.helidon.extensions.mcp.server.McpResourceRequest; +import io.helidon.extensions.mcp.server.McpResourceResult; import io.helidon.extensions.mcp.server.McpServerFeature; import io.helidon.webserver.http.HttpRouting; @@ -85,17 +82,15 @@ public MediaType mediaType() { } @Override - public Function> resource() { - return this::read; - } - - List read(McpRequest request) { + public McpResourceResult resource(McpResourceRequest request) { CountDownLatch readLatch = context().get(CountDownLatch.class).orElseThrow(); if (readLatch.getCount() > 0) { readLatch.countDown(); request.features().subscriptions().sendUpdate(uri()); // trigger another resource read } - return List.of(McpResourceContents.textContent("text")); + return McpResourceResult.builder() + .addTextContent("text") + .build(); } } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/RootsServer.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/RootsServer.java index a22d8fe8..7d4df5aa 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/RootsServer.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/RootsServer.java @@ -18,16 +18,13 @@ import java.net.URI; import java.util.List; import java.util.Optional; -import java.util.function.Function; -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpRoot; import io.helidon.extensions.mcp.server.McpServerFeature; import io.helidon.extensions.mcp.server.McpTool; -import io.helidon.extensions.mcp.server.McpToolContent; -import io.helidon.extensions.mcp.server.McpToolContents; -import io.helidon.extensions.mcp.server.McpToolErrorException; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; +import io.helidon.extensions.mcp.server.McpToolTextContent; import io.helidon.webserver.http.HttpRouting; /** @@ -66,18 +63,18 @@ public String schema() { } @Override - public Function tool() { - return request -> { - List roots = request.features().roots().listRoots(); - List contents = roots.stream() - .map(McpRoot::name) - .flatMap(Optional::stream) - .map(McpToolContents::textContent) - .toList(); - return McpToolResult.builder() - .addContents(contents) - .build(); - }; + public McpToolResult tool(McpToolRequest request) { + List roots = request.features().roots().listRoots(); + List contents = roots.stream() + .map(McpRoot::name) + .flatMap(Optional::stream) + .map(text -> McpToolTextContent.builder() + .text(text) + .build()) + .toList(); + return McpToolResult.builder() + .addTextContents(contents) + .build(); } } @@ -98,21 +95,24 @@ public String schema() { } @Override - public Function tool() { - return request -> { - if (!request.features().roots().enabled()) { - throw new McpToolErrorException("Roots is disabled"); - } - List roots = request.features().roots().listRoots(); - List contents = roots.stream() - .map(McpRoot::uri) - .map(URI::toASCIIString) - .map(McpToolContents::textContent) - .toList(); + public McpToolResult tool(McpToolRequest request) { + if (!request.features().roots().enabled()) { return McpToolResult.builder() - .addContents(contents) + .error(true) + .addTextContent("Roots is disabled") .build(); - }; + } + List roots = request.features().roots().listRoots(); + List contents = roots.stream() + .map(McpRoot::uri) + .map(URI::toASCIIString) + .map(text -> McpToolTextContent.builder() + .text(text) + .build()) + .toList(); + return McpToolResult.builder() + .addTextContents(contents) + .build(); } } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/SamplingServer.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/SamplingServer.java index 50c33720..727fd605 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/SamplingServer.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/SamplingServer.java @@ -17,26 +17,23 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.util.function.Function; +import java.util.Optional; import io.helidon.common.media.type.MediaTypes; -import io.helidon.extensions.mcp.server.McpContent; +import io.helidon.extensions.mcp.server.McpContentType; import io.helidon.extensions.mcp.server.McpException; -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpSampling; import io.helidon.extensions.mcp.server.McpSamplingException; -import io.helidon.extensions.mcp.server.McpSamplingMessage; -import io.helidon.extensions.mcp.server.McpSamplingMessages; +import io.helidon.extensions.mcp.server.McpSamplingRequest; import io.helidon.extensions.mcp.server.McpSamplingResponse; import io.helidon.extensions.mcp.server.McpServerFeature; import io.helidon.extensions.mcp.server.McpTool; -import io.helidon.extensions.mcp.server.McpToolErrorException; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.json.schema.Schema; import io.helidon.webserver.http.HttpRouting; import static io.helidon.extensions.mcp.server.McpRole.USER; -import static io.helidon.extensions.mcp.server.McpToolContents.textContent; /** * Sampling server. @@ -78,40 +75,52 @@ public String schema() { } @Override - public Function tool() { - return this::sampling; + public McpToolResult tool(McpToolRequest request) { + return sampling(request); } - McpToolResult sampling(McpRequest request) { + McpToolResult sampling(McpToolRequest request) { McpSampling sampling = request.features().sampling(); - McpContent.ContentType requestType = request.parameters() + Optional requestType = request.arguments() .get("type") .asString() .map(String::toUpperCase) - .map(McpContent.ContentType::valueOf) - .orElseThrow(() -> new McpToolErrorException("Error while parsing content type")); + .map(McpContentType::valueOf); - McpSamplingMessage message = createMessage(requestType); - McpSamplingResponse response = sampling.request(req -> req.addMessage(message)); + if (requestType.isEmpty()) { + return McpToolResult.builder() + .addTextContent("Error while parsing content type") + .error(true) + .build(); + } + + McpSamplingRequest message = createMessage(requestType.get()); + McpSamplingResponse response = sampling.request(message); var type = response.message().type(); var result = McpToolResult.builder(); return switch (type) { - case TEXT -> result.addContent(textContent(response.asTextMessage().text())).build(); - case IMAGE -> result.addContent(textContent(new String(response.asImageMessage().data()))).build(); - case AUDIO -> result.addContent(textContent(new String(response.asAudioMessage().data()))).build(); + case TEXT -> result.addTextContent(response.asTextMessage().text()).build(); + case IMAGE -> result.addTextContent(new String(response.asImageMessage().data())).build(); + case AUDIO -> result.addTextContent(new String(response.asAudioMessage().data())).build(); }; } - McpSamplingMessage createMessage(McpContent.ContentType type) { + McpSamplingRequest createMessage(McpContentType type) { return switch (type) { - case TEXT -> McpSamplingMessages.textMessage("samplingMessage", USER); - case IMAGE -> McpSamplingMessages.imageMessage("samplingMessage".getBytes(StandardCharsets.UTF_8), - MediaTypes.TEXT_PLAIN, - USER); - case AUDIO -> McpSamplingMessages.audioMessage("samplingMessage".getBytes(StandardCharsets.UTF_8), - MediaTypes.TEXT_PLAIN, - USER); - default -> throw new McpToolErrorException(textContent("Unsupported sampling message type: " + type)); + case TEXT -> McpSamplingRequest.builder() + .addTextMessage(message -> message.text("samplingMessage").role(USER)) + .build(); + case IMAGE -> McpSamplingRequest.builder() + .addImageMessage(message -> message.data("samplingMessage".getBytes(StandardCharsets.UTF_8)) + .mediaType(MediaTypes.TEXT_PLAIN) + .role(USER)) + .build(); + case AUDIO -> McpSamplingRequest.builder() + .addAudioMessage(message -> message.data("samplingMessage".getBytes(StandardCharsets.UTF_8)) + .mediaType(MediaTypes.TEXT_PLAIN) + .role(USER)) + .build(); + default -> throw new McpException("Unsupported sampling message type: " + type); }; } } @@ -123,37 +132,29 @@ public String name() { } @Override - public Function tool() { - return this::enabledSampling; - } - - private McpToolResult enabledSampling(McpRequest request) { + public McpToolResult tool(McpToolRequest request) { McpSampling sampling = request.features().sampling(); if (sampling.enabled()) { return sampling(request); } return McpToolResult.builder() - .addContent(textContent("sampling is disabled")) + .addTextContent("sampling is disabled") .error(true) .build(); } } private static class MultipleSamplingRequestTool extends SamplingTool { - private final McpSamplingMessage message = McpSamplingMessages.textMessage("ignored", USER); - @Override public String name() { return "multiple-sampling-tool"; } @Override - public Function tool() { - return request -> { - McpSampling sampling = request.features().sampling(); - var response = sampling.request(req -> req.addMessage(message)); - return sampling(request); - }; + public McpToolResult tool(McpToolRequest request) { + McpSampling sampling = request.features().sampling(); + var response = sampling.request(req -> req.addTextMessage(message -> message.text("ignored").role(USER))); + return sampling(request); } } @@ -164,21 +165,19 @@ public String name() { } @Override - public Function tool() { - return request -> { - try { - request.features() - .sampling() - .request(req -> req.timeout(Duration.ofSeconds(2)) - .addMessage(McpSamplingMessages.textMessage("timeout", USER))); - throw new McpException("Timeout should have been triggered"); - } catch (McpSamplingException e) { - return McpToolResult.builder() - .addContent(textContent(e.getMessage())) - .error(true) - .build(); - } - }; + public McpToolResult tool(McpToolRequest request) { + try { + request.features() + .sampling() + .request(req -> req.timeout(Duration.ofSeconds(2)) + .addTextMessage(message -> message.text("timeout").role(USER))); + throw new McpException("Timeout should have been triggered"); + } catch (McpSamplingException e) { + return McpToolResult.builder() + .addTextContent(e.getMessage()) + .error(true) + .build(); + } } } @@ -189,20 +188,18 @@ public String name() { } @Override - public Function tool() { - return request -> { - try { - request.features() - .sampling() - .request(req -> req.addMessage(McpSamplingMessages.textMessage("error", USER))); - throw new McpException("MCP sampling exception should have been triggered"); - } catch (McpSamplingException e) { - return McpToolResult.builder() - .addContent(textContent(e.getMessage())) - .error(true) - .build(); - } - }; + public McpToolResult tool(McpToolRequest request) { + try { + request.features() + .sampling() + .request(req -> req.addTextMessage(message -> message.text("error").role(USER))); + throw new McpException("MCP sampling exception should have been triggered"); + } catch (McpSamplingException e) { + return McpToolResult.builder() + .addTextContent(e.getMessage()) + .error(true) + .build(); + } } } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ToolAnnotationsServer.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ToolAnnotationsServer.java index fa1ab254..63aaa6bb 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ToolAnnotationsServer.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ToolAnnotationsServer.java @@ -15,13 +15,12 @@ */ package io.helidon.extensions.mcp.tests.common; -import java.util.function.Function; +import java.util.Optional; -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpServerFeature; import io.helidon.extensions.mcp.server.McpTool; import io.helidon.extensions.mcp.server.McpToolAnnotations; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.webserver.http.HttpRouting; @@ -54,7 +53,7 @@ public static void setUpRoute(HttpRouting.Builder builder) { .idempotentHint(false) .openWorldHint(true)) .tool(request -> McpToolResult.builder() - .addContent(McpToolContents.textContent("")) + .addTextContent("") .build()))); } @@ -76,21 +75,21 @@ public String schema() { } @Override - public Function tool() { - return request -> McpToolResult.builder() - .addContent(McpToolContents.textContent("")) + public McpToolResult tool(McpToolRequest request) { + return McpToolResult.builder() + .addTextContent("") .build(); } @Override - public McpToolAnnotations annotations() { + public Optional annotations() { var builder = McpToolAnnotations.builder(); builder.title("") .readOnlyHint(false) .destructiveHint(true) .idempotentHint(false) .openWorldHint(true); - return builder.build(); + return Optional.of(builder.build()); } } @@ -113,21 +112,21 @@ public String schema() { } @Override - public Function tool() { - return request -> McpToolResult.builder() - .addContent(McpToolContents.textContent("")) + public McpToolResult tool(McpToolRequest request) { + return McpToolResult.builder() + .addTextContent("") .build(); } @Override - public McpToolAnnotations annotations() { + public Optional annotations() { var builder = McpToolAnnotations.builder(); builder.title("tool2 title") .readOnlyHint(true) .destructiveHint(false) .idempotentHint(true) .openWorldHint(false); - return builder.build(); + return Optional.of(builder.build()); } } } diff --git a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ToolErrorResultServer.java b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ToolErrorResultServer.java index d8f081af..456414d6 100644 --- a/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ToolErrorResultServer.java +++ b/tests/common/src/main/java/io/helidon/extensions/mcp/tests/common/ToolErrorResultServer.java @@ -15,17 +15,12 @@ */ package io.helidon.extensions.mcp.tests.common; -import java.util.function.Function; - -import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpServerFeature; import io.helidon.extensions.mcp.server.McpTool; -import io.helidon.extensions.mcp.server.McpToolContent; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.webserver.http.HttpRouting; -import static io.helidon.extensions.mcp.server.McpToolContents.textContent; - /** * Tool error result server. */ @@ -63,14 +58,11 @@ public String schema() { } @Override - public Function tool() { - return request -> { - McpToolContent content = textContent("Tool error message"); - return McpToolResult.builder() - .error(true) - .addContent(content) - .build(); - }; + public McpToolResult tool(McpToolRequest request) { + return McpToolResult.builder() + .error(true) + .addTextContent("Tool error message") + .build(); } } @@ -81,14 +73,11 @@ public String name() { } @Override - public Function tool() { - return request -> { - McpToolContent content = textContent("Tool error message"); - return McpToolResult.builder() - .error(true) - .addContent(content) - .build(); - }; + public McpToolResult tool(McpToolRequest request) { + return McpToolResult.builder() + .error(true) + .addTextContent("Tool error message") + .build(); } } @@ -99,16 +88,12 @@ public String name() { } @Override - public Function tool() { - return request -> { - McpToolContent content = textContent("Tool error message"); - McpToolContent content1 = textContent("Second error message"); - return McpToolResult.builder() - .error(true) - .addContent(content) - .addContent(content1) - .build(); - }; + public McpToolResult tool(McpToolRequest request) { + return McpToolResult.builder() + .error(true) + .addTextContent("Tool error message") + .addTextContent("Second error message") + .build(); } } } diff --git a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpCancellationServer.java b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpCancellationServer.java index 587b62be..bf89b94e 100644 --- a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpCancellationServer.java +++ b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpCancellationServer.java @@ -16,29 +16,23 @@ package io.helidon.extensions.mcp.tests.declarative; -import java.util.List; - import io.helidon.common.media.type.MediaTypes; import io.helidon.extensions.mcp.server.Mcp; import io.helidon.extensions.mcp.server.McpCancellation; import io.helidon.extensions.mcp.server.McpLogger; -import io.helidon.extensions.mcp.server.McpPromptContent; -import io.helidon.extensions.mcp.server.McpPromptContents; +import io.helidon.extensions.mcp.server.McpPromptResult; import io.helidon.extensions.mcp.server.McpRequest; -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.extensions.mcp.server.McpResourceResult; +import io.helidon.extensions.mcp.server.McpToolResult; @Mcp.Server @Mcp.Path("/cancellation") class McpCancellationServer { @Mcp.Tool("Cancellation Tool") - List cancellationTool(McpCancellation cancellation) { + McpToolResult cancellationTool(McpCancellation cancellation) { String reason = cancellation.result().reason(); - return List.of(McpToolContents.textContent(reason)); + return McpToolResult.builder().addTextContent(reason).build(); } @Mcp.Tool("Cancellation Tool") @@ -47,9 +41,9 @@ String cancellationTool1(McpRequest request, McpCancellation cancellation, McpLo } @Mcp.Prompt("Cancellation Prompt") - List cancellationPrompt(McpCancellation cancellation) { + McpPromptResult cancellationPrompt(McpCancellation cancellation) { String reason = cancellation.result().reason(); - return List.of(McpPromptContents.textContent(reason, McpRole.USER)); + return McpPromptResult.builder().addTextContent(reason).build(); } @Mcp.Prompt("Cancellation Prompt") @@ -60,9 +54,9 @@ String cancellationPrompt1(McpRequest request, McpCancellation cancellation, Mcp @Mcp.Resource(uri = "file://cancellation", mediaType = MediaTypes.TEXT_PLAIN_VALUE, description = "Cancellation Resource") - List cancellationResource(McpCancellation cancellation) { + McpResourceResult cancellationResource(McpCancellation cancellation) { String reason = cancellation.result().reason(); - return List.of(McpResourceContents.textContent(reason)); + return McpResourceResult.builder().addTextContent(reason).build(); } @Mcp.Resource(uri = "file://cancellation1", diff --git a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpCompletionsServer.java b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpCompletionsServer.java index 6eedf0ad..79d40f25 100644 --- a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpCompletionsServer.java +++ b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpCompletionsServer.java @@ -19,8 +19,8 @@ import java.util.List; 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.McpCompletionRequest; +import io.helidon.extensions.mcp.server.McpCompletionResult; import io.helidon.extensions.mcp.server.McpCompletionType; import io.helidon.extensions.mcp.server.McpFeatures; import io.helidon.extensions.mcp.server.McpParameters; @@ -30,63 +30,68 @@ @Mcp.Path("/completions") class McpCompletionsServer { @Mcp.Completion("prompt1") - McpCompletionContent completionPrompt(McpParameters parameters) { + McpCompletionResult completionPrompt(McpParameters parameters) { String argument = parameters.get("argument").get("value").asString().orElse(null); - return McpCompletionContents.completion(argument); + return McpCompletionResult.create(argument); } @Mcp.Completion("prompt2") - McpCompletionContent completionPromptFeatures(McpFeatures features) { - return McpCompletionContents.completion("prompt2"); + McpCompletionResult completionPromptFeatures(McpFeatures features) { + return McpCompletionResult.create("prompt2"); } @Mcp.Completion("prompt3") - McpCompletionContent completionPromptParametersFeatures(McpParameters parameters, McpFeatures features) { + McpCompletionResult completionPromptParametersFeatures(McpParameters parameters, McpFeatures features) { String argument = parameters.get("argument").get("value").asString().orElse(null); - return McpCompletionContents.completion(argument); + return McpCompletionResult.create(argument); } @Mcp.Completion("prompt4") - McpCompletionContent completionPromptFeaturesParameters(McpFeatures features, McpParameters parameters) { + McpCompletionResult completionPromptFeaturesParameters(McpFeatures features, McpParameters parameters) { String argument = parameters.get("argument").get("value").asString().orElse(null); - return McpCompletionContents.completion(argument); + return McpCompletionResult.create(argument); } @Mcp.Completion("prompt5") - McpCompletionContent completionPromptArgument(String argument) { - return McpCompletionContents.completion(argument); + McpCompletionResult completionPromptArgument(String argument) { + return McpCompletionResult.create(argument); } @Mcp.Completion("prompt6") - McpCompletionContent completionPromptArgumentFeatures(String argument, McpFeatures features) { - return McpCompletionContents.completion(argument); + McpCompletionResult completionPromptArgumentFeatures(String argument, McpFeatures features) { + return McpCompletionResult.create(argument); } @Mcp.Completion("prompt7") - McpCompletionContent completionPromptFeaturesArgument(McpFeatures features, String argument) { - return McpCompletionContents.completion(argument); + McpCompletionResult completionPromptFeaturesArgument(McpFeatures features, String argument) { + return McpCompletionResult.create(argument); } @Mcp.Completion(value = "resource/{path1}", type = McpCompletionType.RESOURCE) - McpCompletionContent completionResource(McpParameters parameters) { + McpCompletionResult completionResource(McpParameters parameters) { String argument = parameters.get("argument").get("value").asString().orElse(null); - return McpCompletionContents.completion(argument); + return McpCompletionResult.create(argument); } @Mcp.Completion(value = "resource/{path2}", type = McpCompletionType.RESOURCE) - McpCompletionContent completionResourceArgument(String path2) { - return McpCompletionContents.completion(path2); + McpCompletionResult completionResourceArgument(String path2) { + return McpCompletionResult.create(path2); } @Mcp.Completion(value = "resource/{path8}", type = McpCompletionType.RESOURCE) - McpCompletionContent completionMcpRequest(McpRequest request) { + McpCompletionResult completionMcpRequest(McpRequest request) { String argument = request.parameters().get("value").asString().orElse(null); - return McpCompletionContents.completion(argument); + return McpCompletionResult.create(argument); + } + + @Mcp.Completion("prompt15") + McpCompletionResult completionPromptResult(McpCompletionRequest request) { + return McpCompletionResult.create(request.value()); } @Mcp.Completion(value = "resource/{path10}", type = McpCompletionType.RESOURCE) - McpCompletionContent completion1StringMcpRequest(String argument, McpRequest request) { - return McpCompletionContents.completion(argument); + McpCompletionResult completion1StringMcpRequest(String argument, McpRequest request) { + return McpCompletionResult.create(argument); } @Mcp.Completion("prompt8") diff --git a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpLoggingFeatureServer.java b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpLoggingFeatureServer.java index 9573e62b..d22eb6ef 100644 --- a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpLoggingFeatureServer.java +++ b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpLoggingFeatureServer.java @@ -16,19 +16,13 @@ package io.helidon.extensions.mcp.tests.declarative; -import java.util.List; - import io.helidon.common.media.type.MediaTypes; import io.helidon.extensions.mcp.server.Mcp; import io.helidon.extensions.mcp.server.McpFeatures; import io.helidon.extensions.mcp.server.McpLogger; -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.extensions.mcp.server.McpPromptResult; +import io.helidon.extensions.mcp.server.McpResourceResult; +import io.helidon.extensions.mcp.server.McpToolResult; @Mcp.Server @Mcp.Path("/logging") @@ -41,9 +35,9 @@ String loggingTool(McpFeatures features) { } @Mcp.Tool("Tool description") - List loggerTool(McpLogger logger) { + McpToolResult loggerTool(McpLogger logger) { logger.info("Logging notification"); - return List.of(McpToolContents.textContent("Hello World")); + return McpToolResult.builder().addTextContent("Hello World").build(); } @Mcp.Prompt("Prompt description") @@ -53,9 +47,9 @@ String loggingPrompt(McpFeatures features) { } @Mcp.Prompt("Prompt description") - List loggerPrompt(McpLogger logger) { + McpPromptResult loggerPrompt(McpLogger logger) { logger.info("Logging notification"); - return List.of(McpPromptContents.textContent("Hello World", McpRole.ASSISTANT)); + return McpPromptResult.builder().addTextContent("Hello World").build(); } @Mcp.Resource( @@ -71,8 +65,8 @@ String loggingResource(McpFeatures features) { uri = "file://hello/world1", mediaType = MediaTypes.TEXT_PLAIN_VALUE, description = "Resource description") - List loggerResource(McpLogger logger) { + McpResourceResult loggerResource(McpLogger logger) { logger.info("Logging notification"); - return List.of(McpResourceContents.textContent("Hello World")); + return McpResourceResult.builder().addTextContent("Hello World").build(); } } diff --git a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpMixedComponentServer.java b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpMixedComponentServer.java index 4f9c78a5..170dd6ea 100644 --- a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpMixedComponentServer.java +++ b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpMixedComponentServer.java @@ -16,44 +16,39 @@ package io.helidon.extensions.mcp.tests.declarative; -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.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.extensions.mcp.server.McpCompletionResult; +import io.helidon.extensions.mcp.server.McpPromptResult; +import io.helidon.extensions.mcp.server.McpResourceResult; +import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.json.schema.JsonSchema; @Mcp.Server("mcp-weather-server") class McpMixedComponentServer { @Mcp.Tool("Tool description") - List weatherAlert(String state, Alert alert) { - return List.of(McpToolContents.textContent("state: %s, alert name: %s".formatted(state, alert.name))); + McpToolResult weatherAlert(String state, Alert alert) { + return McpToolResult.builder() + .addTextContent("state: %s, alert name: %s".formatted(state, alert.name)) + .build(); } @Mcp.Prompt("Prompt description") - List weatherInTown(@Mcp.Description("town's name") String town) { - return List.of(McpPromptContents.textContent("Town: " + town, McpRole.USER)); + McpPromptResult weatherInTown(@Mcp.Description("town's name") String town) { + return McpPromptResult.builder().addTextContent("Town: " + town).build(); } @Mcp.Resource(uri = "resource:resource", mediaType = MediaTypes.TEXT_PLAIN_VALUE, description = "Resource description") - List weatherAlerts() { - return List.of(McpResourceContents.textContent("Resource content")); + McpResourceResult weatherAlerts() { + return McpResourceResult.builder().addTextContent("Resource content").build(); } @Mcp.Completion("weatherInTown") - McpCompletionContent completion() { - return McpCompletionContents.completion(); + McpCompletionResult completion() { + return McpCompletionResult.create(); } @JsonSchema.Schema diff --git a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpPaginationServer.java b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpPaginationServer.java index de1dc2c8..c6e69314 100644 --- a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpPaginationServer.java +++ b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpPaginationServer.java @@ -16,17 +16,11 @@ package io.helidon.extensions.mcp.tests.declarative; -import java.util.List; - import io.helidon.common.media.type.MediaTypes; import io.helidon.extensions.mcp.server.Mcp; -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.extensions.mcp.server.McpPromptResult; +import io.helidon.extensions.mcp.server.McpResourceResult; +import io.helidon.extensions.mcp.server.McpToolResult; @Mcp.Server @Mcp.Path("/pagination") @@ -37,54 +31,54 @@ class McpPaginationServer { @Mcp.Tool("Tool description") - List tool1() { - return List.of(McpToolContents.textContent("text1")); + McpToolResult tool1() { + return McpToolResult.builder().addTextContent("text1").build(); } @Mcp.Tool("Tool description") - List tool2() { - return List.of(McpToolContents.textContent("text2")); + McpToolResult tool2() { + return McpToolResult.builder().addTextContent("text2").build(); } @Mcp.Prompt("Prompt description") - List prompt1() { - return List.of(McpPromptContents.textContent("text1", McpRole.USER)); + McpPromptResult prompt1() { + return McpPromptResult.builder().addTextContent("text1").build(); } @Mcp.Prompt("Prompt description") - List prompt2() { - return List.of(McpPromptContents.textContent("text2", McpRole.USER)); + McpPromptResult prompt2() { + return McpPromptResult.builder().addTextContent("text2").build(); } @Mcp.Resource( uri = "https://path1", mediaType = MediaTypes.TEXT_PLAIN_VALUE, description = "Resource description") - List resource1() { - return List.of(McpResourceContents.textContent("text1")); + McpResourceResult resource1() { + return McpResourceResult.builder().addTextContent("text1").build(); } @Mcp.Resource( uri = "https://path2", mediaType = MediaTypes.TEXT_PLAIN_VALUE, description = "Resource description") - List resource2() { - return List.of(McpResourceContents.textContent("text2")); + McpResourceResult resource2() { + return McpResourceResult.builder().addTextContent("text2").build(); } @Mcp.Resource( uri = "https://{path1}", mediaType = MediaTypes.TEXT_PLAIN_VALUE, description = "Resource Template description") - List resourceTemplate1() { - return List.of(McpResourceContents.textContent("text1")); + McpResourceResult resourceTemplate1() { + return McpResourceResult.builder().addTextContent("text1").build(); } @Mcp.Resource( uri = "https://{path2}", mediaType = MediaTypes.TEXT_PLAIN_VALUE, description = "Resource Template description") - List resourceTemplate2() { - return List.of(McpResourceContents.textContent("text2")); + McpResourceResult resourceTemplate2() { + return McpResourceResult.builder().addTextContent("text2").build(); } } diff --git a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpPromptsServer.java b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpPromptsServer.java index 131e5338..9c82d56a 100644 --- a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpPromptsServer.java +++ b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpPromptsServer.java @@ -16,12 +16,10 @@ package io.helidon.extensions.mcp.tests.declarative; -import java.util.List; - import io.helidon.extensions.mcp.server.Mcp; import io.helidon.extensions.mcp.server.McpFeatures; -import io.helidon.extensions.mcp.server.McpPromptContent; -import io.helidon.extensions.mcp.server.McpPromptContents; +import io.helidon.extensions.mcp.server.McpPromptRequest; +import io.helidon.extensions.mcp.server.McpPromptResult; import io.helidon.extensions.mcp.server.McpRequest; import io.helidon.extensions.mcp.server.McpRole; @@ -70,27 +68,32 @@ String promptRoleDefault() { } @Mcp.Prompt(PROMPT_DESCRIPTION) - List prompt4(String prompt) { - return List.of(McpPromptContents.textContent(PROMPT_CONTENT, McpRole.USER)); + McpPromptResult prompt4(String prompt) { + return McpPromptResult.builder().addTextContent(PROMPT_CONTENT).build(); } @Mcp.Prompt(PROMPT_DESCRIPTION) - List prompt5(McpFeatures features) { - return List.of(McpPromptContents.textContent(PROMPT_CONTENT, McpRole.USER)); + McpPromptResult prompt5(McpFeatures features) { + return McpPromptResult.builder().addTextContent(PROMPT_CONTENT).build(); } @Mcp.Prompt(PROMPT_DESCRIPTION) - List prompt6(McpFeatures features) { - return List.of(McpPromptContents.textContent(PROMPT_CONTENT, McpRole.USER)); + McpPromptResult prompt6(McpFeatures features) { + return McpPromptResult.builder().addTextContent(PROMPT_CONTENT).build(); } @Mcp.Prompt(PROMPT_DESCRIPTION) - List prompt7(McpRequest request) { - return List.of(McpPromptContents.textContent(PROMPT_CONTENT, McpRole.USER)); + McpPromptResult prompt7(McpRequest request) { + return McpPromptResult.builder().addTextContent(PROMPT_CONTENT).build(); } @Mcp.Prompt(PROMPT_DESCRIPTION) String prompt8(McpRequest request) { return PROMPT_CONTENT; } + + @Mcp.Prompt(PROMPT_DESCRIPTION) + McpPromptResult prompt9(McpPromptRequest request) { + return McpPromptResult.builder().addTextContent(PROMPT_CONTENT).build(); + } } diff --git a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpResourceTemplatesServer.java b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpResourceTemplatesServer.java index ef421de1..f3412d05 100644 --- a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpResourceTemplatesServer.java +++ b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpResourceTemplatesServer.java @@ -16,15 +16,12 @@ package io.helidon.extensions.mcp.tests.declarative; -import java.util.List; - import io.helidon.common.media.type.MediaTypes; import io.helidon.extensions.mcp.server.Mcp; import io.helidon.extensions.mcp.server.McpFeatures; import io.helidon.extensions.mcp.server.McpParameters; import io.helidon.extensions.mcp.server.McpRequest; -import io.helidon.extensions.mcp.server.McpResourceContent; -import io.helidon.extensions.mcp.server.McpResourceContents; +import io.helidon.extensions.mcp.server.McpResourceResult; @Mcp.Server @Mcp.Path("/resource/templates") @@ -53,16 +50,16 @@ String resource1(McpFeatures features) { uri = "file://{path}", mediaType = RESOURCE_MEDIA_TYPE, description = RESOURCE_DESCRIPTION) - List resource2() { - return List.of(McpResourceContents.textContent(RESOURCE_CONTENT)); + McpResourceResult resource2() { + return McpResourceResult.builder().addTextContent(RESOURCE_CONTENT).build(); } @Mcp.Resource( uri = "git://{path}", mediaType = RESOURCE_MEDIA_TYPE, description = RESOURCE_DESCRIPTION) - List resource3(McpFeatures features) { - return List.of(McpResourceContents.textContent(RESOURCE_CONTENT)); + McpResourceResult resource3(McpFeatures features) { + return McpResourceResult.builder().addTextContent(RESOURCE_CONTENT).build(); } @Mcp.Resource( diff --git a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpResourcesServer.java b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpResourcesServer.java index c932c8ea..2ef154d7 100644 --- a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpResourcesServer.java +++ b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpResourcesServer.java @@ -16,14 +16,12 @@ package io.helidon.extensions.mcp.tests.declarative; -import java.util.List; - import io.helidon.common.media.type.MediaTypes; import io.helidon.extensions.mcp.server.Mcp; import io.helidon.extensions.mcp.server.McpFeatures; import io.helidon.extensions.mcp.server.McpRequest; -import io.helidon.extensions.mcp.server.McpResourceContent; -import io.helidon.extensions.mcp.server.McpResourceContents; +import io.helidon.extensions.mcp.server.McpResourceRequest; +import io.helidon.extensions.mcp.server.McpResourceResult; @Mcp.Server @Mcp.Path("/resources") @@ -60,23 +58,31 @@ String resource4(McpRequest request) { uri = "resource2", mediaType = RESOURCE_MEDIA_TYPE, description = RESOURCE_DESCRIPTION) - List resource2() { - return List.of(McpResourceContents.textContent(RESOURCE_CONTENT)); + McpResourceResult resource2() { + return McpResourceResult.builder().addTextContent(RESOURCE_CONTENT).build(); } @Mcp.Resource( uri = "resource3", mediaType = RESOURCE_MEDIA_TYPE, description = RESOURCE_DESCRIPTION) - List resource3(McpFeatures features) { - return List.of(McpResourceContents.textContent(RESOURCE_CONTENT)); + McpResourceResult resource3(McpFeatures features) { + return McpResourceResult.builder().addTextContent(RESOURCE_CONTENT).build(); } @Mcp.Resource( uri = "resource5", mediaType = RESOURCE_MEDIA_TYPE, description = RESOURCE_DESCRIPTION) - List resource5(McpRequest request) { - return List.of(McpResourceContents.textContent(RESOURCE_CONTENT)); + McpResourceResult resource5(McpRequest request) { + return McpResourceResult.builder().addTextContent(RESOURCE_CONTENT).build(); + } + + @Mcp.Resource( + uri = "resource6", + mediaType = RESOURCE_MEDIA_TYPE, + description = RESOURCE_DESCRIPTION) + McpResourceResult resource6(McpResourceRequest request) { + return McpResourceResult.builder().addTextContent(RESOURCE_CONTENT).build(); } } diff --git a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpRootsServer.java b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpRootsServer.java index 761d4416..a227b4da 100644 --- a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpRootsServer.java +++ b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpRootsServer.java @@ -15,31 +15,25 @@ */ package io.helidon.extensions.mcp.tests.declarative; -import java.util.List; - import io.helidon.common.media.type.MediaTypes; import io.helidon.extensions.mcp.server.Mcp; -import io.helidon.extensions.mcp.server.McpPromptContent; -import io.helidon.extensions.mcp.server.McpPromptContents; +import io.helidon.extensions.mcp.server.McpPromptResult; import io.helidon.extensions.mcp.server.McpRequest; -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.McpResourceResult; import io.helidon.extensions.mcp.server.McpRoots; -import io.helidon.extensions.mcp.server.McpToolContent; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolResult; @Mcp.Server @Mcp.Path("/roots") class McpRootsServer { @Mcp.Tool("Roots tool") - List tool(McpRoots roots) { - return List.of(McpToolContents.textContent("")); + McpToolResult tool(McpRoots roots) { + return McpToolResult.builder().addTextContent("").build(); } @Mcp.Tool("Roots tool") - List tool1(McpRoots roots, String value) { - return List.of(McpToolContents.textContent("")); + McpToolResult tool1(McpRoots roots, String value) { + return McpToolResult.builder().addTextContent("").build(); } @Mcp.Tool("Roots tool") @@ -53,13 +47,13 @@ String tool3(McpRoots roots, String value) { } @Mcp.Prompt("Roots prompt") - List prompt(McpRoots roots) { - return List.of(McpPromptContents.textContent("", McpRole.USER)); + McpPromptResult prompt(McpRoots roots) { + return McpPromptResult.builder().addTextContent("").build(); } @Mcp.Prompt("Roots prompt") - List prompt1(McpRoots roots, String value) { - return List.of(McpPromptContents.textContent("", McpRole.USER)); + McpPromptResult prompt1(McpRoots roots, String value) { + return McpPromptResult.builder().addTextContent("").build(); } @Mcp.Prompt("Roots prompt") @@ -75,15 +69,15 @@ String prompt3(McpRoots roots, String value) { @Mcp.Resource(uri = "https://resource", description = "Roots resource", mediaType = MediaTypes.TEXT_PLAIN_VALUE) - List resource(McpRoots roots) { - return List.of(McpResourceContents.textContent("")); + McpResourceResult resource(McpRoots roots) { + return McpResourceResult.create(""); } @Mcp.Resource(uri = "https://resource1", description = "Roots resource", mediaType = MediaTypes.TEXT_PLAIN_VALUE) - List resource1(McpRoots roots, McpRequest request) { - return List.of(McpResourceContents.textContent("")); + McpResourceResult resource1(McpRoots roots, McpRequest request) { + return McpResourceResult.create(""); } @Mcp.Resource(uri = "https://resource2", diff --git a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpSamplingServer.java b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpSamplingServer.java index aca773f6..27dd150d 100644 --- a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpSamplingServer.java +++ b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpSamplingServer.java @@ -16,28 +16,26 @@ package io.helidon.extensions.mcp.tests.declarative; -import java.util.List; - import io.helidon.common.media.type.MediaTypes; import io.helidon.extensions.mcp.server.Mcp; -import io.helidon.extensions.mcp.server.McpPromptContent; +import io.helidon.extensions.mcp.server.McpPromptResult; import io.helidon.extensions.mcp.server.McpRequest; -import io.helidon.extensions.mcp.server.McpResourceContent; +import io.helidon.extensions.mcp.server.McpResourceResult; import io.helidon.extensions.mcp.server.McpSampling; -import io.helidon.extensions.mcp.server.McpToolContent; +import io.helidon.extensions.mcp.server.McpToolResult; @Mcp.Server @Mcp.Path("/sampling") class McpSamplingServer { @Mcp.Tool("Sampling tool") - List tool(McpSampling sampling) { - return List.of(); + McpToolResult tool(McpSampling sampling) { + return McpToolResult.create(); } @Mcp.Tool("Sampling tool") - List tool1(McpSampling sampling, String value) { - return List.of(); + McpToolResult tool1(McpSampling sampling, String value) { + return McpToolResult.create(); } @Mcp.Tool("Sampling tool") @@ -51,13 +49,13 @@ String tool5(McpSampling sampling, String value) { } @Mcp.Prompt("Sampling prompt") - List prompt(McpSampling sampling) { - return List.of(); + McpPromptResult prompt(McpSampling sampling) { + return McpPromptResult.create(); } @Mcp.Prompt("Sampling prompt") - List prompt1(McpSampling sampling, String value) { - return List.of(); + McpPromptResult prompt1(McpSampling sampling, String value) { + return McpPromptResult.create(); } @Mcp.Prompt("Sampling prompt") @@ -73,15 +71,15 @@ String prompt5(McpSampling sampling, String value) { @Mcp.Resource(uri = "https://example.com", description = "Sampling resource", mediaType = MediaTypes.TEXT_PLAIN_VALUE) - List resource(McpSampling sampling) { - return List.of(); + McpResourceResult resource(McpSampling sampling) { + return McpResourceResult.create(); } @Mcp.Resource(uri = "https://example.com", description = "Sampling resource", mediaType = MediaTypes.TEXT_PLAIN_VALUE) - List resource1(McpSampling sampling, McpRequest request) { - return List.of(); + McpResourceResult resource1(McpSampling sampling, McpRequest request) { + return McpResourceResult.create(); } @Mcp.Resource(uri = "https://example.com", diff --git a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpSubscribersServer.java b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpSubscribersServer.java index 2895578e..7897149c 100644 --- a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpSubscribersServer.java +++ b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpSubscribersServer.java @@ -20,6 +20,8 @@ import io.helidon.extensions.mcp.server.McpFeatures; import io.helidon.extensions.mcp.server.McpLogger; import io.helidon.extensions.mcp.server.McpRequest; +import io.helidon.extensions.mcp.server.McpSubscribeRequest; +import io.helidon.extensions.mcp.server.McpUnsubscribeRequest; @Mcp.Server @Mcp.Path("/subscribers") @@ -43,4 +45,12 @@ void subscribe(McpRequest request, McpFeatures features, McpLogger logger) { @Mcp.ResourceUnsubscriber("http://myresource") void unsubscribe(McpRequest request, McpFeatures features, McpLogger logger) { } + + @Mcp.ResourceSubscriber("http://myresource") + void subscribe(McpSubscribeRequest request, McpFeatures features, McpLogger logger) { + } + + @Mcp.ResourceUnsubscriber("http://myresource") + void unsubscribe(McpUnsubscribeRequest request, McpFeatures features, McpLogger logger) { + } } diff --git a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpToolsServer.java b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpToolsServer.java index 4dbc569d..2ea17d8e 100644 --- a/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpToolsServer.java +++ b/tests/declarative/src/main/java/io/helidon/extensions/mcp/tests/declarative/McpToolsServer.java @@ -16,13 +16,10 @@ package io.helidon.extensions.mcp.tests.declarative; -import java.util.List; - import io.helidon.extensions.mcp.server.Mcp; import io.helidon.extensions.mcp.server.McpFeatures; import io.helidon.extensions.mcp.server.McpRequest; -import io.helidon.extensions.mcp.server.McpToolContent; -import io.helidon.extensions.mcp.server.McpToolContents; +import io.helidon.extensions.mcp.server.McpToolRequest; import io.helidon.extensions.mcp.server.McpToolResult; import io.helidon.json.schema.JsonSchema; @@ -32,6 +29,12 @@ class McpToolsServer { public static final String TOOL_CONTENT = "Tool Content"; public static final String TOOL_DESCRIPTION = "Tool description"; public static final String OUTPUT_SCHEMA = "{\"type\":\"object\",\"properties\": {}}"; + public static final String OUTPUT_SCHEMA_MULTI_LINE = """ + { + "type":"object", + "properties": { + } + }"""; @Mcp.Tool(TOOL_DESCRIPTION) String tool(String value, Foo foo) { @@ -48,47 +51,47 @@ String tool1(McpFeatures features) { } @Mcp.Tool(TOOL_DESCRIPTION) - List tool2(String value, Foo foo) { - return List.of(McpToolContents.textContent(""" - value=%s - foo=%s - bar=%d - """.formatted(value, foo.foo, foo.bar))); + McpToolResult tool2(String value, Foo foo) { + return McpToolResult.builder() + .addTextContent("value=" + value) + .addTextContent("foo=" + foo.foo) + .addTextContent("bar=" + foo.bar) + .build(); } @Mcp.Tool(TOOL_DESCRIPTION) - List tool3(McpFeatures features) { - return List.of(McpToolContents.textContent(TOOL_CONTENT)); + McpToolResult tool3(McpFeatures features) { + return McpToolResult.builder().addTextContent(TOOL_CONTENT).build(); } @Mcp.Tool(TOOL_DESCRIPTION) - List tool4(Byte aByte) { - return List.of(McpToolContents.textContent(aByte.toString())); + McpToolResult tool4(Byte aByte) { + return McpToolResult.builder().addTextContent(aByte.toString()).build(); } @Mcp.Tool(TOOL_DESCRIPTION) - List tool5(Short aShort) { - return List.of(McpToolContents.textContent(aShort.toString())); + McpToolResult tool5(Short aShort) { + return McpToolResult.builder().addTextContent(aShort.toString()).build(); } @Mcp.Tool(TOOL_DESCRIPTION) - List tool6(Integer aInteger) { - return List.of(McpToolContents.textContent(aInteger.toString())); + McpToolResult tool6(Integer aInteger) { + return McpToolResult.builder().addTextContent(aInteger.toString()).build(); } @Mcp.Tool(TOOL_DESCRIPTION) - List tool7(Long aLong) { - return List.of(McpToolContents.textContent(aLong.toString())); + McpToolResult tool7(Long aLong) { + return McpToolResult.builder().addTextContent(aLong.toString()).build(); } @Mcp.Tool(TOOL_DESCRIPTION) - List tool8(Double aDouble) { - return List.of(McpToolContents.textContent(aDouble.toString())); + McpToolResult tool8(Double aDouble) { + return McpToolResult.builder().addTextContent(aDouble.toString()).build(); } @Mcp.Tool(TOOL_DESCRIPTION) - List tool9(Float aFloat) { - return List.of(McpToolContents.textContent(aFloat.toString())); + McpToolResult tool9(Float aFloat) { + return McpToolResult.builder().addTextContent(aFloat.toString()).build(); } @Mcp.Tool(TOOL_DESCRIPTION) @@ -97,21 +100,64 @@ String tool10(McpRequest request) { } @Mcp.Tool(TOOL_DESCRIPTION) - List tool11(McpRequest request) { - return List.of(McpToolContents.textContent(TOOL_CONTENT)); + McpToolResult tool11(McpRequest request) { + return McpToolResult.builder() + .addTextContent(TOOL_CONTENT) + .build(); } @Mcp.Tool(TOOL_DESCRIPTION) McpToolResult tool12(McpRequest request) { return McpToolResult.builder() - .addContent(McpToolContents.textContent(TOOL_CONTENT)) + .addTextContent(TOOL_CONTENT) .build(); } - @Mcp.Tool(value = TOOL_DESCRIPTION, outputSchema = OUTPUT_SCHEMA) + @Mcp.Tool(value = TOOL_DESCRIPTION) + @Mcp.ToolOutputSchemaText(OUTPUT_SCHEMA) McpToolResult tool13(McpRequest request) { return McpToolResult.builder() - .addContent(McpToolContents.textContent(TOOL_CONTENT)) + .addTextContent(TOOL_CONTENT) + .build(); + } + + @Mcp.Tool(value = TOOL_DESCRIPTION) + McpToolResult tool14(McpToolRequest request) { + return McpToolResult.builder() + .addTextContent(TOOL_CONTENT) + .build(); + } + + @Mcp.Tool(value = TOOL_DESCRIPTION) + @Mcp.ToolOutputSchema(OutputSchema.class) + McpToolResult tool15(McpToolRequest request) { + return McpToolResult.builder() + .addTextContent(TOOL_CONTENT) + .build(); + } + + @Mcp.Tool(value = TOOL_DESCRIPTION) + @Mcp.ToolOutputSchemaText(OUTPUT_SCHEMA_MULTI_LINE) + McpToolResult tool16(McpToolRequest request) { + return McpToolResult.builder() + .addTextContent(TOOL_CONTENT) + .build(); + } + + @Mcp.Tool(value = TOOL_DESCRIPTION) + @Mcp.ToolOutputSchema(OutputSchema.class) + @Mcp.ToolOutputSchemaText(OUTPUT_SCHEMA) + McpToolResult tool17(McpToolRequest request) { + return McpToolResult.builder() + .addTextContent(TOOL_CONTENT) + .build(); + } + + @Mcp.Tool(value = TOOL_DESCRIPTION) + @Mcp.ToolOutputSchema(Bar.class) + McpToolResult tool18(McpToolRequest request) { + return McpToolResult.builder() + .addTextContent(TOOL_CONTENT) .build(); } @@ -120,4 +166,12 @@ public static class Foo { public String foo; public int bar; } + + @JsonSchema.Schema + public static class OutputSchema { + } + + @JsonSchema.Schema + public record Bar(String bar) { + } } diff --git a/tests/declarative/src/test/java/io/helidon/extensions/mcp/tests/declarative/AbstractLangchain4jPromptsServerTest.java b/tests/declarative/src/test/java/io/helidon/extensions/mcp/tests/declarative/AbstractLangchain4jPromptsServerTest.java index 4f77b150..53c90135 100644 --- a/tests/declarative/src/test/java/io/helidon/extensions/mcp/tests/declarative/AbstractLangchain4jPromptsServerTest.java +++ b/tests/declarative/src/test/java/io/helidon/extensions/mcp/tests/declarative/AbstractLangchain4jPromptsServerTest.java @@ -16,21 +16,24 @@ package io.helidon.extensions.mcp.tests.declarative; +import java.util.List; import java.util.Map; import dev.langchain4j.mcp.client.McpClient; import dev.langchain4j.mcp.client.McpGetPromptResult; +import dev.langchain4j.mcp.client.McpPrompt; import dev.langchain4j.mcp.client.McpPromptContent; import dev.langchain4j.mcp.client.McpRole; import dev.langchain4j.mcp.client.McpTextContent; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import static io.helidon.extensions.mcp.tests.declarative.McpPromptsServer.PROMPT_CONTENT; -import static io.helidon.extensions.mcp.tests.declarative.McpPromptsServer.PROMPT_DESCRIPTION; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; abstract class AbstractLangchain4jPromptsServerTest { protected static McpClient client; @@ -42,6 +45,12 @@ static void afterAll() throws Exception { } } + @Test + void listPrompts() { + List prompts = client.listPrompts(); + assertThat(prompts.size(), is(13)); + } + @ParameterizedTest @ValueSource(strings = { "prompt1", "prompt2", "prompt3", "prompt8", "promptRoleAssistant", "promptRoleDefault" @@ -52,7 +61,7 @@ void assistantTest(String promptName) { @ParameterizedTest @ValueSource(strings = { - "prompt4", "prompt5", "prompt6", "prompt7", "promptRoleUser" + "prompt4", "prompt5", "prompt6", "prompt7", "prompt9", "promptRoleUser" }) void userTest(String promptName) { runTest(promptName, McpRole.USER); @@ -60,7 +69,7 @@ void userTest(String promptName) { void runTest(String promptName, McpRole role) { McpGetPromptResult result = client.getPrompt(promptName, Map.of("prompt", "prompt")); - assertThat(result.description(), is(PROMPT_DESCRIPTION)); + assertThat(result.description(), is(nullValue())); var messages = result.messages(); assertThat(messages.size(), is(1)); diff --git a/tests/declarative/src/test/java/io/helidon/extensions/mcp/tests/declarative/AbstractLangchain4jResourcesServerTest.java b/tests/declarative/src/test/java/io/helidon/extensions/mcp/tests/declarative/AbstractLangchain4jResourcesServerTest.java index f3c52069..f3390eda 100644 --- a/tests/declarative/src/test/java/io/helidon/extensions/mcp/tests/declarative/AbstractLangchain4jResourcesServerTest.java +++ b/tests/declarative/src/test/java/io/helidon/extensions/mcp/tests/declarative/AbstractLangchain4jResourcesServerTest.java @@ -19,9 +19,11 @@ import java.util.List; import dev.langchain4j.mcp.client.McpClient; +import dev.langchain4j.mcp.client.McpResource; import dev.langchain4j.mcp.client.McpResourceContents; import dev.langchain4j.mcp.client.McpTextResourceContents; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -40,10 +42,16 @@ static void afterAll() throws Exception { } } + @Test + void listResources() { + List resources = client.listResources(); + assertThat(resources.size(), is(7)); + } + @ParameterizedTest @ValueSource(strings = { "resource", "resource1", "resource2", "resource3", - "resource4", "resource5" + "resource4", "resource5", "resource6" }) void readResource(String uri) { var result = client.readResource(uri); diff --git a/tests/declarative/src/test/java/io/helidon/extensions/mcp/tests/declarative/AbstractLangchain4jToolsServerTest.java b/tests/declarative/src/test/java/io/helidon/extensions/mcp/tests/declarative/AbstractLangchain4jToolsServerTest.java index beb578c2..c7ecb6de 100644 --- a/tests/declarative/src/test/java/io/helidon/extensions/mcp/tests/declarative/AbstractLangchain4jToolsServerTest.java +++ b/tests/declarative/src/test/java/io/helidon/extensions/mcp/tests/declarative/AbstractLangchain4jToolsServerTest.java @@ -41,7 +41,7 @@ static void afterAll() throws Exception { @Test void testListTools() { var list = client.listTools(); - assertThat(list.size(), is(14)); + assertThat(list.size(), is(19)); } @ParameterizedTest @@ -67,7 +67,7 @@ void runToolWithArgumentsTest(String name) { } @ParameterizedTest - @ValueSource(strings = {"tool1", "tool3", "tool10", "tool11", "tool12", "tool13"}) + @ValueSource(strings = {"tool1", "tool3", "tool10", "tool11", "tool12", "tool13", "tool14", "tool15", "tool16", "tool17"}) void runToolTest(String name) { var result = client.executeTool(ToolExecutionRequest.builder() .name(name)