☰
快速掌握MCP——Spring AI MCP包教包会:从零搭建智能体工具调用链
2026/10/1 6:40:14 网站建设 项目流程

1. 从 Function Calling 到 MCP:Spring AI 工具调用链到底解决了什么问题

如果你最近在 Java 后端圈子里刷到 MCP 这个词,大概率会有点懵:它和之前 Spring AI 里的 Function Calling 是什么关系?为什么官方把 Function Calling 标成了 Deprecated?我该从哪下手?

先把结论说清楚:MCP 全称 Model Context Protocol,是一个开放协议,用来让大模型应用以统一的方式接入各种数据源和工具。你可以把它理解成 AI 世界的 Type-C 接口——以前每接一个工具就要写一套适配代码,现在只要工具方按 MCP 协议暴露能力,客户端就能即插即用。它适合谁?适合所有想把大模型能力落到真实业务里的 Java 后端开发者,尤其是已经在用 Spring AI、想快速搭一个能跑起来的智能体 Demo 的人。

在 MCP 出现之前,Spring AI 用的是 Function Calling:你在代码里定义一堆函数,注册给 ChatClient,模型根据用户问题决定调哪个。这套机制能用,但问题很明显——工具定义和业务代码强耦合,换一个模型或换一个工具就要改代码,多个工具之间也没法复用。Spring AI 官方现在把 Function Calling 标记为废弃,转向更具范式的 Tool Calling,而 MCP 就是 Tool Calling 在协议层的标准化落地。

MCP 的架构里分了几个角色:MCP Hosts(承载 AI 应用的主机)、MCP Clients(发起调用的客户端)、MCP Servers(提供工具和数据的服务端),以及背后的 Local Data Sources 和 Remote Services。在 Spring AI 里,这套东西被包装成了两个 starter:spring-ai-starter-mcp-client和spring-ai-starter-mcp-server。客户端负责连服务端、拉取工具列表、把工具注册给 ChatClient;服务端负责用@Tool注解暴露具体能力。两者之间通过 STDIO 或 SSE 通信。

我试过用这套组合搭一个「查天气 + 写文件」的链路,整个过程比想象中顺。核心链路是这样的:用户提问 → ChatClient 把问题和可用工具列表一起发给模型 → 模型返回要调用的工具名和参数 → Spring AI 通过 MCP Client 转发给 MCP Server → Server 执行真实逻辑 → 结果回传模型 → 模型生成最终回答。整条链路里,你只需要关心两件事:Server 端怎么写工具,Client 端怎么把模型和工具接起来。

这里有个容易踩的坑:很多人以为 MCP 是替代 Spring AI 的,其实不是。MCP 是协议层,Spring AI 是框架层,两者是配合关系。Spring AI 的 MCP 模块底层基于官方 mcp-java-sdk,做了 Spring 风格的封装,所以你写起来还是熟悉的@Bean、@Tool、application.yml那一套。

另外,模型服务这块建议统一走一个 API 通道,不然你每换一个模型就要改一遍 base-url 和 key。我后面会用 TaoToken 来统一管理 Key 和 API 地址,这样 MCP Client 里的模型配置只写一份,切换模型只改 model 名就行。官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api ,后面配置里会用到。

这一节先把背景和链路讲透,下一节开始动手:建工程、加依赖、配 Key,把 MCP Client 和 Server 都跑起来。

2. TaoToken 前置准备:统一 Key 与 API 通道,让 MCP Client 只配一次模型

在动手写 MCP 代码之前,先把模型服务这一层理顺。原因很简单:MCP Client 本质上还是要把用户问题和工具列表发给大模型,模型服务如果配置混乱,后面调试工具调用时你根本分不清是 MCP 链路的问题还是模型接入的问题。

TaoToken 在这里扮演的角色是统一的模型 API 通道。你不需要在代码里硬编码某一家厂商的地址,也不用为每个模型单独申请 Key。它的 API 入口是 https://taotoken.net/api ,兼容 OpenAI 风格的接口,所以 Spring AI 里spring.ai.openai.*那套配置可以直接用,只需要把 base-url 指向 TaoToken,api-key 换成你在控制台生成的 Key。

