StackTraces
Spring AI從零跑通

從零跑通 Spring AI 工具調用與 MCP:6 步,外加 5 個容易卡住的地方

@debug_master2026-09-29

跟著這篇走完,你會得到一個能跑的最小工程:模型先調用你自己寫的 @Tool 方法,再調用一個跑在本地進程裡的 MCP Server 提供的工具。全程 6 步,每步末尾都有一條命令告訴你這一步成了沒有。

文中的 gpt-4o-mini、OPENAI_API_KEY、8080 都是示例值,照抄之前先換成你自己的。

Spring AI 工具調用與 MCP:從零跑通的六個步驟

前置條件

項 要求 說明
JDK 17 或更高 Spring AI 1.1.x 的編譯基線就是 17,低於它連依賴都解析不了
Spring Boot 3.5.x 本文按 3.5.15 寫。Spring AI 1.1.x 對應的就是這一檔,配到 3.4 或 4.0 都會在自動設定階段出問題
Spring AI 1.1.x 本文按 1.1.8 寫。版本統一交給 BOM 管,別給單個依賴寫版本號
Node.js 18 或更高 只在第三步啟動 stdio 的 MCP Server 時用到,自帶 npm 8+
API Key 任意 OpenAI 兼容服務的 Key 換成別的兼容 OpenAI 協議的服務也行,改 base-url 就夠
網絡 能訪問模型服務與 npm 源 首次要拉 Maven 依賴和 npm 包;跑通之後可以換成 Ollama 完全離線

先確認 Java 和 Maven 的版本。

java -version
mvn -version

兩條命令都要正常輸出,而且 mvn -version 裡顯示的 JDK 要和上一條一致。Maven 用的是另一個 JDK(比如 JAVA_HOME 指向 8)時,編譯期報的是類版本不支持,這事和 Spring AI 沒關係,但很容易被當成依賴衝突去查。

第三步之前再確認一次 Node 工具鏈。

node -v
npx -v

有個版本差異要先說清楚:Spring AI 1.0.x 的 MCP 客戶端只有 stdio 和 SSE 兩種傳輸,Streamable HTTP 客戶端是 1.1.x 才加的。停在 1.0.x 上,那組設定寫了不會報錯,但會被靜默忽略。沒有對應的自動設定去讀它。

第一步:建工程,引入 Spring AI 依賴

用 start.spring.io 生成一個 Spring Boot 3.5.15 工程,或者在已有工程上改。核心是三個依賴:Web(後面要用 Controller 驗證)、一個模型 starter、MCP 客戶端 starter。

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-bom</artifactId>
      <version>1.1.8</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
  </dependency>
</dependencies>

版本交給 BOM 管,三個依賴都不寫 <version>。逐個指定版本號是後面內部模塊版本對不上、報 ClassNotFoundException 的常見起點。

模型 Key 寫進設定,別硬編碼到程式碼裡。

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o-mini

驗證。

mvn -q dependency:tree -Dincludes=org.springframework.ai

輸出裡同時出現 spring-ai-starter-model-openai 和 spring-ai-starter-mcp-client 兩行就是對的。少了前者,第二步注入 ChatModel 會失敗;少了後者,第三步開始的 MCP 設定項會全部被忽略。設定寫了不報錯,但沒有自動設定去讀它。

第二步:寫一個 @Tool 方法,先用 ChatClient 調通

先把工具調用這條鏈路單獨跑通,別一上來就接 MCP。兩個概念要先分清:@Tool 註解的方法跑在你的 JVM 裡,MCP 工具跑在另一個進程裡,但註冊進 ChatClient 之後,對模型來說沒差別。

工具類:

package com.example.demo.tools;

import org.springframework.ai.tool.annotation.Tool;

public class WeatherTools {

    @Tool(description = "查询指定城市的实时天气,返回天气状况和温度")
    public String getWeather(String city) {
        // 真实项目里换成 HTTP 调用,这里返回固定值便于验证
        return city + ":晴,26℃";
    }
}

掛進 ChatClient,再暴露一個接口用於驗證:

package com.example.demo;

import com.example.demo.tools.WeatherTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient.Builder builder) {
        this.chatClient = builder
                .defaultTools(new WeatherTools())
                .build();
    }

    @GetMapping("/chat/tool")
    public String chat(@RequestParam String msg) {
        return chatClient.prompt(msg).call().content();
    }
}

defaultTools(...) 收的是方法級工具物件,框架會掃描裡面的 @Tool 方法、生成工具描述。方法註釋裡的 description 不是裝飾,模型就是靠它判斷什麼時候該調這個工具。

驗證。

