springboot 版本: 3.5.4
cherry studio版本:1.4.7
通义灵码版本: 2.5.13
文章目录
- 问题描述:
- 1. 通义灵码添加mcp server ,配置测试
- 2. cherry studio工具添加mcp server ,配置测试
- 项目源代码:
- 解决方案:
- 1. 项目改造
- 2. 项目重新打包测试
- 参考链接
问题描述:
基于Spring AI 开发本地天气 mcp server,该mcp server 采用stdio模式与MCP client 通信,本地cherry studio工具测试连接报错,通义灵码测试MCP server连接极易不稳定
引用依赖如下:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>1.0.0-M7</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-server</artifactId><version>1.0.0-M7</version></dependency>
application.yml 配置如下:
项目打包后,
1. 通义灵码添加mcp server ,配置测试
mcp server 配置信息如下:
测试连接效果如下:
demo111 – pom.xml (demo111) 2025-07-05 11-19-32
2. cherry studio工具添加mcp server ,配置测试
配置信息如下:
测试连接效果如下:
项目源代码:
①天气服务
package com.example.demo111.service;import com.example.demo111.model.CurrentCondition;
import com.example.demo111.model.WeatherResponse;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;import javax.net.ssl.SSLException;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;@Service
public class WeatherService1 {private static final String BASE_URL = "https://wttr.in";private final RestClient restClient;public WeatherService1() {this.restClient = RestClient.builder().baseUrl(BASE_URL).defaultHeader("Accept", "application/geo+json").defaultHeader("User-Agent", "WeatherApiClient/1.0 (your@email.com)").build();}@Tool(description = "Get current weather information for a China city. Input is city name (e.g. 杭州, 上海)")public String getWeather(String cityName) {WeatherResponse response = restClient.get().uri("/{city_name}?format=j1", cityName).retrieve().body(WeatherResponse.class);if (response != null && response.getCurrent_condition() != null && !response.getCurrent_condition().isEmpty()) {CurrentCondition currentCondition = response.getCurrent_condition().get(0);String result = String.format("""城市: %s天气情况: %s气压: %s(mb)温度: %s°C (Feels like: %s°C)湿度: %s%%降水量:%s (mm)风速: %s km/h (%s)能见度: %s 公里紫外线指数: %s观测时间: %s""",cityName,currentCondition.getWeatherDesc().get(0).getValue(),currentCondition.getPressure(),currentCondition.getTemp_C(),currentCondition.getFeelsLikeC(),currentCondition.getHumidity(),currentCondition.getPrecipMM(),currentCondition.getWindspeedKmph(),currentCondition.getWinddir16Point(),currentCondition.getVisibility(),currentCondition.getUvIndex(),currentCondition.getLocalObsDateTime());return result;} else {return "无法获取天气信息,请检查城市名称是否正确或稍后重试。";}}
}
②数据模型
@Data
public class CurrentCondition {private String feelsLikeC;private String humidity;private String localObsDateTime;private String precipMM;private String pressure;private String temp_C;private String uvIndex;private String visibility;private List<WeatherDesc> weatherDesc;private String winddir16Point;private String windspeedKmph;
}
@Data
public class WeatherDesc {private String value;}
@Data
public class WeatherResponse {private List<CurrentCondition> current_condition;
}
③MCP SERVER配置
@Configuration
public class McpConfig {//@Tool注解的方法注册为可供 LLM 调用的工具@Beanpublic ToolCallbackProvider weatherTools(WeatherService1 weatherService) {return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();}
}
解决方案:
1. 项目改造
将基于Tomcat实现mcp server换成基于netty实现
注释或排除Tomcat依赖,引入netty依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-reactor-netty</artifactId></dependency>
天气服务重新基于netty改造实现
@Service
public class WeatherService {private static final Logger logger = LoggerFactory.getLogger(WeatherService.class);private static final String BASE_URL = "https://wttr.in";private static final ObjectMapper objectMapper = new ObjectMapper();private static final int HTTP_TIMEOUT_SECONDS = 10;private static final int MAX_RESPONSE_SIZE = 1024 * 1024; // 1MBprivate final SslContext sslContext;public WeatherService() throws SSLException {this.sslContext = SslContextBuilder.forClient().build();}@Tool(description = "Get current weather information for a China city. Input is city name (e.g. 杭州, 上海)")public String syncGetWeather(String cityName) {try {return getWeather(cityName).join();} catch (Exception e) {logger.error("Failed to get weather for city: {}", cityName, e);return "获取天气信息失败,请稍后重试或检查城市名称是否正确。";}}public CompletableFuture<String> getWeather(String cityName) {CompletableFuture<String> future = new CompletableFuture<>();if (cityName == null || cityName.trim().isEmpty()) {future.completeExceptionally(new IllegalArgumentException("城市名称不能为空"));return future;}EventLoopGroup group = new NioEventLoopGroup();try {Bootstrap bootstrap = configureBootstrap(group, future);URI uri = buildWeatherUri(cityName);ChannelFuture channelFuture = bootstrap.connect(uri.getHost(), 443);channelFuture.addListener((ChannelFutureListener) f -> {if (f.isSuccess()) {sendWeatherRequest(f.channel(), uri);} else {handleConnectionFailure(future, f.cause(), group);}});} catch (Exception e) {handleInitializationError(future, e, group);}return future;}private Bootstrap configureBootstrap(EventLoopGroup group, CompletableFuture<String> future) {return new Bootstrap().group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(sslContext.newHandler(ch.alloc()));pipeline.addLast(new HttpClientCodec());pipeline.addLast(new ReadTimeoutHandler(HTTP_TIMEOUT_SECONDS, TimeUnit.SECONDS));pipeline.addLast(new HttpObjectAggregator(MAX_RESPONSE_SIZE));pipeline.addLast(new WeatherResponseHandler(future, group));}});}URI buildWeatherUri(String cityName) throws Exception {String encodedCityName = URLEncoder.encode(cityName.trim(), StandardCharsets.UTF_8.toString());return new URI(BASE_URL + "/" + encodedCityName + "?format=j1");}void sendWeatherRequest(Channel channel, URI uri) {FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,HttpMethod.GET,uri.getRawPath() + "?format=j1" // 确保参数在路径中);HttpHeaders headers = request.headers();headers.set(HttpHeaderNames.HOST, uri.getHost());headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);headers.set(HttpHeaderNames.ACCEPT, "application/json");headers.set(HttpHeaderNames.USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");headers.set(HttpHeaderNames.ACCEPT_LANGUAGE, "zh-CN");channel.writeAndFlush(request).addListener(f -> {if (!f.isSuccess()) {logger.error("Failed to send weather request", f.cause());}});}void handleConnectionFailure(CompletableFuture<String> future, Throwable cause, EventLoopGroup group) {logger.error("Connection to weather service failed", cause);future.completeExceptionally(new RuntimeException("无法连接到天气服务"));group.shutdownGracefully();}void handleInitializationError(CompletableFuture<String> future, Exception e, EventLoopGroup group) {logger.error("Weather service initialization failed", e);future.completeExceptionally(e);group.shutdownGracefully();}private static class WeatherResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> {private final CompletableFuture<String> future;private final EventLoopGroup group;public WeatherResponseHandler(CompletableFuture<String> future, EventLoopGroup group) {this.future = future;this.group = group;}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse response) {try {if (response.status().code() != 200) {String errorMsg = String.format("天气服务返回错误状态码: %d", response.status().code());future.complete(errorMsg);return;}String json = response.content().toString(io.netty.util.CharsetUtil.UTF_8);logger.debug("Received JSON: {}", json); // 记录原始JSON// 配置ObjectMapper忽略未知属性ObjectMapper objectMapper = new ObjectMapper();objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);// 先验证JSON格式JsonNode jsonNode = objectMapper.readTree(json);WeatherResponse weatherResponse = objectMapper.treeToValue(jsonNode, WeatherResponse.class);// WeatherResponse weatherResponse = objectMapper.readValue(json, WeatherResponse.class);if (weatherResponse.getCurrent_condition() == null || weatherResponse.getCurrent_condition().isEmpty()) {future.complete("无法获取天气信息,请检查城市名称是否正确或稍后重试。");return;}CurrentCondition condition = weatherResponse.getCurrent_condition().get(0);System.out.println("condition = " + condition);String result = formatWeatherInfo(condition);future.complete(result);} catch (Exception e) {future.completeExceptionally(new RuntimeException("解析天气数据失败", e));} finally {ctx.close();group.shutdownGracefully();}}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {future.completeExceptionally(new RuntimeException("获取天气信息时发生错误", cause));ctx.close();group.shutdownGracefully();}private String formatWeatherInfo(CurrentCondition condition) {return String.format("""天气情况: %s温度: %s°C (体感温度: %s°C)湿度: %s%%气压: %s mb降水量: %s mm风速: %s km/h (%s方向)能见度: %s 公里紫外线指数: %s观测时间: %s""",condition.getWeatherDesc().get(0).getValue(),condition.getTemp_C(),condition.getFeelsLikeC(),condition.getHumidity(),condition.getPressure(),condition.getPrecipMM(),condition.getWindspeedKmph(),condition.getWinddir16Point(),condition.getVisibility(),condition.getUvIndex(),condition.getLocalObsDateTime());}}
}
2. 项目重新打包测试
通义灵码测试效果如下:
cherry studio 工具测试如下:
emmme,cherry studio工具依旧报错
参考链接
- 速看!新版SpringAI的2个致命问题
- Github cherry studio 报错issue