具体操作分三步。第一步,打开 https://taotoken.net/api-keys 生成一个 API Key,复制下来,形如sk-xxxxxxxx。这个 Key 就是你后面所有模型调用的凭证,MCP Client、MCP Server 里如果涉及模型调用,都用这一个。第二步,确认你要用的模型 ID。TaoToken 支持多种模型,你在控制台或文档里能看到可用的 model 名称,比如claude-sonnet-4-20250514、gpt-4o这类。第三步,把 base-url 和 Key 写进 Spring AI 的配置。

这里要强调一个点:MCP 本身不负责模型调用,它只管工具注册和调用转发。模型调用是 Spring AI 的 ChatClient 在做。所以你在application.yml里配的spring.ai.openai.base-url和spring.ai.openai.api-key,是给 ChatClient 用的,MCP Client 只是把工具挂到 ChatClient 上。理解这一点,后面排查问题时就不会把模型报错和 MCP 报错搞混。

我建议你在 TaoToken 控制台里把 Key 的用途标注清楚,比如「spring-ai-mcp-demo」,这样后面如果有多个项目,不会搞混。另外,Key 不要直接提交到 Git,用环境变量或者本地application-local.yml覆盖。Spring AI 支持${OPENAI_API_KEY}这种占位符写法,你可以在启动时通过-DOPENAI_API_KEY=sk-xxx传入。

如果你后面要长期跑编码类 Agent,或者需要频繁切换模型做对比,可以考虑 TaoToken 的 Coding Plan,它更适合这种持续调用的场景。入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。不过对于本篇的 MCP Demo 来说,按量调用就够了。

配置片段大概长这样,先放在这里,下一节建工程时会完整展开:

spring: ai: openai: base-url: https://taotoken.net/api api-key: ${OPENAI_API_KEY} chat: options: model: claude-sonnet-4-20250514

注意 base-url 后面不要多加/v1,Spring AI 的 OpenAI 客户端会自己拼路径。如果你写成了https://taotoken.net/api/v1,请求就会变成/api/v1/v1/chat/completions,直接 404。这个坑我在第一次配的时候踩过,日志里看到 URL 拼接异常才反应过来。

还有一点:MCP Client 在启动时会去连 MCP Server,如果 Server 是 STDIO 类型,它会拉起一个子进程。这个过程和模型调用是独立的,所以即使模型 Key 没配好,MCP Client 也能初始化成功,只是后面 ChatClient 调用时会报 401。排查时先看 MCP 初始化日志,再看模型请求日志,分层定位。

准备好 Key 和 base-url 之后,就可以进入编码环节了。下一节我会给出完整的 pom 依赖、application.yml和 MCP Server 配置 JSON,你可以直接复制运行。

3. 可复制配置:pom 依赖、application.yml 与 mcp-servers-config.json 三件套

这一节是整篇的核心,目标只有一个:让你复制完这些配置就能跑起来。我会按「MCP Server 端」和「MCP Client 端」分开给,因为这两个可以放在同一个工程里,也可以拆成两个工程。为了演示完整链路,我这里拆成两个模块:mcp-weather-server和mcp-client-demo。

先看 MCP Server 端的 pom 依赖。如果你要做 STDIO 类型的 Server,只需要spring-ai-starter-mcp-server;如果要做 SSE 类型,换成spring-ai-starter-mcp-server-webmvc。这里以 STDIO 为例:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> </dependency>

spring-web是为了用 RestClient 调外部天气接口。版本方面,Spring AI 的 BOM 建议用 1.0.0-M6 或更高,MCP 模块在这个版本已经比较稳定。如果你用的是 Spring Boot 3.3.x,直接引入 Spring AI BOM 即可。

Server 端的application.yml很简洁,因为 STDIO 类型不需要暴露端口:

spring: application: name: mcp-weather-server main: web-application-type: none banner-mode: off logging: level: io.modelcontextprotocol: DEBUG

web-application-type: none是关键,STDIO 类型的 Server 不需要 Web 容器,否则启动时会因为端口占用或容器初始化报错。banner-mode: off是为了避免 banner 输出污染 STDIO 通道——STDIO 协议靠标准输入输出通信,任何多余输出都可能导致协议解析失败。这个坑很隐蔽,我第一次跑的时候 Server 一直初始化失败,最后发现是 banner 打到了 stdout。

