spring-ai-alibaba第五章阿里dashscope集成mcp远程天气查询tools

1、之前实现过百度翻译的tools,什么是mcp,个人理解 这里在代码的表现形式上就是把工具和AI程序分开,AI程序通过远程调用mcp-tools服务端实现远程tools的调用

spring-ai-alibaba应用程序:

集成阿里大模型,是mcp客户端,通过调用mcp服务端实现天气查询,等于把集成在程序中的tools分离出,通过远程调用的方式来调用tools

mcp服务端:

远程tools,实现方式类似本地tools,此处实现天气tools

2、MCP服务端 天气查询工具实现代码

mcp服务端 pom文件

java 复制代码
  <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>
            <version>${spring-ai.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
    </dependencies>

mcp服务端 天气查询 tools

java 复制代码
/*
 * Copyright 2025-2026 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @author brianxiadong
 */

package org.springframework.ai.mcp.sample.server;

import java.util.List;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

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 org.springframework.web.client.RestClientException;

/**
 * 利用OpenMeteo的免费天气API提供天气服务
 * 该API无需API密钥,可以直接使用
 */
@Service
public class OpenMeteoService {

    // OpenMeteo免费天气API基础URL
    private static final String BASE_URL = "https://api.open-meteo.com/v1";

    private final RestClient restClient;

    public OpenMeteoService() {
        this.restClient = RestClient.builder()
                .baseUrl(BASE_URL)
                .defaultHeader("Accept", "application/json")
                .defaultHeader("User-Agent", "OpenMeteoClient/1.0")
                .build();
    }

    // OpenMeteo天气数据模型
    @JsonIgnoreProperties(ignoreUnknown = true)
    public record WeatherData(
            @JsonProperty("latitude") Double latitude,
            @JsonProperty("longitude") Double longitude,
            @JsonProperty("timezone") String timezone,
            @JsonProperty("current") CurrentWeather current,
            @JsonProperty("daily") DailyForecast daily,
            @JsonProperty("current_units") CurrentUnits currentUnits) {

        @JsonIgnoreProperties(ignoreUnknown = true)
        public record CurrentWeather(
                @JsonProperty("time") String time,
                @JsonProperty("temperature_2m") Double temperature,
                @JsonProperty("apparent_temperature") Double feelsLike,
                @JsonProperty("relative_humidity_2m") Integer humidity,
                @JsonProperty("precipitation") Double precipitation,
                @JsonProperty("weather_code") Integer weatherCode,
                @JsonProperty("wind_speed_10m") Double windSpeed,
                @JsonProperty("wind_direction_10m") Integer windDirection) {
        }

        @JsonIgnoreProperties(ignoreUnknown = true)
        public record CurrentUnits(
                @JsonProperty("time") String timeUnit,
                @JsonProperty("temperature_2m") String temperatureUnit,
                @JsonProperty("relative_humidity_2m") String humidityUnit,
                @JsonProperty("wind_speed_10m") String windSpeedUnit) {
        }

        @JsonIgnoreProperties(ignoreUnknown = true)
        public record DailyForecast(
                @JsonProperty("time") List<String> time,
                @JsonProperty("temperature_2m_max") List<Double> tempMax,
                @JsonProperty("temperature_2m_min") List<Double> tempMin,
                @JsonProperty("precipitation_sum") List<Double> precipitationSum,
                @JsonProperty("weather_code") List<Integer> weatherCode,
                @JsonProperty("wind_speed_10m_max") List<Double> windSpeedMax,
                @JsonProperty("wind_direction_10m_dominant") List<Integer> windDirection) {
        }
    }

    /**
     * 获取天气代码对应的描述
     */
    private String getWeatherDescription(int code) {
        return switch (code) {
            case 0 -> "晴朗";
            case 1, 2, 3 -> "多云";
            case 45, 48 -> "雾";
            case 51, 53, 55 -> "毛毛雨";
            case 56, 57 -> "冻雨";
            case 61, 63, 65 -> "雨";
            case 66, 67 -> "冻雨";
            case 71, 73, 75 -> "雪";
            case 77 -> "雪粒";
            case 80, 81, 82 -> "阵雨";
            case 85, 86 -> "阵雪";
            case 95 -> "雷暴";
            case 96, 99 -> "雷暴伴有冰雹";
            default -> "未知天气";
        };
    }

    /**
     * 获取风向描述
     */
    private String getWindDirection(int degrees) {
        if (degrees >= 337.5 || degrees < 22.5)
            return "北风";
        if (degrees >= 22.5 && degrees < 67.5)
            return "东北风";
        if (degrees >= 67.5 && degrees < 112.5)
            return "东风";
        if (degrees >= 112.5 && degrees < 157.5)
            return "东南风";
        if (degrees >= 157.5 && degrees < 202.5)
            return "南风";
        if (degrees >= 202.5 && degrees < 247.5)
            return "西南风";
        if (degrees >= 247.5 && degrees < 292.5)
            return "西风";
        return "西北风";
    }

    /**
     * 获取指定经纬度的天气预报
     * 
     * @param latitude  纬度
     * @param longitude 经度
     * @return 指定位置的天气预报
     * @throws RestClientException 如果请求失败
     */
    @Tool(description = "获取指定经纬度的天气预报")
    public String getWeatherForecastByLocation(double latitude, double longitude) {
        // 获取天气数据(当前和未来7天)
        var weatherData = restClient.get()
                .uri("/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m,apparent_temperature,relative_humidity_2m,precipitation,weather_code,wind_speed_10m,wind_direction_10m&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code,wind_speed_10m_max,wind_direction_10m_dominant&timezone=auto&forecast_days=7",
                        latitude, longitude)
                .retrieve()
                .body(WeatherData.class);

        // 拼接天气信息
        StringBuilder weatherInfo = new StringBuilder();

        // 添加当前天气信息
        WeatherData.CurrentWeather current = weatherData.current();
        String temperatureUnit = weatherData.currentUnits() != null ? weatherData.currentUnits().temperatureUnit()
                : "°C";
        String windSpeedUnit = weatherData.currentUnits() != null ? weatherData.currentUnits().windSpeedUnit() : "km/h";
        String humidityUnit = weatherData.currentUnits() != null ? weatherData.currentUnits().humidityUnit() : "%";

        weatherInfo.append(String.format("""
                当前天气:
                温度: %.1f%s (体感温度: %.1f%s)
                天气: %s
                风向: %s (%.1f %s)
                湿度: %d%s
                降水量: %.1f 毫米

                """,
                current.temperature(),
                temperatureUnit,
                current.feelsLike(),
                temperatureUnit,
                getWeatherDescription(current.weatherCode()),
                getWindDirection(current.windDirection()),
                current.windSpeed(),
                windSpeedUnit,
                current.humidity(),
                humidityUnit,
                current.precipitation()));

        // 添加未来天气预报
        weatherInfo.append("未来天气预报:\n");
        WeatherData.DailyForecast daily = weatherData.daily();

        for (int i = 0; i < daily.time().size(); i++) {
            String date = daily.time().get(i);
            double tempMin = daily.tempMin().get(i);
            double tempMax = daily.tempMax().get(i);
            int weatherCode = daily.weatherCode().get(i);
            double windSpeed = daily.windSpeedMax().get(i);
            int windDir = daily.windDirection().get(i);
            double precip = daily.precipitationSum().get(i);

            // 格式化日期
            LocalDate localDate = LocalDate.parse(date);
            String formattedDate = localDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd (EEE)"));

            weatherInfo.append(String.format("""
                    %s:
                    温度: %.1f%s ~ %.1f%s
                    天气: %s
                    风向: %s (%.1f %s)
                    降水量: %.1f 毫米

                    """,
                    formattedDate,
                    tempMin, temperatureUnit,
                    tempMax, temperatureUnit,
                    getWeatherDescription(weatherCode),
                    getWindDirection(windDir),
                    windSpeed, windSpeedUnit,
                    precip));
        }

        return weatherInfo.toString();
    }

    /**
     * 获取指定位置的空气质量信息 (使用备用模拟数据)
     * 注意:由于OpenMeteo的空气质量API可能需要额外配置或不可用,这里提供备用数据
     * 
     * @param latitude  纬度
     * @param longitude 经度
     * @return 空气质量信息
     */
    @Tool(description = "获取指定位置的空气质量信息(模拟数据)")
    public String getAirQuality(@ToolParam(description = "纬度") double latitude,
            @ToolParam(description = "经度") double longitude) {

        try {
            // 从天气数据中获取基本信息
            var weatherData = restClient.get()
                    .uri("/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m&timezone=auto",
                            latitude, longitude)
                    .retrieve()
                    .body(WeatherData.class);

            // 模拟空气质量数据 - 实际情况下应该从真实API获取
            // 根据经纬度生成一些随机但相对合理的数据
            int europeanAqi = (int) (Math.random() * 100) + 1;
            int usAqi = (int) (europeanAqi * 1.5);
            double pm10 = Math.random() * 50 + 5;
            double pm25 = Math.random() * 25 + 2;
            double co = Math.random() * 500 + 100;
            double no2 = Math.random() * 40 + 5;
            double so2 = Math.random() * 20 + 1;
            double o3 = Math.random() * 80 + 20;

            String aqiLevel = getAqiLevel(europeanAqi);
            String usAqiLevel = getUsAqiLevel(usAqi);

            // 构建空气质量信息字符串
            String aqiInfo = String.format("""
                    空气质量信息 (纬度: %.4f, 经度: %.4f, 时区: %s):

                    欧洲空气质量指数 (EAQI): %d (%s)
                    美国空气质量指数 (US AQI): %d (%s)

                    详细污染物信息:
                    PM10: %.1f μg/m³
                    PM2.5: %.1f μg/m³
                    一氧化碳 (CO): %.1f μg/m³
                    二氧化氮 (NO2): %.1f μg/m³
                    二氧化硫 (SO2): %.1f μg/m³
                    臭氧 (O3): %.1f μg/m³

                    注意:以上是模拟数据,仅供示例。
                    """,
                    latitude, longitude, weatherData.timezone(),
                    europeanAqi, aqiLevel,
                    usAqi, usAqiLevel,
                    pm10, pm25, co, no2, so2, o3);

            return aqiInfo;
        } catch (Exception e) {
            return "无法获取空气质量信息: " + e.getMessage();
        }
    }

    /**
     * 获取欧洲AQI等级描述
     */
    private String getAqiLevel(Integer aqi) {
        if (aqi <= 20) {
            return "优 (0-20): 空气质量非常好";
        } else if (aqi <= 40) {
            return "良 (20-40): 空气质量良好";
        } else if (aqi <= 60) {
            return "中等 (40-60): 对敏感人群可能有影响";
        } else if (aqi <= 80) {
            return "较差 (60-80): 对所有人群健康有影响";
        } else if (aqi <= 100) {
            return "差 (80-100): 可能对所有人群健康造成损害";
        } else {
            return "非常差 (>100): 对所有人群健康有严重影响";
        }
    }

    /**
     * 获取美国AQI等级描述
     */
    private String getUsAqiLevel(Integer aqi) {
        if (aqi <= 50) {
            return "优 (0-50): 空气质量令人满意,污染风险很低";
        } else if (aqi <= 100) {
            return "良 (51-100): 空气质量尚可,对极少数敏感人群可能有影响";
        } else if (aqi <= 150) {
            return "对敏感人群不健康 (101-150): 敏感人群可能会经历健康影响";
        } else if (aqi <= 200) {
            return "不健康 (151-200): 所有人可能开始经历健康影响";
        } else if (aqi <= 300) {
            return "非常不健康 (201-300): 健康警告,所有人可能经历更严重的健康影响";
        } else {
            return "危险 (>300): 健康警报,所有人更可能受到影响";
        }
    }

    public static void main(String[] args) {
        OpenMeteoService service = new OpenMeteoService();
        // 测试北京的天气预报
        System.out.println("北京天气预报:");
        System.out.println(service.getWeatherForecastByLocation(39.9042, 116.4074));

        // 测试北京的空气质量
        System.out.println("北京空气质量:");
        System.out.println(service.getAirQuality(39.9042, 116.4074));
    }
}

mcp 服务端 启动类

java 复制代码
/*
 * Copyright 2025-2026 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @author brianxiadong
 */

package org.springframework.ai.mcp.sample.server;

import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.function.FunctionToolCallback;
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 McpServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(McpServerApplication.class, args);
    }

    @Bean
    public ToolCallbackProvider weatherTools(OpenMeteoService openMeteoService) {
        return MethodToolCallbackProvider.builder().toolObjects(openMeteoService).build();
    }

}

