资讯动态

Spring AI MCP源码浅析:从自动装配到工具调用的完整链路

发布时间:2026/10/20 8:53:05 来源:尧图企业网站定制
最近在做一个基于 Spring AI 的智能体项目服务端要把十几个内部能力开放给大模型调用。最初是手写函数调用function calling每个工具都手写注册、解析参数、定义描述接到第三个工具时已经开始怀疑人生。后来把项目迁到 MCPModel Context Protocol模型上下文协议这一套标准上用 Spring AI 内置的 MCP 支持把普通 Spring Bean 方法直接暴露成模型可调用的工具。迁移过程中我逼着自己把 Spring-AI MCP 相关源码完整过了一遍这篇文章就是这次源码浅析的过程、结论和踩坑记录。这篇内容适合对 Spring AI 有基本了解、想搞清楚“注解背后的工具是怎么被扫描、注册、调用的”的开发者。读完之后你至少能回答几个问题MCP 客户端是怎么把远端工具变成本地 Tool 的服务端是怎么把 Tool 方法暴露成协议工具的一次真实的工具调用失败应该往哪个环节排查。1. 为什么值得拆一遍 Spring AI 的 MCP 实现1.1 MCP 在解决什么问题先交代背景。凡是做过 AI 应用的人都体会过这种重复劳动大模型要调用我们的接口得把接口包装成“工具描述 参数 Schema 实际执行逻辑”三件套。工具一多三件套全部手动维护换一个模型供应商描述格式又要调整再遇到数据源分散在文件、数据库、外部服务里连工具发现机制都是各写各的。MCP 要解决的正是这个“接口不好标准化”的问题。MCP 的逻辑可以这样理解AI 应用是前端具体的数据源和工具是各种设备MCP 就是那个统一插座。设备按照插座的规格供电前端不需要关心设备内部怎么工作。在代码层面MCP 定义了工具Tool、提示词Prompt和资源Resource三大类能力默认用 JSON-RPC 2.0 做消息协议传输层可以走标准输入输出stdio、HTTP/SSE 等。这种“能力标准化 传输可插拔”的设计让工具可以像插件一样被发现和调用。1.2 框架层接入的价值Spring AI 的定位是 AI 应用开发的 Spring 风格解决方案它本身已经抽象好了 Tool 接口、ToolCallback 回调、ChatClient 等概念。MCP 模块要做的就是把 MCP 协议与这套抽象无缝对接服务端把 Spring Bean 变成 MCP Server客户端把远端 MCP Server 变成普通 Tool。对开发者来说最大好处是心智负担降低本地工具和远端工具都表现为 Tool模型侧完全无感。我理解“源码浅析”和“直接看文档用”的区别就在这里文档告诉你配置 key源码告诉你 key 背后的自动装配在什么条件下生效、注解扫描跟 Bean 生命周期怎么挂钩、工具描述在哪个环节被转成 JSON Schema。这些知识不会立刻体现在功能上但出了问题、要扩展新工具协议时能不能快速定位全看这一层理解。1.3 源码阅读的总体思路我的读法一共三步。第一步先跑起来启动一个最简单的 Server 端点和 Client打开 DEBUG 日志观察启动阶段输出了什么、工具注册发生在哪一行。第二步找入口定位自动装配类顺着装配顺序把核心对象Server、Client、ToolProvider的创建过程串起来。第三步追链路选一个完整的“模型发起工具调用”的例子从 Tool 接口一路追到远端方法执行和返回结果。刻意不去逐行读每个类有两个原因一是 MCP 实现里协议解析、序列化占了大头这些部分对理解主流程帮助不大二是源码阅读最怕陷进细节先建立主干模型后续需要时再回头看具体分支即可。2. 源码阅读前的关键准备与整体切分2.1 模块与包结构切分进入源码之前先看模块划分。Spring AI 的 MCP 支持主要拆成几个独立模块服务端能力模块、客户端能力模块、传输层实现stdio、HTTP/SSE、自动装配模块包含与 MCP 相关的配置类。自动装配模块不直接实现协议它做的事情是把核心类放到 Spring 容器里并读取配置项。我的建议是有条件就把几个模块同时打开用 IDE 的全局搜索跨模块找类比单模块跳转高效得多。阅读顺序建议自动装配 - 工具定义 - 工具注册与发现 - 调用链路 - 传输层。自动装配能告诉你“框架究竟帮我 new 了哪些对象”工具定义是理解协议和框架抽象映射的关键调用链路是整篇阅读的高潮。2.2 核心概念映射表阅读时会频繁看到两套名词MCP 协议侧的概念Spring AI 抽象侧的概念。我整理了一张映射表遇到概念时直接对着看MCP 协议概念Spring AI 抽象说明ToolToolDefinition ToolCallback协议里“工具”对应可调用的能力框架里拆成定义名称/描述/Schema和执行回调 MethodToolCallbackTool 调用请求CallToolRequestJSON-RPC 消息内部携带工具名和输入参数MCP ServerMcpServer服务端协调者负责工具注册、会话管理MCP ClientMcpClient客户端协调者负责连接服务端、发现工具ResourceResourceDefinition数据资源的描述框架侧也有对应抽象这张表盯着看几分钟再进源码效率会明显不一样。我一开始直接跳进 McpServer 的类里看到一堆 Session、Transport 的概念半天没对上号后来退回这张表再读就顺了。2.3 从一次工具调用倒推理解链不必一上来就按顺序读源码。我更推荐先拿一个真实场景倒推客户端接入了一个 MCP Server模型在对话中决定调用其中的获取天气工具。这条链路一共经历模型生成工具名和参数 - Client 收到后找到对应的本地 Tool - Tool 内部把调用转换成 JSON-RPC 请求消息 - 传输层发送给远端 - Server 解析请求 - 找到注册的工具回调 - 反射调用 Spring Bean 方法 - 结果反向封装回模型。带着这条链路去看代码你会发现每个类的职责都自然浮现本地 Tool 是“协议调用”和“框架 Tool 接口”之间的适配器方法回调是“协议方法调用”和“Spring Bean 方法”之间的最终执行器而传输层只是负责把 JSON-RPC 消息搬过去。源码阅读不再是一个一个类读而是沿着这条链路走两遍走向就清晰了。3. 自动装配入口框架是如何把 MCP 接进来的3.1 服务端自动装配的结构先看服务端入口。首次使用 Spring AI 的 MCP 时配置文件里通常只要打开服务端 enabled 配置然后定义一个带 Tool 注解的 Bean服务就起来了。这个“起来”是怎么发生的核心在一个服务端自动装配类。装配过程可以简单描述配置类读取 enabled 配置检查类路径里是否有必要的传输层实现条件满足则创建 McpServer 的构造器把传输实例、工具发现器、会话工厂装配进去最后通过一个 ApplicationRunner 在 Spring 容器启动完成后启动 MCP Server。这类“启动后启动”的设计我很喜欢因为它避开了与 Web 容器、事务等组件抢占初始化顺序的问题。真正容易忽略的是服务端工具暴露不只是在启动时一次性注册。MCP 协议支持运行时动态新增工具框架侧通过工具提供者机制实现。工具注册中心在收到新增工具的通知时会向所有已连接的会话广播工具列表变更。这也是很多 AI 应用能做到“插件热加载”而不用重启进程的原因。3.2 客户端自动装配的结构客户端入口是客户端自动装配类。与服务端对称客户端 enabled 配置控制是否启用启用后框架会为每个配置的 MCP Server 创建一个 McpClient 实例再把这些实例交给工具发现器一次性拉取服务端声明的工具列表转换成框架的工具定义。初次阅读时最值得注意的一点是客户端连接服务端拉取工具列表时默认在启动阶段同步完成。这意味着如果远端 MCP Server 启动耗时较长应用启动会被拖住。源码里的处理方式是把“连接”和“工具发现”两个动作拆开连接可以异步在后台建立工具发现则等连接完成后触发。如果你遇到“应用起来了但工具列表是空的”多半是这个时序没有走通。3.3 配置项与 Bean 覆盖时机自动装配类里散落着很多条件注解和属性绑定。排查问题时要注意配置项的前置条件服务端 enabled 配置同时影响配置类是否加载与传输类是否注册工具、资源等能力开关控制协议侧能力是否被启用自定义实现会影响装配一旦你定义了传输层、工具提供者等类型的 Bean框架的默认装配可能被覆盖提示如果你自定义了同名 Bean 但发现没有生效优先检查自动装配类上是否有 ConditionalOnMissingBean。这个注解的语义是“容器里没有才创建”自动装配生成的默认实现往往会排在比较靠后的顺序自定义 Bean 需要确保在它们之前被声明。4. Tool 注解背后的工具发现与 Schema 生成4.1 注解扫描的核心环节服务端把方法暴露成工具最直接的方式是 Tool。扫描逻辑由一个 BeanPostProcessor 承担它在 Bean 初始化完成后检查目标 Bean如果发现类里标注了 Tool 的公开方法就为这些方法生成工具定义和执行回调。这里有几个细节值得记录。第一扫描对象不只是单独的 Component也包括 Bean 方法返回的实例只要对象在 Spring 容器里都有机会被扫描到。第二一个类里可以标记多个 Tool 方法框架会把它们统一注册成多个工具工具名默认是方法名也可以由注解属性指定。第三处理器的后置处理阶段执行意味着 AOP 代理可能影响方法元数据获取。我实际踩到过一次工具方法被业务切面代理后注解没有被正确识别后来确认是代理场景下注解元数据获取路径不同换到目标对象的方法元数据后解决。4.2 工具描述的组成要素MCP 协议要求工具的“描述”能够被模型理解框架侧的工具定义主要由三部分组成名称、描述、输入 Schema。其中名称和描述来自 Tool 注解或方法名输入 Schema 则来自对方法签名的分析。源码里这一段取值优先级值得注意如果 Tool 注解显式给了描述使用注解值否则尝试读取方法上的 javadoc 摘要再没有就退到方法名。这也是很多工具看起来“没有写注释描述就是方法名”的原因。4.3 从方法签名生成 JSON Schema 的过程这一块是源码中最容易劝退人的部分。Schema 生成类拿到方法参数类型后要做几件事解析泛型类型、嵌套对象递归展开、处理集合和 Map、生成可读的字段描述。关键原则是所有字段默认必填除非标注了可选参数注解或使用 Optional 类型。如果你的工具方法参数是个复杂对象我建议看一眼生成的 Schema 是否符合预期。方法启动服务端后调用 Schema 查询接口或者从客户端的工具列表里打印工具定义用 JSON Schema 检查字段类型、嵌套层级、必填标记重点观察日期时间类型会被转成什么格式默认是字符串但模式里不会自动标注格式注意泛型嵌套如果特别深Schema 生成可能出现字段名冲突。我项目中遇到过两个嵌套类都有同名业务字段生成的 Schema 直接合并成了一个字段导致远端传参时把值填到了错误的位置。处理方式是给字段加上区分性更强的属性名或使用参数注解显式重命名。以下是简化示意代码重点看 Schema 生成的走向public class MethodToolSchemaGenerator { public JsonObject generate(Method method) { SchemaGenerationContext context new SchemaGenerationContext(); for (Parameter param : method.getParameters()) { String paramName getParamName(param); if (isOptional(param)) { context.markOptional(paramName); } context.put(paramName, resolveTypeSchema(param.getParameterizedType())); } return context.build(); } }以上为抽象简化并非完整源码5. 核心调用链路从模型到远端工具5.1 客户端侧的调用适配现在看模型发起工具调用后发生了什么。先明确一个概念模型本身不直接和 MCP 协议打交道它只认识框架抽象的 Tool。框架内部会为每个远端工具创建一个本地 Tool 实例这个实例的名字、描述与协议侧的声明一致模型以为是本地工具实际执行时消息会被转发到远端。这个本地 Tool 的核心逻辑可以概括为两步第一步把模型传入的参数转成 MCP 协议要求的调用请求第二步把响应里的内容提取出来转回框架期望的响应结构。转换过程中最需要重视的是 JSON 的容错模型给出的参数往往不是合法 JSON框架会在这一步做容错解析但也只能缓解少量问题参数结构错乱时最终还是会丢到远端才能暴露。进一步看客户端工具列表不是静态的。框架支持通过客户端的异步方法在运行时更新工具列表这对接“服务端动态注册工具”的场景很重要。前面第 1 章提到的工具列表广播在客户端对应的就是监听事件、刷新本地 Tool 缓存。如果你在做插件市场类应用这条路径比重启进程方案好得多。5.2 服务端侧的请求中转请求到达服务端后McpServer 先做一层协议层面的校验消息是不是合法的 JSON-RPC、工具方法是否存在。通过校验后框架会查找工具名对应的回调调用时做两件事反序列化工具参数反射调用 Spring Bean 方法。反序列化这一步在源码里很轻巧地复用了一整套 Jackson 配置。好处是 Spring MVC 那套习惯配置时间格式、空值处理、泛型在 MCP 调用中同样生效坏处是如果你配置了全局的严格反序列化策略MCP 的宽松容错可能被覆盖导致远端传来的缺省字段直接报错。反射调用是最后一步参数解析依赖前面的 Schema 生成逻辑保持一致必填参数缺失会抛参数异常参数类型不匹配会在 Jackson 转换阶段报错方法内部抛出的异常会被包装成协议错误返回而不是直接泄漏堆栈。5.3 完整链路的时序复盘用一个例子把整条链串起来某服务端有一个 Tool 方法 getUser(Long id)客户端已成功发现该工具。模型决定调用于是在响应里带了工具名 getUser 和参数 {id: 123}。客户端的本地 Tool 收到参数把 id 保持为字符串或转成 Long取决于 Schema 定义组装成 JSON-RPC 请求发送。传输层把请求发给服务端。服务端先检查工具是否存在再调用方法回调用 Jackson 把 JSON 参数绑定到方法入参反射执行 getUser返回 User 对象。回调把结果包装成响应传回客户端客户端再提取内容返回给模型模型根据内容生成最终回复。整个过程下来真正跟协议强相关的部分只有请求封装、传输、请求解析、结果封装。其余全是框架抽象的衔接。这也是我读完源码最大的感受MCP 协议本身的厚度并不大模块里的协议实现代码是很薄的一层更大的复杂度在传输层和 Schema 生成。6. 调度、连接与传输层里的细节6.1 stdio 传输的进程生命周期本地开发时最常用的传输是 stdio也就是父进程启动子进程依赖标准输入输出传递 JSON-RPC。源码实现里一个典型的 stdio Server 需要处理进程启动参数与工作目录、环境变量透传、输入流的逐行读取、输出流的消息写出、子进程退出码监控。有几个实际注意点。第一子进程的输出不能被业务日志污染否则父进程的协议解析会崩溃所以在子进程内不要把工具函数里的 System.out 当作日志输出。第二stdio 的进程生命周期由父进程控制父进程退出时子进程可能成为孤儿进程源码里通常有连接关闭钩子处理但生产中仍然建议在外部进程管理工具里做兜底。第三stdio 天然适合本地和单机场景不适合跨主机跨主机请选择 HTTP/SSE 传输但代价是连接管理、心跳、重试都得自己上。6.2 会话隔离与并发边界MCP 服务端的会话概念对应一组连接上的工具状态。不同的客户端连接最好相互隔离避免一个客户端修改资源影响另一个。源码层面会话对象负责持有协议会话状态而工具注册表是全局的还是会话隔离的取决于实现方式。默认的注册表全局共享工具列表变更对所有会话可见这通常没问题但如果你的工具内部持有会话级上下文比如请求跟踪 ID就需要在回调中显式传递。并发方面需要注意的是同一个工具方法可能被多个模型会话同时调用。Spring Bean 默认单例方法内共享状态必须自己保证线程安全。我项目里有一个统计类工具直接用的普通 HashMap 缓存请求计数上线后被并发请求打出并发修改异常换成 ConcurrentHashMap 后一切正常。从源码阅读的经验讲传输层代码是最接近“真实网络”的部分stdio 读线程、重连逻辑、心跳超时每一处边界条件几乎都来自真实场景。读这些代码时不要只看成功路径要重点看连接断开会重试几次、消息超时会怎样、半包怎么处理。7. 常见问题与排查实录7.1 工具没有出现在模型可调用列表里这是最高频的问题。先分客户端和服务端两侧排查服务端看启动日志里有没有类似“Registered tool xxx”的输出没有说明工具扫描没命中检查注解方法是否 public、类是否在 Spring 容器里客户端看工具发现日志如果没有发现请求检查 server 地址和 enabled 配置。再不行就查能力开关提示词和资源能力默认可能关闭工具能力如果也被关闭列表自然为空。7.2 Schema 生成不合法导致接口调试失败现象通常是客户端拿到了 Schema但模型生成的参数一直校验失败。优先打印 Schema 看字段字段是否少了必填标记、复杂对象是否被展平、数组类型是否正确。最常见的坑是参数名显示成 arg0、arg1Java 编译器默认不保留方法参数名源码解析拿不到名称时会退化成占位名工具描述和模型生成的参数对不上。解决办法是给参数注解显式命名或者确保编译时保留了参数名。7.3 调用远端工具提示反序列化失败问题可能出在两端 JSON 配置不一致。服务端如果配置了严格模式远端传来的缺少字段直接报错客户端如果做了自定义转换器输出可能不是合法 JSON。排查建议先开启协议层的 DEBUG 日志消息收发都会打印对照报文找到失败的位置再确认两端的 Jackson 配置是否一致最后检查自定义参数类型是否有正确的反序列化器。7.4 协议版本不一致导致连接失败MCP 协议自身有版本演进。当客户端和服务端来自不同版本的依赖时可能出现版本协商失败或字段对不上。排查时先看双方把 MCP 相关依赖锁定到同一版本如果临时无法升级确认协议版本的兼容性说明。源码中版本协商过程发生在连接建立阶段会交换版本号并取交集日志里通常会留下协商结果。7.5 排查速查表现象优先排查方向常见根因工具列表为空客户端发现日志、服务端注册日志配置未开、扫描未命中、能力开关关闭Schema 字段名是占位名编译参数、参数注解未保留参数名、未显式命名调用即报错协议 DEBUG 日志、报文复现JSON 配置不一致、参数不匹配连接失败传输层日志、协议版本依赖版本不一致、地址配置错误并发异常工具方法实现Spring Bean 单例共享状态未做并发控制7.6 一条高效的源码阅读路线如果这篇文章让你有读源码的冲动我推荐一条最快的路线先按第 1 章讲的方式跑通最小 demo确认两个核心对象是怎么创建的然后在工具调用时打断点。打断点位置我建议选在本地 Tool 的调用方法上那里是协议和框架的边界最容易观察数据形态的变化顺着调用栈往回退你会自然看到协议解析、传输、回调执行每一个环节。调试时把 MCP 相关包的日志级别调到 DEBUG会看到完整的 JSON-RPC 报文。这一步对理解协议帮助巨大比读十篇源码讲解都直观。我个人在啃源码过程中的体会是源码阅读真正的产出不是“我看完了某个类”而是建立了一个可迁移的心智模型。读完这部分的 MCP 实现之后再去看其他框架的 MCP 接入思路非常统一先找工具定义从哪来再找调用请求往哪走最后看传输层怎么接。建议你从一个最小 demo 开始把断点打在一次真实的工具调用上花一个下午完整走一遍收获会远超预期。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