Server 端的工具定义用@Tool注解,注册用MethodToolCallbackProvider:

@Service public class WeatherService { private final RestClient restClient = RestClient.create(); public record WeatherResponse(Current current) { public record Current(LocalDateTime time, int interval, double temperature_2m) {} } @Tool(description = "Get the temperature in celsius for a specific location") public String getTemperature( @ToolParam(description = "The location latitude") double latitude, @ToolParam(description = "The location longitude") double longitude) { WeatherResponse resp = restClient.get() .uri("https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&current=temperature_2m", latitude, longitude) .retrieve() .body(WeatherResponse.class); return "当前温度: " + resp.current().temperature_2m() + "°C"; } }

然后在配置类里注册:

@Bean public ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder() .toolObjects(weatherService) .build(); }

再看 MCP Client 端。pom 依赖:

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

Client 端的application.yml是重点,这里把 TaoToken 的模型配置和 MCP Server 配置都放进去:

spring: application: name: mcp-client-demo main: web-application-type: none ai: openai: base-url: https://taotoken.net/api api-key: ${OPENAI_API_KEY} chat: options: model: claude-sonnet-4-20250514 mcp: client: toolcallback: enabled: true stdio: servers-configuration: classpath:mcp-servers-config.json logging: level: io.modelcontextprotocol.client: DEBUG io.modelcontextprotocol.spec: DEBUG

注意spring.ai.mcp.client.toolcallback.enabled: true这一行,它让 Spring AI 自动把 MCP Server 暴露的工具注册成 ToolCallback,你不需要手动写SyncMcpToolCallbackProvider。这是 Spring AI 1.0.0-M6 之后推荐的写法,比早期手动创建McpSyncClient优雅很多。

然后是mcp-servers-config.json,放在src/main/resources下:

{ "mcpServers": { "weather-server": { "command": "java", "args": [ "-jar", "E:/projects/mcp-weather-server/target/mcp-weather-server-0.0.1-SNAPSHOT.jar" ], "env": {} }, "filesystem": { "command": "npx.cmd", "args": [ "-y", "@modelcontextprotocol/server-filesystem", "E:/projects/mcp-client-demo/target" ], "env": {} } } }

这里配了两个 Server:一个是自己写的天气 Server,一个是官方的 filesystem Server。Windows 下npx要写成npx.cmd,Linux/Mac 下写npx。args里的路径按你本机实际路径改。filesystem 的最后一个参数是允许访问的目录,不要写成根目录,否则模型可能读写到不该碰的文件。

三件套齐了:pom 依赖、application.yml、mcp-servers-config.json。下一节跑一次完整调用,验证链路是否通。

4. 验证请求:一次完整的工具调用链路与成功结果

配置写完之后,最激动人心的就是看它真的跑起来。这一节我会给出 Client 端的启动代码,然后跑两个问题:一个触发天气工具,一个触发文件写入工具,最后看日志里工具调用的完整链路。

Client 端的启动类很简单,核心是把ToolCallbackProvider注入 ChatClient:

@SpringBootApplication public class McpClientApplication { public static void main(String[] args) { SpringApplication.run(McpClientApplication.class, args); } @Bean public CommandLineRunner run(ChatClient.Builder builder, ToolCallbackProvider tools, ConfigurableApplicationContext ctx) { return args -> { ChatClient chatClient = builder.defaultTools(tools).build(); String q1 = "成都现在多少度?纬度30.67,经度104.06"; System.out.println("Q1: " + q1); System.out.println("A1: " + chatClient.prompt(q1).call().content()); String q2 = "把刚才的温度结果写成一个 Markdown 文件,保存为 target/weather.md"; System.out.println("Q2: " + q2); System.out.println("A2: " + chatClient.prompt(q2).call().content()); ctx.close(); }; } }

启动命令:

export OPENAI_API_KEY=sk-你的TaoToken密钥 mvn -pl mcp-client-demo spring-boot:run

Windows 下用set OPENAI_API_KEY=sk-xxx。启动后你会看到 MCP Client 初始化日志,类似:

MCP Initialized: InitializeResult[protocolVersion=2024-11-05, capabilities=...]

