【注意避坑】基于Spring AI 开发本地天气 mcp server,通义灵码测试MCP server连接不稳定,cherry studio连接报错

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/88318.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/88318.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Paimon LSM Tree Compaction 策略

压缩怎么进行的这里的操作都是KValue&#xff0c;内部有row kind&#xff0c;标记了删除和插入MergeTreeCompactManager 是 Paimon 中 Merge-Tree 结构压缩任务的总调度中心。它的核心职责就是监控文件的层级状态&#xff08;Levels&#xff09;&#xff0c;并在合适的时机&…

小米路由器3C刷OpenWrt,更换系统/变砖恢复 指南

基础篇看这里&#xff1a; 小米路由器3C如何安装OpenWrt官方编译的ROM - 哔哩哔哩 小米路由器 3C 刷入 Breed 和 OpenWrt - Snoopy1866 - 博客园 一、路由器注入 如果按照上面的文章&#xff0c; telnet、ftp一直连接失败,那么可以尝试看 这里&#xff1a; 获取路由器root权…

Spring Boot 项目启动时按需初始化加载数据

1、新建类&#xff0c;类上添加注解 Component &#xff0c;该类用于在项目启动时处理数据加载任务&#xff1b; 2、该类实现 ApplicationRunner 接口&#xff0c;并重写 run 方法&#xff1b; 3、在重写的 run 方法里处理数据加载任务&#xff1b; 注意&#xff1a; 有定时加载…

MCP快速入门—快速构建自己的服务器

引言 随着大语言模型(LLM)技术的快速发展&#xff0c;如何扩展其能力边界成为开发者关注的重点。MCP(Model Capability Protocol)作为一种协议标准&#xff0c;允许开发者构建自定义服务器来增强LLM的功能。 正文内容 1. MCP核心概念与技术背景 MCP服务器主要提供三种能力类…

Vue 事件总线深度解析:从实现原理到工程实践

在 Vue 组件通信体系中&#xff0c;事件总线&#xff08;Event Bus&#xff09;是处理非父子组件通信的轻量解决方案。本文将从技术实现细节、工程化实践、内存管理等维度展开&#xff0c;结合源码级分析与典型场景&#xff0c;带你全面掌握这一核心技术点。​一、事件总线的技…

CMake Qt静态库中配置qrc并使用

CMake Qt序言环境代码序言 看网上这资料较少&#xff0c;且我理解起来有歧义&#xff0c;特地补充 环境 CMake&#xff1a;3.29.2 Qt&#xff1a;5.15.2 MSVC&#xff1a;2022 IDE&#xff1a;QtCreator 代码 方式一&#xff1a; 在CMakeLists.txt里&#xff0c;add_libr…

记录一下:成功部署k8s集群(部分)

前提条件&#xff1a;安装了containerd、docker 关闭了firewalld、selinux 配置了时间同步服务 chronyd 关闭swap分区等1、在控制节点、工作节点&#xff0c;安装kubelet、kubeadm、kubectlyum install -y kubelet-1.26.0 kubeadm-1.26.0 kubectl-1.26.0 …

Idea如何解决包冲突

Idea如何解决包冲突1.Error信息&#xff1a;JAR列表。 在扫描期间跳过不需要的JAR可以缩短启动时间和JSP编译时间。SLF4J: Class path contains multiple SLF4J bindings.SLF4J: Found binding in [jar:file:/E:/javapojects/stww-v4-gjtwt-seal/target/stww--v4-platform-proj…

python 协程学习笔记

目录 python 协程 通俗理解 Python 的 asyncio 协程&#xff0c;最擅长的是&#xff1a; 批量下载文件的例子&#xff1a; 协程的优势&#xff1a; python 协程 通俗理解 def my_coroutine():print("开始")x yield 1print("拿到了&#xff1a;", x)yi…

【学习笔记】蒙特卡洛仿真与matlab实现

概述 20 世纪 40 年代&#xff0c;由于电子计算机的出现&#xff0c; 借助计算机可以实现大量的随机抽样试验&#xff0c;为利用随机试验方法解决实际问题提供了便捷。 非常具代表性的例子是&#xff0c; 美国在第二次世界大战期间研制原子弹的“曼哈顿计划”中&#xff0c;为了…

