)
SpringAI MCP 服务端开发实战图片搜索服务本章定位承接上一章《Spring AI 使用 MCP 客户端调用高德 MCP》。上一章我们从消费方的角度使用别人提供的 MCP 服务本章反过来从提供方的角度基于 Spring AI 从 0 到 1 开发一个图片搜索 MCP 服务端并用客户端分别通过stdio和SSE两种传输方式完成调用。技术栈Spring AI 1.0.0-M6 · Spring Boot 3.4.x · JDK 17 · Maven · Hutool · Pexels API一、MCP 架构回顾客户端与服务端1.1 MCP 客户端MCP Client 是 MCP 架构中的关键组件主要负责和 MCP 服务器建立连接并进行通信。它能自动匹配服务器的协议版本、确认可用功能、负责数据传输和 JSON-RPC 交互。此外它还能发现和使用各种工具、管理资源、和提示词系统进行交互。除了这些核心功能MCP 客户端还支持一些额外特性比如根管理Roots、采样控制Sampling以及同步或异步操作。为了适应不同场景它提供了多种数据传输方式包括Stdio 标准输入 / 输出客户端启动一个子进程通过标准输入输出流与其通信适用于本地调用SSE 传输基于 Java HttpClient / WebFlux通过 HTTP Server-Sent Events 与远程服务通信适用于远程调用。客户端可以通过不同传输方式调用不同的 MCP 服务可以是本地的、也可以是远程的。上一章节已经实现 MCP 客户端调用高德 MCP 服务本章在此基础上新增对自研图片搜索服务的调用。1.2 MCP 服务端MCP Server 也是整个 MCP 架构的关键组件主要用来为客户端提供各种工具、资源和功能支持。它负责处理客户端的请求包括解析协议、提供工具、管理资源以及处理各种交互信息。同时它还能记录日志、发送通知并且支持多个客户端同时连接保证高效的通信和协作。和客户端一样它也可以通过多种方式进行数据传输比如 Stdio 标准输入 / 输出、基于 Servlet / WebFlux / WebMVC 的 SSE 传输满足不同应用场景。这种设计使得客户端和服务端完全解耦任何语言开发的客户端都可以调用我们开发的 MCP 服务。二、MCP 服务端开发图片搜索服务服务端开发主要基于 Spring AI MCP Server Boot Starter它能够自动配置 MCP 服务端组件使开发者能够轻松创建 MCP 服务向 AI 客户端提供工具、资源和提示词模板从而扩展 AI 模型的能力范围。图片搜索服务使用 Pexels 图片资源网站的 API 构建。2.1 前置准备开始之前请确认环境满足以下要求项目要求JDK17 及以上Maven3.6Spring Boot3.4.x与 Spring AI 1.0.0-M6 匹配Spring AI1.0.0-M6示例基于该里程碑版本Pexels一个可用的 API Key见 2.2版本提示重要Spring AI 迭代极快本文示例基于1.0.0-M6对应 Spring Boot 3.4.x。升级到 Spring AI 1.0.0 GA 后MCP 相关 starter 名称发生过调整见 2.3 与 3.1 的提示注解也有变化ToolParam→ToolParameter实操时请以官方文档和 Maven 仓库中的实际坐标为准重点关注思路和方法而非死记版本。2.2 申请 Pexels API Key进入 Pexels 官网注册登录后在 API 管理页面创建一个 Key 即可。免费额度足够本地开发调试使用。2.3 新建 Maven moduleimgSearch_mcpServer在项目根目录下新建一个 Maven module名称为imgSearch_mcpServer示例名可按自己的规范命名。建议单独打开该模块或在 IDE 中将它作为独立工程操作不要在原有项目的子文件夹里混着改避免 Maven 依赖、资源路径等出现不必要的问题。2.4 引入服务端依赖引入必要的依赖包括 Lombok、Hutool 工具库和 Spring AI MCP 服务端依赖。MCP 服务端依赖有 3 种可选开发流程完全一致只是配置不同依赖说明spring-ai-mcp-server-spring-boot-starter仅支持 stdio无需 Web 容器spring-ai-mcp-server-webmvc-spring-boot-starter基于 Spring MVC 的 SSE 传输同时可选用 stdio本章选择spring-ai-mcp-server-webflux-spring-boot-starter基于 Spring WebFlux 的响应式 SSE 传输可选用 stdio此处我们选择引入WebMVC版本同时支持 stdio 与 SSE方便后续切换测试dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-mcp-server-webmvc-spring-boot-starter/artifactIdversion1.0.0-M6/version/dependency再补上 Hutool 工具库发起 HTTP 请求、解析 JSON 用与 LombokdependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion5.8.25/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependency版本提示1.0.0-M6阶段的服务端 starter 坐标形如spring-ai-mcp-server-webmvc-spring-boot-starterSpring AI 1.0.0 GA 后更名为spring-ai-starter-mcp-server-webmvc。建议通过spring-ai-bom统一管理 Spring AI 各模块版本。引入 WebMVC 依赖后启动时会自动注册 MCP 的SSE 端点与消息端点默认路径为/sse与/mcp/message供客户端连接使用无需我们自己写 Controller。2.5 编写配置文件stdio / SSE 双模式在src/main/resources目录下编写三份配置通过 Spring Profile 灵活切换 stdio / SSE 两种传输模式。① stdio 配置application-stdio.yml需关闭 Web 支持因为 stdio 模式不启动 Web 容器spring:ai:mcp:server:name:image-search-mcp-serverversion:0.0.1type:SYNC# 同步模式若需响应式可改为 ASYNCstdio:true# 启用 stdio 传输main:web-application-type:none# 关闭 Web 容器banner-mode:off② SSE 配置application-sse.yml需关闭 stdio 模式由 WebMVC 提供 SSE 端点spring:ai:mcp:server:name:image-search-mcp-serverversion:0.0.1type:SYNCstdio:false# 关闭 stdio走 SSE③ 主配置application.yml指定激活哪套配置并声明服务端口与 Pexels Keyserver:port:9090spring:application:name:imgSearch_mcpServerprofiles:active:sse,local# 切换 stdio 时改为: stdio,local## Pexels 图片搜索 API Key申请地址: https://www.pexels.com/zh-cn/api/key/Pexels:api-key:你的api-key提示上面激活了local这个 Profile一般用于存放本地私有配置比如 API Key、数据库密码等。可以把Pexels.api-key挪到application-local.yml中并把该文件加入.gitignore避免密钥提交到代码库application.yml只保留公共配置。如果没有这个文件也可以先把api-key直接写在主配置文件里或删除local激活项。2.6 编写图片搜索工具 ImageSearchTool在tools包下新建ImageSearchTool使用Tool注解标注方法作为 MCP 服务对外提供的工具。核心逻辑调用 Pexels 搜索接口把返回的多张图片 URL 用逗号拼接后返回给 AI。packagecom.example.imgsearch.tools;importcn.hutool.core.util.StrUtil;importcn.hutool.http.HttpUtil;importcn.hutool.json.JSONObject;importcn.hutool.json.JSONUtil;importorg.springframework.ai.tool.annotation.Tool;importorg.springframework.ai.tool.annotation.ToolParam;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.stereotype.Component;importjava.util.HashMap;importjava.util.List;importjava.util.Map;importjava.util.stream.Collectors;/** * 图片搜索工具对外暴露为一个 MCP Tool */ComponentpublicclassImageSearchTool{// 替换为你的 Pexels API 密钥从官网申请建议通过环境变量/配置中心注入勿硬编码Value(${Pexels.api-key})privateStringapiKey;// Pexels 常规搜索接口请以官方文档为准privatestaticfinalStringAPI_URLhttps://api.pexels.com/v1/search;Tool(descriptionsearch image from web)publicStringsearchImage(ToolParam(descriptionSearch query keyword)Stringquery){try{// 多张图片 URL 用逗号分隔便于客户端/AI 解析returnString.join(,,searchMediumImages(query));}catch(Exceptione){returnError search image: e.getMessage();}}/** * 搜索图片列表 * * param query 搜索关键词 * return 图片 URL 列表 */publicListStringsearchMediumImages(Stringquery){// 设置请求头Pexels 要求将 API Key 直接放在 Authorization 头无需 Bearer 前缀MapString,StringheadersnewHashMap();headers.put(Authorization,apiKey);// 设置请求参数query 必填可按需补充 page、per_page 等MapString,ObjectparamsnewHashMap();params.put(query,query);params.put(per_page,10);// 发送 GET 请求StringresponseHttpUtil.createGet(API_URL).addHeaders(headers).form(params).execute().body();// 解析响应 JSONphotos[] 数组 → 每项的 src 对象 → 图片地址returnJSONUtil.parseObj(response).getJSONArray(photos).stream().map(photoObj-(JSONObject)photoObj).map(photoObj-photoObj.getJSONObject(src)).map(src-src.getStr(original))// 图片规格original 为原图可选 large/medium/small 等.filter(StrUtil::isNotBlank).collect(Collectors.toList());}}说明Tool/ToolParam注解来自 Spring AI 的org.springframework.ai.tool.annotation包与原生工具调用一致Spring AI 1.0.0 GA 后ToolParam更名为ToolParameter。Pexels 的photos[].src对象提供了多种尺寸original原图、large2x/large宽 ≤ 940px、medium宽 ≤ 350px、small宽 ≤ 130px等按需选择即可。工具内部做了 try-catch 兜底即使 Pexels 接口异常也会返回友好的错误文本避免整个对话流程中断这也是 MCP 服务端的最佳实践之一。2.7 单元测试验证工具编写对应的单元测试类先不启动 MCP直接验证工具类本身是否可用packagecom.example.imgsearch.tools;importjakarta.annotation.Resource;importorg.junit.jupiter.api.Assertions;importorg.junit.jupiter.api.Test;importorg.springframework.boot.test.context.SpringBootTest;SpringBootTestclassImageSearchToolTest{ResourceprivateImageSearchToolimageSearchTool;TestvoidsearchImage(){StringresultimageSearchTool.searchImage(美女图片);Assertions.assertNotNull(result);System.out.println(result);}}测试通过后控制台会输出搜索到的多张图片地址。2.8 在主类中注册工具在主类中通过定义ToolCallbackProviderBean 来注册工具Spring AI 会自动把ImageSearchTool中Tool注解的方法转换为 MCP 工具并在启动时发布packagecom.example.imgsearch;importcom.example.imgsearch.tools.ImageSearchTool;importorg.springframework.ai.tool.MethodToolCallbackProvider;importorg.springframework.ai.tool.ToolCallbackProvider;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.annotation.Bean;SpringBootApplicationpublicclassImgSearchMcpServerApplication{publicstaticvoidmain(String[]args){SpringApplication.run(ImgSearchMcpServerApplication.class,args);}BeanpublicToolCallbackProviderimageSearchTools(ImageSearchToolimageSearchTool){returnMethodToolCallbackProvider.builder().toolObjects(imageSearchTool).build();}}提示示例将主类名统一为ImgSearchMcpServerApplication与 module 名保持一致。JAR 包名由 Maven 的artifactId决定默认artifactId-version.jar与主类名无关但主类名必须与文件名一致、且被SpringBootApplication标注。2.9 打包可执行 JAR至此服务端就开发完成了。在imgSearch_mcpServer模块下执行 Maven Package 打包mvn clean package-DskipTests打包成功后会在target目录下生成可执行 JAR 包例如imgSearch_mcpServer-0.0.1-SNAPSHOT.jar等会儿客户端调用时会依赖这个文件。三、MCP 客户端开发调用图片搜索服务接下来直接在根项目中开发客户端调用刚才创建的图片搜索服务。前置条件客户端项目已完成上一章调用高德 MCP的全部配置引入了 MCP 客户端依赖、实现doChatWithMcp方法、具备mcp-servers.json。本章只需在此基础上新增一个图片搜索 Server 的配置即可。3.1 引入客户端依赖如果上一章已引入过可跳过本步。未引入的话先加入 MCP 客户端依赖dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-mcp-client-spring-boot-starter/artifactIdversion1.0.0-M6/version/dependency实际开发中你也可以按需添加 WebFlux 支持但传输方式要与服务端模式匹配stdio 客户端用 Java HttpClientSSE 客户端可选用 WebFlux。GA 版本后该 starter 更名为spring-ai-starter-mcp-client。3.2 stdio 模式调用1配置mcp-servers.json在客户端resources目录的mcp-servers.json中新增我们刚打包好的图片搜索服务。通过java命令执行 JAR 包-D参数相当于命令行注入 stdio 相关配置{mcpServers:{amap-maps:{command:npx.cmd,args:[-y,amap/amap-maps-mcp-server],env:{AMAP_MAPS_API_KEY:你的api-key}},image-search-mcp-server:{command:java,args:[-Dspring.ai.mcp.server.stdiotrue,-Dspring.main.web-application-typenone,-Dlogging.pattern.console,-jar,imgSearch_mcpServer/target/imgSearch_mcpServer-0.0.1-SNAPSHOT.jar],env:{}}}}要点说明amap-maps是上一章配置的高德 MCP保留即可两者可共存-Dspring.ai.mcp.server.stdiotrue让服务端以 stdio 模式运行-Dspring.main.web-application-typenone关闭 Web 容器-Dlogging.pattern.console置空控制台日志格式必须配置——因为 stdio 使用标准输入输出流通信多余的控制台日志会干扰 JSON-RPC 报文-jar后的路径是相对客户端项目根目录的 JAR 路径请确保已先打包、且路径与你的实际产物一致。2客户端 Spring 配置在客户端application.yml中启用 stdio 模式并指定mcp-servers.json位置spring:ai:mcp:client:stdio:servers-configuration:classpath:mcp-servers.json3启动项目发起对话沿用上一章的doChatWithMcp方法内部通过ToolCallbackProvider把 MCP 工具注入ChatClient4运行效果以 Debug 模式运行通过断点可以看到functionCallbacks中成功加载了图片搜索工具最终输出结果包含多个图片地址。小贴士stdio 模式下服务端是以子进程形式被客户端拉起的代码上打断点不方便调试。若想调试服务端逻辑建议使用下文 3.3 的 SSE 模式。3.3 SSE 模式调用SSE 模式下服务端是一个独立启动的 Web 服务客户端通过网络连接它调试体验更好服务端可以单独打断点、看日志。1服务端切换到 SSE 配置修改服务端application.yml激活 SSE 配置server:port:9090spring:application:name:imgSearch_mcpServerprofiles:active:sse,localapplication-sse.yml中stdio: false让服务端以 SSE 模式启动。2以 Debug 模式启动服务端直接运行ImgSearchMcpServerApplication主类即可启动成功后服务端监听9090端口并提供/sse与/mcp/message两个端点。3修改客户端配置修改客户端application.yml添加 SSE 连接配置同时注释掉原有的 stdio 配置避免两种模式冲突、产生端口或进程问题spring:ai:mcp:client:sse:connections:server1:url:http://localhost:9090# stdio:# servers-configuration: classpath:mcp-servers.json注意url的端口必须与服务端server.port保持一致示例均为9090。如果端口不一致客户端会连接失败。4运行测试再次运行doChatWithMcp测试会发现这次 MCP 服务端的代码被真实执行可在服务端打断点确认。3.4 stdio 与 SSE 对比小结对比维度stdioSSE传输方式标准输入 / 输出子进程HTTP Server-Sent Events运行方式由客户端自动拉起子进程独立部署、独立启动的 Web 服务适用场景本地调用、小型项目远程调用、多客户端共享调试难度较高stdout 被协议占用低服务端可打断点、看日志安全本地进程不暴露网络端口需自行处理鉴权 / 网络安全性能无网络开销相对更高有网络传输开销客户端配置mcp-servers.json stdio 配置sse.connections.url指向服务端地址四、常见问题排查JAR 包找不到 / 报无法执行客户端启动前必须先mvn package打包服务端并核对mcp-servers.json中-jar的路径与文件名是否与 target 目录中的实际产物一致。stdio 模式启动后控制台乱码 / 卡住 / 通信异常确认已在启动参数中加入-Dlogging.pattern.console关闭控制台日志服务端代码里不要用System.out.println直接输出业务信息会污染 stdio 协议报文。Pexels 返回 401 / 鉴权失败检查Pexels.api-key是否配置正确、Key 是否已生效。Pexels 要求把 Key 直接放在Authorization请求头中无需Bearer前缀。SSE 连接失败Connection refused / timeout服务端是否已启动客户端url的端口是否与服务端server.port一致切换模式后是否残留了旧的 stdio 配置切换传输模式后出现端口冲突 / 重复进程客户端配置里 stdio 与 SSE二选一把另一份注释掉服务端 Profilesse/stdio同样只激活一份。工具未加载functionCallbacks为空检查服务端ToolCallbackProviderBean 是否已注册、依赖是否引入完整、JAR 是否已重新打包再看客户端启动日志中 MCP 连接的tools/list是否正常参考上一章的经验。ToolParam报找不到符号Spring AI 版本差异导致。1.0.0-M6使用ToolParamGA 版本后更名为ToolParameter请按依赖版本替换。Windows 下命令找不到在 Windows 下npx需写成npx.cmd高德 MCP 场景服务端用java命令时请确认 JDK 已加入 PATH。五、最佳实践与部署建议慎用 MCPMCP 不是银弹本质就是标准化的工具调用。如果只是应用内部自用、不需要共享的工具直接用原生Tool即可能省去打包、部署、进程管理的成本。建议能不用就不用先用工具调用确有共享 / 生态诉求时再转 MCP。传输模式选型stdio 适合本地小型项目安全且性能好SSE 适合独立服务、多客户端共享的中大型项目。服务端开发时推荐引入 WebMVC 版本一套代码两种模式随意切换。工具描述要清晰Tool、ToolParam的description要写清楚用途、参数含义AI 才能准确判断何时调用、传什么参数。注意容错工具方法内部捕获所有可能异常并返回友好错误信息本章示例已体现避免一次失败拖垮整个对话流程。性能与超时服务端耗时的操作可改用 ASYNC 模式或设置合理超时客户端也要配置request-timeout防止 MCP 调用过久阻塞 AI 应用。安全与密钥管理API Key 不要硬编码、不要提交 Git。用环境变量 /application-local.ymlgitignore/ 配置中心注入。远程部署 SSE 服务时务必考虑鉴权与网络隔离避免服务被滥用。跨平台兼容stdio 模式注意 Windows 与 Linux 的命令差异.cmd后缀、路径分隔符、进程启动方式。生产环境建议用 Linux / Docker 统一运行时。部署方式本地部署stdio把服务端 JAR 放到客户端可访问的机器上通过mcp-servers.json配置即可适合小项目远程部署SSE与部署普通后端 Web 项目一致JAR 容器 / systemd / 云主机适合需要共享的服务Serverless阿里云百炼等平台支持将 MCP 服务部署到函数计算。注意目前主要通过npx/uvx方式部署Java 服务一般需要自行部署或选择兼容方案。六、小结MCP 服务端开发 用Tool写工具 用ToolCallbackProvider注册工具再通过 starter 依赖自动发布为 MCP 服务开发过程与原生工具调用几乎一致。服务端支持 stdio / SSE 两种传输模式通过配置文件即可切换客户端在mcp-servers.jsonstdio或sse.connectionsSSE中登记服务即可调用。MCP 的本质是标准化的工具调用不是 AI 主动调服务而是客户端把工具清单告诉 AIAI 决定调用时由我们的程序执行并回填结果。