如果你已经在 Spring AI 里用Tool写过工具这篇讲的是下一步怎么把这些工具从应用进程里搬出去变成一个任何 AI 客户端都能远程调用的 MCP Server再反过来让 AI 应用以纯 Client 的身份把它接回来。文中的所有结论都来自一次真实搭建包括踩过的坑和最后的验证数据。示例代码基于 Spring AI 1.x MCP Java SDK 0.17.0。一、MCP 到底解决了什么问题一句话MCP 是 AI 世界的 USB 接口。它把焊死在应用里的工具变成即插即用的网络服务。以前用Tool写工具方法跟主进程一起编译部署。换一个 AI 应用工具就得重写一遍想给 Claude Desktop 或 Cursor 用更是无从谈起。现在把工具包成独立进程MCP Server任何 MCP 客户端都能远程调用。模型那一侧完全无感它看到的还是同一份工具说明书。协议层只需要记住三件事三大原语Tools模型可调用的函数、Resources模型可读取的数据、Prompts预置提示词模板。日常用得最多的是 Tools。通信本质JSON-RPC 2.0一次典型交互就是initialize→tools/list→tools/call。两种传输SSE2024-11-05 协议和 Streamable HTTP2025-03-26 协议。新项目直接上后者。两种传输的区别值得单独说因为这是最容易踩坑的地方SSE旧Streamable HTTP新通道两条 TCPGET /sse 收POST /mcp/message 发单通道所有请求 POST /mcp响应从 SSE 流里回来直接在 POST 的响应 body 里会话sessionId 挂在 URL 参数上断线即作废Mcp-Session-Id 放响应头扩展性服务端要维护 session 表可无状态扩展测试要开两个终端一条 curl 就够我们最初用 SSE撞上了 0.17.0 Java SDK 的 Reactor 管道竞态响应数据已经完整到达SDK 处理时却报Failed to send message。切到protocolSTREAMABLE之后问题彻底消失——单通道没有 SSE 流转发管道那个坑就不存在了。二、搭建 MCP ServerStep 1加依赖dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webmvc/artifactId !-- 版本由 spring-ai-bom 管理 -- /dependencyStep 2写工具类这一步没有任何新东西和普通的Tool写法完全一样Component public class TimeTools { Tool(description 获取当前的完整日期时间格式 yyyy-MM-dd HH:mm:ss) public String now() { ... } Tool(description 查询A股股票的实时行情...) public String quote(ToolParam(description 股票代码如 sz300890) String code) { ... } }关键认知工具类零改动。MCP 化的是注册和调用方式不是工具本身。Step 3注册成 MCP 工具这是整个 Server 侧唯一的核心配置Bean public ToolCallbackProvider studyTools(TimeTools timeTools, StockTools stockTools) { return MethodToolCallbackProvider.builder() .toolObjects(timeTools, stockTools) .build(); }MethodToolCallbackProvider扫描Tool方法生成 inputSchema暴露成 MCP 协议端点。Step 4配置 application.propertiesspring.ai.mcp.server.namestudy-mcp-server spring.ai.mcp.server.version1.0.0 spring.ai.mcp.server.instructions提供时间查询/日期推算/A股行情工具 spring.ai.mcp.server.tool-change-notificationtrue # 传输模式SSE(默认) 或 STREAMABLE(推荐) spring.ai.mcp.server.protocolSTREAMABLE两个注意点protocolSTREAMABLE会停用/sse端点两者互斥instructions写中文如果出现乱码是 properties 文件按 Latin-1 读入导致的改用 yml 或在 IDEA 里设成 UTF-8 即可。一个容易困惑的细节Streamable 模式下响应的 Content-Type 仍然是text/event-streambody 用 SSE 格式包装。这是为流式进度通知预留的通道客户端 SDK 会自动处理不用管。三、MCP Client 的三种写法三种都实测过按从底层到上层的顺序说。这里只是用java编写调用mcp服务的代码便于理解mcp服务的调用过程写法一裸协议手写约 60 行SSE 读线程 BlockingQueue 中转 手拼 JSON-RPC 字符串POST 发出请求(只发不收) → SSE 线程收到响应 → 按 jsonrpc id 匹配 → offer 进队列 → 主线程 poll 取走这段代码的价值不在于用而在于看穿 SDK 封装下真实发生了什么。所有 MCP 客户端内部干的都是这件事。写法二官方 SDK推荐HttpClientStreamableHttpTransport transport HttpClientStreamableHttpTransport.builder(http://localhost:8848).build(); McpSyncClient client McpClient.sync(transport) .requestTimeout(Duration.ofSeconds(10)) .build(); client.initialize(); // 握手 ListToolsResult tools client.listTools(); // 工具清单 CallToolResult r client.callTool( new CallToolRequest(quote, Map.of(code, sz300890))); // 调用 String text ((TextContent) r.content().get(0)).text(); // 取文本 client.close();SDK 0.17.0 的几个 API 备忘builder(baseUrl)带参构建取文本要强转 TextContent没有asText()这种语法糖CallToolRequest直接 new参数是 name 加 arguments Map。写法三Spring 声明式生产级spring: ai: mcp: client: enabled: true toolcallback: enabled: true # 远程工具自动包装成 ToolCallback streamable-http: # 注意传输模式对应配置节 connections: study-server: url: http://localhost:8848/mcp注入ListToolCallback挂给 ChatClient远程工具用起来和本地Tool一模一样。四、Tool 和 MCP Tool 的真正区别这是这次学习最值得说清楚的一点。模型视角完全无感模型收到的都是工具说明书name description inputSchema它只负责决定调什么。模型根本不知道工具代码在哪执行。真正的区别在调用路径模型响应两者完全相同 {name:now,arguments:{}} │ Spring AI 解析工具调用意图 │ ┌─────────┴─────────┐ ▼ ▼ 本地 ToolCallback 远程 ToolCallback(MCP) │ │ Method.invoke() HTTP POST /mcp 反射进程内纳秒级 响应回来后server端才反射 毫秒级但工具调用对延迟不敏感精确表述MCP 不是替换了反射是在反射外面包了一层 HTTP 壳。Server 端的TimeTools.now()还是被Method.invoke()反射调用的那个方法一点没变。变的只是请求怎么到达这个方法。工程维度的对比维度本地 ToolMCP 工具部署跟主应用同进程、同发布周期独立进程独立升级消费者只有主应用任何 MCP 客户端通信方法调用HTTP JSON-RPC修改工具主应用重新编译部署server 重启即可客户端无感类比自己家厨房做饭中央厨房外卖用熟悉的架构类比本地Tool相当于进程内直接new ServiceImpl().method()MCP 工具相当于 Feign/RPC 调远端服务。这和微服务拆分是同一个演进逻辑——为了解耦部署付出一次网络开销。五、架构拆分与 AI 闭环5.1 从同进程拆成两个项目最初的形态是工具和 AI 应用在同一个进程里后来拆成了这样┌──────────────────────────────┐ ┌──────────────────────────────┐ │ spring-ai-alibaba-study:8848 │ │ stock-mcp-server:8090 │ │ (AI应用 纯 client) │ HTTP │ (纯工具服务 server) │ │ │─────▶│ │ │ · ChatClient DashScope │ POST │ · TimeTools (4个) │ │ · ReactAgent 框架 │ /mcp │ · StockTools.quote │ │ · MCP Client (手写配置) │ │ · 独立进程/可独立部署 │ └──────────────────────────────┘ └──────────────────────────────┘5.2 验证过的完整链路查一下翔丰华的实时行情 ↓ agent ReactAgent 收到提问 模型看到5个远程工具说明书 → 决定调 quote(codesz300890) ↓ Spring AI 识别为MCP工具 → 转成 JSON-RPC HTTP POST http://localhost:8090/mcp 调用mcp服务 ↓ mcp服务 收到 tools/call MethodToolCallbackProvider 反射执行 StockTools.quote() → 调腾讯行情API ↓ 结果回流 模型二次加工 → 现价26.59元 2.03%实测结果翔丰华 26.59 元涨 2.03%问今天几号30 天后是几号返回 2026-11-04 和 2026-12-04。5.3 三个坑坑一注入ListToolCallback拿到空列表静默降级。自动配置注册的 Bean 是SyncMcpToolCallbackProviderProvider 模式不是散装的 ToolCallback。而 Spring 对不存在的 Bean 做集合注入时返回空列表而不报错。后果很严重模型没有工具可用于是自信地编数据实测编出过 28.45 元和2024 年 6 月 28 日。教训是 AI 应用里工具挂载失败是静默的必须打印工具清单自检。坑二ReactAgent.Builder 的方法名。挂ToolCallback[]用.tools(...)挂 Provider 用.toolCallbackProviders(...)。两者等价后者更优——本地MethodToolCallbackProvider和远程SyncMcpToolCallbackProvider是同一个接口统一抽象能真正落地。坑三启动时 starter 连接远端失败会阻断应用启动。initialize 握手在启动期完成好处是连接不通能早暴露代价是必须保证 server 先起。5.4 手写 McpClientProviderConfigSyncMcpToolCallbackProvider spring-start会把这个bean装配好构建Agent的时候可以直接挂载但是我们可能需要同时连接多个MCP服务那就把McpToolCallbackAutoConfiguration干的事自己抄一遍分三步// 第1步 管道一个传输 一条通往远端的通道 HttpClientStreamableHttpTransport transport HttpClientStreamableHttpTransport.builder(http://localhost:8090) .endpoint(/mcp).build(); // 第2步 会话客户端 initialize 握手 McpSyncClient client McpClient.sync(transport) // 这里是定义当前的调用者因为mcp服务可能有很多调用者给自己一个标识让mcp记录你的调用 .clientInfo(new McpSchema.Implementation(my-app, 1.0.0)) .requestTimeout(Duration.ofSeconds(30)) .build(); client.initialize(); // 协商协议版本、交换能力清单 // 第3步 汇总N个服务的工具聚成一个 Provider new SyncMcpToolCallbackProvider(client1, client2, ...);多服务接入可以定义一个service列表循环创建client使用client集合创建provider三个值得注意的点手动Bean优先级更高starter 的ConditionalOnMissingBean会让位McpSyncClient是 AutoCloseable配置类实现 DisposableBean 统一归还连接Provider 还实现了ApplicationListenerMcpToolsChangedEvent配合 server 侧的tool-change-notificationtrue远端工具增删时 client 能动态感知。5.5 两种接入方式怎么选声明式properties 三行手写配置类写法spring.ai.mcp.client.streamable-http.connections.xx.url...McpClientProviderConfig适合快速接入、标准场景多服务管理、定制超时/鉴权/重连原理starter 自动配置代你执行手写版的三步曲自己控制每一步BeanSyncMcpToolCallbackProviderSyncMcpToolCallbackProvider六、MCP Client 的断开重连背景mcpService跟client有连接那么当mcp-service上的工具更新了client如何实时感知mcpservice-client 的断开重连SyncMcpToolCallbackProvider 提供了invalidateCache标志位当标志位true的时候下次请求Agent获取工具时Provider会去mcp-service重新加载工具列表核心认知client 端 invalidateCache 只是标脏重连发生在两跳之后。invalidateCache() → 纯内存操作invalidateCache trueO(1)零网络 ↓ 代码识别标志位 → getToolCallbacks() 里的 if (invalidateCache) 分支 ↓ 惰性只在下次有人调用时走进来 拉取工具 → client.listTools() 发 POST ↓ 发现连接已断 → transport 层 re-initialize → 新 session → 拉取成功三层各司其职标志位管要不要刷新事件回调、定时器都只做这一件事getToolCallbacks/listTools管怎么刷新加锁防并发重复拉transport 的 re-initialize 管刷新路上连接断了怎么办对上层透明SDK 自愈的精确边界字节码验证修正SDK 不会重连的粗说法通道断后行为机制POST 业务通道tools/call、listTools✅ 自动恢复HttpClientStreamableHttpTransport.sendMessage内部session 失效404→ reconnectlambda$reconnect系列方法→ 重新 initialize → 新 session id → 原请求继续常开 GET 流通知通道❌ 死了不复活无任何下次请求来触发它的 reconnect——它为通知而生没有主动使用者两个推论惰性设计的隐含特性如果没人调用定时器跑 100 次也不会重连——重连被推迟到有人要用那一刻省资源没人调用就不值得维护连接。通知通道永远救不活的原因它是唯一没有被动触发者的组件。想让它复活需主动心跳client.listTools()兼做心跳比ping()好——不只验证连通顺手把工具清单也刷了一石二鸟。server 侧重启的结构性失效协议层无解通知 对当前 TCP 连接的推送电话振铃模式没有留言信箱模式server 重启 旧连接全灭 新进程 session 表为空 →notifyToolsListChanged()无人可发MCP 通知是 fire-and-forget无 offset、无 backlog、无 ack、无离线补发协议自己留的活口tools/list带nextCursor分页——工具清单会变client 应重新拉拉取永远可靠推拉结合的架构模式和 Nacos/Apollo 同构第一层拉取保命绳client 主动 listTools —— 独立 POST任何场景都可靠 第二层通知加速器list_changed 推送 —— 只在server 活着 连接活着时有效 第三层兜底自动拉Scheduled invalidateCache —— 把第一层变成自动的业界同款Nacos 长轮询推送 客户端全量拉取兜底MQ push 消费 offset 重放Redis PSYNC 全量 RDB。附版本兼容矩阵classpath 实况 mcp-core 0.14.0 ← spring-ai-alibaba-agent-framework 传递依赖老API mcp-core 0.17.0 ← server starter 传递依赖 mcp-spring-webmvc 0.17.0 解决pom 显式声明 io.modelcontextprotocol.sdk:mcp:0.17.0 统一版本 坑0.17.0 client SSE 传输 → Reactor 竞态Streamable 传输 → 正常多版本共存这件事本身不报错但会让行为变得难以预测。遇到明明代码一样、表现却不同的情况先mvn dependency:tree看一眼 mcp-core 到底加载了哪个版本。
企业数字化 ERP 产品动态
相关推荐
WebSocket基本知识(附带@ServerEndpoint注解) /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 7:51:32
学习笔记(策略梯度) 前文所述方法都是基于value进行评估,现在我们转向policy,并有值近似转向策略近似。Basic idea of policy gradient在先前,策略是用表格来表示的,我们可以直观地取出或改变表格的值。现在,策略可以用参数方程表示&#… · 2026/11/6 7:51:32
诚迈科技萤火Pi AI开发套件,基于高通跃龙IQ系列,原型到量产一步到位! 边缘AI全面爆发,具身智能机器人、工业视觉质检、智能穿戴……AI正从云端快速下沉到设备终端。但算力有了,怎么快速变成能量产的产品?硬件集成、BSP适配、系统裁剪、驱动开发、成像调优、模型部署,这一整套工程链耗时耗人。诚迈科技… · 2026/11/6 7:51:32
基于CDC的MySQL实时数据同步方案实践 1. 项目背景与核心价值 在数据驱动的业务场景中,数据库表间数据同步是个高频需求。我最近在电商订单系统中就遇到了这个问题:订单主表数据变更后,需要实时同步到统计分析表和风控表。传统的手动同步或定时任务方式存在延迟高、易出错的问题&a… · 2026/11/6 14:00:34
JSP农贸交易系统开发与智能撮合算法实践 ## 1. 项目概述与核心价值农贸产品交易系统是连接农产品生产端与消费端的重要数字化桥梁。这个基于JSP技术的交易平台(项目编号g3608)完整包含了程序源码、数据库设计、部署指南和开发环境配置,是一套开箱即用的农产品B2B解决方案。我在参与某… · 2026/11/6 14:00:34
2025年3D建模工具选型实战指南:按工作流断点匹配生产力 1. 这不是选软件,是选你未来三年的“数字手”——为什么2025年建模工具选择比以往任何时候都更关键 2025年做3D建模,你面对的早已不是“用哪个软件画个模型”这么简单的事。它直接决定你能不能接单、能不能按时交付、能不能把想法一秒变现实、甚至决定你… · 2026/11/6 14:00:34
Qt飞机大战源码解析:QGraphicsView游戏循环与碰撞检测实战 简介:这份资源是面向Qt初学者与游戏开发入门者的「飞机大战」完整项目源码包,围绕Qt框架在经典空战游戏中的实际应用展开,可用于课程设计、自学练手或二次开发参考。压缩包共223个文件,约6.21MB,以105个C头文件与32个c… · 2026/11/6 14:00:34
老旧CMS的XSS与getshell风险防护及应急治理 项目中遇到过不少次类似的场面:群里忽然冒出一条消息,标题写得耸人听闻,核心关键词是 XSS 、 0day 、 通杀所有版本 ,还带着一个"可 getshell"的尾巴,配图往往是半个命令行窗口。消息传得飞快… · 2026/11/6 13:56:30
BOM多视图与版本基线:NX装配映射、大树性能及状态权限实践 BOM 这两个字,在研发产品设计管理平台这个圈子里出现频率极高,但真正把它做扎实的项目并不多。我参与过的这套基于 NX 的平台,第一版已经把设计 BOM 从三维装配里抽了出来,能按树展开、能导出表格、能看图号,当时团队觉… · 2026/11/6 13:56:30
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 10:29:23
三菱PLC数据采集到数据库的时序设计:Python+MC协议实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11
ASIC主模式RTL设计:状态机与三态驱动的流片级实现 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11
STM32嵌入式C++开发:GDB调试与Renode仿真实战指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 10:29:23