Bug description
I have implemented an MCP Server for weather queries via Baidu, and two Tools have been implemented: String getDistrictId(String district) and String getWeatherForecastDistrictId(String districtId). When using the MCP Client of Spring AI to query "济南今天的天气", the following error occurs. Based on the preliminary analysis, it seems that an error occurred when calling the getWeatherForecastDistrictId tool. During the preliminary troubleshooting, it was found that the text in the response returned by the first call to getDistrictId is already a String. Then, when the large language model is called to obtain the tool to be called next time, the toolInputArguments that should be provided is supposed to be a String that can be deserialized into a Map. However, what is actually provided is '{"districtId": "370100"}', so an error occurs during the deserialization again. The error message is as follows.
2025-04-19T15:07:06.172+08:00 DEBUG 4904 --- [mcp] [nio-8089-exec-1] o.s.a.m.tool.DefaultToolCallingManager : Executing tool call: spring_ai_mcp_client_server1_getWeatherForecastDistrictId
2025-04-19T15:07:06.193+08:00 DEBUG 4904 --- [mcp] [nio-8089-exec-1] o.s.web.servlet.DispatcherServlet : Failed to complete request: java.lang.RuntimeException: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `java.util.HashMap` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('{"districtId": "370100"}')
at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 1]
2025-04-19T15:07:06.195+08:00 ERROR 4904 --- [mcp] [nio-8089-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `java.util.HashMap` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('{"districtId": "370100"}')
at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 1]] with root cause
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `java.util.HashMap` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('{"districtId": "370100"}')
at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 1]
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63) ~[jackson-databind-2.17.3.jar:2.17.3]
at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1754) ~[jackson-databind-2.17.3.jar:2.17.3]
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1379) ~[jackson-databind-2.17.3.jar:2.17.3]
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._deserializeFromString(StdDeserializer.java:311) ~[jackson-databind-2.17.3.jar:2.17.3]
at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:454) ~[jackson-databind-2.17.3.jar:2.17.3]
at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:32) ~[jackson-databind-2.17.3.jar:2.17.3]
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342) ~[jackson-databind-2.17.3.jar:2.17.3]
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4905) ~[jackson-databind-2.17.3.jar:2.17.3]
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3848) ~[jackson-databind-2.17.3.jar:2.17.3]
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3831) ~[jackson-databind-2.17.3.jar:2.17.3]
at org.springframework.ai.model.ModelOptionsUtils.jsonToMap(ModelOptionsUtils.java:91) ~[spring-ai-model-1.0.0-M7.jar:1.0.0-M7]
at org.springframework.ai.mcp.SyncMcpToolCallback.call(SyncMcpToolCallback.java:112) ~[spring-ai-mcp-1.0.0-M7.jar:1.0.0-M7]
at org.springframework.ai.mcp.SyncMcpToolCallback.call(SyncMcpToolCallback.java:125) ~[spring-ai-mcp-1.0.0-M7.jar:1.0.0-M7]
at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCall(DefaultToolCallingManager.java:227) ~[spring-ai-model-1.0.0-M7.jar:1.0.0-M7]
at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCalls(DefaultToolCallingManager.java:139) ~[spring-ai-model-1.0.0-M7.jar:1.0.0-M7]
at org.springframework.ai.openai.OpenAiChatModel.internalCall(OpenAiChatModel.java:242) ~[spring-ai-openai-1.0.0-M7.jar:1.0.0-M7]
at org.springframework.ai.openai.OpenAiChatModel.internalCall(OpenAiChatModel.java:252) ~[spring-ai-openai-1.0.0-M7.jar:1.0.0-M7]
at org.springframework.ai.openai.OpenAiChatModel.call(OpenAiChatModel.java:180) ~[spring-ai-openai-1.0.0-M7.jar:1.0.0-M7]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$1.aroundCall(DefaultChatClient.java:680) ~[spring-ai-client-chat-1.0.0-M7.jar:1.0.0-M7]
at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.lambda$nextAroundCall$1(DefaultAroundAdvisorChain.java:98) ~[spring-ai-client-chat-1.0.0-M7.jar:1.0.0-M7]
at io.micrometer.observation.Observation.observe(Observation.java:565) ~[micrometer-observation-1.13.8.jar:1.13.8]
at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.nextAroundCall(DefaultAroundAdvisorChain.java:98) ~[spring-ai-client-chat-1.0.0-M7.jar:1.0.0-M7]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.doGetChatResponse(DefaultChatClient.java:493) ~[spring-ai-client-chat-1.0.0-M7.jar:1.0.0-M7]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.lambda$doGetObservableChatResponse$1(DefaultChatClient.java:482) ~[spring-ai-client-chat-1.0.0-M7.jar:1.0.0-M7]
at io.micrometer.observation.Observation.observe(Observation.java:565) ~[micrometer-observation-1.13.8.jar:1.13.8]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.doGetObservableChatResponse(DefaultChatClient.java:482) ~[spring-ai-client-chat-1.0.0-M7.jar:1.0.0-M7]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.doGetChatResponse(DefaultChatClient.java:466) ~[spring-ai-client-chat-1.0.0-M7.jar:1.0.0-M7]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.content(DefaultChatClient.java:516) ~[spring-ai-client-chat-1.0.0-M7.jar:1.0.0-M7]
at com.junxi.demo.ai.mcp.client.ctrl.WeatherWithMPCController.chat(WeatherWithMPCController.java:37) ~[classes/:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.1.15.jar:6.1.15]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188) ~[spring-web-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.1.15.jar:6.1.15]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.33.jar:6.0]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.15.jar:6.1.15]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.33.jar:6.0]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.33.jar:10.1.33]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.15.jar:6.1.15]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.15.jar:6.1.15]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.15.jar:6.1.15]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.15.jar:6.1.15]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:113) ~[spring-web-6.1.15.jar:6.1.15]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.15.jar:6.1.15]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.15.jar:6.1.15]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.15.jar:6.1.15]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
Environment Please provide as many details as possible: Spring AI version, Java version, which vector store you use if any, etc Spring AI version:1.0.0-M7 java version:21.0.2
Steps to reproduce
Steps to reproduce the issue.
The implementation of the MCP Server is as described above; the core implementation of the MCP Client is as follows:
this.chatClient = chatClientBuilder
.defaultSystem("你是一个专业的智能助手,回答需简洁准确")
.defaultOptions(OpenAiChatOptions.builder()
.model("Qwen/QwQ-32B")
.build())
.defaultTools(tools)
.build();
Expected behavior According to the implementation expectation, it should be to call getDistrictId to obtain the city ID of Jinan, and then call getWeatherForecastDistrictId to obtain the local weather conditions in Jinan based on the city ID. In reality, the above-mentioned error occurred when calling getWeatherForecastDistrictId.
Minimal Complete Reproducible example The implementation of the MCP Server is as follows:
public class BaiDuWeatherService {
// 百度免费天气API基础URL
private static final String BASE_URL = "https://api.map.baidu.com";
private final RestClient restClient;
public BaiDuWeatherService() {
this.restClient = RestClient.builder()
.baseUrl(BASE_URL)
.defaultHeader("Accept", "application/json")
.build();
}
@Tool(description = "根据城市名称获取城市ID")
public String getDistrictId(@ToolParam(description = "城市名称") String district) {
return new BaiDuTools().getDistrictId(district);
}
@Tool(description = "根据城市ID获取天气")
public String getWeatherForecastDistrictId(@ToolParam(description = "城市Id") String districtId) {
String res = restClient.get()
.uri(new BaiDuTools().getSn(districtId))
.retrieve()
.body(String.class);
System.out.println(res);
return res;
}
public static void main(String[] args) {
BaiDuWeatherService service = new BaiDuWeatherService();
// System.out.println(service.getWeatherForecastDistrictId(service.getDistrictId("北京")));
}
}
The implementation of BaiDuTools is as follows:
public class BaiDuTools {
private static final String sk = "百度的SK"; //需替换为正式的
private static final String ak = "百度的ak"; //需替换为正式的
private static final Map<String, String> districts = new HashMap<>(4600); //总共3395个区县
static {
String csvFilePath = "F:\\Downloads\\weather_district_id.csv";
String districtHeader = "district";
String districtIdHeader = "district_id";
try (BufferedReader reader = new BufferedReader(new FileReader(csvFilePath));
CSVParser csvParser = new CSVParser(reader, CSVFormat.DEFAULT.withFirstRecordAsHeader())
/*csvParser = new CSVParser(reader, CSVFormat.DEFAULT.builder().setHeader(districtIdHeader, districtHeader).setSkipHeaderRecord(true).build())*/) {
CSVFormat.DEFAULT.builder().setSkipHeaderRecord(true);
for (CSVRecord csvRecord : csvParser) {
String district = csvRecord.get(districtHeader);
String districtId = csvRecord.get(districtIdHeader);
districts.put(district, districtId);
}
// 打印HashMap中的内容(可根据需要删除)
for (Map.Entry<String, String> entry : districts.entrySet()) {
System.out.println("District: " + entry.getKey() + ", District ID: " + entry.getValue());
}
} catch (IOException e) {
e.printStackTrace();
}
}
public String getSn(String districtId) {
Map<String, String> paramsMap = new LinkedHashMap<>();
paramsMap.put("district_id", districtId);
paramsMap.put("data_type", "all");
paramsMap.put("ak", ak);
// 调用下面的toQueryString方法,对LinkedHashMap内所有value作utf8编码,拼接返回结果address=%E7%99%BE%E5%BA%A6%E5%A4%A7%E5%8E%A6&output=json&ak=yourak
String paramsStr = toQueryString(paramsMap);
// 对paramsStr前面拼接上/geocoder/v2/?,后面直接拼接yoursk得到/geocoder/v2/?address=%E7%99%BE%E5%BA%A6%E5%A4%A7%E5%8E%A6&output=json&ak=yourakyoursk
// String wholeStr = "/geocoder/v2/?" + paramsStr + sk;
String wholeStr = "/weather/v1/?" + paramsStr + sk;
// 对上面wholeStr再作utf8编码
String tempStr = URLEncoder.encode(wholeStr, StandardCharsets.UTF_8);
System.out.println("sn is " + MD5(tempStr));
// 调用下面的MD5方法得到最后的sn签名7de5a22212ffaa9e326444c75a58f9a0
return "/weather/v1/?" + paramsStr + "&sn=" + MD5(tempStr);
}
public String getDistrictId(String district) {
return districts.get(district);
}
// 对Map内所有value作utf8编码,拼接返回结果
private String toQueryString(Map<?, ?> data) {
StringBuilder queryString = new StringBuilder();
for (Entry<?, ?> pair : data.entrySet()) {
queryString.append(pair.getKey()).append("=");
queryString.append(URLEncoder.encode((String) pair.getValue(),
StandardCharsets.UTF_8)).append("&");
}
if (!queryString.isEmpty()) {
queryString.deleteCharAt(queryString.length() - 1);
}
return queryString.toString();
}
// 来自stackoverflow的MD5计算方法,调用了MessageDigest库函数,并把byte数组结果转换成16进制
private String MD5(String md5) {
try {
java.security.MessageDigest md = java.security.MessageDigest
.getInstance("MD5");
byte[] array = md.digest(md5.getBytes());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < array.length; ++i) {
sb.append(Integer.toHexString((array[i] & 0xFF) | 0x100), 1, 3);
}
return sb.toString();
} catch (java.security.NoSuchAlgorithmException e) {
}
return null;
}
}
Comment From: LeanderCherry
To add:
There is no problem with verifying this MCP Server using Cherry Studio. See the figure below.
Comment From: yangtuooc
- 工具调用的返回结果必须是一个JSON字符串
- 默认情况下会使用Jackson进行序列化
因此,在这段代码中你可以直接返回一个类对象或者Map对象
@Tool(description = "根据城市ID获取天气")
public Map<String,Object> getWeatherForecastDistrictId(@ToolParam(description = "城市Id") String districtId) {
return restClient.get()
.uri(new BaiDuTools().getSn(districtId))
.retrieve()
.body(Map.class);
}
另外,你可以参考官方的文档,有明确的关于工具调用结果转换的说明:https://docs.spring.io/spring-ai/reference/api/tools.html#_result_conversion
Comment From: LeanderCherry
多谢答复。 但是同样的Server在Cherry Studio和Cline里使用都是正常的,我还是认为这是Spring AI的一个Bug
Comment From: yangtuooc
多谢答复。 但是同样的Server在Cherry Studio和Cline里使用都是正常的,我还是认为这是Spring AI的一个Bug
这是不同框架在设计层面上约束的差异,不算严格意义上的Bug
Comment From: LeanderCherry
不对呢,我看你发的文档里面的例子也是直接返回的String呢(如下图),为什么我第一个返回的是String就不行呢,而且实际上从功能实现角度讲返回String是最合理的(通过城市名称获取城市ID,城市ID实际就是一个String最合理)
Comment From: LeanderCherry
多谢答复。 但是同样的Server在Cherry Studio和Cline里使用都是正常的,我还是认为这是Spring AI的一个Bug
这是不同框架在设计层面上约束的差异,不算严格意义上的Bug
忘记引用你的回复了,不好意思。 我看你发的文档里面的例子也是直接返回的String呢(如下图),为什么我第一个tool返回的是String就不行呢,而且实际上从功能实现角度讲返回String是最合理的(通过城市名称获取城市ID,城市ID实际是一个String)
Comment From: LeanderCherry
- 工具调用的返回结果必须是一个JSON字符串
- 默认情况下会使用Jackson进行序列化
因此,在这段代码中你可以直接返回一个类对象或者Map对象
@Tool(description = "根据城市ID获取天气") public Map
getWeatherForecastDistrictId(@ToolParam(description = "城市Id") String districtId) { return restClient.get() .uri(new BaiDuTools().getSn(districtId)) .retrieve() .body(Map.class); } 另外,你可以参考官方的文档,有明确的关于工具调用结果转换的说明:https://docs.spring.io/spring-ai/reference/api/tools.html#_result_conversion
我把获取城市ID的工具改成如下方式实现,但是依然报同样的错误
@Tool(description = "根据城市名称获取城市ID")
public Map<String, Object> getDistrictId(@ToolParam(description = "城市名称") String district) {
return Map.of("districtId", new BaiDuTools().getDistrictId(district));
// return new BaiDuTools().getDistrictId(district);
}
我之前试过把District定义为一个class,包含 district和districtId两个字段,结果返回是可以正常序列化的,但是也报同样的错误。
我调试了下是在调用第二个Tool(getWeatherForecastDistrictId)获取天气预报信息的时候,DefaultToolCallingManager.java中获取到的toolInputArguments是错误的('{"districtId": "370100"}')这是一个整个字符串,而不是可以反序列化为Map的Json串,再进一步调用到SyncMcpToolCallback的call方法时就报错了,具体报错的代码是SyncMcpToolCallback.java中的如下地址
Comment From: markpollack
I can't follow this, if someone could describe the issue in English, we can take a look. Thanks.
Comment From: LeanderCherry
I can't follow this, if someone could describe the issue in English, we can take a look. Thanks.
thanks. I used a translation software to re - describe the issue in English. Please help me check it. Thank you. In addition: I changed the implementation of the tool for getDistrictId to the following way, but I still got the same error.
@Tool(description = "根据城市名称获取城市ID")
public Map<String, Object> getDistrictId(@ToolParam(description = "城市名称") String district) {
return Map.of("districtId", new BaiDuTools().getDistrictId(district));
// return new BaiDuTools().getDistrictId(district);
}
I also tried to define District as a class containing two fields, district and districtId. As a result, the return could be serialized normally, but I still got the same error.
I did some debugging and found that when calling the second tool (getWeatherForecastDistrictId) to get the weather forecast information, the toolInputArguments obtained in DefaultToolCallingManager.java was incorrect ('{"districtId": "370100"}'). This is an entire string, not a JSON string that can be deserialized into a Map. When further calling the call method of SyncMcpToolCallback, an error occurred. The specific error - reporting code is at the following position in SyncMcpToolCallback.java.
Comment From: yangtuooc
- 工具调用的返回结果必须是一个JSON字符串
- 默认情况下会使用Jackson进行序列化
因此,在这段代码中你可以直接返回一个类对象或者Map对象 @tool(description = "根据城市ID获取天气") public Map
getWeatherForecastDistrictId(@ToolParam(description = "城市Id") String districtId) { return restClient.get() .uri(new BaiDuTools().getSn(districtId)) .retrieve() .body(Map.class); } 另外,你可以参考官方的文档,有明确的关于工具调用结果转换的说明:https://docs.spring.io/spring-ai/reference/api/tools.html#_result_conversion 我把获取城市ID的工具改成如下方式实现,但是依然报同样的错误
@Tool(description = "根据城市名称获取城市ID") public Map<String, Object> getDistrictId(@ToolParam(description = "城市名称") String district) { return Map.of("districtId", new BaiDuTools().getDistrictId(district));
// return new BaiDuTools().getDistrictId(district); } 我之前试过把District定义为一个class,包含 district和districtId两个字段,结果返回是可以正常序列化的,但是也报同样的错误。 我调试了下是在调用第二个Tool(getWeatherForecastDistrictId)获取天气预报信息的时候,DefaultToolCallingManager.java中获取到的toolInputArguments是错误的('{"districtId": "370100"}')这是一个整个字符串,而不是可以反序列化为Map的Json串,再进一步调用到SyncMcpToolCallback的call方法时就报错了,具体报错的代码是SyncMcpToolCallback.java中的如下地址
我按照你的这几种方式都进行了验证,没有复现问题...
Comment From: LeanderCherry
- 工具调用的返回结果必须是一个JSON字符串
- 默认情况下会使用Jackson进行序列化
因此,在这段代码中你可以直接返回一个类对象或者Map对象 @tool(description = "根据城市ID获取天气") public Map
getWeatherForecastDistrictId(@ToolParam(description = "城市Id") String districtId) { return restClient.get() .uri(new BaiDuTools().getSn(districtId)) .retrieve() .body(Map.class); } 另外,你可以参考官方的文档,有明确的关于工具调用结果转换的说明:https://docs.spring.io/spring-ai/reference/api/tools.html#_result_conversion 我把获取城市ID的工具改成如下方式实现,但是依然报同样的错误
@Tool(description = "根据城市名称获取城市ID") public Map<String, Object> getDistrictId(@ToolParam(description = "城市名称") String district) { return Map.of("districtId", new BaiDuTools().getDistrictId(district));
// return new BaiDuTools().getDistrictId(district); } 我之前试过把District定义为一个class,包含 district和districtId两个字段,结果返回是可以正常序列化的,但是也报同样的错误。 我调试了下是在调用第二个Tool(getWeatherForecastDistrictId)获取天气预报信息的时候,DefaultToolCallingManager.java中获取到的toolInputArguments是错误的('{"districtId": "370100"}')这是一个整个字符串,而不是可以反序列化为Map的Json串,再进一步调用到SyncMcpToolCallback的call方法时就报错了,具体报错的代码是SyncMcpToolCallback.java中的如下地址
我按照你的这几种方式都进行了验证,没有复现问题...
我试了怎么都不行,我用的Springboot版本是3.4.4,然后Client和Server都是使用的webflux实现,LLM使用的硅基流动的Qwen/QwQ-32B,方便说下你使用的基本信息吗
Comment From: yangtuooc
- 工具调用的返回结果必须是一个JSON字符串
- 默认情况下会使用Jackson进行序列化
因此,在这段代码中你可以直接返回一个类对象或者Map对象 @tool(description = "根据城市ID获取天气") public Map
getWeatherForecastDistrictId(@ToolParam(description = "城市Id") String districtId) { return restClient.get() .uri(new BaiDuTools().getSn(districtId)) .retrieve() .body(Map.class); } 另外,你可以参考官方的文档,有明确的关于工具调用结果转换的说明:https://docs.spring.io/spring-ai/reference/api/tools.html#_result_conversion 我把获取城市ID的工具改成如下方式实现,但是依然报同样的错误
@Tool(description = "根据城市名称获取城市ID") public Map<String, Object> getDistrictId(@ToolParam(description = "城市名称") String district) { return Map.of("districtId", new BaiDuTools().getDistrictId(district));
// return new BaiDuTools().getDistrictId(district); } 我之前试过把District定义为一个class,包含 district和districtId两个字段,结果返回是可以正常序列化的,但是也报同样的错误。 我调试了下是在调用第二个Tool(getWeatherForecastDistrictId)获取天气预报信息的时候,DefaultToolCallingManager.java中获取到的toolInputArguments是错误的('{"districtId": "370100"}')这是一个整个字符串,而不是可以反序列化为Map的Json串,再进一步调用到SyncMcpToolCallback的call方法时就报错了,具体报错的代码是SyncMcpToolCallback.java中的如下地址
我按照你的这几种方式都进行了验证,没有复现问题...
我试了怎么都不行,我用的Springboot版本是3.4.4,然后Client和Server都是使用的webflux实现,LLM使用的硅基流动的Qwen/QwQ-32B,方便说下你使用的基本信息吗
你可以将你的项目上传到Github,我clone下来试试
我使用的框架版本跟你是保持一致的,除了模型,我使用的是qwen-plus,但应该跟这没有关系
Comment From: luojinggit
我的报错和你类似,但是我是调试时偶尔出现,模型有时候弄得参数好,有时候弄得参数不对就会出错。
Comment From: LeanderCherry
- 工具调用的返回结果必须是一个JSON字符串
- 默认情况下会使用Jackson进行序列化
因此,在这段代码中你可以直接返回一个类对象或者Map对象 @tool(description = "根据城市ID获取天气") public Map
getWeatherForecastDistrictId(@ToolParam(description = "城市Id") String districtId) { return restClient.get() .uri(new BaiDuTools().getSn(districtId)) .retrieve() .body(Map.class); } 另外,你可以参考官方的文档,有明确的关于工具调用结果转换的说明:https://docs.spring.io/spring-ai/reference/api/tools.html#_result_conversion 我把获取城市ID的工具改成如下方式实现,但是依然报同样的错误
@Tool(description = "根据城市名称获取城市ID") public Map<String, Object> getDistrictId(@ToolParam(description = "城市名称") String district) { return Map.of("districtId", new BaiDuTools().getDistrictId(district));
// return new BaiDuTools().getDistrictId(district); } 我之前试过把District定义为一个class,包含 district和districtId两个字段,结果返回是可以正常序列化的,但是也报同样的错误。 我调试了下是在调用第二个Tool(getWeatherForecastDistrictId)获取天气预报信息的时候,DefaultToolCallingManager.java中获取到的toolInputArguments是错误的('{"districtId": "370100"}')这是一个整个字符串,而不是可以反序列化为Map的Json串,再进一步调用到SyncMcpToolCallback的call方法时就报错了,具体报错的代码是SyncMcpToolCallback.java中的如下地址
我按照你的这几种方式都进行了验证,没有复现问题...
我试了怎么都不行,我用的Springboot版本是3.4.4,然后Client和Server都是使用的webflux实现,LLM使用的硅基流动的Qwen/QwQ-32B,方便说下你使用的基本信息吗
你可以将你的项目上传到Github,我clone下来试试
我使用的框架版本跟你是保持一致的,除了模型,我使用的是qwen-plus,但应该跟这没有关系
我稍后传一下,今天试的结果和昨天不完全一样,昨天是全部不行,今天是时而行,时而不行,不行的时候居多。
Comment From: LeanderCherry
我的报错和你类似,但是我是调试时偶尔出现,模型有时候弄得参数好,有时候弄得参数不对就会出错。
我今天也是这样的,时行时不行。我也跟踪调试看了,从调试情况看最大可能是LLM进行识别需要调用哪个工具,以及调用参数的时候返回的参数,有时候是能反序列化为Map的,有时候是不行的(其实是String类型输出的有问题),但是我觉得这应该还是SpringAI处理的有问题,因为Cherry Studio和Cline这两个Client我验证过了,是没问题的,所以问题不能说是LLM的。
Comment From: dxn95
我遇到了相同的问题,[当需要调用MCP Server的多个tool时,MCP Client在调用第二个tool时出现反序列化错误],单个tool没有问题
Comment From: johnyannj
+1 1.0.0 version
Comment From: johnyannj
I debug。this problem is about Qwen/QwQ-32B ,which return wrong json。