ARTICLE DETAIL

资讯详情

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

MCP服务开发和进阶:用Spring AI与ToolCallbackProvider打通stdio与SSE

MCP服务开发和进阶:用Spring AI与ToolCallbackProvider打通stdio与SSE 1. 从 stdio 到 SSEMCP 服务为什么需要两种传输方式MCPModel Context Protocol这两年被讨论得很多但真正落到代码里绕不开一个基础问题你的工具服务到底跑在哪、谁来调用它。Spring AI 提供了spring-ai-starter-mcp-server-webmvc这个 starter让 Java 开发者可以用注解的方式把普通方法暴露成 MCP 工具。但很多人第一次写的时候会卡在传输层stdio 和 SSE 到底怎么选、配置怎么写、调用端怎么接。stdio 的本质是标准输入输出流。被调用端不监听任何端口调用端通过启动一个子进程、往它的 stdin 写 JSON-RPC、从 stdout 读结果来完成通信。它的优点是零网络配置、进程隔离干净适合本机开发、单机工具、CI 里跑一次性任务。缺点是只能同机跨机器就没法用了。SSE 则是把 MCP 服务做成一个 HTTP 服务被调用端监听端口调用端通过 HTTP 长连接接收事件流。它天然支持远程调用适合把工具服务部署到内网某台机器、让多个 Agent 或客户端共享。代价是要处理端口、网络、鉴权这些工程问题。这篇内容会用一个「图片搜索 MCP 服务」作为例子把 stdio 和 SSE 两条路径都走一遍重点放在ToolCallbackProvider的注册骨架、SSE 端点的验证动作以及本地到远程联调时容易踩的坑。如果你正在用 Spring AI 做 Agent 工具链或者想把本地写好的工具暴露给远程调用方下面的配置可以直接复制改。2. TaoToken 前置统一 Key 与 API 通道在写 MCP 服务之前先解决一个现实问题工具本身要调用外部 API比如图片搜索、网页抓取、模型推理。如果每个工具都各自管理一套 Key配置会散落在各个类里换环境时非常痛苦。我的做法是把模型调用和工具调用的出口统一到 TaoToken。它提供 OpenAI 兼容的 API 通道一个 Key 可以走多个模型MCP 服务里需要调用模型做意图理解或结果整理时直接指向同一个 base_url 就行。这样工具服务本身不关心上游是哪家模型只关心「我发一个请求、拿一个结果」。具体来说你需要在 TaoToken 控制台创建一个 API Key然后把它写进 MCP 服务的配置里。模型对话、Coding Plan、API Keys 这些入口都在官网可以找到官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基地址https://taotoken.net/api模型对话https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteCoding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite拿到 Key 之后在 MCP 服务的application.yml里这样配置spring: ai: openai: api-key: ${TAOTOKEN_API_KEY} base-url: https://taotoken.net/api chat: options: model: gpt-4o-mini这里用环境变量注入 Key避免把密钥写进代码仓库。工具服务里如果需要调用模型做二次处理直接注入ChatClient即可不需要再关心上游地址。3. stdio 模式ToolCallbackProvider 配置骨架先走 stdio 路径。被调用端的依赖只需要一个 starterdependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webmvc/artifactId /dependencystdio 模式下应用不能启动 Web 容器否则会占用端口、和 stdio 通信冲突。所以application.yml里要显式关掉 Web 应用类型spring: application: name: yu-image-search-mcp-server profiles: active: stdio main: web-application-type: none banner-mode: off ai: mcp: server: name: yu-image-search-mcp-server version: 0.0.1 type: SYNC stdio: trueweb-application-type: none是关键它告诉 Spring Boot 不要启动 Tomcat/Jetty/Undertow。banner-mode: off是为了避免 Banner 字符画混进 stdout污染 JSON-RPC 消息。这一点在 stdio 模式下特别重要因为 stdout 是协议通道任何多余输出都会导致调用端解析失败。接下来写工具类。以图片搜索为例用Tool注解标记方法ToolParam描述参数Service public class ImageSearchTool { private static final String API_KEY System.getenv(PEXELS_API_KEY); private static final String API_URL https://api.pexels.com/v1/search; Tool(description search image from web) public String searchImage(ToolParam(description Search query keyword) String query) { try { return String.join(,, searchMediumImages(query)); } catch (Exception e) { return Error search image: e.getMessage(); } } public ListString searchMediumImages(String query) { MapString, String headers new HashMap(); headers.put(Authorization, API_KEY); MapString, Object params new HashMap(); params.put(query, query); String response HttpUtil.createGet(API_URL) .addHeaders(headers) .form(params) .execute() .body(); return JSONUtil.parseObj(response) .getJSONArray(photos) .stream() .map(photoObj - (JSONObject) photoObj) .map(photoObj - photoObj.getJSONObject(src)) .map(photo - photo.getStr(medium)) .filter(StrUtil::isNotBlank) .collect(Collectors.toList()); } }然后在启动类里注册ToolCallbackProviderSpringBootApplication public class YuImageSearchMcpServerApplication { public static void main(String[] args) { SpringApplication.run(YuImageSearchMcpServerApplication.class, args); } Bean public ToolCallbackProvider imageSearchTools(ImageSearchTool imageSearchTool) { return MethodToolCallbackProvider.builder() .toolObjects(imageSearchTool) .build(); } }MethodToolCallbackProvider.builder().toolObjects(...)是 Spring AI 官方推荐的写法它会把Tool注解的方法扫描出来包装成 MCP 可识别的回调。如果你有多个工具类可以继续.toolObjects(toolA, toolB)链式添加。打包mvn clean package -DskipTests产物在target/yu-image-search-mcp-server-0.0.1-SNAPSHOT.jar。调用端这边需要配置一个mcp-servers.json告诉 Spring AI 怎么启动这个子进程{ mcpServers: { yu-image-search-mcp-server: { command: java, args: [ -Dspring.ai.mcp.server.stdiotrue, -Dspring.main.web-application-typenone, -Dlogging.pattern.console, -jar, yu-image-search-mcp-server/target/yu-image-search-mcp-server-0.0.1-SNAPSHOT.jar ], env: {} } } }注意-Dlogging.pattern.console这一行它把控制台日志格式清空防止日志输出到 stdout 干扰协议。调用端的application.yml里指向这个 jsonspring: ai: mcp: client: stdio: servers-configuration: classpath:mcp-servers.json业务代码里注入ToolCallbackProvider挂到toolCallbacks上Resource private ToolCallbackProvider toolCallbackProvider; public String doChatWithMcp(String message, String chatId) { ChatResponse chatResponse chatClient .prompt() .user(message) .advisors(spec - spec.param(ChatMemory.CONVERSATION_ID, chatId)) .advisors(new MyLoggerAdvisor()) .toolCallbacks(toolCallbackProvider) .call() .chatResponse(); return chatResponse.getResult().getOutput().getText(); }测试方法Test void doChatWithMcp() { String chatId UUID.randomUUID().toString(); String message 帮我搜索一些哄另一半开心的图片; String answer loveApp.doChatWithMcp(message, chatId); Assertions.assertNotNull(answer); }跑起来之后调用端会启动子进程、加载 jar、通过 stdio 完成工具调用。整个过程没有端口适合本机开发。4. SSE 模式端点配置与远程调用验证SSE 模式的核心变化是被调用端要启动 Web 容器、监听端口调用端通过 HTTP 连接。依赖不变还是那个 starter但application.yml要改spring: application: name: yu-image-search-mcp-server profiles: active: sse server: port: 8127 ai: mcp: server: name: yu-image-search-mcp-server version: 0.0.1 type: SYNC stdio: false这里stdio: false并且不再设置web-application-type: none让 Spring Boot 正常启动 Web 容器。端口 8127 就是 SSE 端点监听的端口。工具类和ToolCallbackProvider的注册代码完全不用改和 stdio 模式一样。启动之后MCP 服务会暴露一个 SSE 端点默认路径是/sse。你可以用 curl 验证curl -N http://localhost:8127/sse如果连接成功会看到类似这样的事件流event: endpoint data: /mcp/message?sessionIdxxxxx这说明 SSE 通道已经建立服务端在等待客户端通过/mcp/message发送 JSON-RPC 请求。-N参数关闭 curl 的缓冲方便实时看到事件。调用端这边不再需要mcp-servers.json直接在application.yml里配置 SSE 连接spring: ai: mcp: client: sse: connections: server1: url: http://localhost:8127如果被调用端部署在另一台机器把localhost换成那台机器的 IP 或域名即可。业务代码同样注入ToolCallbackProvider挂到toolCallbacks上和 stdio 模式一致。这里有一个容易忽略的点SSE 和 stdio 在调用端只能选一种。如果你同时配置了stdio.servers-configuration和sse.connectionsSpring AI 可能会加载两套客户端导致工具重复注册或连接冲突。切换模式时记得把另一套配置注释掉。远程联调时先确认被调用端端口可达telnet 192.168.1.100 8127如果连不上检查防火墙、安全组、以及服务是否真的监听在0.0.0.0而不是127.0.0.1。Spring Boot 默认监听所有网卡但有些环境会显式绑定回环地址需要改成server.address0.0.0.0。5. 本篇常见错排查stdout 被日志污染。stdio 模式下任何非 JSON-RPC 的输出都会导致调用端解析失败。除了banner-mode: off还要检查日志框架是否往控制台输出。可以在application.yml里加logging: pattern: console: 或者把日志级别调到ERROR减少无关输出。web-application-type: none没生效。如果 stdio 模式下应用仍然启动了 Tomcat检查这个配置是否写在正确的 profile 下。多 profile 时spring.main.web-application-type可能被其他 profile 覆盖。建议在 stdio 专属的application-stdio.yml里单独声明。SSE 端点 404。确认依赖是spring-ai-starter-mcp-server-webmvc而不是spring-ai-starter-mcp-server。后者不带 Web 容器不会暴露 SSE 端点。另外检查spring.ai.mcp.server.stdio是否为false如果为true服务会走 stdio 而不启动 Web。调用端工具没注册。ToolCallbackProvider注入后必须显式挂到toolCallbacks()上否则模型看不到工具。如果用的是ChatClient确认.toolCallbacks(toolCallbackProvider)在.call()之前调用。跨机器调用超时。SSE 是长连接中间如果有反向代理或负载均衡可能会因为空闲超时断开。可以在代理层调大proxy_read_timeout或者在客户端加心跳。本地联调时先直连排除网络中间件干扰。Key 泄露。不要把 TaoToken API Key 或 Pexels Key 硬编码在代码里。用环境变量或配置中心注入.gitignore里排除本地配置文件。如果 Key 已经提交过尽快在控制台轮换。6. 语义一致 CTA如果你在排障或接入阶段卡住优先看 API Keys 和接入文档确认 base_url 和 Key 的用法API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite如果你要验证模型在 MCP 工具链里的表现比如意图识别、参数抽取是否准确可以直接在模型对话里试模型对话https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite如果你打算长期跑编码类 Agent或者把 MCP 服务接入日常开发流程Coding Plan 更适合持续调用Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite我自己的习惯是本地开发用 stdio快速验证工具逻辑需要多机共享或部署到测试环境时切 SSE调用端只改一行 url。两种模式共用同一套ToolCallbackProvider注册代码切换成本很低。真正花时间的不是写工具而是把 stdout 日志清干净、把端口和防火墙理清楚。这两件事做完MCP 服务从本地到远程的联调基本就通了。
返回列表