curl -s "http://localhost:8080/chat/tool?msg=调用 getWeather 工具查一下北京的天气"

返回內容裡出現 26℃ 就說明工具真的被執行了。這個字符串只存在於工具方法裡,模型編不出來。如果只看到模型在客套,先回去改 description,再換一個支持工具調用的模型試。

第三步:接一個 stdio 的 MCP Server

MCP Server 獨立運行,客戶端通過 stdio 或 HTTP 連它。先用 stdio 接官方的 filesystem server,它把本地目錄暴露成一組可讀寫的工具。

先確認這個 server 自己能跑起來。

npx -y @modelcontextprotocol/server-filesystem /tmp/mcp-demo

進程起來後不輸出內容、停在等待狀態就是對的(stdout 是協議通道,正常運行時本來就不該有東西打出來)。Ctrl+C 退出,然後寫設定。

spring:
  ai:
    mcp:
      client:
        stdio:
          connections:
            filesystem:
              command: npx
              args:
                - -y
                - "@modelcontextprotocol/server-filesystem"
                - /tmp/mcp-demo

/tmp/mcp-demo 是這個 server 的目錄邊界,先手工建出來,別直接授權整個家目錄。

加一個只用於自檢的接口,把當前註冊進來的工具名吐出來:

package com.example.demo;

import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;

@RestController
public class ToolDebugController {

    private final ToolCallbackProvider toolCallbackProvider;

    public ToolDebugController(ToolCallbackProvider toolCallbackProvider) {
        this.toolCallbackProvider = toolCallbackProvider;
    }

    @GetMapping("/debug/tools")
    public List<String> tools() {
        return Arrays.stream(toolCallbackProvider.getToolCallbacks())
                .map(cb -> cb.getToolDefinition().name())
                .toList();
    }
}

驗證。

curl -s http://localhost:8080/debug/tools

返回的陣列不為空,說明 MCP 客戶端的自動設定生效、連接建立成功、遠端工具已經被發現,裡面能看到 read_file、list_directory 這類名字。陣列是空的但應用正常啟動,通常就兩個方向:連接根本沒建起來(服務端進程啟動失敗),或者 spring.ai.mcp.client.toolcallback.enabled 被關掉了(預設是 true)。

第四步:把 MCP 工具註冊進 ChatClient

到這一步,MCP 工具已經進了 Spring 容器,但還沒進 ChatClient。這兩件事是分開的,最常見的錯誤就是配好了 MCP 卻漏了這一步,然後看著模型完全不理遠端工具。

改一下構造方法,把 ToolCallbackProvider 裡的工具一起掛上去。本地 @Tool 和 MCP 工具可以共存:

public ChatController(ChatClient.Builder builder, ToolCallbackProvider toolCallbackProvider) {
    this.chatClient = builder
            .defaultTools(new WeatherTools())
            .defaultToolCallbacks(toolCallbackProvider.getToolCallbacks())
            .build();
}

驗證。先在授權目錄裡放一個帶標記的文件:

mkdir -p /tmp/mcp-demo
echo "云栈科技技术验证标记" > /tmp/mcp-demo/note.txt
curl -s "http://localhost:8080/chat/tool?msg=用 read_file 工具读取 /tmp/mcp-demo/note.txt,把内容原样告诉我"

回答裡出現 云栈科技,說明模型調用的是 MCP Server 那個進程裡的工具,不是 JVM 裡的方法。如果它回你「我沒有文件讀取權限」之類的話,就是工具沒註冊進 ChatClient,回到這一步檢查程式碼。

第五步:讓模型在一次對話裡自己選工具

前兩步都是你點名讓模型用某個工具。真實場景裡,該由模型自己判斷用哪個。這一步不加新程式碼,只把本地工具和遠端工具放進同一個問題,看它會不會分派。

兩個工具的返回值特意做得不一樣:getWeather 返回 26℃,MCP 的 read_file 返回 云栈科技技术验证标记。一次問它兩件事:

curl -s "http://localhost:8080/chat/tool?msg=北京今天天气怎么样?另外读一下 /tmp/mcp-demo/note.txt 告诉我里面写了什么"

驗證。

回答裡同時出現 26℃ 和 云栈科技,就說明模型在一輪裡調用了兩個工具,一個來自 JVM、一個來自 MCP 進程,而且把兩邊的結果都拼進了回答。只出現其中一個,通常不是「不支持多工具」,而是某一邊的 description 太模糊,模型沒意識到那個工具和問題相關。

第六步:換成 Streamable HTTP 接遠程 Server(可選)

