Spring AI MCP实战(1)调用高德api查询天气
有一些模型不支持mcp:ollama中的gemma和deepseek现在我使用的支持的:ollama中的qwen3, gpt, openai(deepseek)
关于MCP的简介
Spring AI支持MCP的模型
有一些模型不支持mcp:ollama中的gemma和deepseek
现在我使用的支持的:ollama中的qwen3, gpt, openai(deepseek)
Server和Client
MCP的关键是构建server和client
server的作用是提供发送http请求和接收响应的工具
client的作用是创建chatclient,与用户进行交互,然后调用工具
MCP和Function Calling的区别
个人的理解是,function calling就是在一台服务器上进行的,大模型可以根据用户的输入选择本地的工具进行调用
MCP中,client只负责对chatclient进行规定,mcp把server上的tool注册给大模型使用,tool是在另一个服务器上的
项目结构
首先是项目的结构
📁 mcp-project (根项目 - Maven 多模块项目)
├── 📄 pom.xml (父 POM: 定义模块、公共依赖 spring-boot-starter-web, lombok, jackson-databind, spring-ai-bom)
│
├── 📁 mcp-client (模块 - 天气查询客户端)
│ ├── 📄 pom.xml (依赖: spring-ai-starter-mcp-client, spring-ai-starter-model-*, mybatis-plus, mysql-connector-j)
│ └── 📁 src/main/java/com/hyk/mcpclient
│ ├── 📄 McpClientApplication.java (Spring Boot 启动类, @MapperScan)
│ ├── 📁 client
│ │ └── 📄 WeatherClientConfig.java (配置 ChatClient Bean, 使用 OpenAI 模型)
│ ├── 📁 common
│ │ └── 📄 prompt.java (定义系统提示词 PROMPT_WEATHER)
│ ├── 📁 controller
│ │ └── 📄 WeatherController.java (REST 控制器, /ai/weather 接口)
│ ├── 📁 domain/pojo
│ │ └── 📄 City.java (MyBatis Plus 实体类)
│ ├── 📁 mapper
│ │ └── 📄 CityMapper.java (MyBatis Plus Mapper)
│ ├── 📁 service
│ │ ├── 📄 CityService.java (接口)
│ │ └── 📁 Impl
│ │ └── 📄 CityServiceImpl.java (实现类)
│ └── 📁 tool
│ └── 📄 GetCityAdcodeTool.java (@Tool 注解, 调用 CityService)
│ └── 📁 src/main/resources
│ ├── 📄 application.yaml (配置: OpenAI, MCP Client SSE 连接, 数据源)
│ ├── 📄 flux.html (用于测试 SSE 流式输出的 HTML 页面)
│ └── 📄 mcp-servers-config.json (MCP 服务器进程配置)
│
├── 📁 mcp-server (模块 - 提供天气查询工具的 MCP 服务器)
│ ├── 📄 pom.xml (依赖: spring-ai-starter-mcp-server-webmvc)
│ └── 📁 src/main/java/com/hyk/mcpserver
│ ├── 📄 McpServerApplication.java (Spring Boot 启动类)
│ ├── 📁 config
│ │ ├── 📄 RestTemplateConfig.java (配置 RestTemplate Bean)
│ │ └── 📄 ToolCallbackProviderConfig.java (注册 WeatherService 工具)
│ └── 📁 service
│ ├── 📄 WeatherService.java (接口)
│ └── 📁 Impl
│ └── 📄 WeatherServiceImpl.java (@Tool 注解实现, 调用高德天气 API)
│ └── 📁 src/main/resources
│ └── 📄 application.yaml (配置: server port 8080, MCP server name, weather.url)
│ └── 📁 src/test/java/com/hyk/mcpserver
│ └── 📄 WeatherTest.java (测试高德天气 API 调用)
│
└── 📁 mcp-tools (模块 - 工具模块,例如数据导入)
├── 📄 pom.xml (依赖: spring-boot-starter-data-jpa, spring-boot-starter-web, poi-ooxml)
└── 📁 src/main/java/com/hyk/mcptools
├── 📄 McpToolsApplication.java (Spring Boot 启动类)
├── 📁 pojo
│ └── 📄 City.java (JPA 实体类)
├── 📁 repo
│ └── 📄 CityRepository.java (Spring Data JPA Repository)
└── 📁 service
└── 📄 LocalFileImportService.java (从 Excel 导入城市数据)
└── 📁 src/main/resources
└── 📄 application.yaml (配置: 数据源, JPA)
└── 📁 src/test/java/com/hyk/mcptools
└── 📄 ImportCitiesTest.java (测试城市数据导入)
现在我只完成了使用高德地图的api查询指定城市天气的功能,后序还将添加更多功能完善
引入的共同依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<!-- 依赖管理 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
jackson用来对json进行处理
Server模块
Server模块定义的是向高德网站发送请求这一步的代码
依赖和配置
依赖:
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>
这个是作为mcp server的必要依赖
配置文件:
server:
port: 8080
spring:
ai:
mcp:
server:
name: weather-server
sse-message-endpoint: /mcp/weather
weather:
url: https://restapi.amap.com/v3/weather/weatherInfo
server和client使用的是sse通信,也有http通信

