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

springboot 版本: 3.5.4
cherry studio版本:1.4.7
通义灵码版本: 2.5.13


在这里插入图片描述



问题描述:

基于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 调用的工具
    @Bean
    public 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; // 1MB

    private 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>() {
                    @Override
                    protected 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;
        }

        @Override
        protected 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();
            }
        }

        @Override
        public 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工具依旧报错


参考链接

### MCP 本地部署与数据库连接配置 #### 安装依赖项 为了在 Cherry Studio 中实现 MCP本地部署,首先需要安装 Python 和 Node.js 环境。这是由于 MCP 使用 STDIO 协议时会调用这些工具来处理数据流和上下文交互[^2]。 以下是所需软件及其版本建议: - **Python**: 推荐使用最新稳定版(如 Python 3.9 或更高版本)。 - **Node.js**: 推荐 LTS 版本(如 v16.x),以确保兼容性和稳定性。 可以通过以下命令验证已安装的版本: ```bash python --version node --version ``` 如果尚未安装上述组件,则可以分别通过官方渠道下载并完成安装。 --- #### 配置基础环境 对于基于 STDIO 类型的基础配置,在启动之前需要设置一些必要的参数。具体操作如下: 1. 创建一个新的项目目录用于存储 MCP 文件及相关配置; 2. 初始化 `mcp.json` 文件作为核心配置入口; 示例结构可能类似于下面的内容: ```json { "protocol": "stdio", "contextPath": "./contexts" } ``` 此 JSON 对象定义了使用的协议以及模型上下文路径的位置[^2]。 --- #### 数据库集成方案 当涉及到数据库的操作时,通常有两种方式可选——直接嵌入 SQL 查询逻辑或者借助 ORM 工具简化开发流程。这里提供一种常见的做法即利用 SQLAlchemy 库管理关系型数据库中的表单映射关系[^1]。 假设目标是要链接 MySQL 数据源,那么第一步便是引入相应的驱动程序包: ```bash pip install mysql-connector-python sqlalchemy ``` 接着编写一段简单的脚本来展示如何初始化引擎并与指定实例建立通信链路: ```python from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String engine = create_engine('mysql+mysqlconnector://username:password@localhost/dbname') metadata = MetaData() users_table = Table( 'users', metadata, Column('id', Integer, primary_key=True), Column('name', String(50)), Column('age', Integer) ) metadata.create_all(engine) print("Database and table created successfully.") ``` 以上代片段展示了创建名为 users 表的过程,并设置了三个字段:ID、姓名(name) 及年龄(age)[^1]。 --- #### 启动服务端口监听 最后一步就是激活整个框架使其能够响应外部请求。这一般涉及设定特定端口号以及其他网络选项以便客户端顺利接入。例如: ```javascript const express = require('express'); const app = express(); app.get('/data', function(req, res){ // 处理获取数据业务... }); // 设置监听地址为 localhost 并绑定到自定端口上 let serverPort = process.env.PORT || 8080; app.listen(serverPort, () => { console.log(`Server running at http://localhost:${serverPort}/`); }); ``` 这样就可以让应用处于待命状态等待进一步指令执行啦! --- 问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陌上少年,且听这风吟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值