stdio 適合本地工具,部署到服務器上更常見的是 HTTP 形態。MCP 的 HTTP 傳輸已經從 HTTP+SSE 遷到 Streamable HTTP,端點也從 /sse 變成了 /mcp。只有當你連的服務端是 HTTP 形態時才需要這一步,純本地工具可以跳過。

spring:
  ai:
    mcp:
      client:
        streamable-http:
          connections:
            remote:
              url: http://localhost:8081
              endpoint: /mcp

url 只寫協議、主機和端口,路徑交給 endpoint。endpoint 的預設值就是 /mcp,服務端不是這個路徑時才需要顯式寫。

先別急著啟動應用,直接探一下服務端這個端點活沒活。

curl -i -X POST http://localhost:8081/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

返回 200 且響應體裡帶 result,說明端點可用,可以回來把設定補上。返回 404 說明路徑不對,先確認服務端開在 /mcp 還是舊的 /sse,再回來改 endpoint。

最後再提醒一次:這一節在 Spring AI 1.0.x 上不成立。那個版本的 MCP 客戶端只支持 stdio 和 SSE,streamable-http 這組設定項沒有對應的自動設定,寫了會被靜默忽略。

核心概念

概念 是什麼 類比
ChatModel 某一家模型服務的客戶端,負責發請求收響應 像 JDBC 驅動,換廠商就換一個實現
ChatClient 在 ChatModel 上包一層,管提示詞、工具、會話這些橫切邏輯 像 JdbcTemplate,用起來比裸驅動順手
@Tool 標在方法上,框架據此生成工具描述發給模型 像給方法掛了一份接口文檔,模型照著決定調不調
MCP 把外部工具和資源用統一協議暴露給 AI 應用的標準 像 USB-C,誰實現這個口都能插
Transport MCP 消息的傳輸方式,stdio 走子進程管道,Streamable HTTP 走 HTTP 像同一個協議跑在本地 socket 還是網絡上
ToolCallbackProvider MCP 客戶端把發現的遠端工具統一交給 Spring 的出口 像適配器,一端是 MCP,一端是 Spring AI 的工具框架

常見問題

Unexpected token 'S', "Starting server..." is not valid JSON

現象:MCP Client 一連就報 JSON 解析失敗,報錯裡還夾著服務端打印的那句日誌。

原因:stdio 模式下 stdout 是 JSON-RPC 專用通道,服務端任何一句日誌寫進 stdout 都會讓客戶端解析失敗。

相關文章:Spring AI MCP stdio:日誌汙染 stdout 導致解析失敗

failed to start command: exec: "npx": executable file not found in $PATH

現象:終端裡手敲 npx 能跑,Spring Boot 一啟動就說找不到它。

原因:JVM 子進程繼承的 PATH 不等於你交互式 shell 的 PATH,IDE 和系統服務拿到的更精簡。

相關文章:Spring AI MCP stdio npx not found

404 Not Found on GET request for "http://localhost:8080/sse"

現象:服務端明明活著,客戶端連過去直接 404。

原因:客戶端還按舊的 SSE 路徑連,服務端已經切到 Streamable HTTP 的 /mcp,協議和端點都沒對上。

相關文章:Spring AI MCP 客戶端 404:改用 Streamable HTTP

工具註冊了,模型就是不調用

現象:編譯啟動都沒問題,模型完全無視你寫的 @Tool;偶爾會拋 No ToolCallback found for tool name: xxx。

原因:defaultOptions 用了不支持工具調用的 DefaultChatOptions;或者 .tools(...) 匹配到了會吞掉回調的重載;或者工具執行階段又新建了一次 options。

相關文章:Spring AI Tool Calling 不生效的三種靜默失效

No qualifying bean of type 'ChatModel' available

現象:啟動直接失敗,說容器裡找不到 ChatModel。

原因:模型 starter 沒引,或者引了但自動設定被關掉,Spring 也就沒東西可以注入。

相關文章:Spring AI 報 No qualifying bean of type ChatModel

下一步

換成本地模型跑

把模型服務換成本機的 Ollama,就不用一直消耗 API 額度了;連不上通常只是服務沒起或地址不對。

Spring AI 接 Ollama 報 Connection refused

改成流式輸出

把回答一個字一個字推給前端,接進 Web 層之後還要處理 SSE 的連接生命週期和超時。

Spring AI 流式輸出:stream 與 SseEmitter

管好長會話的上下文

多輪對話接上工具之後歷史漲得很快,主動截斷或摘要,別等報 400 才回頭加。

Spring AI 報 400 context_length_exceeded

卡在哪一步了?

從零跑通的過程中撞到報錯?把命令和輸出記下來,幫助下一個剛起步的人。

貢獻一條解法