这说明 Client 已经成功连上了mcp-servers-config.json里配的两个 Server,并且拉到了工具列表。如果这一步失败,先看 Server 的 jar 路径对不对,再看npx.cmd能不能在命令行里直接跑。

第一个问题的预期输出:

Q1: 成都现在多少度?纬度30.67,经度104.06 A1: 成都当前温度约为 18.3°C。

日志里会看到工具调用的详细过程:

DEBUG io.modelcontextprotocol.client - Sending request: tools/call, params: {name=getTemperature, arguments={latitude=30.67, longitude=104.06}} DEBUG io.modelcontextprotocol.spec - Received response: {content=[{type=text, text=当前温度: 18.3°C}]}

这就是完整链路:模型识别出需要调getTemperature,参数是纬度和经度,MCP Client 把请求转发给天气 Server,Server 调 Open-Meteo 接口拿到温度,回传给模型,模型生成自然语言回答。

第二个问题的预期输出:

Q2: 把刚才的温度结果写成一个 Markdown 文件,保存为 target/weather.md A2: 已成功将温度结果写入 target/weather.md 文件。

日志里会看到write_file工具的调用。这里有个细节:模型需要知道「刚才的温度结果」是什么,所以它会把上一轮的对话上下文一起带上。这也是为什么 MCP Client 和 ChatClient 要配合使用——MCP 负责工具,ChatClient 负责对话记忆和模型交互。

验证成功后,你可以打开target/weather.md看内容,应该是类似:

# 成都天气 - 温度: 18.3°C - 纬度: 30.67 - 经度: 104.06

到这里,一个完整的智能体工具调用链就跑通了。整个过程你只写了两个工具(一个自定义、一个官方),配了一个 JSON,模型就自动完成了「查天气 → 写文件」的编排。这就是 MCP 的价值:工具即插即用,模型负责编排。

如果你想在浏览器里单独调试某个 MCP Server,可以用官方 Inspector:

npx @modelcontextprotocol/inspector@0.7.0

然后在浏览器打开输出的 URL,传输类型选 STDIO,Command 填java,Arguments 填-jar 你的server.jar,就能看到这个 Server 暴露的所有 tool,还能手动传参测试。这个工具在排查「工具没被模型调用」时特别有用——先确认工具本身能跑通,再看模型侧的问题。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth 逐条对照

工具调用链路跑通之后,真正的挑战才刚开始:报错。这一节我把 MCP + Spring AI + TaoToken 组合下最容易遇到的几类错误整理出来,每条都给出真实报错文本和定位思路。

第一类:401 Unauthorized。报错通常长这样:

401 Unauthorized from POST https://taotoken.net/api/chat/completions

这说明模型调用这一层鉴权失败。排查顺序:先确认OPENAI_API_KEY环境变量有没有真的传进去,可以在启动日志里打印System.getenv("OPENAI_API_KEY")的前几位;再确认 Key 有没有过期或被禁用,去 TaoToken 控制台的 API Keys 页面看状态;最后确认 base-url 有没有写错,必须是https://taotoken.net/api,不能带/v1。如果 Key 是对的但还报 401,检查一下是不是有多余空格,YAML 里${OPENAI_API_KEY}如果环境变量没设置,会原样传进去,导致鉴权失败。

第二类:local proxy failed。这个报错在 MCP Client 启动时出现:

Failed to initialize MCP client: local proxy failed to start

或者:

Error starting stdio transport: Cannot run program "npx.cmd"

这是 STDIO 传输层的问题,和模型无关。原因通常是mcp-servers-config.json里的command在本机不存在。Windows 下npx要写npx.cmd,Linux/Mac 写npx;java命令要确认在 PATH 里,或者写绝对路径。还有一个隐蔽原因:Server 的 jar 路径里有空格或中文,STDIO 拉起子进程时解析失败。建议路径全用英文、无空格。如果用的是npx,先确认 Node.js 装好了,npx -v能输出版本号。

第三类:reading choices 相关报错。这个通常出现在模型返回格式不符合预期时:

Error reading choices from response: Cannot deserialize value of type `java.util.List<...>` from Object value

或者:

No choices found in response