在配置项中设置了sse-message-endpoint,这一步让我们可以不用去写controller,它代替了controller的作用直接可以去调用WeatherService

具体实现类
ToolCallbackProviderConfig
作用是作为配置类,定义tools
@Configuration
public class ToolCallbackProviderConfig {
@Bean
public ToolCallbackProvider WeatherTools(WeatherService weatherService){
return MethodToolCallbackProvider.builder()
.toolObjects(weatherService)
.build();
}
}
这样我们就将weatherService作为一个tool了
WeatherService和impl
package com.hyk.mcpserver.service;
import com.fasterxml.jackson.core.JsonProcessingException;
public interface WeatherService {
String getWeather() throws JsonProcessingException;
String getWeatherByCityAdcode(String city) throws JsonProcessingException;
}
package com.hyk.mcpserver.service.Impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hyk.mcpserver.service.WeatherService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class WeatherServiceImpl implements WeatherService {
@Resource
private RestTemplate restTemplate;
private static final ObjectMapper objectMapper = new ObjectMapper();
@Value("${weather.url}")
String url;
@Tool(description = "获取本地的天气信息")
@Override
public String getWeather() throws JsonProcessingException {
String key = "04c55a94d23ed89125256f13407fe339"; // 使用有效的key
String city = "510100";
// 只使用必需参数
String url = "https://restapi.amap.com/v3/weather/weatherInfo" +
"?key=" + key +
"&city=" + city; // 移除了 extensions 和 output
System.out.println("测试URL: " + url);
ResponseEntity<String> response = restTemplate.exchange(
url, HttpMethod.GET, new HttpEntity<>(new HttpHeaders()), String.class
);
String body = response.getBody();
Map<String, String> map = objectMapper.readValue(body, new TypeReference<Map<String, String>>() {
});
return map.get("province") + " " + map.get("city")
+ "今日的天气为" + map.get("weather")
+ ",温度:" + map.get("temperature") + "℃"
+ "湿度:" + map.get("humidity");
}
@Tool(description = "获取指定城市Adcode的天气信息")
@Override
public String getWeatherByCityAdcode(@ToolParam(description = "城市Adcode") String adcode) throws JsonProcessingException {
String key = "04c55a94d23ed89125256f13407fe339";
String url = "https://restapi.amap.com/v3/weather/weatherInfo" +
"?key=" + key +
"&city=" + adcode;
System.out.println("测试URL: " + url);
ResponseEntity<String> response = restTemplate.exchange(
url, HttpMethod.GET, new HttpEntity<>(new HttpHeaders()), String.class
);
String body = response.getBody();
// 使用 Map<String, Object> 而不是 Map<String, String>
Map<String, Object> map = objectMapper.readValue(body, new TypeReference<Map<String, Object>>() {});
// 先检查API调用状态
String status = (String) map.get("status");
if (!"1".equals(status)) {
System.out.println("API调用失败: " + map.get("info"));
return "未找到天气数据";
}
// 获取 lives 数组
@SuppressWarnings("unchecked")
List<Map<String, String>> lives = (List<Map<String, String>>) map.get("lives");
if (lives != null && !lives.isEmpty()) {
Map<String, String> weatherData = lives.get(0); // 取第一个城市的天气数据
String province = (String) weatherData.get("province");
String cityName = (String) weatherData.get("city");
String weather = (String) weatherData.get("weather");
String temperature = (String) weatherData.get("temperature");
String humidity = (String) weatherData.get("humidity");
System.out.println(province + " " + cityName
+ " 今日的天气为 " + weather
+ ", 温度:" + temperature + "℃"
+ ", 湿度:" + humidity + "%");
return province + " " + cityName
+ " 今日的天气为 " + weather
+ ", 温度:" + temperature + "℃"
+ ", 湿度:" + humidity + "%";
} else {
return "未找到天气数据";
}
}
}
获取本地的天气信息这个方法是有问题的,我没有直接改,在处理返回参数的时候解析的逻辑是不正确的,正确的参考根据Adcode获取天气信息的方法
其中重要的是两个annotation
@Tool:把这个方法标记为tool,并进行定义
@ToolParam:定义这个tool方法的传入参数的意义是什么,向ai解释
然后Server就差不多完成了
Client模块
依赖和配置
依赖:
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<!-- 使用的大模型依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
</dependencies>
之所以要mysql是因为我们需要去查询数据库得到用户输入城市的Adcode,然后在让大模型拿着Adcode去调用server的tool
配置文件:
server:
port: 8081
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
model: qwen3:14b
openai:
enabled: true
base-url: https://api.deepseek.com
api-key: sk-xxxxxx
chat:
options:
model: deepseek-chat
mcp:
client:
enabled: true
stdio:
enabled: false # 禁用 STDIO
sse:
connections:
server1:
url: http://localhost:8080/mcp/weather
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxxxxx:3306/weather_city?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&allowPublicKeyRetrieval=true
username: xxxx
password: xxxxx
# 连接池配置(可选)
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
logging:
level:
org.springframework.ai: DEBUG # Spring AI 相关日志
org.springframework.ai.chat.client.advisor: DEBUG # Advisor 日志
root: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
这里需要指定sse连接的server的url
具体实现类
定义Client的config类
package com.hyk.mcpclient.client;
import com.hyk.mcpclient.common.prompt;
import com.hyk.mcpclient.tool.GetCityAdcodeTool;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WeatherClientConfig {
@Resource
private OllamaChatModel ollamaChatModel;
@Resource
private OpenAiChatModel openAiChatModel;
@Resource
private ToolCallbackProvider toolCallbackProvider;
@Resource
private GetCityAdcodeTool getCityAdcodeTool;
@Bean
public ChatClient weatherClient() {
return ChatClient.builder(openAiChatModel)
.defaultSystem(prompt.PROMPT_WEATHER)
.defaultAdvisors(new SimpleLoggerAdvisor())
.defaultTools(getCityAdcodeTool)
.defaultToolCallbacks(toolCallbackProvider.getToolCallbacks())
.build();
}
}
可以选ollama qwen3(本地)和openai(deepseek),本地的会慢很多
prompt提示词
package com.hyk.mcpclient.common;
public class prompt {
public static final String PROMPT_WEATHER = """
你是一个专业的天气查询助手。请遵循以下规则:
查询流程:
1. 首先使用 getCityAdcode 工具确认城市的adcode
2. 然后使用 getWeatherByAdcode 工具查询具体天气
3. 如果城市不存在,直接告知用户
回复要求:
- 用友好的中文回复
- 包含完整的天气信息
- 如果查询失败,给出友好的错误提示
示例回复:
"北京当前天气:晴,温度25°C,湿度60%,东南风3级,祝您有愉快的一天!🌞"
如果查询失败,请告诉我失败的具体原因
""";
}
Controller
package com.hyk.mcpclient.controller;
import com.hyk.mcpclient.client.WeatherClientConfig;
import com.hyk.mcpclient.tool.GetCityAdcodeTool;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
@Slf4j
public class WeatherController {
@Resource
private ChatClient weatherClient;
@GetMapping("/ai/weather")
public String generateCityWeather(@RequestParam String city) {
return weatherClient.prompt()
.user("帮我查询" + city + "的天气")
.call()
.content();
}
@GetMapping(value = "/ai/weatherFlux", produces = "text/html; charset=utf-8")
public Flux<String> generateCityWeatherByFlux(@RequestParam String city) {
return weatherClient.prompt()
.user("帮我查询" + city + "的天气")
.stream()
.content();
}
}
Service
package com.hyk.mcpclient.service;
public interface CityService {
public String getCityAdcodeByName(String chineseName);
}
ServiceImpl
package com.hyk.mcpclient.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hyk.mcpclient.domain.pojo.City;
import com.hyk.mcpclient.mapper.CityMapper;
import com.hyk.mcpclient.service.CityService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class CityServiceImpl extends ServiceImpl<CityMapper, City> implements CityService {
@Override
public String getCityAdcodeByName(String chineseName) {
log.info("根据中文名查询Adcode: {}", chineseName);
City city = lambdaQuery()
.like(City::getChineseName, chineseName) // 模糊查询
.select(City::getAdcode)
.one();
return city != null ? city.getAdcode() : null;
}
}
Mapper
package com.hyk.mcpclient.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hyk.mcpclient.domain.pojo.City;
public interface CityMapper extends BaseMapper<City> {
}
我这里使用的MB Plus的mysql操作
mapper的指定在启动项添加@MapperScanner
Pojo类
package com.hyk.mcpclient.domain.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("city") // MyBatis Plus 的表名注解
public class City {
@TableId(type = IdType.AUTO) // MyBatis Plus 的主键注解
private Integer id;
@TableField("chinese_name") // MyBatis Plus 的字段注解
private String chineseName;
private String adcode;
private String citycode;
// 自定义构造方法
public City(String chineseName, String adcode, String citycode) {
this.chineseName = chineseName;
this.adcode = adcode;
this.citycode = citycode;
}
}
整个项目的运行结果:

额外:
添加了mcp工具后使用本地的ollama qwen3不知道为什么不能流式输出了,感觉是springAi的bug
更多推荐

所有评论(0)