Skip to content

Commit 2b95c3d

Browse files
committed
fix(#8): Mcp tools params type is Number will error
1 parent dc4aa1c commit 2b95c3d

File tree

5 files changed

+162
-140
lines changed

5 files changed

+162
-140
lines changed

src/main/java/com/github/codeboyzhou/mcp/declarative/util/StringHelper.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ public final class StringHelper {
1212
/** The empty string constant. */
1313
public static final String EMPTY = "";
1414

15+
/** The dot character constant. */
16+
public static final String DOT = ".";
17+
1518
/** The space character constant. */
1619
public static final String SPACE = " ";
1720

Lines changed: 73 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package com.github.codeboyzhou.mcp.declarative.util;
22

3-
import com.github.codeboyzhou.mcp.declarative.enums.JsonSchemaDataType;
3+
import java.util.Map;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
import java.util.function.Function;
6+
import org.jetbrains.annotations.NotNull;
7+
import org.jetbrains.annotations.Nullable;
48
import org.jetbrains.annotations.VisibleForTesting;
59

610
/**
@@ -10,6 +14,19 @@
1014
*/
1115
public final class TypeConverter {
1216

17+
/** Map of Java classes to their corresponding type conversion functions. */
18+
private static final Map<Class<?>, Function<@NotNull String, @NotNull Object>> CLASS_CONVERTERS;
19+
20+
/** Map of Java classes to their default values. */
21+
private static final Map<Class<?>, Object> DEFAULT_VALUES;
22+
23+
static {
24+
CLASS_CONVERTERS = new ConcurrentHashMap<>();
25+
DEFAULT_VALUES = new ConcurrentHashMap<>();
26+
initializeClassConverters();
27+
initializeDefaultValues();
28+
}
29+
1330
/**
1431
* Private constructor to prevent instantiation of the utility class.
1532
*
@@ -20,121 +37,78 @@ public final class TypeConverter {
2037
throw new UnsupportedOperationException("Utility class should not be instantiated");
2138
}
2239

40+
/** Initializes the map of Java classes to their corresponding type conversion functions. */
41+
private static void initializeClassConverters() {
42+
CLASS_CONVERTERS.put(String.class, value -> value);
43+
CLASS_CONVERTERS.put(int.class, Integer::parseInt);
44+
CLASS_CONVERTERS.put(Integer.class, Integer::parseInt);
45+
CLASS_CONVERTERS.put(long.class, Long::parseLong);
46+
CLASS_CONVERTERS.put(Long.class, Long::parseLong);
47+
CLASS_CONVERTERS.put(float.class, Float::parseFloat);
48+
CLASS_CONVERTERS.put(Float.class, Float::parseFloat);
49+
CLASS_CONVERTERS.put(double.class, Double::parseDouble);
50+
CLASS_CONVERTERS.put(Double.class, Double::parseDouble);
51+
CLASS_CONVERTERS.put(Number.class, TypeConverter::parseNumber);
52+
CLASS_CONVERTERS.put(boolean.class, Boolean::parseBoolean);
53+
CLASS_CONVERTERS.put(Boolean.class, Boolean::parseBoolean);
54+
}
55+
56+
/** Initializes the map of Java classes to their default values. */
57+
private static void initializeDefaultValues() {
58+
DEFAULT_VALUES.put(String.class, StringHelper.EMPTY);
59+
DEFAULT_VALUES.put(int.class, 0);
60+
DEFAULT_VALUES.put(Integer.class, 0);
61+
DEFAULT_VALUES.put(long.class, 0L);
62+
DEFAULT_VALUES.put(Long.class, 0L);
63+
DEFAULT_VALUES.put(float.class, 0.0F);
64+
DEFAULT_VALUES.put(Float.class, 0.0F);
65+
DEFAULT_VALUES.put(double.class, 0.0);
66+
DEFAULT_VALUES.put(Double.class, 0.0);
67+
DEFAULT_VALUES.put(Number.class, 0.0);
68+
DEFAULT_VALUES.put(boolean.class, false);
69+
DEFAULT_VALUES.put(Boolean.class, false);
70+
}
71+
2372
/**
24-
* Converts the given value to the specified target type. If the value is null, returns the
25-
* default value for the target type.
73+
* Parses the given string as a number, preferring double precision if the string contains a dot,
74+
* and falling back to integer or long if not.
2675
*
27-
* @param value the value to convert
28-
* @param targetType the target type to convert to
29-
* @return the converted value
76+
* @param number the string representation of the number
77+
* @return the parsed number
3078
*/
31-
public static Object convert(Object value, Class<?> targetType) {
32-
if (value == null) {
33-
return getDefaultValue(targetType);
79+
private static Number parseNumber(@NotNull String number) {
80+
// Parse as a double if it contains a dot
81+
if (number.contains(StringHelper.DOT)) {
82+
return Double.parseDouble(number);
3483
}
3584

36-
final String valueAsString = value.toString();
37-
38-
if (targetType == String.class) {
39-
return valueAsString;
40-
}
41-
if (targetType == int.class || targetType == Integer.class) {
42-
return Integer.parseInt(valueAsString);
43-
}
44-
if (targetType == long.class || targetType == Long.class) {
45-
return Long.parseLong(valueAsString);
46-
}
47-
if (targetType == float.class || targetType == Float.class) {
48-
return Float.parseFloat(valueAsString);
85+
// If it doesn't contain a dot, try to parse as an integer first
86+
try {
87+
return Integer.parseInt(number);
88+
} catch (NumberFormatException e) {
89+
// If it's not an integer, try to parse as a long
90+
return Long.parseLong(number);
4991
}
50-
if (targetType == double.class || targetType == Double.class) {
51-
return Double.parseDouble(valueAsString);
52-
}
53-
if (targetType == boolean.class || targetType == Boolean.class) {
54-
return Boolean.parseBoolean(valueAsString);
55-
}
56-
57-
return valueAsString;
5892
}
5993

6094
/**
61-
* Converts the given value to the specified target type based on the JSON schema type. If the
62-
* value is null, returns the default value for the JSON schema type.
95+
* Converts the given value to the specified target type. If the value is null, returns the
96+
* default value for the target type.
6397
*
6498
* @param value the value to convert
65-
* @param jsonSchemaType the JSON schema type to convert to
99+
* @param targetType the target type to convert to
66100
* @return the converted value
67101
*/
68-
public static Object convert(Object value, String jsonSchemaType) {
102+
public static Object convert(@Nullable Object value, Class<?> targetType) {
69103
if (value == null) {
70-
return getDefaultValue(jsonSchemaType);
104+
return DEFAULT_VALUES.get(targetType);
71105
}
72106

73-
final String valueAsString = value.toString();
74-
75-
if (JsonSchemaDataType.STRING.getType().equals(jsonSchemaType)) {
76-
return valueAsString;
77-
}
78-
if (JsonSchemaDataType.INTEGER.getType().equals(jsonSchemaType)) {
79-
return Integer.parseInt(valueAsString);
80-
}
81-
if (JsonSchemaDataType.NUMBER.getType().equals(jsonSchemaType)) {
82-
return Double.parseDouble(valueAsString);
83-
}
84-
if (JsonSchemaDataType.BOOLEAN.getType().equals(jsonSchemaType)) {
85-
return Boolean.parseBoolean(valueAsString);
86-
}
87-
88-
return valueAsString;
89-
}
90-
91-
/**
92-
* Returns the default value for the specified type.
93-
*
94-
* @param type the type to get the default value for
95-
* @return the default value for the specified type
96-
*/
97-
private static Object getDefaultValue(Class<?> type) {
98-
if (type == String.class) {
99-
return StringHelper.EMPTY;
100-
}
101-
if (type == int.class || type == Integer.class) {
102-
return 0;
107+
if (CLASS_CONVERTERS.containsKey(targetType)) {
108+
Function<String, Object> converter = CLASS_CONVERTERS.get(targetType);
109+
return converter.apply(value.toString());
103110
}
104-
if (type == long.class || type == Long.class) {
105-
return 0L;
106-
}
107-
if (type == float.class || type == Float.class) {
108-
return 0.0f;
109-
}
110-
if (type == double.class || type == Double.class) {
111-
return 0.0;
112-
}
113-
if (type == boolean.class || type == Boolean.class) {
114-
return false;
115-
}
116-
return null;
117-
}
118111

119-
/**
120-
* Returns the default value for the specified JSON schema type.
121-
*
122-
* @param jsonSchemaType the JSON schema type to get the default value for
123-
* @return the default value for the specified JSON schema type
124-
*/
125-
private static Object getDefaultValue(String jsonSchemaType) {
126-
if (JsonSchemaDataType.STRING.getType().equalsIgnoreCase(jsonSchemaType)) {
127-
return StringHelper.EMPTY;
128-
}
129-
if (JsonSchemaDataType.INTEGER.getType().equalsIgnoreCase(jsonSchemaType)) {
130-
return 0;
131-
}
132-
if (JsonSchemaDataType.NUMBER.getType().equalsIgnoreCase(jsonSchemaType)) {
133-
return 0.0;
134-
}
135-
if (JsonSchemaDataType.BOOLEAN.getType().equalsIgnoreCase(jsonSchemaType)) {
136-
return false;
137-
}
138-
return null;
112+
return value;
139113
}
140114
}

src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ private void verifyPromptCalled(
308308

309309
private void verifyToolsRegistered(McpSyncClient client) {
310310
List<McpSchema.Tool> tools = client.listTools().tools();
311-
assertEquals(10, tools.size());
311+
assertEquals(16, tools.size());
312312

313313
verifyToolRegistered(tools, "toolWithDefaultName", "title", "description", 0);
314314
verifyToolRegistered(tools, "toolWithDefaultTitle", "Not specified", "description", 0);
@@ -320,6 +320,12 @@ private void verifyToolsRegistered(McpSyncClient client) {
320320
verifyToolRegistered(tools, "toolWithMixedParams", "Not specified", "Not specified", 1);
321321
verifyToolRegistered(tools, "toolWithVoidReturn", "Not specified", "Not specified", 0);
322322
verifyToolRegistered(tools, "toolWithReturnNull", "Not specified", "Not specified", 0);
323+
verifyToolRegistered(tools, "toolWithIntegerParam", "Not specified", "Not specified", 1);
324+
verifyToolRegistered(tools, "toolWithLongParam", "Not specified", "Not specified", 1);
325+
verifyToolRegistered(tools, "toolWithFloatParam", "Not specified", "Not specified", 1);
326+
verifyToolRegistered(tools, "toolWithDoubleParam", "Not specified", "Not specified", 1);
327+
verifyToolRegistered(tools, "toolWithNumberParam", "Not specified", "Not specified", 1);
328+
verifyToolRegistered(tools, "toolWithBooleanParam", "Not specified", "Not specified", 1);
323329
}
324330

325331
private void verifyToolRegistered(
@@ -374,6 +380,36 @@ private void verifyToolsCalled(McpSyncClient client) {
374380
"toolWithReturnNull",
375381
Map.of(),
376382
"The method call succeeded but the return value is null");
383+
verifyToolCalled(
384+
client,
385+
"toolWithIntegerParam",
386+
Map.of("param", 123),
387+
"toolWithIntegerParam is called with param: 123");
388+
verifyToolCalled(
389+
client,
390+
"toolWithLongParam",
391+
Map.of("param", 123L),
392+
"toolWithLongParam is called with param: 123");
393+
verifyToolCalled(
394+
client,
395+
"toolWithFloatParam",
396+
Map.of("param", 123.0F),
397+
"toolWithFloatParam is called with param: 123.0");
398+
verifyToolCalled(
399+
client,
400+
"toolWithDoubleParam",
401+
Map.of("param", 123.0),
402+
"toolWithDoubleParam is called with param: 123.0");
403+
verifyToolCalled(
404+
client,
405+
"toolWithNumberParam",
406+
Map.of("param", 123),
407+
"toolWithNumberParam is called with param: 123");
408+
verifyToolCalled(
409+
client,
410+
"toolWithBooleanParam",
411+
Map.of("param", true),
412+
"toolWithBooleanParam is called with param: true");
377413
}
378414

379415
private void verifyToolCalled(

src/test/java/com/github/codeboyzhou/mcp/declarative/test/TestMcpToolsInDifferentClasses.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.github.codeboyzhou.mcp.declarative.test;
22

33
import com.github.codeboyzhou.mcp.declarative.annotation.McpTool;
4+
import com.github.codeboyzhou.mcp.declarative.annotation.McpToolParam;
45
import org.slf4j.Logger;
56
import org.slf4j.LoggerFactory;
67

@@ -18,4 +19,40 @@ public String toolWithReturnNull() {
1819
log.debug("calling toolWithReturnNull");
1920
return null;
2021
}
22+
23+
@McpTool
24+
public String toolWithIntegerParam(@McpToolParam(name = "param", required = true) Integer param) {
25+
log.debug("calling toolWithIntegerParam with param: {}", param);
26+
return "toolWithIntegerParam is called with param: " + param;
27+
}
28+
29+
@McpTool
30+
public String toolWithLongParam(@McpToolParam(name = "param", required = true) Long param) {
31+
log.debug("calling toolWithLongParam with param: {}", param);
32+
return "toolWithLongParam is called with param: " + param;
33+
}
34+
35+
@McpTool
36+
public String toolWithFloatParam(@McpToolParam(name = "param", required = true) Float param) {
37+
log.debug("calling toolWithFloatParam with param: {}", param);
38+
return "toolWithFloatParam is called with param: " + param;
39+
}
40+
41+
@McpTool
42+
public String toolWithDoubleParam(@McpToolParam(name = "param", required = true) Double param) {
43+
log.debug("calling toolWithDoubleParam with param: {}", param);
44+
return "toolWithDoubleParam is called with param: " + param;
45+
}
46+
47+
@McpTool
48+
public String toolWithNumberParam(@McpToolParam(name = "param", required = true) Number param) {
49+
log.debug("calling toolWithNumberParam with param: {}", param);
50+
return "toolWithNumberParam is called with param: " + param;
51+
}
52+
53+
@McpTool
54+
public String toolWithBooleanParam(@McpToolParam(name = "param", required = true) Boolean param) {
55+
log.debug("calling toolWithBooleanParam with param: {}", param);
56+
return "toolWithBooleanParam is called with param: " + param;
57+
}
2158
}

0 commit comments

Comments
 (0)