<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
# stdio 模式
spring:
main:
web-application-type: none
banner-mode: off
ai:
mcp:
server:
name: my-weather-server
version: 0.0.1
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.extern.slf4j.Slf4j;
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 org.springframework.web.client.RestClientException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Service
@Slf4j
public class OpenMeteoService {
private static final Logger LOG = LoggerFactory.getLogger(OpenMeteoService.class);
// 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("Content-Type", "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}¤t=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();
}
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = {"com.am.mcp"})
public class McpServerStdioApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerStdioApplication.class, args);
}
@Bean
public ToolCallbackProvider weatherTools(OpenMeteoService openMeteoService) {
return MethodToolCallbackProvider.builder().toolObjects(openMeteoService).build();
}
}
java -jar D:\code\spring\spring_ai\spring-ai-mcp\mcp-server\target\mcp-server-1.0.0.jar
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
server:
port: 8888
spring:
application:
name: mcp-server
main:
banner-mode: off
# web-application-type: none
ai:
mcp:
server:
name: my-weather-server
version: 0.0.1
type: ASYNC # Recommended for reactive applications
# 配置 sse 的根路径,默认值为 /sse
# 下面的最终路径为 ip:port/sse/mcp
instructions: "This reactive server provides weather information tools and resources"
sse-endpoint: /sse
sse-message-endpoint: /mcp
capabilities:
tool: true
resource: true
prompt: true
completion: t
import com.am.mcp.service.OpenMeteoService;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ToolConfiguration {
@Bean
public ToolCallbackProvider weatherTools(OpenMeteoService openMeteoService) {
return MethodToolCallbackProvider.builder().toolObjects(openMeteoService).build();
}
public record TextInput(String input) {
}
@Bean
public ToolCallback toUpperCase() {
return FunctionToolCallback.builder("toUpperCase", (TextInput input) -> input.input().toUpperCase())
.inputType(TextInput.class)
.description("Put the text to upper case")
.build();
}
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = {"com.am.mcp"})
public class McpServerWebFluxApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerWebFluxApplication.class, args);
}
}
效果展示:
STDIO与SSE说明
参考文档:
https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。