mcp服务端配置文件

html 复制代码
#
# Copyright 2025-2026 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

# spring.main.web-application-type=none

# NOTE: You must disable the banner and the console logging 
# to allow the STDIO transport to work !!!
spring:
  main:
    banner-mode: off
  ai:
    mcp:
      server:
        name: my-weather-server
        version: 0.0.1

# logging.pattern.console=

mcp服务端代码就两个java类

3、spring-ai 程序(mcp客户端)

pom文件

html 复制代码
	<dependencies>
	
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>
			<version>${spring-ai.version}</version>
		</dependency>

		<dependency>
			<groupId>com.alibaba.cloud.ai</groupId>
			<artifactId>spring-ai-alibaba-starter</artifactId>
			<version>${spring-ai-alibaba.version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>


	</dependencies>

yml配置文件

html 复制代码
server:
  port: 58888
  servlet:
    encoding:
      charset: UTF-8
      enabled: true
      force: true

spring:
  application:
    name: mcp
  ai:
    dashscope:
      api-key: sk-xxxxxxxxxxx
    mcp:
      client:
        sse:
          connections:
            server1:
              url: http://localhost:8080
  mandatory-file-encoding: UTF-8

# 调试日志
logging:
  level:
    io:
      modelcontextprotocol:
        client: DEBUG
        spec: DEBUG

controller代码

java 复制代码
package org.springframework.ai.mcp.samples.client.controller;


import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/weather")
public class McpWeatherController {

    private final ChatClient dashScopeChatClient;
    private final ToolCallbackProvider tools;

    public McpWeatherController(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools) {
//        使用方式只需注入ToolCallbackProvider和ChatClient.Builder
        this.tools = tools;
        this.dashScopeChatClient = chatClientBuilder
                .build();
    }


    /**
     * 调用工具版 - function
     */
    @GetMapping("/chat-mcp")
    public String chatMcp(@RequestParam(value = "query", defaultValue = "请告诉我北京1天以后的天气",required = false) String query) {
        System.out.println("\n>>> QUESTION: " + query);
        String msg = dashScopeChatClient.prompt(query).tools(tools).call().content();
        System.out.println("\n>>> ASSISTANT: " + msg);
        return msg;
    }

    @GetMapping("/chat")
    public String chat(@RequestParam(value = "query", defaultValue = "请告诉我北京1天以后的天气",required = false) String query) {
        System.out.println("\n>>> QUESTION: " + query);
        String msg = dashScopeChatClient.prompt(query).call().content();
        System.out.println("\n>>> ASSISTANT: " + msg);
        return msg;
    }


}

启动类

java 复制代码
/*
 * Copyright 2025-2026 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @author brianxiadong
 */
package org.springframework.ai.mcp.samples.client;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


@SpringBootApplication(exclude = {
        org.springframework.ai.autoconfigure.mcp.client.SseHttpClientTransportAutoConfiguration.class
})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }


}

