spring-ai icon indicating copy to clipboard operation
spring-ai copied to clipboard

当需要调用MCP Server的多个tool时,MCP Client在调用第二个tool时出现反序列化错误(When multiple tools of the MCP Server need to be called, the MCP Client encounters a deserialization error when calling the second tool.)

Open LeanderCherry opened this issue 7 months ago • 6 comments

Bug description 英语不好中文描述了哈 我实现了一个通过百度进行天气查询的MCP Server,实现了两个Tool:String getDistrictId(String district)和String getWeatherForecastDistrictId(String districtId),在使用Spring AI的MCP Client进行查询”济南今天的天气“时,报如下错误,从基本的分析情况看时在调用getWeatherForecastDistrictId这个tool时出现的了错误,初步排查时第一次调用getDistrictId返回的response里text已经是String了,然后调用大模型获取下次需要调用的tool时给出的toolInputArguments应该时一个可以反序列化为Map的String,但实际给出的是'{"districtId": "370100"}',因此再次反序列化出现错误 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. Image MCP Server实现见如上;MCP Client的核心实现如如下:

        this.chatClient  = chatClientBuilder
                .defaultSystem("你是一个专业的智能助手,回答需简洁准确")
                .defaultOptions(OpenAiChatOptions.builder()
                        .model("Qwen/QwQ-32B")
                        .build())
                .defaultTools(tools)
                .build();

Expected behavior A clear and concise description of what you expected to happen. 根据实现预期应该是跟调用getDistrictId获取到济南的城市ID,然后调用getWeatherForecastDistrictId根据城市ID获取到济南当地的天气情况,实际在调用getWeatherForecastDistrictId时出现了上述的错误。

Minimal Complete Reproducible example MCP Server的实现如下:

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("北京")));
    }
}

BaiDuTools实现:

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

LeanderCherry avatar Apr 21 '25 02:04 LeanderCherry

To add: There is no problem with verifying this MCP Server using Cherry Studio. See the figure below. Image

LeanderCherry avatar Apr 21 '25 02:04 LeanderCherry

  1. 工具调用的返回结果必须是一个JSON字符串
  2. 默认情况下会使用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

yangtuooc avatar Apr 21 '25 07:04 yangtuooc

多谢答复。 但是同样的Server在Cherry Studio和Cline里使用都是正常的,我还是认为这是Spring AI的一个Bug

LeanderCherry avatar Apr 21 '25 09:04 LeanderCherry

多谢答复。 但是同样的Server在Cherry Studio和Cline里使用都是正常的,我还是认为这是Spring AI的一个Bug

这是不同框架在设计层面上约束的差异,不算严格意义上的Bug

yangtuooc avatar Apr 21 '25 10:04 yangtuooc

不对呢,我看你发的文档里面的例子也是直接返回的String呢(如下图),为什么我第一个返回的是String就不行呢,而且实际上从功能实现角度讲返回String是最合理的(通过城市名称获取城市ID,城市ID实际就是一个String最合理) Image

LeanderCherry avatar Apr 21 '25 11:04 LeanderCherry

多谢答复。 但是同样的Server在Cherry Studio和Cline里使用都是正常的,我还是认为这是Spring AI的一个Bug

这是不同框架在设计层面上约束的差异,不算严格意义上的Bug

忘记引用你的回复了,不好意思。 我看你发的文档里面的例子也是直接返回的String呢(如下图),为什么我第一个tool返回的是String就不行呢,而且实际上从功能实现角度讲返回String是最合理的(通过城市名称获取城市ID,城市ID实际是一个String)

Image

LeanderCherry avatar Apr 22 '25 00:04 LeanderCherry

  1. 工具调用的返回结果必须是一个JSON字符串
  2. 默认情况下会使用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

我把获取城市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中的如下地址 Image

LeanderCherry avatar Apr 22 '25 03:04 LeanderCherry

I can't follow this, if someone could describe the issue in English, we can take a look. Thanks.

markpollack avatar Apr 22 '25 15:04 markpollack

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. Image

LeanderCherry avatar Apr 23 '25 02:04 LeanderCherry

  1. 工具调用的返回结果必须是一个JSON字符串
  2. 默认情况下会使用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

我把获取城市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中的如下地址 Image

我按照你的这几种方式都进行了验证,没有复现问题...

yangtuooc avatar Apr 23 '25 03:04 yangtuooc

  1. 工具调用的返回结果必须是一个JSON字符串
  2. 默认情况下会使用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

我把获取城市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中的如下地址 Image

我按照你的这几种方式都进行了验证,没有复现问题...

我试了怎么都不行,我用的Springboot版本是3.4.4,然后Client和Server都是使用的webflux实现,LLM使用的硅基流动的Qwen/QwQ-32B,方便说下你使用的基本信息吗

LeanderCherry avatar Apr 23 '25 03:04 LeanderCherry

  1. 工具调用的返回结果必须是一个JSON字符串
  2. 默认情况下会使用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

我把获取城市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中的如下地址 Image

我按照你的这几种方式都进行了验证,没有复现问题...

我试了怎么都不行,我用的Springboot版本是3.4.4,然后Client和Server都是使用的webflux实现,LLM使用的硅基流动的Qwen/QwQ-32B,方便说下你使用的基本信息吗

你可以将你的项目上传到Github,我clone下来试试

