SpringAI之MCP 服务端
MCP 概述和客户端示例代码可查看
https://blog.ZEEKLOG.net/weixin_45948519/article/details/157142319?spm=1011.2415.3001.5331
SpringAI官方文档地址
https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html
目录
4、Steam http 使用 SearXNG 实现联网MCP
1、stdio 服务端(使用高德API查询天气信息)
这里示例一个调用高德API获取天气信息的MCP
高德文档地址 https://lbs.amap.com/api/webservice/guide/api-advanced/weatherinfo
1.1 创建stdio工具
官方文档地址 https://docs.spring.io/spring-ai/reference/api/mcp/mcp-stdio-sse-server-boot-starter-docs.html
- 创建一个springboot项目 引入pom依赖
<properties> <java.version>17</java.version> <spring-ai.version>1.1.2</spring-ai.version> </properties> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>- 创建一个service 工具类
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; import java.util.List; @Service public class WeatherService { private final String KEY; private final RestClient restClient; public WeatherService() { this.restClient = RestClient.builder().build(); this.KEY = System.getenv("AMAP_KEY"); } public static void main(String[] args) { WeatherService weatherService = new WeatherService(); String code = weatherService.getAdCode("深圳"); System.out.println(code); String weather = weatherService.getWeather(code); System.out.println(weather); } @Tool(description = "根据城市名称查询对应的城市编码") public String getAdCode(@ToolParam(description = "查询的城市,比如: 北京") String keywords) { if (keywords == null || keywords.isEmpty()) { return "请输入城市名称"; } String url = String.format("https://restapi.amap.com/v3/config/district?keywords=%s&subdistrict=0&key=%s", keywords, KEY); CodeResponse response = this.restClient.get().uri(url).retrieve().body(CodeResponse.class); if (response == null || response.districts() == null || response.districts().isEmpty()) { return String.format("查询不到%s对应的城市编码", keywords); } return response.districts().get(0).adcode(); } @Tool(description = "根据城市编码查询对应城市的天气信息") public String getWeather(@ToolParam(description = "查询的城市编码,比如: 110101") String adCode) { String url = String.format("https://restapi.amap.com/v3/weather/weatherInfo?city=%s&key=%s", adCode, KEY); WeatherResponse response = this.restClient.get().uri(url).retrieve().body(WeatherResponse.class); if (response == null || response.lives() == null || response.lives().isEmpty()) { return String.format("查询不到%s对应城市的天气信息", adCode); } WeatherResponse.Live live = response.lives().get(0); return String.format("当前城市: %s, 天气: %s, 温度: %s 摄氏度, 风向: %s, 风力: %s, 湿度: %s, 更新时间: %s", live.city, live.weather, live.temperature, live.winddirection, live.windpower, live.humidity, live.reporttime); } @JsonIgnoreProperties(ignoreUnknown = true) public record CodeResponse(@JsonProperty("districts") List<Districts> districts) { @JsonIgnoreProperties(ignoreUnknown = true) public record Districts(@JsonProperty("adcode") String adcode, @JsonProperty("name") String name) { } } @JsonIgnoreProperties(ignoreUnknown = true) public record WeatherResponse(@JsonProperty("lives") List<Live> lives) { @JsonIgnoreProperties(ignoreUnknown = true) public record Live(@JsonProperty("province") String province, @JsonProperty("city") String city, @JsonProperty("weather") String weather, @JsonProperty("temperature") String temperature, @JsonProperty("winddirection") String winddirection, @JsonProperty("windpower") String windpower, @JsonProperty("humidity") String humidity, @JsonProperty("reporttime") String reporttime ) { } } }- 注册工具
import com.weimin.springaimcpstdioserver.server.WeatherService; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class SpringAiMcpStdioServerApplication { public static void main(String[] args) { SpringApplication.run(SpringAiMcpStdioServerApplication.class, args); } @Bean public ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder().toolObjects(weatherService).build(); } }- 然后使用maven 打包 mvn package
1.2 测试
- 使用java代码进行测试
import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.ServerParameters; import io.modelcontextprotocol.client.transport.StdioClientTransport; import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.spec.McpSchema; import java.util.Map; public class ClientStdio { public static void main(String[] args) { ServerParameters stdioParams = ServerParameters.builder("java") .args( "-Dfile.encoding=UTF-8", "-Dsun.jnu.encoding=UTF-8", "-jar", // 这是相对于项目的路径 "spring-ai-mcp-stdio-server/target/spring-ai-mcp-stdio-server-0.0.1-SNAPSHOT.jar" ) .env(Map.of("AMAP_KEY", "换成你的API_KEY")) .build(); StdioClientTransport transport = new StdioClientTransport(stdioParams, McpJsonMapper.createDefault()); McpSyncClient client = McpClient.sync(transport).build(); client.initialize(); McpSchema.ListToolsResult toolsList = client.listTools(); System.out.println("所有的工具 = " + toolsList); McpSchema.CallToolResult callToolResult = client.callTool( McpSchema.CallToolRequest.builder() .name("getAdCode") .arguments(Map.of("keywords", "深圳")) .build()); String code = ((McpSchema.TextContent) callToolResult.content().get(0)).text(); System.out.println("得到的城市code = " + code); McpSchema.CallToolResult weatherResult = client.callTool( McpSchema.CallToolRequest.builder() .name("getWeather") .arguments(Map.of("adCode", code)) .build()); String weather = ((McpSchema.TextContent) weatherResult.content().get(0)).text(); System.out.println("天气信息 = " + weather); client.closeGracefully(); } }- 使用 Cherry Studio 进行测试

- 选择从json导入工具
{ "mcpServers": { "weather-server": { "command": "java", "args": [ "-Dfile.encoding=UTF-8", "-Dsun.jnu.encoding=UTF-8", "-jar", "D:\\学习\\spring-ai-mcp-stdio-server\\target\\spring-ai-mcp-stdio-server-0.0.1-SNAPSHOT.jar" ], "env": { "AMAP_KEY": "换成你的API_KEY" } } } }- 启用工具,并对话进行测试

2、SSE 服务端(使用百度翻译API翻译语言)
SSE服务端的示例创建一个翻译的服务端,用来翻译语言,这里调用的是百度翻译平台的API
百度翻译平台地址 https://api.fanyi.baidu.com/product/113
2.1 创建工具服务端
- 创建spring web项目后引入依赖
<properties> <java.version>17</java.version> <spring-ai.version>1.1.2</spring-ai.version> </properties> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> <version>${spring-ai.version}</version> </dependency>- 配置文件配置
server.port=8086 spring.ai.mcp.server.protocol=sse spring.ai.mcp.server.sse-endpoint=/sse- 创建工具类
import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Slf4j @Service public class TranslationService { private static final String appId = "123455"; private static final String securityKey = "44444444"; private final RestClient restClient; public TranslationService() { this.restClient = RestClient.builder().build(); } public static void main(String[] args) throws Exception { TranslationService translationService = new TranslationService(); String result = translationService.translate("hello world", Language.EN, Language.ZH); System.out.println(result); } @Tool(description = "将句子或词从一种语言翻译为另一种语言") public String translate( @ToolParam(description = "需要翻译的句子或词") String text, @ToolParam(description = "句子当前语言标识,如果不清楚属于什么语言请填入 auto") Language languageFrom, @ToolParam(description = "将要翻译的语言标识") Language languageTo) throws Exception { log.info("开始翻译 {} -> {} {}", languageFrom.getDescription(), languageTo.getDescription(), text); Map<String, String> params = buildTranslateParams(text, languageFrom.getCode(), languageTo.getCode()); StringBuilder urlBuilder = new StringBuilder("https://fanyi-api.baidu.com/api/trans/vip/translate"); urlBuilder.append("?"); params.forEach((k, v) -> { log.info("参数: {} = {}", k, v); urlBuilder.append(k).append("=").append(v).append("&"); }); urlBuilder.deleteCharAt(urlBuilder.length() - 1); String url = urlBuilder.toString(); log.info("请求地址: {}", url); TranslationResult result = this.restClient.get().uri(url).retrieve().body(TranslationResult.class); log.info("响应结果: {}", result); if (result != null && result.trans_result != null && !result.trans_result.isEmpty()) { return result.trans_result.stream().map(TranslationMsg::dst).collect(Collectors.joining(" ")); } return "转换失败"; } private Map<String, String> buildTranslateParams(String query, String from, String to) throws Exception { Map<String, String> params = new HashMap<String, String>(); params.put("q", query); params.put("from", from); params.put("to", to); params.put("appid", appId); // 随机数 String salt = String.valueOf(System.currentTimeMillis()); params.put("salt", salt); // 签名 String src = appId + query + salt + securityKey; // 加密前的原文 params.put("sign", md5(src)); return params; } public static String md5(String str) throws NoSuchAlgorithmException { MessageDigest messageDigest = MessageDigest.getInstance("MD5"); messageDigest.update(str.getBytes(StandardCharsets.UTF_8)); byte[] resultByteArray = messageDigest.digest(); return byteArrayToHex(resultByteArray); } private static final char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; private static String byteArrayToHex(byte[] byteArray) { char[] resultCharArray = new char[byteArray.length * 2]; int index = 0; for (byte b : byteArray) { resultCharArray[index++] = hexDigits[b >>> 4 & 0xf]; resultCharArray[index++] = hexDigits[b & 0xf]; } return new String(resultCharArray); } public record TranslationResult(String from, String to, List<TranslationMsg> trans_result) { } public record TranslationMsg(String src, String dst) { } @Getter public enum Language { AUTO("auto", "自动检测"), ZH("zh", "中文"), EN("en", "英语"), YUE("yue", "粤语"), WYW("wyw", "文言文"), JP("jp", "日语"), KOR("kor", "韩语"), FRA("fra", "法语"), SPA("spa", "西班牙语"), TH("th", "泰语"), ARA("ara", "阿拉伯语"), RU("ru", "俄语"), PT("pt", "葡萄牙语"), DE("de", "德语"), IT("it", "意大利语"), EL("el", "希腊语"), NL("nl", "荷兰语"), PL("pl", "波兰语"), BUL("bul", "保加利亚语"), EST("est", "爱沙尼亚语"), DAN("dan", "丹麦语"), FIN("fin", "芬兰语"), CS("cs", "捷克语"), ROM("rom", "罗马尼亚语"), SLO("slo", "斯洛文尼亚语"), SWE("swe", "瑞典语"), HU("hu", "匈牙利语"), CHT("cht", "繁体中文"), VIE("vie", "越南语"); private final String code; private final String description; Language(String code, String description) { this.code = code; this.description = description; } public static Language fromCode(String code) { for (Language lang : values()) { if (lang.code.equals(code)) { return lang; } } throw new IllegalArgumentException("Unknown language code: " + code); } @Override public String toString() { return code; } } }- 注册工具
import com.weimin.springaimcpsseserver.service.TranslationService; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class SpringAiMcpSseServerApplication { public static void main(String[] args) { SpringApplication.run(SpringAiMcpSseServerApplication.class, args); } @Bean public ToolCallbackProvider weatherToolCallbackProvider(TranslationService translationService) { return MethodToolCallbackProvider.builder().toolObjects(translationService).build(); } }2.2 测试
- 启动springboot项目 使用 Cherry Studio 进行添加MCP

- 使用 Cherry Studio 进行测试

3、Steam http 服务端 (邮件发送MCP)
SpringAI文档地址 https://docs.spring.io/spring-ai/reference/api/mcp/mcp-streamable-http-server-boot-starter-docs.html
这里示例的是一个简单的邮件发送MCP示例,使用的是163邮箱,要提前开启SMTP
3.1 创建工具服务端
- 导入依赖
<properties> <java.version>17</java.version> <spring-ai.version>1.1.2</spring-ai.version> </properties> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> <version>${spring-ai.version}</version> </dependency> <!-- 邮件依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <!-- md转html --> <dependency> <groupId>com.vladsch.flexmark</groupId> <artifactId>flexmark-all</artifactId> <version>0.64.8</version> </dependency>- 配置文件配置
# server.port=8081 # spring.ai.mcp.server.protocol=streamable #使用webflux需要配置这个 #spring.main.web-application-type=reactive spring.ai.mcp.server.streamable-http.mcp-endpoint=/mcp # # 邮件服务器地址 spring.mail.host=smtp.163.com # 端口 spring.mail.port=465 # 邮箱账号 [email protected] # 授权码 spring.mail.password=YPuKQXPuGACgiK2M # 编码 spring.mail.default-encoding=UTF-8 # SSL配置 spring.mail.properties.mail.smtp.ssl.enable=true spring.mail.properties.mail.smtp.auth=true- 定义一个邮箱工具
import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.data.MutableDataSet; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; @Slf4j @Service @RequiredArgsConstructor public class EmailService { private final JavaMailSender mailSender; @Tool(description = "给指定邮箱发送邮件信息") public String sendSimpleEmail( @ToolParam(description = "收件人邮箱") String[] to, @ToolParam(description = "发送邮件的标题/主题") String subject, @ToolParam(description = "发送邮件的消息/正文内容") String content, @ToolParam(description = "邮件类型 有 MARKDOWN、HTML、TEXT 三种类型") EmailType emailType) throws MessagingException { log.info("开始发送邮件 {} {} {} {}", to, subject, content, emailType); if (mailSender instanceof JavaMailSenderImpl senderImpl) { String username = senderImpl.getUsername(); if (username != null) { // 创建MIME消息对象(支持复杂内容) MimeMessage mimeMessage = mailSender.createMimeMessage(); // 创建MimeMessageHelper(true表示支持多部分消息,UTF-8解决中文乱码) MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); helper.setFrom(username); helper.setTo(to); helper.setSubject(subject); if (emailType == EmailType.MARKDOWN) { helper.setText(convertToHtml(content), true); } else if (emailType == EmailType.HTML) { helper.setText(content, true); } else { helper.setText(content); } mailSender.send(mimeMessage); return "邮件发送成功"; } } return "参数错误,发送失败"; } // @Tool(description = "将Markdown格式的文本转换为HTML格式") public static String convertToHtml( @ToolParam(description = "Markdown格式的文本") String markdownStr) { log.info("开始将Markdown格式的文本转换为HTML格式 {}", markdownStr); MutableDataSet dataSet = new MutableDataSet(); return HtmlRenderer.builder(dataSet).build() .render(Parser.builder(dataSet).build() .parse(markdownStr)); } public enum EmailType { MARKDOWN, HTML, TEXT } }- 注册工具
import com.weimin.springaimcpstreamserver.service.EmailService; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class SpringAiMcpStreamServerApplication { public static void main(String[] args) { SpringApplication.run(SpringAiMcpStreamServerApplication.class, args); } @Bean public ToolCallbackProvider emailTools(EmailService emailService) { return MethodToolCallbackProvider.builder().toolObjects(emailService).build(); } }3.2 测试
- 使用 Cherry Studio 进行工具设置

- 测试

4、Steam http 使用 SearXNG 实现联网MCP
4.1 环境准备
SearXNG 官方文档 https://docs.searxng.org/
Docker安装文档地址 https://docs.searxng.org/admin/installation-docker.html#installation-container
DockerHub 地址 https://hub.docker.com/r/searxng/searxng
- 使用docker 安装SearXNG
docker pull searxng/searxng:latest mkdir -p ./searxng/config/ ./searxng/data/ cd ./searxng/ docker run --name searxng -d \ -p 8888:8080 \ -v "./config/:/etc/searxng/" \ -v "./data/:/var/cache/searxng/" \ docker.io/searxng/searxng:latest- 使用浏览器验证访问 http://127.0.0.1:8888
- 修改配置文件 config/settings.yml 开启输出json格式

- 根据API构建MCP api文件地址 https://docs.searxng.org/dev/search_api.html
- 发送链接测试API
curl 'http://127.0.0.1:8888/search?q=人工智能&format=json&engines=bing&categories=general&language=zh-CN'- 参数示例
- q 搜索词,比如这里的搜索词是人工智能
- format 结果的输出格式,需要在配置文件开启,这里使用的是json
- engines 指定使用搜索引擎,逗号拼接,需要在配置文件开启。可在文档查看枚举https://docs.searxng.org/user/configured_engines.html#configured-engines
- categories 指定类别,这里使用的是综合搜索,可指定为新闻news等
- language 指定输出的语言,这里是中文简体。枚举可在链接查看 https://github.com/searxng/searxng/blob/master/searx/sxng_locales.py
4.2 创建MCP工具服务端
- 配置配置文件相关信息
# server.port=8081 # spring.ai.mcp.server.protocol=streamable #使用webflux需要配置这个 #spring.main.web-application-type=reactive spring.ai.mcp.server.streamable-http.mcp-endpoint=/mcp- 构建工具服务类
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; import java.util.List; import java.util.stream.Collectors; @Slf4j @Service public class SearXNGService { private final RestClient restClient; public SearXNGService() { this.restClient = RestClient.builder().baseUrl("http://127.0.0.1:8888").build(); } @Tool(description = "根据关键字联网搜索") public String search(@ToolParam(description = "搜索关键字") String keyword) { String url = "/search?q=" + keyword + "&format=json&engines=bing&categories=general&language=zh-CN"; log.info("开始查询:{}", url); Response response = restClient.get().uri(url).retrieve().body(Response.class); if (response == null || response.results() == null || response.results().isEmpty()) { return "没有找到结果"; } String result = response.results().stream().map((item) -> String.format("标题: %s\n内容: %s\n链接: %s\n搜索引擎: %s\n\n", item.title(), item.url(), item.content(), item.engine())).collect(Collectors.joining("\n")); log.info("查询结果:{}", result); return result; } @JsonIgnoreProperties(ignoreUnknown = true) public record Response(@JsonProperty("query") String query, @JsonProperty("number_of_results") Integer number_of_results, @JsonProperty("results") List<Results> results) { @JsonIgnoreProperties(ignoreUnknown = true) public record Results(@JsonProperty("url") String url, @JsonProperty("title") String title, @JsonProperty("content") String content, @JsonProperty("engine") String engine ) { } } }- 注册工具
import com.weimin.springaimcpstreamserver.service.SearXNGService; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class SpringAiMcpStreamServerApplication { public static void main(String[] args) { SpringApplication.run(SpringAiMcpStreamServerApplication.class, args); } @Bean public ToolCallbackProvider emailTools(SearXNGService searXNGService) { return MethodToolCallbackProvider.builder().toolObjects(searXNGService).build(); } }4.3 测试
- 使用 Cherry Studio 测试
