ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring AI MCP实战:从原理到源码,构建安全高效的AI工具调用体系

Spring AI MCP实战:从原理到源码,构建安全高效的AI工具调用体系 干这一行最怕的就是信息差。早几年做大模型应用最折磨人的不是模型能力不够而是怎么把各种外部系统“安全、高效、规范“地接进来——写插件、调API、做鉴权、处理数据结构。每接一个工具都得从零开始写一套胶水代码还得祈祷对方接口别乱变。直到MCPModel 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 groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency !-- 智谱AI -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-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还是WebFluxSpring AI官方对MCP Server提供了两种技术支持WebMvc同步servlet栈和WebFlux响应式栈。如果你要跟Spring Boot的传统web项目共存优先选WebMvc如果你整个应用已经是响应式的或者追求高并发IO就选WebFlux。两个依赖长这样!-- WebMvc 方式 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webmvc/artifactId /dependency !-- WebFlux 方式 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webflux/artifactId /dependency我自己的建议是新手选WebMvc。为什么因为出了问题是真好看日志同步栈的报错链路清晰很多。依赖配好直接在配置类里声明一个工具方法它就是MCP Server暴露给外部的能力Configuration public class McpServerConfig { Bean Tool public FunctionMapString, Object, String getServerTime() { return args - { LocalDateTime now LocalDateTime.now(); return Server current time: now; }; } Bean Tool(description 读取服务器文件系统指定路径的文件内容) public FunctionFileReadRequest, 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和一个WebMvcAutoConfigurationSpring Boot会自动配置一个WebMvcMcpServer把工具通过/mcp端点暴露出去。启动项目后你可以直接请求http://localhost:8080/mcp看返回值如果正常会返回MCP协议要求的JSON-RPC错误格式因为还没走握手流程这反而说明Server已经起来了。2.3 客户端依赖配置如果你的Spring Boot应用要向这个MCP Server发请求、让模型调用它的工具那还需要在客户端依赖dependency groupIdorg.springframework.ai/groupId artifactIdspring-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 FunctionMapString, 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 FunctionFilePathRequest, 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 { ListString 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, ObjectProviderMcpAsyncClientCustomizer 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不再用早期版本的HTTPSSE模型。传输层的构建全部隐藏在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。这个推断逻辑很有讲究方法名 -nameTool注解的description-description参数类型 -properties里的type映射String-stringInteger-integerList-arrayrecord-objectrequired字段根据参数是否带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 SchemaWebFlux线程卡死在事件循环里用了同步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集成层的问题——这条排查路径能帮你省掉大量无效时间。
返回列表