大家最近应该都有同一个感受Java 群里聊 MCP 的人越来越多各种场景都开始拿 Spring AI 接 MCP 做工具调用。不管是查菜单、查库存、生成报表还是让模型帮你操作内部系统MCP 已经成了这轮 AI 应用落地绕不开的协议。我花了一周时间把 Spring AI 的 MCP 客户端、本地 Server、工具注册、Agent 串联整条链路全部跑通踩了几个比较隐蔽的坑也验证了不少网上说法。这篇文章我尽量按零基础能照着抄的标准来写先讲清楚 MCP 到底解决什么问题再给一套可以直接复现的 Spring Boot 工程最后把排查经验也放进来希望你看完能少走弯路。1. 为什么是 MCP模型时代的驱动安装协议1.1 模型很聪明但它天生手短先说一个被很多人忽略的事实大模型本质上是一个只能对话的系统。你让它帮我查一下今天餐厅有哪些菜它如果没接过任何真实数据源就只能凭训练时的记忆瞎编。你让它帮我生成一个订单调用业务系统的下单接口它连你的接口地址都不知道更别说怎么调。早期的解决方案是 Function Calling把函数描述和参数 Schema 发给模型模型智能地选择调用哪个函数然后由我们的程序去执行。这个思路没问题但每个框架都有自己的函数描述格式、参数校验方式、执行回调逻辑。你接了 OpenAI 的模型是一套写法换一个国产模型又是一套写法你做一个内部工具平台是一套规范别人做的 AI 助手又是一套规范。结果就是每接入一个新的能力提供方就要重新写一层适配代码维护成本非常高。1.2 MCP 不是什么黑魔法它只是把工具调用做成了标准协议MCP 全称是 Model Context Protocol模型上下文协议。你可以把它理解成模型世界的驱动安装协议设备以前要装各种专用驱动现在统一走一个标准接口MCP 也是把外部数据源/工具统一封装成标准接口让任何支持 MCP 的大模型应用都能直接发现、调用、拿到结果。具体到协议层面MCP 基于 JSON-RPC 2.0 通信核心就三件事初始化握手机制交换“这个 Server 支持哪些工具”调用工具把参数传给 ServerServer 返回结构化文本或资源数据事件与通知处理工具列表变更、日志等消息。它不关心你内部用了什么语言、什么框架只要大家按同一套消息格式说话就能互通。很多初学者会把 MCP 和 USB 这类硬件协议混在一起其实 MCP 是纯应用层软件协议跟硬件扯不上关系。拿 USB-C 来类比只是因为一根线通吃所有设备的体验很接近你的 AI 应用是 HostMCP Server 是被插上的外围设备工具就是设备提供的功能。1.3 Spring AI 在其中扮演的角色Java 生态的适配层如果你只写 Python你可能选择 LangChain 或直接使用 MCP Python SDK。但如果你在 Spring Boot 工程里Spring AI 是更顺手的粘合层。它做了三件很关键的事第一自动把 MCP Server 里暴露的工具转换成 Java 对象并且转成模型能理解的 Tool Schema。这个转换过程不需要你手写 JSON Schema对 Java 开发者来说非常省事。第二提供统一的 ChatClient 接口。你现在可能是接 OpenAI 兼容接口、Ollama 本地模型、或者阿里云百炼的模型Spring AI 都统一给你一个 ChatModel上层代码不用换。第三它兼容 Spring Boot 的配置体系。MCP Server 的地址、参数、超时时间都可以写在 application.yml 里通过配置切换不同环境。所以如果你已经是一个 Spring Boot 开发者想在现有系统里接入 AI 工具能力Spring AI 是成本最低的一条路。很多人纠结现在到底用 Spring AI 还是 LangGraph4j我的看法很明确如果只是做一个能调用工具、能多轮对话的智能体Spring AI 足够LangGraph4j 更适合需要复杂状态流、循环和多人协作编排的场景属于更重的选型。2. 开跑前准备版本、依赖和一个小型本地 Server2.1 环境与版本建议直接用 JDK 17 以上Spring Boot 3.3 以上。我记得 Spring AI 到 1.0 GA 之后 API 才稳定下来我这边用的是 Spring AI 1.1.0整条链路跑下来没有问题。如果你的项目里已经引入了其它 AI 相关依赖注意统一版本否则很容易出现类冲突。Maven 工程里先用 BOM 管理版本dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.1.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement核心依赖是三个模型客户端、MCP 客户端、Web。如果你后面需要自己开发 MCP Server再加一个spring-ai-starter-mcp-server。dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency虽然 starter 名字里带 openai但它支持所有 OpenAI 兼容协议的模型服务包括国内可直连的阿里云百炼、DeepSeek 等。我后面测试就是用百炼的 OpenAI 兼容模式不需要额外的网络配置普通开发机就能跑。2.2 模型通道机制很多教材只讲 MCP 怎么配置却忘了模型本身要支持 Function Calling。不是所有模型都能正确返回工具调用指令如果你用的模型太老或不支持后面 MCP 工具注册得再完美也没用。我选模型有两条硬标准一是支持 Function Calling / Tool Call二是有足够的上下文长度。下面这段配置是我测试时用的spring: ai: openai: base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus如果你本地有 Ollama也可以把模型配置指向http://localhost:11434/v1模型用支持工具调用的版本。第一次跑通时换个便宜的国产模型接口试是明智的省钱也省时间。2.3 造一个点餐系统MCP Server 当靶场我看不少教程喜欢用公共 MCP Server比如 GitHub 或浏览器操作类的但这些服务依赖账号、Token新手根本没法复现。我的建议是自己写一个本地 MCP Server 当靶场数据完全可控排错也容易。这里我用 Python 的 FastMCP 实现因为它代码最短几行就能暴露一个工具。pip install mcp之后写一个restaurant_server.pyfrom mcp.server.fastmcp import FastMCP mcp FastMCP(restaurant-mcp, transportstdio) MENU [ {id: 1, name: 经典牛肉面, category: 主食, price: 32}, {id: 2, name: 口水鸡, category: 凉菜, price: 28}, {id: 3, name: 酸辣土豆丝, category: 热菜, price: 18}, {id: 4, name: 玉米排骨汤, category: 热菜, price: 25}, {id: 5, name: 冰柠檬茶, category: 饮品, price: 12}, ] mcp.tool() def query_menu(category: str None) - str: 查询餐厅菜单可按分类过滤返回 JSON 数组 import json if category: items [item for item in MENU if item[category] category] else: items MENU return json.dumps(items, ensure_asciiFalse) mcp.tool() def create_order(item_ids: list[str]) - str: 根据菜品 id 列表创建订单返回订单号与总金额 import json total sum(item[price] for item in MENU if item[id] in item_ids) return json.dumps({order_id: ORD001, total: total}, ensure_asciiFalse) if __name__ __main__: mcp.run()这个 Server 虽然简单但承上启下读操作、写操作都覆盖了后面你可以在此基础上扩展数据库或真实接口。启动方式是先单独跑一遍python3 restaurant_server.pyCommand 会进程起来挂住说明 Server 本身没问题。注意它走的是 stdio也就是标准输入输出所以别直接回车往终端里丢消息客户端会用协议格式跟它通信。3. 核心 API 拆解McpClient、McpTool 与 ToolCallingManager3.1 四个关键类各管一段Spring AI 的 MCP 集成其实由四个角色协作完成每个角色职责非常清晰类 / 接口职责我的理解McpClient负责与 MCP Server 建立连接、初始化握手、读取工具列表、发送工具调用请求相当于网络层McpTool将 MCP Server 暴露的一个工具包装成 Spring AI 的 ToolCallback这个对象能被模型发现和调用相当于适配器ToolCallingManager管理所有 ToolCallback提供一个统一的工具执行入口相当于调度中心ChatClient面向业务方的最终接口支持把 ToolCallingManager 注册进去相当于门面动态关系是Spring AI 自动配置启动时读取spring.ai.mcp.client下的配置为每个 Server 创建McpClient然后向 Server 发tools/list请求拿到工具定义列表。每个远程工具被包装成McpTool再由ToolCallingManager汇总最终交给ChatClient。这里有个容易忽略的点工具是发现出来的不是写死在代码里的。也就是说只要 MCP Server 增加一个新工具Spring AI 启动时就会自动发现并注册代码里不用改。这个特性对后期扩展非常友好。我用的注册方式如下不同小版本的 API 名称可能有差异但思路一致Configuration public class McpConfig { Bean public ToolCallingManager toolCallingManager(ListMcpTool mcpTools) { return ToolCallingManager.builder() .tools(mcpTools) .build(); } }如果它提示 builder 方法不存在去确认下你的 Spring AI 版本是不是 1.0 以上的 GA 版本旧版本用的是静态工厂方法。3.2 三种连接方式怎么选MCP 的传输层有三种主流模式Spring AI 都支持。选型错误会直接影响你的部署形态所以我特别整理了一张对照表传输方式适用场景优点缺点stdio本地进程、开发调试、Server 和客户端同机部署配置简单、无需暴露端口、安全边界清晰无法远程调用、Server 生命周期跟随客户端HTTP SSE跨机器部署、提供远程 MCP 服务给多个应用共享支持远程、可用 HTTP 层做鉴权和网关控制需要 Server 端部署 HTTP 服务调试相对麻烦WebFlux 流式需要流式输出、响应式链路、高并发场景吞吐好、支持实时推送工程复杂度更高普通 CRUD 应用没必要这么早上我第一次练手用的 stdio最简单适合理解协议本身。等到你的 Server 需要给多个 AI 应用共用时再把它拆成独立的 HTTP MCP 服务。日常配置里stdio 就是告诉 Spring 启动哪个命令、传什么参数spring: ai: mcp: client: stdio: servers: - name: restaurant-mcp command: python3 args: ./servers/restaurant_server.py注意command要写它能直接执行的命令不要写python这种带 shell 行为的词因为 MCP 客户端是直接用进程 API 拉起的不经过 shell。路径要写对否则会看到进程启动失败或直接退出。3.3 从模型说要调用工具到工具结果回填的完整链路理解这一节你才算真正入门。一次带 MCP 工具的请求内部经历了六步用户把问题发给 ChatClient比如两个人吃饭预算 80帮我推荐三个菜然后直接下单。ChatClient 先把 MCP Server 传来的工具 schema 一并放到请求里发给大模型。模型看到的是我能使用 query_menu 和 create_order 这两个工具它们的参数格式是这样。大模型不直接给出最终答案而是先输出一个工具调用指令比如调query_menu(fulltrue)。Spring AI 收到这个指令后通过ToolCallingManager找到对应的McpTool再通过McpClient发送给 MCP Server。Server 执行对应 Python 函数把结果用 JSON-RPC 响应返回。执行结果被当作工具返回消息重新发给大模型。模型根据工具返回的真实数据组织出最终的自然语言回答。用户收到为你推荐牛肉面、口水鸡、酸辣土豆丝合计 78 元订单号 ORD001。整个过程看起来像模型在自主操作实际每一环都是确定性的代码在控制模型只在决定调用哪个工具、填什么参数这一步起作用。这也是 MCP 的边界协议负责传输业务安全仍然掌握在你手里。4. 实战让 AI 替你完成点餐闭环4.1 场景设计为什么用点餐来练手点餐场景在我做的餐饮 SaaS 相关项目里很常见拿它做 Demo 有两个好处一是数据量小、字段清晰MCP 工具返回结果一眼能看懂二是同时包含查菜单和下单两类操作能验证 MCP 对读操作和写操作的处理能力。你别小看写操作真到生产环境里模型能帮你创建订单也会帮你删除数据所以第一步就要想清楚哪些工具该暴露给模型。4.2 配置与代码把 Server 挂载进 Spring 容器假设你已经有了 2.3 的 Python Server现在创建 Spring Boot 工程目录结构大概是src/main/java/com/example/mcpdemo/ ├── McpDemoApplication.java ├── config/McpConfig.java ├── config/ChatConfig.java └── controller/ChatController.java src/main/resources/ ├── application.yml └── servers/restaurant_server.pyapplication.yml同时配置模型和 MCP Serverspring: application: name: spring-ai-mcp-demo ai: openai: base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus mcp: client: stdio: servers: - name: restaurant-mcp command: python3 args: ./servers/restaurant_server.py然后写两个配置类。McpConfig负责把自动装配好的McpTool收集起来注册到ToolCallingManager。ChatConfig把ChatModel和ToolCallingManager合并成业务层用的ChatClientConfiguration public class ChatConfig { Bean public ChatClient chatClient( ChatModel chatModel, ToolCallingManager toolCallingManager) { return ChatClient.builder(chatModel) .defaultTools(toolCallingManager) .build(); } }控制器就一个接口接收用户消息直接交ChatClient处理RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient chatClient; } PostMapping(/chat) public String chat(RequestBody MapString, String body) { return chatClient.prompt() .user(body.get(message)) .call() .content(); } }建议在ChatClient构造时打开调试日志Spring AI 中有对应的高级选项方便看模型实际返回的工具调用指令。第一次跑通时一定要看到这样的启动日志Registered MCP tools: query_menu, create_order。如果看到的是空注册表直接去 5.1 查原因。4.3 跑一把真实请求看数据是怎么流动的启动项目后用 curl 发一个请求curl -X POST http://localhost:8080/chat \ -H Content-Type: application/json \ -d {message:两个人吃饭预算80帮我推荐三个菜然后下单}正常情况下模型会先调用query_menu拿到全部菜品再根据预算挑选组合再调用create_order最后输出答复。我这里实测的结果大体是根据菜单我推荐经典牛肉面32元、口水鸡28元、酸辣土豆丝18元合计78元符合预算。我已为你创建订单订单号 ORD001。注意一个细节如果你用的是千问加百炼它返回 JSON 是content数组文本内容可能有换行或者被 Markdown 包裹自己写代码展示时建议做一层清洗。另外预算计算、菜品搭配这些规划动作是模型自己做的但它调用的所有数据都来自真实 MCP Server所以答案不会凭空编价格。工具调用的每一步我建议都打日志。比如在ToolCallingManager外面包一层ToolExecutionListener或者在 Python Server 里 print 到 stderrMCP 协议会把 stderr 透传到客户端日志里这样哪一步失败一目了然。这个习惯帮我省了大量排查时间。5. 实测中逃不掉的坑与被验证过的排查链路5.1 坑一工具注册了模型却一直不调用现象是最常见的启动日志里能看到注册成功但你无论怎么问模型就是不用工具或者用词含糊说我无法访问实时菜单。排查链路我建议按这个顺序走先确认模型本身支持 Function Calling。不支持工具调用的模型你把工具描述喂过去也没用它只会当作普通文本理解甚至当成干扰信息。再确认请求里真的带了工具。开启 Spring AI 的请求日志或者在ChatClient.prompt()里显式指定.tools()。有时你的defaultTools配置被复用错Bean 覆盖导致工具没进入当前请求。然后检查提示词是否适合触发工具。模型不会无缘无故用一个看起来不重要的工具。你在系统提示词里加上你是一个餐厅助手必须先查询菜单再推荐这样明确的指令工具调用率会立刻上升。最后看是不是工具描述太弱。MCP Server 里给工具写的 description 质量直接决定模型想不想调用它。query_menu的描述如果写成查询菜单模型可能觉得可用可不用写成获取当前可用菜品及真实价格推荐前必须调用它获取数据模型就会把它当作必要步骤。5.2 坑二stdio 子进程连不上或秒退配置没写错、代码没报错但一启动就发现McpClient连接失败日志里能看到子进程退出码。这个坑多半出在进程拉起细节上。第一要检查绝对路径。Spring AI 拉 stdio 进程时不经过 shell所以~/xxx.py这种路径不会被展开python3也不一定在你的 PATH 里。我建议command全路径比如/usr/bin/python3args也用绝对路径。第二要确认脚本能独立运行。先自己在终端里跑一遍如果脚本有语法错误或者 import 缺失它启动后会立刻退出MCP 客户端这边只能看到连接断开。第三是环境变量问题。如果你本地靠uv或者虚拟环境跑 Python那个虚拟环境里的mcp包在 Spring Boot 拉起的子进程里可能根本不存在。直接用系统 Python 安装依赖或者把虚拟环境的 bin 路径写到 command 里。第四如果你在 Windows 上开发stdio 模式容易踩编码坑。建议统一用 UTF-8Python 侧加环境变量PYTHONUTF81避免中文返回乱码把 JSON 撑爆。5.3 坑三返回了 JSON回答却还在编这个坑特别隐蔽。工具确实被调用了Server 也正常返回了 JSON结果模型输出的菜品名、价格依然和真实菜单对不上。问题往往不是模型抽风而是工具返回格式缺少机器可读的结构说明。模型收到的是大段 JSON 文本它要自己猜测每个字段的含义和单位。如果 Server 返回裸字符串模型的解读空间很大。我的标准做法是工具返回值里除了数据还加上一行字段说明。比如return json.dumps({ data: items, fields: { id: 菜品ID, name: 菜品名称, category: 分类, price: 价格单位:元 } }, ensure_asciiFalse)模型看到字段注释后就不再靠猜。另外某些模型的 JSON 输出会在返回内容后面拼接多余文本这不影响 MCP 协议但会影响最终回答质量你可以在 Server 端先把内容清洗干净再返回。5.4 坑四超时、重复调用与响应过长当你把工具列表做得越来越大工具返回的数据越来越长会遇到两个问题一是 MCP 往返超时二是模型把同样的工具连调好几次白白浪费 Token。超时配置很简单Spring AI 里可以调整连接和调用超时。我实际建议别等系统默认值显式设置spring: ai: mcp: client: stdio: servers: - name: restaurant-mcp command: python3 args: ./servers/restaurant_server.py timeout: 30s如果某个工具耗时很久比如要调一个慢速第三方接口就别把太重的工作塞进 MCP 工具里。MCP 工具应该做查询、提交这种原子操作复杂的异步任务放服务端后台执行MCP 只返回一个任务号和状态查询地址。重复调用问题通常是因为工具返回内容无法满足模型的决策条件。模型调query_menu后如果发现返回里没有需要的信息比如缺少是否售罄字段它会反复调整参数再调。解决办法是让每个工具返回的信息尽量完备或者调整你给模型的系统提示词如果菜单数据已经齐全不要重复查询。5.5 必须提醒的安全边界写操作工具一旦暴露给模型风险是几何级上升的。我见过有人在测试环境跑通了AI 自动下单然后顺手把删除接口也包成了 MCP 工具结果一个测试指令就把数据库清空了。这种事真不是段子。我给自己定的安全底线如下工具权限最小化。MCP Server 暴露给模型的工具必须是业务允许范围内最细粒度的那一类能只读就不要给写权限能按 ID 操作就不要给全表操作。参数校验前置。模型填参偶尔会非常离谱服务端必须像防御正常用户请求一样去校验每个字段尤其是金额、状态、数量这类敏感字段。操作确认环节。对 create、update、delete 这类写工具建议在业务流程里加一个待确认状态AI 先把草稿订单创建出来由人工或前端二次确认后再真正生效。这个设计能挡住绝大多数误操作和幻觉参数。不要把 Token 放进代码或配置仓库。MCP Server 如果走远程 HTTP鉴权信息一定要走环境变量注入。我在搜索资料时看到很多例子里直接硬编码了某个公共服务的 URL 和 Token这种习惯千万别学。6. 从能用到好用Agent、RAG 和多服务聚合6.1 让 MCP 工具参与 Agent 的自主决策实战项目跑通之后你多半不满足于一问一答而是想让它自己规划多个工具的调用顺序这就进入了 Agent 阶段。Spring AI 的做法是给ChatClient加上记忆能力让多轮对话里工具调用产生的结果能够被后续对话引用。具体点说第一轮用户问今天有什么菜第二轮用户说把第一轮里最便宜的凉菜加入订单如果没有记忆第二轮模型根本不知道第一轮查过什么。解决办法是给ChatClient配置MessageChatMemoryAdvisor把工具调用历史也存进消息上下文。这比你在业务代码里手动拼历史要省心得多也更接近 Agent 的体验。另外Spring AI 社区也在往这个方向扩展比如 spring-ai-alibaba 项目就有针对百炼模型的 Agent 支持和 NL2SQL 场景封装。它允许你用更自然的方式让模型写 SQL、查数据库、再结合 MCP 工具完成任务。如果你要做企业级智能体可以多关注这类扩展避免自己重复造轮子。6.2 MCP 与 RAG 的组合模式很多人问 MCP 和 RAG 是不是互斥的其实它们解决的是不同层面的问题。RAG 负责把知识塞进上下文MCP 负责把动作变成工具调用。比较典型的组合是先用向量检索把相关文档片段找出来喂给模型理解业务规则然后模型根据规则决定调用哪个 MCP 工具去执行操作。比如一个售后助手先检索退货政策文档再调用售后系统的创建退货单工具。检索和工具调用之间不是竞争关系而是先后关系。在 Spring AI 里你可以同时配置向量数据库和 MCP 客户端ChatClient.defaultTools()和defaultSystemMessage()并不冲突。我自己的经验是先跑通 MCP 工具调用再引入 RAG因为 MCP 能让你直观看到模型在真实调用某个东西而 RAG 改动的往往是提示词层面的编排。6.3 多 MCP Server 聚合与工程化生产环境里你不可能只有一个 MCP Server。典型场景是订单系统一个 Server、商品系统一个 Server、行为日志一个 Server它们可能分属不同团队开发用不同技术栈实现。Spring AI 对多 Server 的聚合支持得不错。你可以在配置里声明多个 stdio 或 HTTP Server它会为每个 Server 创建独立的McpClient再统一汇总到ToolCallingManager。这有一个隐含好处每个 Server 的工具列表可以隔离你可以决定哪些工具对当前模型开放。工程化提几个小建议每个 MCP Server 设置唯一命名前缀否则工具名冲突时会出现覆盖行为定期拉取工具清单做比对防止有人偷偷往 Server 里加危险工具日志和指标单独打点记录每个工具的调用次数、耗时、失败率方便后续做权限收敛。这些要求看起来像是过度设计但等你真正把 AI 应用放进生产环境就会发现工具数量和调用频次一上来没有统一监控根本说不清模型到底在做什么。我在实际项目里体会最深的一点是MCP 把工具接入的复杂度压低了但它不会替你治理工具的边界。模型的能力越强、工具暴露得越多业务侧的控制就越要做得扎实。先从一个点餐 Server 上手再逐步加业务深度这条路最稳当。