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..599da76a 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 @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import io.helidon.codegen.classmodel.ClassModel; import io.helidon.codegen.classmodel.Method; @@ -60,6 +61,11 @@ class McpCodegenUtil { private McpCodegenUtil() { } + static boolean isNullable(TypedElementInfo param) { + return Stream.concat(param.annotations().stream(), param.elementTypeAnnotations().stream()) + .anyMatch(a -> a.typeName().className().equals("Nullable")); + } + static boolean isBoolean(TypeName type) { return TypeNames.PRIMITIVE_BOOLEAN.equals(type) || TypeNames.BOXED_BOOLEAN.equals(type); diff --git a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpJsonSchemaCodegen.java b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpJsonSchemaCodegen.java index ce6128f9..0e84b154 100644 --- a/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpJsonSchemaCodegen.java +++ b/codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpJsonSchemaCodegen.java @@ -19,6 +19,7 @@ import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import io.helidon.codegen.classmodel.Method; import io.helidon.common.types.TypeName; @@ -41,6 +42,10 @@ private McpJsonSchemaCodegen() { } static void addSchemaMethodBody(Method.Builder method, List fields) { + addSchemaMethodBody(method, fields, List.of()); + } + + static void addSchemaMethodBody(Method.Builder method, List fields, List requiredFields) { method.addContentLine("var builder = new StringBuilder();"); method.addContentLine("builder.append(\"{\");"); method.addContentLine("builder.append(\"\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\");"); @@ -53,7 +58,18 @@ static void addSchemaMethodBody(Method.Builder method, List fi method.addContentLine("builder.append(\", \");"); } } - method.addContentLine("builder.append(\"}}\");"); + method.addContentLine("builder.append(\"}\");"); + + if (!requiredFields.isEmpty()) { + String requiredJson = requiredFields.stream() + .map(f -> "\\\"" + f + "\\\"") + .collect(Collectors.joining(", ")); + method.addContent("builder.append(\", \\\"required\\\": [") + .addContent(requiredJson) + .addContentLine("]\");"); + } + + method.addContentLine("builder.append(\"}\");"); method.addContentLine("return builder.toString();"); } 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..f8a7da5e 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 @@ -39,6 +39,7 @@ import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.isIgnoredSchemaElement; import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.isList; import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.isMcpType; +import static io.helidon.extensions.mcp.codegen.McpCodegenUtil.isNullable; 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; @@ -101,6 +102,7 @@ private void addToolSchemaMethod(Method.Builder builder, TypedElementInfo elemen .addAnnotation(Annotations.OVERRIDE); List fields = new ArrayList<>(); + List requiredFields = new ArrayList<>(); for (TypedElementInfo param : element.parameterArguments()) { if (isIgnoredSchemaElement(param.typeName())) { continue; @@ -113,10 +115,14 @@ private void addToolSchemaMethod(Method.Builder builder, TypedElementInfo elemen .accessModifier(AccessModifier.PUBLIC); description.ifPresent(desc -> field.addAnnotation(Annotation.create(MCP_DESCRIPTION, desc))); fields.add(field.build()); + + if (!isNullable(param)) { + requiredFields.add(param.elementName()); + } } if (!fields.isEmpty()) { - addSchemaMethodBody(method, fields); + addSchemaMethodBody(method, fields, requiredFields); } else { method.addContentLine("return \"\";"); } diff --git a/tests/codegen/src/test/java/io/helidon/extensions/mcp/codegen/McpCodegenUtilTest.java b/tests/codegen/src/test/java/io/helidon/extensions/mcp/codegen/McpCodegenUtilTest.java new file mode 100644 index 00000000..a95f0bde --- /dev/null +++ b/tests/codegen/src/test/java/io/helidon/extensions/mcp/codegen/McpCodegenUtilTest.java @@ -0,0 +1,55 @@ +/* + * 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.codegen; + +import io.helidon.common.types.Annotation; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class McpCodegenUtilTest { + + private static final TypeName NULLABLE = TypeName.create("javax.annotation.Nullable"); + + @Test + void whenNoAnnotationsThenNotNullable() { + TypedElementInfo myParam = TypedElementInfo.builder() + .elementName("param") + .kind(ElementKind.PARAMETER) + .typeName(TypeName.create(String.class)) + .build(); + + assertThat(McpCodegenUtil.isNullable(myParam), is(false)); + } + + @Test + void whenNullableInParameterAnnotationsThenNullable() { + TypedElementInfo myParam = TypedElementInfo.builder() + .elementName("param") + .kind(ElementKind.PARAMETER) + .typeName(TypeName.create(String.class)) + .addAnnotation(Annotation.create(NULLABLE)) + .build(); + + assertThat(McpCodegenUtil.isNullable(myParam), is(true)); + } +}