#8、SpringAI MCP 服务端开发实战(图片搜索)
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 前置准备
开始之前,请确认环境满足以下要求:
| 项目 | 要求 |
|---|---|
| JDK | 17 及以上 |
| Maven | 3.6+ |
| Spring Boot | 3.4.x(与 Spring AI 1.0.0-M6 匹配) |
| Spring AI | 1.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 module:imgSearch_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,方便后续切换测试):
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId><version>1.0.0-M6</version></dependency>再补上 Hutool 工具库(发起 HTTP 请求、解析 JSON 用)与 Lombok:
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.25</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>版本提示:
1.0.0-M6阶段的服务端 starter 坐标形如spring-ai-mcp-server-webmvc-spring-boot-starter;Spring 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 Key:
server: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_URL="https://api.pexels.com/v1/search";@Tool(description="search image from web")publicStringsearchImage(@ToolParam(description="Search query keyword")Stringquery){try{// 多张图片 URL 用逗号分隔,便于客户端/AI 解析returnString.join(",",searchMediumImages(query));}catch(Exceptione){return"Error search image: "+e.getMessage();}}/** * 搜索图片列表 * * @param query 搜索关键词 * @return 图片 URL 列表 */publicList<String>searchMediumImages(Stringquery){// 设置请求头(Pexels 要求将 API Key 直接放在 Authorization 头,无需 Bearer 前缀)Map<String,String>headers=newHashMap<>();headers.put("Authorization",apiKey);// 设置请求参数(query 必填;可按需补充 page、per_page 等)Map<String,Object>params=newHashMap<>();params.put("query",query);params.put("per_page",10);// 发送 GET 请求Stringresponse=HttpUtil.createGet(API_URL).addHeaders(headers).form(params).execute().body();// 解析响应 JSON:photos[] 数组 → 每项的 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(){Stringresult=imageSearchTool.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 客户端依赖:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId><version>1.0.0-M6</version></dependency>实际开发中,你也可以按需添加 WebFlux 支持,但传输方式要与服务端模式匹配(stdio 客户端用 Java HttpClient,SSE 客户端可选用 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.stdio=true","-Dspring.main.web-application-type=none","-Dlogging.pattern.console=","-jar","imgSearch_mcpServer/target/imgSearch_mcpServer-0.0.1-SNAPSHOT.jar"],"env":{}}}}要点说明:
amap-maps是上一章配置的高德 MCP,保留即可,两者可共存;-Dspring.ai.mcp.server.stdio=true让服务端以 stdio 模式运行;-Dspring.main.web-application-type=none关闭 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.json(3)启动项目,发起对话
沿用上一章的doChatWithMcp方法(内部通过ToolCallbackProvider把 MCP 工具注入ChatClient)
(4)运行效果
以 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,local(application-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 对比小结
| 对比维度 | stdio | SSE |
|---|---|---|
| 传输方式 | 标准输入 / 输出(子进程) | 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二选一,把另一份注释掉;服务端 Profile(
sse/stdio)同样只激活一份。工具未加载(
functionCallbacks为空):检查服务端ToolCallbackProviderBean 是否已注册、依赖是否引入完整、JAR 是否已重新打包;再看客户端启动日志中 MCP 连接的tools/list是否正常(参考上一章的经验)。@ToolParam报"找不到符号":Spring AI 版本差异导致。1.0.0-M6使用@ToolParam,GA 版本后更名为@ToolParameter,请按依赖版本替换。Windows 下命令找不到:在 Windows 下
npx需写成npx.cmd(高德 MCP 场景);服务端用java命令时请确认 JDK 已加入 PATH。
五、最佳实践与部署建议
慎用 MCP:MCP 不是银弹,本质就是"标准化的工具调用"。如果只是应用内部自用、不需要共享的工具,直接用原生
@Tool即可,能省去打包、部署、进程管理的成本。建议"能不用就不用",先用工具调用,确有共享 / 生态诉求时再转 MCP。传输模式选型:stdio 适合本地小型项目,安全且性能好;SSE 适合独立服务、多客户端共享的中大型项目。服务端开发时推荐引入 WebMVC 版本,一套代码两种模式随意切换。
工具描述要清晰:
@Tool、@ToolParam的description要写清楚用途、参数含义,AI 才能准确判断何时调用、传什么参数。注意容错:工具方法内部捕获所有可能异常并返回友好错误信息(本章示例已体现),避免一次失败拖垮整个对话流程。
性能与超时:服务端耗时的操作可改用 ASYNC 模式,或设置合理超时;客户端也要配置
request-timeout,防止 MCP 调用过久阻塞 AI 应用。安全与密钥管理:API Key 不要硬编码、不要提交 Git。用环境变量 /
application-local.yml(gitignore)/ 配置中心注入。远程部署 SSE 服务时,务必考虑鉴权与网络隔离,避免服务被滥用。跨平台兼容:stdio 模式注意 Windows 与 Linux 的命令差异(
.cmd后缀)、路径分隔符、进程启动方式。生产环境建议用 Linux / Docker 统一运行时。部署方式:
- 本地部署(stdio):把服务端 JAR 放到客户端可访问的机器上,通过
mcp-servers.json配置即可,适合小项目; - 远程部署(SSE):与部署普通后端 Web 项目一致(JAR + 容器 / systemd / 云主机),适合需要共享的服务;
- Serverless:阿里云百炼等平台支持将 MCP 服务部署到函数计算。注意目前主要通过
npx/uvx方式部署,Java 服务一般需要自行部署或选择兼容方案。
- 本地部署(stdio):把服务端 JAR 放到客户端可访问的机器上,通过
六、小结
- MCP 服务端开发 = 用
@Tool写工具 + 用ToolCallbackProvider注册工具,再通过 starter 依赖自动发布为 MCP 服务,开发过程与原生工具调用几乎一致。 - 服务端支持 stdio / SSE 两种传输模式,通过配置文件即可切换;客户端在
mcp-servers.json(stdio)或sse.connections(SSE)中登记服务即可调用。 - MCP 的本质是"标准化的工具调用":不是 AI 主动调服务,而是客户端把工具清单告诉 AI,AI 决定调用时由我们的程序执行并回填结果。