我使用的框架版本跟你是保持一致的,除了模型,我使用的是qwen-plus,但应该跟这没有关系

yangtuooc avatar Apr 23 '25 03:04 yangtuooc

我的报错和你类似,但是我是调试时偶尔出现,模型有时候弄得参数好,有时候弄得参数不对就会出错。

luojinggit avatar Apr 23 '25 07:04 luojinggit

  1. 工具调用的返回结果必须是一个JSON字符串
  2. 默认情况下会使用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

我把获取城市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中的如下地址 Image

我按照你的这几种方式都进行了验证,没有复现问题...

我试了怎么都不行,我用的Springboot版本是3.4.4,然后Client和Server都是使用的webflux实现,LLM使用的硅基流动的Qwen/QwQ-32B,方便说下你使用的基本信息吗

你可以将你的项目上传到Github,我clone下来试试

我使用的框架版本跟你是保持一致的,除了模型,我使用的是qwen-plus,但应该跟这没有关系

我稍后传一下,今天试的结果和昨天不完全一样,昨天是全部不行,今天是时而行,时而不行,不行的时候居多。

LeanderCherry avatar Apr 23 '25 08:04 LeanderCherry

我的报错和你类似,但是我是调试时偶尔出现,模型有时候弄得参数好,有时候弄得参数不对就会出错。

我今天也是这样的,时行时不行。我也跟踪调试看了,从调试情况看最大可能是LLM进行识别需要调用哪个工具,以及调用参数的时候返回的参数,有时候是能反序列化为Map的,有时候是不行的(其实是String类型输出的有问题),但是我觉得这应该还是SpringAI处理的有问题,因为Cherry Studio和Cline这两个Client我验证过了,是没问题的,所以问题不能说是LLM的。

LeanderCherry avatar Apr 23 '25 08:04 LeanderCherry

我遇到了相同的问题,[当需要调用MCP Server的多个tool时,MCP Client在调用第二个tool时出现反序列化错误],单个tool没有问题

dxn95 avatar May 29 '25 06:05 dxn95

+1 1.0.0 version

johnyannj avatar Jul 19 '25 11:07 johnyannj

I debug。this problem is about Qwen/QwQ-32B ,which return wrong json。

johnyannj avatar Jul 21 '25 01:07 johnyannj

me too

qingliu-zyc avatar Aug 20 '25 06:08 qingliu-zyc

  1. 工具调用的返回结果必须是一个JSON字符串
  2. 默认情况下会使用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

能问一下您用的版本吗,我这也出现第二个工具调用失败的情况。也是json错误

qingliu-zyc avatar Aug 20 '25 07:08 qingliu-zyc

  1. 工具调用的返回结果必须是一个JSON字符串
  2. 默认情况下会使用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

能问一下您用的版本吗,我这也出现第二个工具调用失败的情况。也是json错误

当时应该是使用的v1.0.0,好久没有关注过了,这个issue还没有被解决掉吗

yangtuooc avatar Aug 20 '25 07:08 yangtuooc

您好,我已收到您的邮件,谢谢。

qingliu-zyc avatar Aug 20 '25 07:08 qingliu-zyc

是的,我今天用1.0.1复现了。一个工具的调用是正常的,调用第二个会反序列化错误。另外,如果让模型两次调用同一个工具,也会出现这个错误。有比较好的解法吗

---原始邮件--- 发件人: @.> 发送时间: 2025年8月20日(周三) 下午3:28 收件人: @.>; 抄送: @.@.>; 主题: Re: [spring-projects/spring-ai] 当需要调用MCP Server的多个tool时,MCP Client在调用第二个tool时出现反序列化错误(When multiple tools of the MCP Server need to be called, the MCP Client encounters a deserialization error when calling the second tool.) (Issue #2818)

yangtuooc left a comment (spring-projects/spring-ai#2818)

工具调用的返回结果必须是一个JSON字符串

默认情况下会使用Jackson进行序列化

因此,在这段代码中你可以直接返回一个类对象或者Map对象 @tool(description = "根据城市ID获取天气") public Map<String,Object> @.***(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

能问一下您用的版本吗,我这也出现第二个工具调用失败的情况。也是json错误

当时应该是使用的v1.0.0,好久没有关注过了,这个issue还没有被解决掉吗

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you commented.Message ID: @.***>

qingliu-zyc avatar Aug 20 '25 07:08 qingliu-zyc

是的,我今天用1.0.1复现了。一个工具的调用是正常的,调用第二个会反序列化错误。另外,如果让模型两次调用同一个工具,也会出现这个错误。有比较好的解法吗

你上传一个复现该问题的代码在Github上,将仓库地址粘贴在这里,我可以帮你看一下

yangtuooc avatar Aug 20 '25 07:08 yangtuooc

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. Image 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;
}

}

请问您解决这个问题了吗,我这边也复现了,不知道怎么搞了

qingliu-zyc avatar Aug 20 '25 09:08 qingliu-zyc

我今天找到了一种解决方法,只要在最后一次调用的tool上,加入returnDirect = true配置,就可以让模型在调用到这个tool之后直接结束任务。我的需求只调用到了两个mcp tool,因此这个方法可以暂时解决我的问题。如果要调用多个tool进行协同,仍然会存在这个反序列化的问题。。。

qingliu-zyc avatar Aug 20 '25 12:08 qingliu-zyc