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=

结果如下

相关推荐
Asthenia041226 分钟前
RBAC模块分析:菜单-权限/角色-权限/用户-角色
后端
uhakadotcom41 分钟前
MQTT入门:轻量级物联网通信协议
后端·面试·github
李少兄1 小时前
Unirest:优雅的Java HTTP客户端库
java·开发语言·http
此木|西贝1 小时前
【设计模式】原型模式
java·设计模式·原型模式
ONE_Gua1 小时前
chromium魔改——navigator.webdriver 检测
前端·后端·爬虫
可乐加.糖1 小时前
一篇关于Netty相关的梳理总结
java·后端·网络协议·netty·信息与通信
Kagol1 小时前
macOS 和 Windows 操作系统下如何安装和启动 MySQL / Redis 数据库
redis·后端·mysql
无名之逆2 小时前
Rust 开发提效神器:lombok-macros 宏库
服务器·开发语言·前端·数据库·后端·python·rust
s9123601012 小时前
rust 同时处理多个异步任务
java·数据库·rust
9号达人2 小时前
java9新特性详解与实践
java·后端·面试