这说明模型返回的 JSON 结构和 Spring AI 期望的不一致。常见原因是 base-url 指向了一个不兼容 OpenAI 格式的端点,或者模型 ID 写错了导致返回了错误结构。排查:确认spring.ai.openai.chat.options.model是 TaoToken 支持的模型 ID,不要自己拼;确认 base-url 是https://taotoken.net/api;如果用了流式,确认stream参数和客户端匹配。还有一种情况是模型返回了工具调用但格式不对,这时候把日志级别调到 DEBUG,看原始响应体。

第四类:OAuth 相关。如果你接的 MCP Server 需要 OAuth 鉴权,可能会看到:

OAuth authentication failed: invalid_client

或者:

Missing required OAuth scopes

Spring AI 的 MCP Client 对 OAuth 的支持在 1.0.0-M6 之后才比较完善,早期版本需要手动配McpClientCustomizer。如果你用的是需要 OAuth 的远程 MCP Server,建议先确认 Spring AI 版本,然后在application.yml里配spring.ai.mcp.client.oauth2.*相关参数。不过本篇的 Demo 用的是 STDIO 本地 Server,不涉及 OAuth,如果你遇到这个报错,大概率是误配了远程 Server。

除了这四类,还有一个高频问题:工具没被调用。模型直接回答了问题,没有走工具。这通常是因为工具描述不够清晰,或者模型不支持工具调用。排查:先用 MCP Inspector 确认工具本身能跑通;再看日志里有没有tools/call请求;如果没有,说明模型没决定调用工具,可以优化@Tool(description=...)的描述,让它更明确地说明「什么时候该用这个工具」。

最后提醒一点:MCP Client 和模型调用是两层,排查时先分层。MCP 初始化失败看 STDIO/SSE 配置,模型调用失败看 Key 和 base-url,工具没被调用看工具描述和模型能力。分层定位,效率会高很多。

6. 从 Demo 到可用:把 MCP 工具链接入你的真实业务

跑通 Demo 只是第一步,真正有价值的是把这套链路接到你的业务里。这一节聊几个落地时的实用建议,以及怎么用 TaoToken 统一管理模型通道。

先说工具设计。MCP Server 里的@Tool方法,本质上是给模型看的 API。描述要写清楚「这个工具做什么、什么时候用、参数是什么」。比如getTemperature的描述写成「Get the temperature in celsius for a specific location」,模型就知道在问天气时调用。如果你写「Get data」,模型根本不知道什么时候该用。参数用@ToolParam加描述,尤其是经纬度这种,要说明单位。

再说多 Server 编排。mcp-servers-config.json里可以配多个 Server,模型会根据问题自动选择。比如你同时配了天气 Server、文件 Server、数据库 Server,用户问「查一下成都天气并保存到文件」,模型会先调天气工具,再调文件工具。这种编排能力是 MCP 的核心价值。但要注意,Server 越多,工具列表越长,模型选择出错的概率也越高。建议按业务域拆分,不要把所有工具塞到一个 Server 里。

模型通道这块,TaoToken 的价值在于统一。你不需要在代码里为每个模型写一套配置,base-url 固定https://taotoken.net/api,Key 固定一个,切换模型只改model字段。这样你在做 A/B 测试或者成本优化时,改一行配置就能换模型。如果你后面要跑长期的编码 Agent,Coding Plan 会更划算,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。

调试方面,MCP Inspector 是必备工具。每次新增一个工具,先用 Inspector 单独测通,再挂到 Client 上让模型调。这样能把「工具本身的问题」和「模型编排的问题」分开。另外,日志级别在开发阶段保持 DEBUG,上线后调到 INFO,避免日志量过大。

最后说一个我踩过的坑:STDIO 类型的 Server 在 Client 关闭时要确保子进程也被回收。Spring AI 的McpSyncClient有close方法,如果你手动创建 Client,记得在@Bean(destroyMethod = "close")里声明。用servers-configuration自动配置的话,Spring 会帮你管理生命周期,但如果你发现启动多次后有一堆僵尸java进程,检查一下是不是手动创建了 Client 没关。

从 Demo 到生产,还有一段路要走:工具鉴权、超时控制、错误重试、并发限制。但核心链路就是本篇讲的这些——Server 暴露工具,Client 注册工具,模型编排调用,TaoToken 统一模型通道。把这套跑顺了,后面加什么工具都是复制粘贴的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询