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