HTTP/3.x协议详解:基于QUIC的下一代Web传输协议

一、HTTP/3协议概述 HTTP/3是超文本传输协议&#xff08;HTTP&#xff09;的第三个正式版本&#xff0c;由IETF&#xff08;互联网工程任务组&#xff09;于2022年正式标准化&#xff08;RFC 9114&#xff09;。其核心创新在于完全基于QUIC协议替代传统TCP&#xff0c;结合UDP…

【SQL】使用UPDATE修改表字段的时候,遇到1054 或者1064的问题怎么办?

我在使用python连接sql修改表格的时间字段的时候&#xff0c;遇到这样一个问题&#xff1a;ProgrammingError: (pymysql.err.ProgrammingError) (1064, “You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the ri…

【字节跳动】数据挖掘面试题0013:怎么做男女二分类问题, 从抖音 app 提供的内容中。

文章大纲 🔍 一、问题定义与数据基础数据源及预处理:⚙️ 二、特征工程方案1. 文本特征2. 视觉特征3. 音频与行为特征4. 上下文特征🤖 三、模型选型与训练1. 基础模型对比2. 多模态融合模型3. 训练技巧📊 四、评估与优化策略1. 评估指标2. 典型问题优化3. 算法偏差控制�…

HTTP请求走私漏洞

一、漏洞定义与核心原理HTTP请求走私&#xff08;HTTP Request Smuggling&#xff09;是一种利用前端服务器&#xff08;如代理、负载均衡器&#xff09;与后端服务器在解析HTTP请求时的不一致性&#xff0c;绕过安全机制并执行恶意操作的攻击技术。其核心在于混淆请求边界&…

Javaweb - 10.1 Servlet

目录 Servlet 简介 动态资源和静态资源 Servlet 简介 Servlet 开发流程 目标 开发过程 开发一个 web 类型的 module 开发一个 form 表单 开发一个 UserServlet 在 web..xml 为 userServlet 配置请求路径 Edit Configurations 启动项目 完&#xff01; Servlet 简介…

手机能用酒精擦吗?

对于电视、电脑屏幕来说&#xff0c;为了避免反光、改善显示效果&#xff0c;会在屏幕表面覆上一层“抗反射涂层”。不同厂商设计的涂层材料并不相同&#xff0c;酒精作为良好的溶剂&#xff0c;确实会损坏可溶的涂层。手机作为触控产品&#xff0c;通常会在屏幕表面增加“疏水…

【图像处理基石】图像超分辨率有哪些研究进展值得关注?

近年来&#xff0c;图像超分辨率&#xff08;SR&#xff09;领域在深度学习技术的推动下取得了显著进展&#xff0c;尤其在模型架构优化、计算效率提升和真实场景适应性等方面涌现出诸多创新。以下是基于最新研究的核心进展梳理&#xff1a; 一、高效大图像处理&#xff1a;像素…

Windows系统下WSL从C盘迁移方案

原因&#xff1a;一开始装WSL的时候放在了C盘&#xff0c;这下好了&#xff0c;跑了几个深度学习模型训练后&#xff0c;C盘快满了&#xff0c;这可怎么办&#xff1f;可愁坏了。没关系&#xff0c;山人自有妙计。我们将WSL迁移到D盘或者E盘呀。一.迁移操作步骤前期准备&#x…

金融时间序列机器学习训练前的数据格式验证系统设计与实现

金融时间序列机器学习训练前的数据格式验证系统设计与实现 前言 在机器学习项目中&#xff0c;数据质量是决定模型成功的关键因素。特别是在金融时间序列分析领域&#xff0c;原始数据往往需要经过复杂的预处理才能用于模型训练。本文将详细介绍一个完整的数据格式验证系统&…

cocos2dx3.x项目升级到xcode15以上的iconv与duplicate symbols报错问题

cocos2dx3.x项目升级xcode15以上后会有几处报错。1. CCFontAtlas.cpp文件下的iconv与iconv_close的报错。修改如下&#xff1a;// iconv_close(_iconv);iconv_close((iconv_t)_iconv);iconv((iconv_t)_iconv, (char**)&pin, &inLen, &pout, &outLen); /…