干这一行最怕的就是信息差。早几年做大模型应用,最折磨人的不是模型能力不够,而是怎么把各种外部系统“安全、高效、规范“地接进来——写插件、调API、做鉴权、处理数据结构。每接一个工具,都得从零开始写一套胶水代码,还得祈祷对方接口别乱变。直到MCP(Model Context Protocol)出现,这套玩法才算彻底变了。
Spring AI在这股浪潮里算是反应最快的那批框架,2025年1.0正式发布后,直接把MCP作为一等公民做了进去。更关键的是,它不仅支持客户端调用MCP Server,还支持自己作为Server端向外部世界暴露工具。你如果只用过Spring AI的聊天接口,那只是用了它四分之一的能力,真正把它推向“应用级”的,恰恰是这一层MCP工具化能力。
这篇文章,我打算按照自己学这套东西的完整路径来写——先搞懂MCP是什么、解决什么问题,再动手把环境搭起来,然后跑通一个完整示例,最后扎进源码里去理解Spring AI是怎么把MCP的协议细节给“消化”掉的。这条路走完,你对Spring AI的理解绝对会甩开大多数人。
1. 先别急着写代码:MCP到底解决了什么问题
我们回头看看没有MCP的时候,AI应用接外部工具是什么样的场景。
你有一个AI助手,想让用户能在对话里查询数据库。传统做法是:定义一个函数searchDb(sql),把参数格式告诉模型,模型在对话中判断该调用这个函数时,返回一个tool_call,你的代码拦截这个调用,执行SQL,再把结果塞回对话上下文。简单吧?但问题在于——每个工具都得这么搞一遍,而且工具的契约完全暴露在prompt或者函数定义里,参数一变,模型就懵。
多Agent协作的时候更痛苦。Agent A要从飞书拉数据,Agent B要把数据写到表格里,你得编写两套完全不同的对接代码,维护两份不同的schema,排查问题的时候还得翻两边的日志。这就像家里装修:电器的插头各不相同,你每个电器都得配一个专属插座,线拉得一团糟。
MCP的核心思路,就是把这套接口标准化——把工具、资源、提示词三大能力统一成一种协议,所有AI应用(Host)通过MCP Client跟任意MCP Server对话,Server侧则把具体的工具实现包起来。
它的定位,其实就一个词:AI时代的USB接口。USB-C把充电、数据传输、视频输出统一到了一个物理接口上,MCP把数据库、文件系统、浏览器、设计稿、代码仓库这些五花八门的工具统一到了一个协议接口上。
Spring AI在这个体系里扮演的角色很清晰:
- 作为Host:你的Spring Boot应用可以连接外部MCP Server(比如蓝湖设计稿、Playwright浏览器、GitHub仓库),让模型调用这些服务器暴露的工具。
- 作为Server:你的Spring Boot应用也可以把自己包成一个MCP Server,把内部的服务能力(比如查询订单、计算价格)安全地暴露给其他AI应用调用。
所以你想想,一个Spring Boot项目同时扮演两种角色,这件事本身就能玩出很多花样。
MCP的协议基础是JSON-RPC 2.0,传输层可以是stdio(本地进程通信),也可以是Streamable HTTP或SSE(跨网络)。所有交互都是围绕三个核心方法展开的:
- initialize:建立连接时握手,客户端和服务端交换能力声明。
- tools/list:客户端获取服务器支持哪些工具,以及每个工具的JSON Schema定义。
- tools/call:客户端调用某个工具,传入参数,服务器执行并返回结果。
这套设计看似简单,实际推断力很强。工具描述是运行时动态获取的,所以新加工具、改参数定义,模型那边完全不需要重新配置——只要服务端把tools/list的结果更新了就行。这比之前硬编码function definition的做法灵活太多。
2. 环境准备与项目搭建:从Spring Initializr开始
讲原理再多都不如动手跑一个demo。我先说下环境选择,这里有个容易踩坑的地方。
2.1 版本选型:为什么我推荐1.0.x而不是最新版
Spring AI的版本迭代非常快,目前GA版本线已经从1.0.x走到1.1.x。至于2.0,要留意一下它的迭代节奏——在写这篇文章的时候2.0还处于M系列快照阶段,maven坐标是2.0.0-M2这种带M后缀的预览版,除非你想动手跟进新功能,否则不要在生产项目里用快照版。
我建议直接用包厢里的1.0.x系列(或者已经发布GA的1.1.x),因为:
- API已经稳定,网上能找到的绝大多数资料都能对得上,而你如果用了2.0的代码去搜1.0的文档,会经常发现接口对不上。
- MCP相关的自动配置类在1.0时期已经完整了,该有的
McpClientAutoConfiguration、McpToolCallbackProvider都在。 - Spring Boot 3.4.x对它的管理是最完善的。
用Spring Initializr生成项目的时候,直接选Java 17+、Spring Boot 3.4.x,依赖方面加上Web、Spring AI(注意Initializr里的Spring AI版本跟随Boot版本,比较稳妥)。
如果你的模型是走OpenAI兼容接口,需要引入对应的starter。这里只举两个常用组合:
<!-- OpenAI 风格 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <!-- 智谱AI --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-zhipuai</artifactId> </dependency>智谱那条线在Spring AI里支持得不错,国内场景用着顺手。配置方面,application.yml里大概是这样的:
spring: ai: model: # 模型prototype,实际命名空间取决于具体starter openai: base-url: https://api.openai.com/v1 api-key: ${OPENAI_API_KEY}2.2 MCP Server依赖选择:WebMvc还是WebFlux
Spring AI官方对MCP Server提供了两种技术支持:WebMvc(同步servlet栈)和WebFlux(响应式栈)。
如果你要跟Spring Boot的传统web项目共存,优先选WebMvc;如果你整个应用已经是响应式的,或者追求高并发IO,就选WebFlux。两个依赖长这样:
<!-- WebMvc 方式 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> </dependency> <!-- WebFlux 方式 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webflux</artifactId> </dependency>我自己的建议是:新手选WebMvc。为什么?因为出了问题是真好看日志,同步栈的报错链路清晰很多。
依赖配好,直接在配置类里声明一个工具方法,它就是MCP Server暴露给外部的能力:
@Configuration public class McpServerConfig { @Bean @Tool public Function<Map<String, Object>, String> getServerTime() { return args -> { LocalDateTime now = LocalDateTime.now(); return "Server current time: " + now; }; } @Bean @Tool(description = "读取服务器文件系统指定路径的文件内容") public Function<FileReadRequest, String> readFile() { return request -> { try { return Files.readString(Path.of(request.path())); } catch (IOException e) { return "Error reading file: " + e.getMessage(); } }; } public record FileReadRequest(String path) {} }等等,这里有个细节:@Tool注解加在@Bean方法上,Spring AI会自动把它扫描为MCP工具,但前提是MCP Server的自动配置处于启用状态。一旦项目里同时存在spring-ai-starter-mcp-server-webmvc和一个WebMvcAutoConfiguration,Spring Boot会自动配置一个WebMvcMcpServer,把工具通过/mcp端点暴露出去。
启动项目后,你可以直接请求http://localhost:8080/mcp看返回值,如果正常会返回MCP协议要求的JSON-RPC错误格式(因为还没走握手流程),这反而说明Server已经起来了。
2.3 客户端依赖配置
如果你的Spring Boot应用要向这个MCP Server发请求、让模型调用它的工具,那还需要在客户端依赖:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency>然后在配置里声明要连接的Server地址:
spring: ai: mcp: client: name: my-host version: 1.0.0 servers: my-server: url: http://localhost:8080/mcpSpring AI的client自动配置会帮你实例化一个McpClient,并且它会自动获取tools/list的结果,生成一批ToolCallback注册进模型调用链路。也就是说,配置到这里,你的模型就已经“长出手脚了”。
3. 实战案例:让AI能读写你的服务器文件
光说不练假把式。我打算做一个完整的小项目,让你能直观感受MCP的威力:一个内部运维问答工具,让大模型直接读取服务器上的日志文件、系统信息,并输出分析结论。
这个场景非常典型——MCP Server跑在运维机器上,把大模型和服务器文件系统安全连接起来,模型既不能随便执行命令,又能在给定工具边界内自助获取信息。
3.1 服务端:定义暴露给模型的工具
服务端就是个Spring Boot 3.4项目,核心逻辑在配置里:
package com.example.mcpdemo.server; import org.springframework.ai.tool.annotation.Tool; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.Function; @Configuration public class OpsToolConfig { @Bean @Tool(description = "获取服务器系统信息,包括操作系统名称、版本、可用处理器数量") public Function<Map<String, Object>, String> systemInfo() { return args -> { String osName = System.getProperty("os.name"); String osVersion = System.getProperty("os.version"); String arch = System.getProperty("os.arch"); int processors = Runtime.getRuntime().availableProcessors(); return "OS=%s, Version=%s, Arch=%s, Processors=%d" .formatted(osName, osVersion, arch, processors); }; } @Bean @Tool(description = "读取指定路径文件内容,path参数为绝对路径,支持文本文件") public Function<FilePathRequest, String> readLogFile() { return request -> { String path = request.path(); // 关键:做路径校验,防止任意文件读取 if (!path.startsWith("/var/log") && !path.startsWith("/tmp")) { return "Access denied, only /var/log and /tmp directories are allowed"; } try { List<String> lines = Files.readAllLines(Path.of(path)); // 限制返回行数,避免把模型上下文撑爆 int maxLines = Math.min(lines.size(), 200); return String.join("\n", lines.subList(0, maxLines)); } catch (IOException e) { return "Read file error: " + e.getMessage(); } }; } public record FilePathRequest(String path) {} }这里有两个关键设计值得展开说。
第一,工具边界。我不是把整台机器都暴露给模型,而是白名单化路径,限定它只能读/var/log和/tmp。很多人在做MCP Server的时候忘了这层管控,结果模型可以满盘乱跑,这是安全隐患。MCP本身是协议标准,但安全边界完全是你自己代码里定义出来的。
第二,上下文长度控制。日志文件通常几百上千行,全塞给模型会浪费token还会让模型顾此失彼。所以我限制最多返回200行。这个数字不是拍脑袋定的,是根据常见LLM的上下文窗口(8k-32k)和日志单行长度(平均50-100字符)估算出来的——200行大约10k-20k字符,占8k窗口的1/8左右,留足余量给对话历史和中间推理。
3.2 客户端:让模型调用MCP工具
客户端项目是另一个Spring Boot应用,主类长这样:
package com.example.mcpdemo.client; import org.springframework.ai.chat.client.ChatClient; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class McpClientApplication { public static void main(String[] args) { SpringApplication.run(McpClientApplication.class, args); } @Bean CommandLineRunner demo(ChatClient.Builder chatClientBuilder) { return args -> { ChatClient chatClient = chatClientBuilder.build(); String response = chatClient.prompt() .user("请帮我看看 /tmp/error.log 的前200行,分析一下有什么异常,并给出可执行的修复建议。") .call() .content(); System.out.println("=== MCP工具分析结果 ==="); System.out.println(response); }; } }注意,这里没有写任何一行跟MCP协议相关的代码。McpClientAutoConfiguration会在启动时连接我们配置的http://localhost:8080/mcp,拉取工具列表,把它们注入Spring AI的工具调用链。ChatClient发起请求时,大模型发现用户问题涉及读文件,自动决定调用readLogFile工具,走向MCP Server请求数据,拿到结果后再组织语言生成回答。
整个过程,模型有哪些工具可用、怎么选、参数怎么填,Spring AI和MCP零零碎碎全包了。
3.3 跑起来看效果
先把MCP Server跑在8080端口,再把客户端跑在8081端口,客户端启动时控制台会打印一行启动日志,大意是“Spring AI McpClient connected to server”,然后启动demo线程发出一条带工具调用意图的对话请求。
模型会做的事,沿着这条链路:
- 解析用户问题:跟文件系统相关,需要工具。
- 从已经注入的工具列表里挑出
readLogFile,填入参数path=/tmp/error.log。 - Spring AI的
DefaultToolCallingManager拦截这个请求,通过McpClient发JSON-RPC调用给MCP Server。 - Server端定位到对应
@Tool注册的Function,执行Files.readAllLines,返回文本结果。 - 模型把工具结果与原始问题结合,生成最终建议。
如果一切正常,你会看到类似“Nginx在2025-01-03 10:00:02报出了504错误,大概率是上游超时,建议检查后端进程健康状态”这样的完整分析——而这里的每个事实,都是模型从工具读出来之后自己归纳的,不是瞎编的。
这里你应该也能感受到MCP的一个强烈优势:不吃prompt,不长上下文就能触达外部数据。传统RAG还得先检索再拼进上下文,MCP是“按需取用”——模型自己决定什么时候调工具有多重要,这就是Agent行为和普通补全行为的分界线。
4. 深入源码:Spring AI如何消化MCP协议
跑通demo之后,我建议你停下来,花一晚上把Spring AI的MCP相关源码翻一翻。你会发现它抽象得特别干净,理解以后你会对工具的注册、初始化、调用机制有完整的掌控力,也方便你排查集成问题。
4.1 自动配置链路
核心的自动配置类有两组:
McpClientAutoConfiguration(客户端)WebMvcMcpServerAutoConfiguration(服务端)
客户端自动配置的逻辑大致是这样:
@Bean @ConditionalOnMissingBean public McpClient mcpClient(McpClientProperties properties, ObjectProvider<McpAsyncClientCustomizer> customizers) { var transport = HttpSseClientTransport.builder() .url(properties.getUrl()) .build(); return McpClient.sync(transport) .initializationFor(properties.getClientInitialization()) .capabilities(properties.getClientCapabilities()) .build(); }这里有个细节:从1.0开始,Spring AI默认走的是Streamable HTTP transport,不再用早期版本的HTTP+SSE模型。传输层的构建全部隐藏在HttpServerTransport里头,你在代码层几乎看不见它,但这不影响我们理解MCP交互模型。
服务端的自动配置,核心是会创建一个WebMvcMcpServer、一个RouterFunction,把POST/GET路由映射到/mcp路径。服务端在初始化的时候会扫描Spring容器里所有标记了@Tool的Bean方法,把它们转换成McpServerFeatures.AsyncToolSpecification,注册进工具仓库。这个扫描动作就在McpServerAutoConfiguration里调用ToolSpecificationConverter完成的。
4.2 协议握手与tools/list
连接建立时,MCP协议要求双方先做initialize握手。Spring AI把它们封装到了ClientSession的initialize()方法里。这个握手请求是JSON-RPC格式:
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": { "tools": { "listChanged": true } }, "clientInfo": { "name": "spring-ai", "version": "1.0.0" } } }服务端响应后,客户端再发一条notifications/initialized,告诉服务端连接已完成,之后才能正常订阅工具列表。tools/list返回的Schema长这样:
{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "readLogFile", "description": "读取指定路径文件内容,path参数为绝对路径,支持文本文件", "inputSchema": { "type": "object", "properties": { "path": { "type": "string", "description": "文件绝对路径" } }, "required": ["path"] } } ] } }Spring AI拿到这份Schema之后,做了一个关键映射——把它转成自己体系里的ToolCallback。这个活干得巧妙的地方在于,它设计了一套无感知转换:MCP的工具定义被包装成McpToolCallback,对外部代码而言,它跟本地用@Tool定义的工具完全一致——调用方根本不需要关心这个工具是本地执行还是远程执行。
4.3 工具调用的回环
模型发出tool_call之后,Spring AI的DefaultToolCallingManager开始接管。它遍历上下文里所有ToolCallback,用工具名做匹配,命中后把模型生成的JSON参数直接交给McpToolCallback的call()方法。
call()里做的事情:
- 把JSON参数解析成
JsonSchema验证过的Map。 - 构造一个
CallToolRequest,带上工具名和参数。 - 通过
McpClient发tools/call请求到远端。 - 同步阻塞等待返回
CallToolResult。 - 把返回的
content列表转成文本,重新拼接回消息列表。
源码里大致是(我简化了):
public String call(String toolInput) { CallToolRequest request = new CallToolRequest(this.toolName, JsonSchemaMapper.toMap(toolInput)); CallToolResult result = this.mcpClient.callTool(request); return result.content().stream() .map(Content::text) .collect(Collectors.joining()); }这就是整个回环最核心的部分——不要被MCP那套JSON-RPC报文吓住,本质就是:模型想要某个能力 → Spring AI定位到Bean方法 → 通过McpClient发请求 → 远端执行 → 文本结果回填。其他都是围绕这个主链路的封装。
4.4 Tool Schema生成的规则
当你自己定义@Tool工具的时候,Spring AI会根据Java方法签名自动推断JSON Schema。这个推断逻辑很有讲究:
- 方法名 ->
name @Tool注解的description->description- 参数类型 ->
properties里的type映射(String->string,Integer->integer,List->array,record->object) required字段根据参数是否带@Nullable或者默认值来定
一旦你理解了这套规则,就能反过来推导:为什么工具参数最好设计成一个扁平record,而不是复杂的嵌套对象?因为嵌套结构在Schema序列化和模型生成参数的时候都容易出错。我见过不少集成问题,最后定位下来都是参数结构过于复杂,模型生成的JSON一直过不了校验。
5. 从单机到生产:MCP的高级玩法与避坑实录
demo跑通了,源码看懂了,但离真正生产使用,还有一段路。我把实际项目里踩过的坑和觉得值得聊的高级话题摆这里。
5.1 认证与安全:不是所有工具都可以裸奔
MCP本身没有强制认证机制,能力边界完全取决于你暴露什么工具。这里有几个层面的建议:
- 网络隔离:生产环境MCP Server不要直接暴露公网,尽量走内网+网关鉴权,或者用mTLS。
- 工具白名单:业务工具只暴露最小能力集合。比如前面那个读文件工具,路径白名单是最基本的;涉及写操作的工具,一定要确认来源Host是可信的。
- 审计日志:每次
tools/call都记录下来(工具名、参数摘要、调用者),出了事能回溯。Spring AI的McpClient没有内置审计,但是你自己Server端可以通过AOP给@Tool加一个切面,很容易做。
5.2 连接管理和阻塞陷阱
McpClient.sync(transport)返回的是同步客户端,但底层是异步的,调用callTool时会阻塞等待。在WebMvc这种servlet模型下问题不大,但如果你把它用在WebFlux的Reactor线程上,就会阻塞事件循环,性能直接崩。
我踩过的坑是:在WebFlux的controller里直接调mcpClient.callTool(...),结果界面卡死,排查好久才意识到是线程模型问题。后来改用McpAsyncClient配合Mono.fromFuture(...)。
另一个坑:MCP Server端多个客户端连接时,连接复用是个问题。Spring AI的WebMvcMcpServer基于Servlet异步支持做的,客户端长轮询也会占住连接。如果客户端数量大,建议前置负载均衡并配置连接超时。
5.3 RAG和MCP的关系:不是替代,是互补
很多人问MCP是不是要取代RAG。我的看法很简单:两者解决的是不同问题。
RAG解决的是“知识不在模型训练资料里,需要先检索再回答”——比如企业文档库、私有知识库。它把相关内容预先拉进上下文,模型基于检索结果生成答案。
MCP解决的是“需要外部系统执行动作或获取实时数据”——比如查数据库、触发部署、改文件。它让模型自己决定什么时候调用什么工具。
在实战中两者常常共用。比如一个客服Agent:先用RAG检索产品手册,回答常规问题;遇到“帮我查一下这个订单的物流状态”,就通过MCP调订单系统API。所以别纠结谁取代谁,它俩是左右手。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 客户端启动时报连接失败 | MCP Server没有在配置的/mcp路径暴露 | 检查服务端是否成功引入WebMvc/WebFlux的server starter;直接curl看/mcp是否响应 |
| 模型不调用任何工具 | 工具schema没被注入 | 看服务端是否有@ToolBean;检查客户端tools/list结果是否包含工具 |
| 调用工具后模型回答“无法找到该工具” | 工具名不匹配 | 检查@Tool注解的name值,或方法名是否被序列化成预期值 |
| 参数一直报校验错误 | 工具参数结构过于复杂或必填字段标记不对 | 简化参数为扁平record,查看实际发送的JSON Schema |
| WebFlux线程卡死 | 在事件循环里用了同步McpClient | 改用McpAsyncClient,或者挪到boundedElastic线程池 |
| 服务端返回工具结果超时 | 工具方法里做了长耗时操作 | 给工具方法设计超时时间,或者主动拆分工具粒度 |
5.5 跟Spring AI Alibaba的联动:NL2SQL实战思路
既然热词里频繁出现“spring ai alibaba”和“nl2sql”,这里简单说下思路。Spring AI Alibaba是围绕Spring AI构建的中间件产品线,它在MCP上的思路是把企业系统封装成标准MCP Server。
NL2SQL是其中一个特别经典的应用场景:用户说“查一下上个月华东区的销售额”,系统通过MCP调用一个executeSql工具,由工具侧做SQL校验和权限控制,把执行结果返回给模型,再由模型组织成自然语言回答。
这里的关键不是让模型写SQL——模型写SQL太容易出错——而是让executeSql工具本身足够安全:只读账号、超时限制、LIMIT强制注入、敏感字段脱敏。工具实现得好,NL2SQL才敢拿到生产用。
从这个例子可以看出来,MCP的价值不仅仅在“AI能连更多东西”,而是“你操作系统的能力边界可以按业务需求精准定义”。
6. 源码阅读路径与进阶方向
如果你想把Spring AI的MCP相关源码吃透,我建议你按这个顺序走,比从零乱翻效率高很多:
McpClientAutoConfiguration—— 先看自动装配,理解客户端怎么连接。McpToolCallbackProvider—— 看工具怎么被扫描、包装、注册。DefaultToolCallingManager—— 看模型tool_call之后触发链路。McpToolCallback(以及McpClient的callTool方法)—— 看远端调用的封装细节。WebMvcMcpServer—— 看服务端的路由和请求处理。
源码读完,建议做两件事来验证理解:
- 自己写一个MCP Server(不依赖Spring,直接用MCP Java SDK),然后让Spring AI客户端调用它。这一步能让你彻底分清“Spring AI做了什么”和“MCP协议做了什么”。
- 试着给
@Tool加一个自定义的切面做审计日志,你会发现工具调用的拦截点就在ToolCallback这一层。
再往后,你可以尝试在真实场景里做组合:比如把你现有的Spring Boot接口快速变成MCP工具,暴露给公司的AI助手使用。我最近把一套内部API做成了MCP Server,同事的AI助手现在可以直接查订单、发通知、看日志——这套东西的价值是立刻可见的。
我个人在实际操作中的体会是:MCP的入门门槛其实不高,难的是工具边界设计和协议理解。很多人在网上看完几个demo就觉得会了,结果一上生产就遇到认证、连接管理、Schema一堆问题。只要你愿意花两三天沿着官方源码走一遍,MCP在你眼里从“黑盒”变成“透明的管道”,那时候再去设计Agent架构、规划工具粒度,就会顺手得多。
最后再分享一个小技巧:调试MCP的时候,不要只依赖Spring的日志。MCP协议层本身是JSON-RPC,你可以用curl -X POST http://localhost:8080/mcp -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'把请求直接打过去,看服务端到底返回了什么。先确认协议层通没通,再去查Spring集成层的问题——这条排查路径能帮你省掉大量无效时间。