基于Spring AI与MCP协议构建企业级气象查询工具链实战
重构AI工具链:Spring AI与MCP协议的深度融合
在企业级AI应用落地过程中,工具复用一直是一个核心痛点。不同的业务线在集成大模型时,往往需要重复开发相似的业务接口适配层,例如天气查询、地理编码等。这种碎片化的开发模式不仅降低了研发效率,也提高了后续维护的成本。采用Spring AI结合MCP(Model Context Protocol)协议构建统一的工具服务层,能够有效解决多模型适配与工具共享的难题,为AI应用提供标准化的外部能力接入方案。
1. MCP协议:大模型外部工具的标准接入层
1.1 协议核心定义
MCP(模型上下文协议)定义了大模型与外部数据源、工具之间交互的通用规范。类似于通用接口在硬件体系中的作用,MCP使得大模型无需关注底层工具的实现细节与调用差异,只需遵循统一的JSON-RPC 2.0标准即可完成能力调用。
MCP架构由以下三个核心组件构成:
- Host(宿主应用):发起交互请求的AI应用程序主体。
- Client(客户端):与宿主应用同生命周期,负责与服务端维持1:1的连接会话。
- Server(服务端):提供具体工具和数据访问能力的独立服务进程。
1.2 MCP与原生Function Call的差异
原生Function Call高度耦合于特定大模型厂商的私有协议,而MCP提供了模型无关的抽象层。
// 原生Function Call的适配困境:不同厂商协议差异导致代码膨胀
public class LegacyModelIntegration {
// 适配OpenAI协议
public Object queryGptWeather(String location) {
return new GptFunctionInvocation("weather_fetch", location);
}
// 适配Claude协议
public Object queryClaudeWeather(String location) {
return new ClaudeFunctionInvocation("climate_info", location);
}
}
两者的核心维度对比如下:
| 评估维度 | 原生Function Call | MCP协议 |
|---|---|---|
| 协议标准 | 厂商私有,互不兼容 | 基于JSON-RPC 2.0的开放标准 |
| 跨模型复用 | 需针对不同模型单独适配 | 一次发布,多模型通用接入 |
| 生态扩展性 | 受限于单一平台 | 支持分布式工具网格与生态共享 |
| 企业级治理 | 基础能力 | 原生支持鉴权、流量控制与可观测性 |
1.3 Spring AI对MCP的工程化支持
Spring AI框架将MCP协议无缝融入Spring Boot生态,为Java开发者提供了极具效率的工程化实践路径:
- 注解驱动:通过
@Tool注解声明式导出方法,零样板代码。 - 自动协商:内置协议握手与消息序列化逻辑,开发者无需处理底层通信细节。
- 生态融合:天然支持Spring的依赖注入、配置管理及WebFlux/WebMVC异步非阻塞模型。
建议:Spring AI 1.0.0-M7及以上版本对MCP Server的WebMVC适配已趋于稳定,推荐作为生产环境基线。
2. 生产级MCP Server工程实践
2.1 依赖构建与版本管理
以气象数据查询服务为例,初始化Maven工程并引入核心依赖:
<!-- pom.xml -->
<properties>
<java.release>17</java.release>
<spring-boot.revision>3.4.4</spring-boot.revision>
<spring-ai.revision>1.0.0-M7</spring-ai.revision>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.revision}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- MCP Server WebMvc支持 (SSE模式) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 缓存与工具库 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
此处采用spring-ai-starter-mcp-server-webmvc构建基于SSE(Server-Sent Events)长连接的分布式服务端,适用于微服务架构下的跨网络调用。若仅为本地单进程集成,可替换为Stdio模式的starter。
2.2 核心通信参数配置
通过YAML配置文件定义MCP服务端元数据与传输层行为:
server:
port: 9090
servlet:
context-path: /mcp-hub
spring:
application:
name: climate-mcp-provider
ai:
mcp:
server:
name: climate-info-provider
version: 2.0.0
type: ASYNC
sse-message-endpoint: /events
tool-change-notification: true
resource-change-notification: true
关键参数解析:
- type: ASYNC:启用异步响应模式,适配I/O密集型的外部API网关调用。
- sse-message-endpoint:SSE消息端点,MCP Client将通过此路径完成双向消息协商。
2.3 高可用气象查询工具实现
面对外部API的不确定性(限流、网络抖动),工具实现需内置缓存降级与超时熔断机制:
@Service
@Slf4j
public class ClimateDataService {
private final WebClient httpClient;
private final Cache<String, ClimateRecord> localForecastCache;
// 区域编码映射
private static final Map<String, String> MUNICIPALITY_DICT = Map.of(
"Beijing", "110100",
"Shanghai", "310100",
"Guangzhou", "440100"
);
public ClimateDataService(WebClient.Builder clientBuilder) {
this.httpClient = clientBuilder
.baseUrl("https://restapi.amap.com/v3")
.defaultHeader("Content-Type", "application/json")
.build();
this.localForecastCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(500)
.build();
}
@Tool(name = "query_local_climate", description = "Retrieve detailed meteorological data including temp, humidity and wind for a specific region")
public ClimatePayload fetchCurrentClimate(
@ToolParameter(description = "Target city name, e.g., Beijing", required = true) String cityName
) {
if (StringUtils.isBlank(cityName)) {
throw new IllegalArgumentException("City name must not be empty");
}
ClimateRecord cachedRecord = localForecastCache.getIfPresent(cityName);
if (cachedRecord != null) {
log.debug("Cache hit for city: {}", cityName);
return ClimatePayload.success(cachedRecord);
}
String regionCode = MUNICIPALITY_DICT.get(cityName);
if (regionCode == null) {
throw new IllegalArgumentException("Unsupported region: " + cityName);
}
try {
String rawResponse = httpClient.get()
.uri(builder -> builder
.path("/weather/weatherInfo")
.queryParam("city", regionCode)
.queryParam("key", "${amap.secret-key}")
.queryParam("extensions", "all")
.build())
.retrieve()
.onStatus(status -> status.is4xxClientError() || status.is5xxServerError(),
resp -> Mono.error(new ServiceException("API invocation failed: " + resp.statusCode())))
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(5))
.block();
ClimateRecord record = deserializePayload(rawResponse);
localForecastCache.put(cityName, record);
return ClimatePayload.success(record);
} catch (TimeoutException ex) {
log.error("Meteorological API timeout for city: {}", cityName, ex);
return ClimatePayload.fail("Request timeout, please retry later");
} catch (Exception ex) {
log.error("Failed to fetch climate data for city: {}", cityName, ex);
return ClimatePayload.fail("Data acquisition failed: " + ex.getMessage());
}
}
private ClimateRecord deserializePayload(String json) {
// 实际生产中需进行严格的JSON反序列化与字段映射
return new ClimateRecord("Sunny", 26.0, 18.5, 31.0, "South", 2, 55, LocalDateTime.now());
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ClimateRecord {
private String status;
private Double currentTemp;
private Double minTemp;
private Double maxTemp;
private String windDir;
private Integer windScale;
private Integer humidityPct;
private LocalDateTime lastUpdate;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ClimatePayload {
private boolean success;
private String message;
private ClimateRecord data;
public static ClimatePayload success(ClimateRecord data) { return new ClimatePayload(true, "OK", data); }
public static ClimatePayload fail(String msg) { return new ClimatePayload(false, msg, null); }
}
}
该实现具备以下工程化特征:
- 前置校验:阻断无效请求向下游传递。
- 本地短时缓存:利用Caffeine降低对第三方API的调用频次,提升响应稳定性。
- 非阻塞超时:通过WebClient的
timeout控制避免线程长时间挂起。 - 异常隔离:将网络异常转化为业务友好的结构化降级响应。
2.4 工具注册与发布
将带有@Tool注解的Service实例注入Spring AI的ToolCallbackProvider,即可完成MCP工具的自动发布:
@Configuration
public class ToolExportConfiguration {
@Bean
public ToolCallbackProvider mcpToolRegistrar(ClimateDataService climateDataService) {
return MethodToolCallbackProvider.builder()
.toolObjects(climateDataService)
.build();
}
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
2.5 启动与连通性验证
@SpringBootApplication
public class WeatherMcpServerApp {
public static void main(String[] args) {
SpringApplication.run(WeatherMcpServerApp.class, args);
}
@Bean
public CommandLineRunner logRegisteredTools(ToolCallbackProvider provider) {
return args -> {
log.info("MCP Server initialized. Exposed tools:");
provider.getTools().forEach(tool -> log.info("- Tool ID: {}, Desc: {}", tool.getName(), tool.getDescription()));
};
}
}
服务启动后,监听http://localhost:9090/mcp-hub/events端点,若客户端能成功建立SSE连接并完成初始化握手,则表明Server端部署就绪。
3. AI应用端MCP Client集成
3.1 客户端依赖体系
在业务应用侧引入MCP Client Starter及对应的大模型驱动依赖:
<dependencies>
<!-- MCP Client -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<!-- 大语言模型驱动 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.2 客户端连接配置
spring:
ai:
mcp:
client:
sse:
connections:
climate-service:
url: http://localhost:9090/mcp-hub/events
zhipuai:
api-key: ${ZHIPU_API_KEY}
chat:
options:
model: glm-4
通过上述配置,Spring AI将在应用启动时自动与目标MCP Server建立SSE连接,拉取远端工具列表,并将其转换为标准Function Call格式注入至大模型的Prompt上下文中,从而实现业务系统与大模型工具能力的无缝打通。