尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring AI 搭建 MCP 客户端实战:协议原理、工具注册与 Qwen 接入

Spring AI 搭建 MCP 客户端实战:协议原理、工具注册与 Qwen 接入 最近技术圈里“Spring AI”和“MCP客户端”这两个词的搜索量涨得厉害尤其是Spring AI连接百炼Qwen这类话题说明Java开发者对MCP的关注已经从“看热闹”进入“要落代码”的阶段。我前几周正好把一个内部订单查询工具接进了Spring AI项目全程自己搭建MCP客户端过程中踩了不少坑也把协议细节、自动配置、工具注册这些理清楚了。这篇文章就把完整的实操路径写出来从为什么选Spring AI做MCP客户端到依赖配置、核心代码、模型接入再到排错经验和从Dify工作流迁移的思路一次性讲透。1. MCP被炒得火热但客户端到底在解决什么问题很多刚接触MCPModel Context Protocol模型上下文协议的人第一反应是“这不就是个API调用框架吗”。还真不是。API是你给模型写好接口模型按固定路径去请求MCP做的是把“模型发现工具、调用工具、拿回结构化结果”这整条链路标准化。工具就好比一个USB设备MCP协议就是这个USB-C接口任何支持该协议的模型、应用、服务端都能互相插拔。1.1 模型再聪明也摸不到你的业务系统LLM本身的强项是理解和生成文本但它碰不到你的订单库、文件系统、数据库、内部API。传统做法是给模型写一堆function calling描述再自己做路由分发每个数据源一套Adapter。工具少还好工具一多就会发现模型侧要维护大量function schema业务侧要维护一堆回调逻辑两边稍不一致就报错。MCP把这件事拆成了两块。MCP服务端Server负责把具体能力暴露出来比如“查询订单详情”“读取本地文件”“操作Git仓库”通过统一协议提供工具列表和工具调用接口。MCP客户端Client负责连接服务端发现工具把工具的schema交给模型等模型决定调用时再转发请求把执行结果返回给模型。我实际体验下来价值不在少写几行代码而在于工具的管理方式变了。以前加一个“查库存”能力要动模型编排代码现在只要起一个新的MCP服务端客户端自动就能发现和注册。1.2 客户端与服务端的分工一次握手、多次调用MCP的核心交互流程并不复杂至少先搞清楚这三个阶段。首先是initialize握手。客户端连上服务端后要发initialize请求带上协议版本和客户端能力描述服务端返回自己的信息和能力。这一步决定两者能不能继续对话。其次是工具发现即tools/list请求客户端从服务端拉取所有可用工具的JSON Schema列表。最后是工具调用即tools/call请求把模型选择调用的工具名和参数传过去服务端执行后返回结构化结果。整个过程走的是JSON-RPC 2.0格式。我刚接触时觉得不就是个RPC嘛但真正去实现才发现那些看似简单的协议细节才是最耗费精力的事情如何维护会话状态、如何处理SSE流式响应、工具Schema怎么转成模型能认的function calling格式、断线重连怎么处理。自己做一遍不是不行但确实繁琐。1.3 为什么选择Spring AI而不是自己去实现JSON-RPC 2.0如果项目里已经用了Spring Boot直接集成Spring AI是顺理成章的选择。Spring AI从1.0 GA开始MCP客户端支持已经进入稳定状态提供了自动配置、starter依赖以及和ChatClient、Agent体系的天然打通。我对比过几条路线。自己基于WebClient或RestClient实现JSON-RPC 2.0优点是看得见细节缺点是初始化握手、SSE解析、协议版本兼容、工具Schema转换这些全要自己处理而且MCP版本还在演进维护成本高。直接用官方TypeScript SDK或Python SDK也不现实毕竟Java团队要的是JVM技术栈不想再引入一套异构运行时。用Spring AI MCP模块它能复用Spring Boot的配置体系、AOP、测试生态而且和ChatClient已经集成好了这是最自然的一条路。从这个角度看Spring AI出现之前的核心痛点其实不是“写不出MCP客户端”而是“写出的客户端怎么跟现有Spring服务无缝对接”。Spring AI解决的正是这后半句。2. 工程准备从依赖到传输方式先弄懂再写码我见过很多朋友上来就复制依赖、写配置结果要么版本冲突要么跑半天不知道MCP客户端怎么起作用的。先花十分钟把依赖和传输方式理清楚远比急着写代码有价值。2.1 版本与依赖清单含踩坑版的starter选择首先确认基础版本。Spring AI对Spring Boot版本有要求比较稳妥的组合是Spring Boot 3.4配合Spring AI 1.0 GA之后的版本。如果你打算用2.0.x的新版本核心的MCP客户端用法并不会大变但要以官方发布的BOM为准。在pom.xml里引入BOM和核心依赖dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后是MCP客户端依赖dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency注意这里有个版本差异。Spring AI 1.0正式版之前你可能搜到的是spring-ai-mcp-client这种非starter命名。1.0之后官方统一为spring-ai-starter-mcp-client这个starter会帮你拉入自动配置省去手写一堆Bean配置。接下来按你的模型服务商选一个通道依赖。比如接阿里云百炼的Qwen可以加dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-dashscope/artifactId /dependency如果你接的是OpenAI、Ollama或其他服务商对应换成各自的starter就行MCP客户端部分是完全一致的这也是协议标准化的好处。2.2 传输方式决定你的部署形态HTTPSSE与STDIOMCP客户端连接服务端有两种主流传输方式必须在配置前选好因为这直接决定你的服务部署形态。HTTPSSEServer-Sent Events服务端推送事件方式适合远程服务端。MCP服务跑在独立容器里客户端通过HTTP URL访问服务端通过SSE单向推送事件客户端再通过POST回传请求。这种方式跨机器、跨网络适合微服务架构下的智能服务中台。STDIO方式适合本地子进程。客户端直接拉起一个MCP服务端的进程通过标准输入输出与子进程通信。这种方式最简单适合本地文件读取、命令行工具等场景。我测试文件系统MCP服务时就是直接本地起的子进程一行npx命令拉起来就能连。这两种方式没有谁绝对更好。如果你的MCP服务端和业务进程部署在同一宿主上STDIO更省心如果服务端是独立团队维护的远程服务无论是跨容器还是跨机房HTTPSSE是唯一选择。Spring AI的McpClient抽象层把两者都封装好了切换只需改配置和Transport构造方式业务代码基本不用动。2.3 最小配置application.yml里的关键参数选HTTPSSE的话application.yml里的最小配置大概是这样的spring: ai: mcp: client: name: my-mcp-client version: 1.0.0 type: SYNC request-timeout: 30s上面的name和version是客户端在initialize握手时上报给服务端的标识type决定是创建同步客户端还是响应式客户端request-timeout控制单次MCP调用的超时时间。但每个MCP服务端的连接信息配置方式在不同Spring AI版本里略有差异而且比较琐碎。我的建议是先把一个单独的MCP服务端用代码方式手动配通再回头整理成yml配置。手动方式更容易看清每一步在做什么也方便排查问题。3. 核心实现注册MCP客户端把工具变成Spring Bean这一章是整篇文章的重头戏。很多教程只讲“配置一下就能用”但没说MCP客户端具体是怎么注册、工具是怎么被发现、又是怎么被模型调用的。我把整个过程拆开来讲。3.1 基于配置文件的McpClient自动配置如果你用的是spring-ai-starter-mcp-clientSpring Boot启动时会自动扫描并创建MCP客户端。默认的自动配置会读取spring.ai.mcp.client前缀下的属性创建McpSyncClient或McpReactiveClient。最简单的验证方式是直接在测试类里注入试试SpringBootTest class McpClientApplicationTests { Autowired McpSyncClient mcpSyncClient; Test void contextLoads() { assertNotNull(mcpSyncClient); } }能注入成功说明自动配置生效了。但我得提醒一句自动配置只会帮你创建MCP“客户端框架”它默认不知道你要连哪个MCP服务端。连接哪个服务端、用什么传输这部分的绑定在不同版本里做法不同所以别指望零配置就能跑起来。最快路径是下面这种手动构建方式。3.2 手动构建McpClient的代码视角要“看得见摸得着”我推荐先手动构建一次流程会更直观。下面以HTTPSSE方式为例。McpClient.Builder builder McpClient.builder(McpClient.SyncSpec.class) .name(my-mcp-client) .version(1.0.0) .serverInfo(new ServerInfo(my-mcp-server, 1.0.0)) .transport(HttpMcpTransport.builder() .baseUrl(http://localhost:8080/mcp) .sseEndpoint(/sse) .build()); McpSyncClient mcpSyncClient builder.build().sync();如果是STDIO方式比如连接本地文件系统服务端代码变为StdioMcpTransport transport StdioMcpTransport.builder() .command(npx) .args(-y, modelcontextprotocol/server-filesystem, /tmp) .build(); McpSyncClient mcpSyncClient McpClient.builder(McpClient.SyncSpec.class) .name(my-stdio-client) .build() .sync(transport);这里有个值得特别注意的点serverInfo是客户端向服务端自报家门的信息有些服务端会用它来做权限校验或日志追踪。很多教程把这个漏了结果服务端收到空客户端信息直接拒绝握手排查半天才发现。从Spring AI 1.0之后手动构建的API有些微调不同版本在类名和构建器组织方式上会有差异我上面这种写法是当前版本比较通用的形式。实际开发时以你引入的Spring AI版本对应的API为准核心思路不变。3.3 工具如何被Agent发现McpToolProvider和ToolCallback手动获取了McpSyncClient之后下一个关键问题是MCP客户端怎么把工具暴露给模型Spring AI在这里引入了一个中间层叫ToolProvider。它统一了各种工具来源既可以是普通Java方法也可以是MCP工具。通过McpToolProvider把MCP客户端“变成”一个工具提供者McpToolProvider toolProvider McpToolProvider.builder() .client(mcpSyncClient) .build();然后在构造ChatClient时把这些工具挂进去ChatClient chatClient ChatClient.builder(chatModel) .defaultTools(toolProvider) .build();这一步完成后模型在回答问题时就能自动看到MCP工具列表并根据用户问题判断是否调用。整个发现链路的本质是MCP客户端把服务端的tools/list结果转换成了模型可理解的function calling工具定义模型决定调用时再把参数通过tools/call转发给服务端。我自己理解这个机制时把它类比为“代理商模式”。MCP服务端是真正的工具提供方模型是最终用户ChatClient是面向用户的前台McpToolProvider是背后的经纪人。经纪人把工具信息整理成模型看得懂的简历模型选中后经纪人负责去后台干实际活。3.4 同步与响应式两套API的区别和取舍Spring AI的MCP客户端同时支持同步McpSyncClient和响应式McpReactiveClient两套API。同步API的调用方式是阻塞的函数调用、后续模型生成都要等MCP服务端返回结果响应式API基于Project Reactor整个调用链是异步非阻塞的。两者选哪套我建议直接看应用场景。维度同步API响应式API编程模型直观、易调试需要理解流式操作并发能力受线程池限制非阻塞适合高并发WebFlux集成需要额外适配天然契合推荐场景内部管理端、并发低对外API、长连接、高QPS对于一开始想要快速验证开发流程的团队同步API是最稳选择。先把同步链路跑通再考虑高并发场景下的响应式改造。我最初直接用同步API开发虽然遇到过一次线程池打满的问题但整体调试体验非常友好。如果你需要并发调用多个MCP工具响应式API的优势会很明显——多个工具可以并行执行而同步API只能逐个等待。所以这一步不要为了“酷炫”直接上响应式而是根据调用频率和并发量来决定。4. 接上百炼与大模型让Qwen真正调用你注册的工具MCP客户端建好了工具也注册了接下来要解决的是模型接入。这里我以阿里云百炼平台的Qwen模型为例因为它对中文场景友好配置也简单而且spring-ai-alibaba在国内落地比较多。4.1 模型通道配置把DashScope接入Spring AI首先确保依赖里已经有spring-ai-starter-model-dashscope。然后在application.yml里配置API Key和默认模型spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plusDASHSCOPE_API_KEY放环境变量里别写死在配置文件里。如果你用的是Qwen 3.7或其他新模型把model字段换成对应模型名称即可。模型通道和MCP客户端这两部分是相互独立的前者负责“模型怎么理解问题”后者负责“模型怎么调用工具”。4.2 ChatClient结合MCP工具的一次完整调用链路接入完成后最直观的测试就是让Qwen回答一个必须调用MCP工具才能解决的问题。假设我有一个order-serviceMCP服务端提供了getOrderById这个工具那么用户问“订单2024001现在是什么状态”时调用链是这样的用户问题进入ChatClient。ChatClient把问题和MCP工具列表发给Qwen。Qwen判断需要调用getOrderById在回复中生成一个function call请求。Spring AI通过McpToolProvider将function call转成MCP的tools/call请求发给order-service。order-service执行后返回订单状态JSON。ChatClient把结构化JSON交回给QwenQwen组织成自然语言返回给我。对应代码就是RestController public class OrderController { private final ChatClient chatClient; public OrderController(ChatClient.Builder builder, McpToolProvider toolProvider) { this.chatClient builder.defaultTools(toolProvider).build(); } GetMapping(/order/status) public String queryOrder(RequestParam String orderId) { return chatClient.prompt() .user(帮我查一下订单 orderId 的状态并结合查询结果用一句话告诉我) .call() .content(); } }这里要注意一个很隐蔽的点chatClient.prompt()里可以直接加工具实例。如果使用defaultTools(toolProvider)这种全局注册方式所有对话都会携带所有工具工具多了会让模型犯选择困难症。更好的做法是使用局部工具注册chatClient.prompt() .tools(toolProvider) .user(...) .call() .content();局部注册意味着只有这一次对话能用到MCP工具其他对话不会携带这些噪音。工具数量大时这个区别直接影响响应准确率和token消耗。4.3 多工具协作时如何控制工具选择范围当MCP服务端注册的工具越来越多模型的选择压力会指数级上升。工具越多模型在function calling阶段越容易选错工具或漏选参数。解决思路有两个。在McpToolProvider构建时指定工具白名单只暴露当前场景需要的工具McpToolProvider.builder() .client(mcpSyncClient) .toolNames(order-service:getOrderById, order-service:getOrderList) .build();在ChatClient层面按业务场景拆分多个ChatClient实例不同的场景带不同的ToolProvider组合。比如售后场景只暴露订单查询工具库存场景只暴露库存变更工具。第一种方式更轻量适合不多不少的几个工具第二种更工程化适合中大型应用。结合我自己的项目经验建议优先把工具按业务域拆分再用白名单二次收窄效果最稳。5. 我踩过的坑与调试方法从连接失败到协议不匹配这一章是我最想写的部分。网上关于Spring AI MCP客户端的教程不少但真正写到“哪里容易挂”的很少。我把这段时间踩过的坑按排查链路整理出来。5.1 服务端路径与命名空间最容易忽略的400与404第一次连接HTTP MCP服务端时我连续碰到了400和404。400的原因是协议版本不一致我本地用了新版本客户端对方服务端还是旧版本协议。404最冤我把baseUrl写成了http://localhost:8080但MCP服务端实际挂载在http://localhost:8080/mcp路径下。排查顺序建议这样。先用curl确认服务端端口通不通、端点对不对curl -i http://localhost:8080/mcp如果返回404说明路径写错了。通的话再发协议握手请求curl -N -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -H Accept: text/event-stream \ -d {jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2025-06-18,capabilities:{},clientInfo:{name:curl-test,version:1.0.0}}}注意MCP规范还在演进不同版本的endpoint和header要求略有区别curl方式主要用来验证路径和连通性协议细节仍以服务端要求的规范为准。这一步能帮你把问题快速定位到“网络/路径问题”还是“协议问题”。5.2 工具Schema格式对不上模型要求时该怎么定位第二种高频问题是模型说“工具不存在”或者“参数格式不合法”。这种情况通常不是MCP客户端出了问题而是工具的JSON Schema转换后和模型侧function calling的预期不一致。排查方法把注册进ChatClient的工具定义原样打印出来看一眼。可以通过调试McpToolProvider构建出来的ToolCallback或者在启动日志里打开对应调试级别。常见的坑有三个。工具描述写得太虚比如“查询订单”四个字模型根本不知道参数含义。参数格式使用了复杂的嵌套object模型生成嵌套参数时容易漏字段。必填字段没标注模型会选择性忽略。解决方案只有一个把工具描述写得像给人看的说明书一样具体。比如“查询订单详情需要订单ID格式为纯数字字符串会返回订单当前状态、金额、物流信息”。模型看到这种描述function calling的准确率会明显提升。5.3 流式传输下的超时与断连处理HTTPSSE模式下连接是长连接中间任何一层的代理都可能导致SSE事件被缓存或隔断。我在一个内网网关后面部署MCP服务端时客户端反复出现建立连接后卡死、接收不到工具列表的情况最后定位到是网关把SSE流当成普通响应做了缓冲。处理办法是把MCP客户端连接的内网超时参数调短让连接尽快失败而不是无限等待spring: ai: mcp: client: request-timeout: 20s另外如果MCP服务端需要轮询式拉取即每次走POST请求触发执行再通过SSE推送结果要确认客户端使用的HTTP客户端支持无缓冲读取流式响应。Spring AI默认的客户端在标准环境通常没问题但一旦经过自定义网关或代理就要重点排查。5.4 用日志和单独验证来隔离客户端与服务端问题遇到MCP调用异常时第一反应不该是去翻业务代码而是要区分“客户端问题”还是“服务端问题”。我的做法分三步。第一步先把服务端当黑盒验证用curl甚至MCP Inspector这类工具直接连服务端确认服务端本身正常。第二步打开客户端调试日志重点看initialize和tools/list两个请求的往返。第三步把模型层去掉直接写一个简单的测试方法调用MCP客户端查询工具列表绕开模型环节只看客户端与服务端的交互。如果能查到工具列表说明协议链路没问题问题很可能在模型function calling阶段如果查不到问题就在客户端或协议层。这个隔离思路能帮你省下大量排查时间。6. 从Dify工作流迁移到Spring AI代码的现实路径最近很多人搜索“dify工作流转成spring ai java代码”这说明大家不想一直停留在可视化编排里而是希望把工作流能力沉淀成Java代码进入自己的应用体系。Dify这类低代码工作流工具确实能快速搭建原型但一旦涉及私有化部署、深度定制、Java生态集成代码化迁移就是绕不开的事。6.1 Dify的节点与Spring AI组件的映射关系Dify的工作流本质上是一系列节点的编排。LLM节点、工具节点、知识检索节点、HTTP请求节点、条件分支节点这些在Spring AI和周边生态里都能找到对应能力。Dify节点Spring AI对应方案LLM节点ChatClient PromptTemplate工具节点ToolCallback / MCP客户端工具知识检索节点VectorStore 向量模型HTTP请求节点RestClient/WebClient或封装成工具条件分支节点Java代码逻辑或Spring AI的Agent分支变量聚合节点ChatMemory或上下文组装这不是一比一的代码翻译而是“节点的意图”转成“代码的能力”。Dify里拖一个LLM节点只是把Prompt模板和模型配置捆绑在一起在Spring AI里就是ChatClient.prompt()加上对应模板和模型实例。6.2 迁移时的最小改造清单如果想把一个Dify工作流搬进Spring AI我建议先跑一个最小改造路径。第一步罗列Dify工作流里的所有节点和连线画出数据流向。第二步把每个节点的能力分类哪些是纯提示词编排用ChatClient和PromptTemplate哪些需要业务工具用MCP或ToolCallback封装哪些是外部API的调用考虑封装成MCP服务端或普通RestClient。第三步重写流程控制逻辑。Dify的条件分支相当于Java代码里的if-else循环节点相当于for循环大多数工作流迁移过程最费时间的其实不是代码编写而是把这些隐式逻辑从可视化界面里“翻译”出来。第四步把原来Dify里的测试用例改成Spring Boot集成测试用JUnit跑一遍完整链路。搞完这四步工作流就彻底从Dify中脱离变成可测试、可版本化的Java代码了。6.3 关于Spring AI Alibaba生态的一点现状观察顺带说一下热词里的“spring ai alibaba 停更了吗”。我目前实际看到的现状是Spring AI Alibaba仍在和Spring AI主干版本保持同步官方仓库的活跃度需要你自己去核对release页面为准。我的判断是Spring AI Alibaba的主要意义在于让国内开发者更方便地接入百炼和Qwen系列模型MCP客户端部分的底层协议逻辑与Spring AI主干完全一致。如果你实在担心生态风险最稳妥的方式是直接基于Spring AI主干做MCP客户端再把模型通道接上百炼。这部分互通性很强不会绑定在某个分支上。我在实际项目里的做法是基础协议和MCP客户端完全依赖Spring AI主干模型接入使用DashScope starter这样即使将来某个分支策略变化更换模型通道的代价也非常小。另外再多说一句关于MCP工具治理的经验。把MCP客户端配好只是开始工具多了以后一定要建立命名规范。我在项目里约定所有工具名必须是“服务名:工具名”的格式比如order-service:getOrderById。这样在监控里能看到是哪个服务提供的工具在ToolProvider白名单阶段也能按服务前缀快速筛选。最后分享一个小技巧在开发和联调阶段不要直接在正式ChatClient里配MCP工具先建一个独立的“调试用ChatClient”单独连一个MCP服务端手动调用几次工具。等确认协议链路没问题再把它并进主流程。这能让问题边界清晰很多也避免模型介入后让排错变得复杂。MCP本身是个好协议但所有的坑几乎都出在集成边界上。把边界理清了客户端开发就会顺很多。
返回列表