Description
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;
}
}