ARTICLE DETAIL

资讯详情

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

【最新最完整】SpringAI-1.0.0开发MCP Server,搭建MCP Client 实战笔记(进阶+详细+完整代码)

【最新最完整】SpringAI-1.0.0开发MCP Server,搭建MCP Client 实战笔记(进阶+详细+完整代码) 1. 为什么 SpringAI 1.0.0 的 MCP 实战值得单独写一篇SpringAI 1.0.0 正式版发布之后MCP Server 和 MCP Client 的 starter 依赖、配置项命名、回调注册方式都发生了不小的变化。网上大量教程还停留在 M7、M6 甚至更早的里程碑版本照着抄很容易在ToolCallbackProvider注入、toolcallback.enabled配置、SSE 连接声明这几处直接卡住。这篇笔记聚焦一件事用 Spring Boot 3.4 WebFlux从零搭一个能对外暴露 Tool 的 MCP Server再搭一个能通过 SSE 和 STDIO 两种方式调用它的 MCP Client最后跑通一次完整的工具调用链路。适合谁看已经写过 SpringAI 基础对话、想把手里的函数调用升级成标准 MCP 协议的开发者或者手里有一批内部工具函数想打包成可复用 Server 给多个项目调用的团队。读完之后你应该能独立完成 Server 打包、Client 接入、工具注册、调用验证这一整条链路而不是只停留在“知道 MCP 是什么”。我试过把同一套 Tool 分别用 STDIO 和 SSE 暴露踩过的坑主要集中在配置项拼写和异步类型上下面会逐个标出来。2. 前置准备TaoToken 与模型接入MCP Client 本身不产生智能它只是把 Tool 列表喂给大模型让模型决定调哪个函数。所以 Client 侧必须有一个支持 Function Calling / Tool Calling 的对话模型。我这边习惯用 TaoToken 做统一入口它的 API 兼容 OpenAI 协议SpringAI 的spring-ai-starter-model-openai可以直接对接不用改任何请求结构。TaoToken 官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台生成 API Key。API 基址用 https://taotoken.net/api 注意这个地址后面不加任何 UTM 参数直接填进base-url即可。需要提前确认两件事第一你选的模型要支持工具调用像 qwen-max-latest、gpt-4o 这类都可以第二Client 的spring.ai.openai.chat.options.model要和你 Key 对应的可用模型一致否则启动时不报错但调用工具时会返回空 tool_calls。如果你只是想先验证模型对话是否通可以走模型对话入口快速试一条请求如果打算长期跑编码类 Agent建议直接看 Coding Plan额度模型更划算。这两个入口在 TaoToken 控制台里都能找到这里不展开。3. MCP Server 侧依赖、配置与 Tool 注册3.1 pom 依赖与版本约束SpringAI 1.0.0 的 MCP Server starter 有三个变体区别只在传输层starter artifactId传输方式适用场景spring-ai-mcp-server-spring-boot-starter仅 STDIO本地进程内调用、打包成 jar 给插件用spring-ai-starter-mcp-server-webmvcSTDIO SSE阻塞式 Web传统 MVC 项目spring-ai-starter-mcp-server-webfluxSTDIO SSE非阻塞式 Web高并发场景我选 WebFlux因为后面要演示 SSE 实时拉取博客数据非阻塞在长连接场景下更稳。完整 pom 关键部分如下properties java.version17/java.version spring-ai.version1.0.0/spring-ai.version /properties dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webflux/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.22/version /dependency dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId version4.12.0/version /dependency dependency groupIdorg.jsoup/groupId artifactIdjsoup/artifactId version1.17.2/version /dependency /dependencies dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagementJDK 必须 17 以上Spring Boot 必须 3.x。这两条是硬约束低于这个版本连 starter 都解析不了。3.2 application.yml 的两种形态Server 要同时支持 SSE 和 STDIO配置需要分两套。SSE 模式下正常启动 Web 端口spring: ai: mcp: server: name: author-info-server version: 1.0.0 server: port: 9090STDIO 模式下要关掉 Web 容器否则进程启动后会一直占着端口插件侧无法通过标准输入输出通信spring: main: web-application-type: none banner-mode: off ai: mcp: server: name: author-info-server version: 1.0.0 stdio: true注意web-application-type: none和banner-mode: off这两行在打包成 jar 给外部插件调用时必须打开否则 jar 一启动就卡在 Web 初始化STDIO 通道根本建立不起来。3.3 写一个带实时抓取的 ToolTool 的写法和 SpringAI 函数调用完全一致核心是Tool注解加ToolParam描述字段。下面这个 Tool 做两件事返回作者基本信息同时实时抓取博客首页的文章列表。Service public class OpenMyBlogTool { private static final String BLOG_HOME_URL https://blog.csdn.net/2201_75669520?typeblog; Tool(name get-author-info, description 获取作者信息包括简介、联系方式和最新博客列表) public AuthorInfo getAuthorInfo() { AuthorInfo info new AuthorInfo(); info.setAuthorIntroduction(一名普通的后端开发者); info.setContact(vx: example); info.setBlogHomeUrl(BLOG_HOME_URL); OkHttpClient client new OkHttpClient(); String html getHtmlContent(client, BLOG_HOME_URL); if (html ! null !html.isEmpty()) { info.setBlogList(parseBlogsFromHtml(html)); } return info; } private String getHtmlContent(OkHttpClient client, String url) { Request request new Request.Builder() .url(url) .header(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36) .build(); try (Response response client.newCall(request).execute()) { if (response.isSuccessful() response.body() ! null) { return response.body().string(); } return null; } catch (IOException e) { return null; } } private ListBlog parseBlogsFromHtml(String html) { ListBlog list new ArrayList(); Document doc Jsoup.parse(html); Elements articles doc.select(article.blog-list-box); for (Element article : articles) { Blog blog new Blog(); Element link article.selectFirst(a); if (link ! null) { blog.setUrl(link.attr(href)); } Element title article.selectFirst(a h4); if (title ! null) { blog.setTitle(title.text().trim()); } Element desc article.selectFirst(div.blog-list-content); if (desc ! null) { blog.setDescription(desc.text().trim()); } if (blog.getTitle() ! null blog.getUrl() ! null) { list.add(blog); } } return list; } }DTO 字段上同样要加ToolParam让模型知道每个字段的含义Data NoArgsConstructor AllArgsConstructor public class AuthorInfo { ToolParam(description 作者介绍) private String authorIntroduction; ToolParam(description 联系方式) private String contact; ToolParam(description 博客首页地址) private String blogHomeUrl; ToolParam(description 博客列表) private ListBlog blogList; }3.4 注册 ToolCallbackProvider这一步是把 Tool 暴露给 MCP 协议层1.0.0 里必须显式声明 BeanConfiguration public class ToolCallbackProviderConfig { Bean public ToolCallbackProvider openMyBlogTool(OpenMyBlogTool openMyBlogTool) { return MethodToolCallbackProvider.builder() .toolObjects(openMyBlogTool) .build(); } }启动后控制台会打印Registered tools: 1看到这行说明 Tool 已经挂到 Server 上了。如果数量是 0八成是Tool注解没加或者 Bean 没被扫描到。4. MCP Client 侧SSE 与 STDIO 双通道接入4.1 Client 依赖与模型配置Client 的核心依赖是spring-ai-starter-mcp-client-webflux加上对话模型依赖dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client-webflux/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependencyapplication.yml 里模型和 MCP 配置要一起写spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: qwen-max-latest mcp: client: enabled: true name: mcp-client type: ASYNC toolcallback: enabled: true sse: connections: author-info-server: url: http://localhost:9090 stdio: servers-configuration: classpath:/mcp-server-config.jsontoolcallback.enabled: true必须开否则 Client 拿不到 Tool 列表。type: ASYNC是因为用了 WebFlux同步类型会阻塞事件循环。4.2 STDIO 外部 Server 导入mcp-server-config.json的格式和主流 AI 插件一致可以直接从 mcp.so 这类站点复制配置{ mcpServers: { baidu-map: { command: cmd, args: [/c, npx, -y, baidumap/mcp-server-baidu-map], env: { BAIDU_MAP_API_KEY: your-key-here } } } }Windows 下command要写cmdargs第一个是/c这是 npx 在 Windows 上的固定写法。Linux/Mac 直接写npx即可。4.3 把自己写的 Server 打包成 STDIO 调用前面 SSE 模式是启动 Web 端口如果想让别人通过 jar 直接调用需要先切到 STDIO 配置然后打包mvn clean package -DskipTests打包后在target目录拿到 jar把绝对路径填进 Client 的 json{ mcpServers: { my-author-server: { type: stdio, command: java, args: [ -Dspring.ai.mcp.server.stdiotrue, -jar, D:/workspace/mcp-server-test/target/mcp-server-test-0.0.1-SNAPSHOT.jar ] } } }路径必须是绝对路径相对路径在插件进程里解析不到。4.4 ChatClient 注入 ToolCallbackProviderClient 侧最关键的一行是把ToolCallbackProvider传给 ChatClientBean public ChatClient chatClient(ChatClient.Builder builder, ToolCallbackProvider toolCallbackProvider) { return builder .defaultToolCallbacks(toolCallbackProvider) .defaultSystem(你可以调用工具获取作者信息和地图数据回答时优先使用工具返回结果。) .build(); }如果模型不支持工具调用这里不会报错但调用时会静默忽略 tool_calls表现为模型直接编造答案。所以选模型时务必确认支持 Function Calling。5. 验证一次完整工具调用链路启动 Server 后先单独验证 SSE 端点是否可达curl -N http://localhost:9090/sse正常会返回一串event: endpoint和data:开头的流式响应说明 SSE 通道已建立。如果连接被拒绝检查 Server 的server.port和 Client 的sse.connections.url是否一致。然后启动 Client观察日志里是否出现两段关键信息一段是 SSE 连接成功一段是 STDIO Server 启动成功。两段都出现后发一条会触发工具的请求String answer chatClient.prompt() .user(帮我查一下作者的最新博客有哪些) .call() .content(); System.out.println(answer);预期结果是模型先返回一次 tool_call参数是get-author-infoClient 执行 Tool 后把结果回传模型再生成自然语言回答。日志里能看到Tool execution request和Tool execution response两条记录说明链路完整跑通。如果模型直接回答而没有触发工具先检查toolcallback.enabled是否为 true再确认模型是否支持工具调用。6. 本篇常见报错与排查报错一No ToolCallbackProvider bean found原因是没声明ToolCallbackProvider的 Bean或者Configuration类没被扫描到。检查包路径是否在启动类的同级或子级。报错二SSE 连接超时Client 的sse.connections下 url 写成了http://localhost:9090/sse正确写法是只写到端口路径由 starter 自动拼接。报错三STDIO 模式下进程启动后无响应忘了加web-application-type: noneWeb 容器占着主线程STDIO 通道没建立。加上这两行配置重新打包即可。报错四工具调用返回空模型不支持工具调用或者chat.options.model填的模型和 Key 不匹配。换成 qwen-max-latest 这类明确支持 Function Calling 的模型再试。报错五type: ASYNC配置后启动报类型转换异常说明当前 starter 版本对 ASYNC 的枚举值命名有变化去官方文档确认当前版本支持的枚举名不要照抄旧版本。7. 继续往下走的方向Server 打包成 jar 之后同一份 Tool 可以同时被多个项目、多个模型复用这才是 MCP 相比普通函数调用最大的价值。你可以把内部常用的数据库查询、日志检索、配置读取都封装成 Tool统一走 MCP 协议暴露Client 侧只负责声明连接地址。需要长期跑编码类 Agent 的话建议把模型额度走 Coding Plan避免按次调用成本失控。接入过程中如果遇到 Key 或模型可用性问题直接去 API Keys 页面确认再对照接入文档检查 base-url 和 model 字段。工具调用链路本身不复杂卡人的永远是配置项拼写和版本差异多看官方文档比抄旧教程省时间。
返回列表