Skip to content

Commit eca8578

Browse files
authored
Merge pull request #32850 from ttmunyunguma/feature-mcp-server-32827
Report an error if ToolArg.name is not set #32827
2 parents 5ebceea + 7ad2d1e commit eca8578

File tree

9 files changed

+166
-8
lines changed

9 files changed

+166
-8
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 IBM Corporation and others.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License 2.0
5+
* which accompanies this distribution, and is available at
6+
* http://www.eclipse.org/legal/epl-2.0/
7+
*******************************************************************************/
8+
configurations {
9+
cdi40 {
10+
canBeResolved = true
11+
}
12+
}
13+
14+
artifacts {
15+
cdi40(file('build/libs/io.openliberty.jakarta.cdi.4.0.jar'))
16+
}

dev/io.openliberty.mcp.internal/src/io/openliberty/mcp/internal/McpCdiExtension.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,10 @@ void afterDeploymentValidation(@Observes AfterDeploymentValidation afterDeployme
6363
private void reportOnToolArgEdgeCases(AfterDeploymentValidation afterDeploymentValidation) {
6464
StringBuilder sbBlankArgs = new StringBuilder("Blank arguments found in MCP Tool:");
6565
StringBuilder sbDuplicateArgs = new StringBuilder("Duplicate arguments found in MCP Tool:");
66+
StringBuilder sbMissingArgs = new StringBuilder("Missing arguments found in MCP Tool:");
6667
boolean blankArgumentsFound = false;
6768
boolean duplicateArgumentsFound = false;
69+
boolean missingArgumentName = false;
6870

6971
for (ToolMetadata tool : tools.getAllTools()) {
7072
Map<String, ArgumentMetadata> arguments = tool.arguments();
@@ -76,6 +78,10 @@ private void reportOnToolArgEdgeCases(AfterDeploymentValidation afterDeploymentV
7678
} else if (arguments.get(argName).isDuplicate()) {
7779
sbDuplicateArgs.append("\n").append("Tool: " + tool.getToolQualifiedName() + " - Argument: " + argName);
7880
duplicateArgumentsFound = true;
81+
} else if (argName.equals(ToolMetadata.MISSING_TOOL_ARG_NAME)) {
82+
sbMissingArgs.append("\n").append("Tool: " + tool.getToolQualifiedName());
83+
sbMissingArgs.append("\n Tool argument name was not provided for the parameter. Either add a name to the @ToolArg annotation, or to add the -parameters compiler option to use the parameter name");
84+
missingArgumentName = true;
7985
}
8086
}
8187
}
@@ -85,6 +91,9 @@ private void reportOnToolArgEdgeCases(AfterDeploymentValidation afterDeploymentV
8591
if (duplicateArgumentsFound) {
8692
afterDeploymentValidation.addDeploymentProblem(new Exception(sbDuplicateArgs.toString()));
8793
}
94+
if (missingArgumentName) {
95+
afterDeploymentValidation.addDeploymentProblem(new Exception(sbMissingArgs.toString()));
96+
}
8897
}
8998

9099
private void reportOnDuplicateTools(AfterDeploymentValidation afterDeploymentValidation) {

dev/io.openliberty.mcp.internal/src/io/openliberty/mcp/internal/ToolMetadata.java

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public record ToolMetadata(Tool annotation, Bean<?> bean, AnnotatedMethod<?> met
2929
String name, String title, String description,
3030
List<Class<? extends Throwable>> businessExceptions) {
3131

32+
public static final String MISSING_TOOL_ARG_NAME = "<<<MISSING TOOL_ARG NAME>>>";
33+
3234
public record ArgumentMetadata(Type type, int index, String description, boolean required, boolean isDuplicate) {}
3335

3436
public record SpecialArgumentMetadata(SpecialArgumentType.Resolution typeResolution, int index) {}
@@ -51,17 +53,42 @@ public static ToolMetadata createFrom(Tool annotation, Bean<?> bean, AnnotatedMe
5153

5254
private static Map<String, ArgumentMetadata> getArgumentMap(AnnotatedMethod<?> method) {
5355
Map<String, ArgumentMetadata> result = new HashMap<>();
54-
for (AnnotatedParameter<?> p : method.getParameters()) {
55-
ToolArg pInfo = p.getAnnotation(ToolArg.class);
56-
if (pInfo != null) {
57-
String toolArgName = (pInfo.name().equals(ToolArg.ELEMENT_NAME)) ? p.getJavaParameter().getName() : pInfo.name(); // p.getJavaParameter().getName() needs java compiler -parameter flag to work
58-
boolean isDuplicateArg = result.containsKey(toolArgName);
59-
result.put(toolArgName, new ArgumentMetadata(p.getBaseType(), p.getPosition(), pInfo.description(), pInfo.required(), isDuplicateArg));
56+
57+
for (AnnotatedParameter<?> param : method.getParameters()) {
58+
59+
ToolArg argAnnotation = param.getAnnotation(ToolArg.class);
60+
61+
if (argAnnotation == null) {
62+
continue;
6063
}
64+
65+
String argName = resolveArgumentName(param, argAnnotation);
66+
boolean isDuplicateArg = result.containsKey(argName);
67+
68+
result.put(argName, new ArgumentMetadata(param.getBaseType(),
69+
param.getPosition(),
70+
argAnnotation.description(),
71+
argAnnotation.required(),
72+
isDuplicateArg));
6173
}
6274
return result.isEmpty() ? Collections.emptyMap() : result;
6375
}
6476

77+
private static String resolveArgumentName(AnnotatedParameter<?> param, ToolArg argAnnotation) {
78+
String argAnnotationName = argAnnotation.name();
79+
80+
if (!argAnnotationName.equals(ToolArg.ELEMENT_NAME)) {
81+
return argAnnotationName;
82+
}
83+
84+
if (param.getJavaParameter().isNamePresent()) {
85+
// needs java compiler -parameter flag to work
86+
return param.getJavaParameter().getName();
87+
}
88+
89+
return MISSING_TOOL_ARG_NAME;
90+
}
91+
6592
private static List<SpecialArgumentMetadata> getSpecialArgumentList(AnnotatedMethod<?> method) {
6693
List<SpecialArgumentMetadata> result = new ArrayList<>();
6794
for (AnnotatedParameter<?> p : method.getParameters()) {

dev/io.openliberty.mcp.internal_fat/.classpath

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
<attribute name="test" value="true"/>
66
</attributes>
77
</classpathentry>
8+
<classpathentry kind="src" path="noParamNames">
9+
<attributes>
10+
<attribute name="test" value="true"/>
11+
</attributes>
12+
</classpathentry>
813
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
914
<attributes>
1015
<attribute name="module" value="true"/>

dev/io.openliberty.mcp.internal_fat/build.gradle

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,38 @@ apply from: '../wlp-gradle/subprojects/repos.gradle'
1515
configurations.requiredLibs {
1616
transitive = false
1717
}
18+
19+
sourceSets {
20+
// Source set where we don't include parameter names
21+
noParamNames {
22+
java {
23+
srcDirs = ['noParamNames']
24+
}
25+
}
26+
}
27+
1828
dependencies {
1929
requiredLibs 'org.json:json:20080701',
2030
'org.skyscreamer:jsonassert:2.0-rc1'
31+
noParamNamesCompileOnly project(':io.openliberty.mcp'),
32+
project(path: ':io.openliberty.jakarta', configuration: 'cdi40')
33+
implementation sourceSets.noParamNames.output
2134
}
35+
2236
/* Added to allow method parmaters to be available for MCP Tool Server*/
2337
// Allow us to test getting the name of a tool argument directly from the Java
2438
compileJava {
25-
options.compilerArgs << '-parameters'
26-
}
39+
options.compilerArgs << '-parameters'
40+
}
41+
42+
// Build the noParamNames code into a jar
43+
tasks.register('noParamNamesJar', Jar) {
44+
dependsOn noParamNamesClasses
45+
from sourceSets.noParamNames.output
46+
archiveFileName = 'noParamNames.jar'
47+
}
48+
49+
// Build the noParamNames jar before the autoFVT zip so that it gets included in the zip
50+
tasks.named('autoFVT') {
51+
dependsOn noParamNamesJar
52+
}

dev/io.openliberty.mcp.internal_fat/fat/src/io/openliberty/mcp/internal/fat/FATSuite.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.openliberty.mcp.internal.fat.protocol.ProtocolVersionTest;
2626
import io.openliberty.mcp.internal.fat.tool.CancellationTest;
2727
import io.openliberty.mcp.internal.fat.tool.DeploymentProblemTest;
28+
import io.openliberty.mcp.internal.fat.tool.NoParamNameTest;
2829
import io.openliberty.mcp.internal.fat.tool.ToolErrorHandlingTest;
2930
import io.openliberty.mcp.internal.fat.tool.ToolTest;
3031

@@ -38,6 +39,7 @@
3839
CancellationTest.class,
3940
HttpTest.class,
4041
LifecycleTest.class,
42+
NoParamNameTest.class,
4143
ProtocolVersionTest.class,
4244
ToolErrorHandlingTest.class,
4345
ToolTest.class
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 IBM Corporation and others.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License 2.0
5+
* which accompanies this distribution, and is available at
6+
* http://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*******************************************************************************/
10+
package io.openliberty.mcp.internal.fat.tool;
11+
12+
import java.util.List;
13+
14+
import org.junit.AfterClass;
15+
import org.junit.BeforeClass;
16+
import org.junit.Test;
17+
import org.junit.runner.RunWith;
18+
19+
import componenttest.annotation.Server;
20+
import componenttest.custom.junit.runner.FATRunner;
21+
import componenttest.topology.impl.LibertyServer;
22+
import componenttest.topology.utils.FATServletClient;
23+
import io.openliberty.mcp.internal.fat.noparamtool.NoParamTools;
24+
25+
@RunWith(FATRunner.class)
26+
public class NoParamNameTest extends FATServletClient {
27+
@Server("mcp-server")
28+
public static LibertyServer server;
29+
30+
@BeforeClass
31+
public static void setup() throws Exception {
32+
ExpectedAppFailureValidator.deployAppToAssertFailure(server, "ExpectedNoParamNameFailureTest", NoParamTools.class.getPackage());
33+
}
34+
35+
@AfterClass
36+
public static void teardown() throws Exception {
37+
server.stopServer(ExpectedAppFailureValidator.APP_START_FAILED_CODE);
38+
}
39+
40+
@Test
41+
public void testNoParamNameToolArg() throws Exception {
42+
String expectedErrorHeader = "Missing arguments found in MCP Tool:";
43+
List<String> expectedErrorList = List.of("io.openliberty.mcp.internal.fat.noparamtool.NoParamTools.missingToolArgNameTool");
44+
ExpectedAppFailureValidator.findAndAssertExpectedErrorsInLogs("Missing arguments found in MCP Tool: ", expectedErrorHeader, expectedErrorList, server);
45+
}
46+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 IBM Corporation and others.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License 2.0
5+
* which accompanies this distribution, and is available at
6+
* http://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*******************************************************************************/
10+
package io.openliberty.mcp.internal.fat.noparamtool;
11+
12+
import io.openliberty.mcp.annotations.Tool;
13+
import io.openliberty.mcp.annotations.ToolArg;
14+
import jakarta.enterprise.context.ApplicationScoped;
15+
16+
@ApplicationScoped
17+
public class NoParamTools {
18+
19+
@Tool(name = "missingToolArgNameTool", title = "missing ToolArgName Tool", description = "ToolArgName is missing so app wont start")
20+
public String missingToolArgNameTool(@ToolArg(description = "input to echo") String input) {
21+
return input;
22+
}
23+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# No Param Names
2+
3+
This source set contains a basic tool called *missingToolArgNameTool*. The reason the tool has been separated out of the the rest of the application is because we need this to be compiled without the -parameters flag. This allows us to test that if parameter names are not included, then we report an error.
4+

0 commit comments

Comments
 (0)