4、启动 mcp服务端和mcp客户端

请求没有工具的接口 http://127.0.0.1:58888/weather/chat

如下

请求mpc远程工具 http://127.0.0.1:58888/weather/chat-mcp?query=

结果如下

相关推荐
百锦再1 分钟前
Vue Scoped样式混淆问题详解与解决方案
java·前端·javascript·数据库·vue.js·学习·.net
刘一说4 分钟前
Spring Boot 启动慢?启动过程深度解析与优化策略
java·spring boot·后端
壹佰大多13 分钟前
【spring如何扫描一个路径下被注解修饰的类】
java·后端·spring
百锦再19 分钟前
对前后端分离与前后端不分离(通常指服务端渲染)的架构进行全方位的对比分析
java·开发语言·python·架构·eclipse·php·maven
间彧26 分钟前
Java双亲委派模型的具体实现原理是什么?
后端
间彧26 分钟前
Java类的加载过程
后端
DokiDoki之父35 分钟前
Spring—注解开发
java·后端·spring
提笔了无痕1 小时前
什么是Redis的缓存问题,以及如何解决
数据库·redis·后端·缓存·mybatis
浪里行舟1 小时前
国产OCR双雄对决?PaddleOCR-VL与DeepSeek-OCR全面解析
前端·后端
CodeCraft Studio1 小时前
【能源与流程工业案例】KBC借助TeeChart 打造工业级数据可视化平台
java·信息可视化·.net·能源·teechart·工业可视化·工业图表