从 0 搭建一个可运行的 MCP Demo,理解 Tool、Streamable HTTP 与自动工具调用。
一、大模型会聊天,但它不知道今天几度
大模型很擅长写代码、总结文档、陪人聊天,但它有几个天然的限制:
- 知识有截止时间,不知道“现在”的天气;
- 不能直接访问你的数据库、内部系统和业务 API;
- 即使接入了工具,每换一个模型、每接一个系统,往往都要重新写一套适配代码。
Function Calling 解决了“模型可以调用函数”的问题,但没有解决“工具如何被标准化地发现、描述和调用”的问题。于是,MCP(Model Context Protocol,模型上下文协议) 出现了。
MCP 由 Anthropic 在 2024 年底开源,目标是做 AI 应用与外部能力之间的标准接口。一个很形象的比喻是:
MCP 是 AI 世界的 USB-C。
工具提供方按 MCP 协议实现一次,任何支持 MCP 的 AI 客户端都可以即插即用。
适合读者:有 Java / Spring Boot 基础,想快速理解 MCP,并落地一个可运行 Demo 的后端开发者。
本文不走纯理论路线,而是直接从一个真实可运行的项目出发:
- 用 Spring AI 写一个 MCP 服务端,对外提供
getWeather天气查询工具; - 用 Spring AI MCP Client + DeepSeek 写一个 AI 客户端;
- 用户在网页上问“今天武汉天气怎么样”,DeepSeek 自动调用 MCP 工具,拿到结果后再用自然语言回答。
服务端的天气数据是写死的武汉市模拟数据,重点是把 MCP 的完整链路跑通。等链路通了,把模拟数据换成真实天气 API 只是替换一个方法的事。
二、MCP 的核心概念
MCP 采用典型的 Client/Server 架构,但和普通 HTTP 接口不同,它把“能力”抽象成了几种标准原语。
2.1 三个角色
| 角色 | 职责 |
|---|---|
| Host(宿主) | 用户直接使用的 AI 应用,例如 IDE、聊天客户端、桌面助手 |
| Client(客户端) | Host 内部与某个 MCP Server 保持连接的组件,负责协议通信 |
| Server(服务端) | 对外暴露工具、资源、提示词等能力的一方 |
一个 Host 可以同时连接多个 MCP Server,例如一个查天气、一个查数据库、一个操作 Git。对用户来说,它们就像同一套工具集。
2.2 三类核心能力
| 原语 | 作用 | 典型例子 |
|---|---|---|
| Tools(工具) | 让模型主动调用的函数 | 查天气、发邮件、创建工单 |
| Resources(资源) | 提供给模型读取的上下文数据 | 文件、数据库记录、日志 |
| Prompts(提示词) | 服务端预置的提示词模板 | 代码审查模板、周报模板 |
本文的 Demo 只用到 Tools,这也是目前落地最广泛的一类能力。
2.3 传输方式
MCP 支持多种传输方式,常见的有:
- stdio:客户端把 MCP Server 作为子进程启动,通过标准输入输出通信,适合本地工具;
- Streamable HTTP:通过 HTTP 传输,支持流式响应,适合远程服务;
- SSE:早期基于 Server-Sent Events 的方案,新版本中已逐步被 Streamable HTTP 取代。
本文使用 Streamable HTTP,服务端端点为 /mcp,客户端通过 http://localhost:8080/mcp 连接。相比 stdio,它更接近真实生产环境:服务可以独立部署、独立扩缩容,也方便加鉴权、限流和监控。
2.4 MCP 和 Function Calling 的关系
两者不是替代关系,而是分工不同:
- Function Calling 解决“模型如何表达我要调用某个函数”;
- MCP 解决“这个函数/工具从哪里来、如何描述、如何被标准化调用”。
可以把 MCP 理解为工具层的“服务发现 + 协议规范”。模型仍然通过 Function Calling 发起调用,只是工具的定义和实际执行由 MCP 统一管理。
三、项目目标与整体架构
最终项目包含两个独立的 Spring Boot 应用:
spring-ai-mcp/
├── mcp-server/ # MCP 服务端,端口 8080,提供 getWeather 工具
└── mcp-client/ # MCP 客户端 + AI 聊天页面,端口 8081,调用 DeepSeek
完整调用链路如下:
sequenceDiagram
autonumber
participant U as 用户/浏览器
participant C as MCP 客户端<br/>Spring Boot :8081
participant L as DeepSeek API
participant S as MCP 服务端<br/>Spring Boot :8080
U->>C: POST /api/chat "今天武汉天气怎么样?"
C->>L: chat/completions(messages + tools)
L-->>C: tool_calls: getWeather(city=武汉, date=...)
C->>S: MCP tools/call
S-->>C: 武汉模拟天气
C->>L: 工具结果 + 继续对话
L-->>C: 自然语言天气总结
C-->>U: {conversationId, answer}
这条链路里有三个关键点:
- 工具定义由 MCP 服务端提供,客户端启动时自动发现,不需要手写工具 Schema;
- 是否调用工具由大模型决定,客户端只负责把工具交给模型;
- 工具执行发生在 MCP 服务端,客户端通过 MCP 协议发起
tools/call。
四、技术栈与工程结构
4.1 版本选择
| 组件 | 版本 |
|---|---|
| JDK | 17 |
| Spring Boot | 3.5.15 |
| Spring AI | 1.1.8 |
| MCP Java SDK | 0.18.3(由 Spring AI Starter 传递) |
| 大模型 | DeepSeek deepseek-chat |
| 构建工具 | Maven 3.6+ |
选择 Spring AI 1.1.8 的原因很简单:它是 1.x 系列最新的稳定版本,Starter 与 Spring Boot 3.5.x 对齐,Spring AI 也提供了 DeepSeek 的 Starter,不需要自己拼 OpenAI 兼容层的配置。
4.2 父 POM
在父工程里统一管理版本,避免两个模块各自引版本导致冲突:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.15</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.1.8</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
有了 BOM,子模块引入 Spring AI 依赖时就不需要再写版本号。
五、MCP 服务端:把普通方法变成 MCP 工具
5.1 引入依赖
mcp-server/pom.xml:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
这个 Starter 基于 Spring MVC 提供 MCP Server 能力,内置了 Streamable HTTP 传输支持。如果需要响应式编程,可以换成 spring-ai-starter-mcp-server-webflux。
5.2 用 @Tool 定义天气工具
核心类 WeatherService 就是一个普通的 Spring @Service,不需要继承任何 MCP 接口:
@Service
public class WeatherService {
private static final Logger log = LoggerFactory.getLogger(WeatherService.class);
@Tool(
name = "getWeather",
description = """
查询指定城市在指定日期的天气情况。
参数 city 为城市中文名称(例如:武汉),参数 date 为日期,
格式必须是 yyyy-MM-dd(例如:2026-10-30)。
当前 Demo 仅内置了武汉市的模拟天气数据,查询其他城市会返回不支持提示。
"""
)
public String getWeather(
@ToolParam(description = "城市名称,例如:武汉") String city,
@ToolParam(description = "查询日期,格式 yyyy-MM-dd,例如:2026-10-30") String date) {
log.info("【MCP 工具被调用】getWeather(city={}, date={})", city, date);
LocalDate targetDate;
try {
targetDate = LocalDate.parse(date);
} catch (DateTimeParseException | NullPointerException e) {
return "查询失败:日期格式不正确,请使用 yyyy-MM-dd 格式,例如 2026-10-30。";
}
if (city == null || city.isBlank()) {
city = "武汉";
}
String normalizedCity = city.trim();
if (!normalizedCity.contains("武汉")
&& !"wuhan".equalsIgnoreCase(normalizedCity)) {
return "抱歉,当前 Demo 仅内置了武汉市(Wuhan)的模拟天气数据,暂不支持查询「"
+ normalizedCity + "」。";
}
// 写死的武汉模拟天气,真实项目里替换成调用气象 API 即可
return """
城市:武汉市
日期:%s
天气:晴间多云
气温:22℃ ~ 28℃
体感温度:26℃
湿度:65%%
风向风力:东南风 2 级
空气质量:良(AQI 72)
紫外线:中等
穿衣建议:短袖或薄外套,注意早晚温差
数据说明:以上为 MCP 服务端内置的模拟数据,仅用于演示,不是真实天气。
""".formatted(targetDate);
}
}
这段代码有几个容易被忽略的细节:
- 工具描述就是提示词。
@Tool的description和@ToolParam的description会进入模型的上下文,直接影响模型是否调用、参数填得对不对。日期格式、默认城市、支持范围最好都写清楚。 - 返回纯文本而不是 JSON。对模型来说,结构清晰的自然语言往往比嵌套 JSON 更容易理解和总结。
- 参数校验放在服务端。模型可能传错日期、传空城市,工具自身要做兜底,不能假设调用方一定正确。
- 日志要打出来。调试 MCP 时,最直观的信号就是服务端有没有打印“工具被调用”。
5.3 注册 ToolCallbackProvider
只写 @Tool 还不够,需要把带有工具方法的 Bean 注册成 ToolCallbackProvider:
@Configuration
public class McpToolConfig {
@Bean
public ToolCallbackProvider weatherToolCallbackProvider(WeatherService weatherService) {
return MethodToolCallbackProvider.builder()
.toolObjects(weatherService)
.build();
}
}
Spring AI 的 MCP Server 自动配置会收集容器里所有的 ToolCallbackProvider,把它们暴露给 MCP 客户端。也就是说:
你只需要关心“工具体”怎么写,协议层、注册表、
tools/list的响应都由 Spring AI 处理。
5.4 服务端配置
mcp-server/src/main/resources/application.yml:
server:
port: 8080
spring:
application:
name: weather-mcp-server
ai:
mcp:
server:
name: weather-mcp-server
version: 1.0.0
protocol: STREAMABLE
instructions: "武汉天气查询 MCP 服务:提供 getWeather 工具,参数为 city 和 date。"
capabilities:
tool: true
resource: false
prompt: false
streamable-http:
mcp-endpoint: /mcp
关键配置:
protocol: STREAMABLE:使用 Streamable HTTP;mcp-endpoint: /mcp:MCP 端点路径;capabilities.tool: true:开启工具能力;instructions:会随初始化响应返回给客户端,相当于服务端的“使用说明”。
启动后看到下面两行日志,说明工具注册成功:
Registered tools: 1
Started McpServerApplication in 4.163 seconds
六、MCP 客户端:让 DeepSeek 自动调用工具
6.1 引入依赖
mcp-client/pom.xml 需要三块能力:
<!-- Web 页面和 REST 接口 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MCP 客户端,负责连接 MCP Server、发现工具 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<!-- DeepSeek 聊天模型 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
6.2 配置 DeepSeek 和 MCP 连接
mcp-client/src/main/resources/application.yml:
server:
port: 8081
error:
include-message: always
spring:
application:
name: weather-mcp-client
ai:
deepseek:
# 把这里换成你自己的 Key,或者设置环境变量 DEEPSEEK_API_KEY
api-key: "${DEEPSEEK_API_KEY:sk-请填写你的DeepSeekAPIKey}"
base-url: "${DEEPSEEK_BASE_URL:https://api.deepseek.com}"
chat:
options:
model: "${DEEPSEEK_MODEL:deepseek-chat}"
temperature: 0.7
mcp:
client:
name: weather-mcp-client
version: 1.0.0
type: SYNC
request-timeout: 30s
streamable-http:
connections:
weather-server:
url: "${MCP_SERVER_URL:http://localhost:8080}"
endpoint: /mcp
这段配置里有两个关键点:
type: SYNC:使用同步 MCP 客户端,工具调用会阻塞等待结果,代码更直观;streamable-http.connections.weather-server:声明一个名为weather-server的连接,客户端启动时会自动连接并拉取工具列表。
Spring AI 会自动创建 SyncMcpToolCallbackProvider,它实现了 ToolCallbackProvider,里面封装了从服务端发现的所有工具。我们只需要把它注入 ChatClient。
6.3 组装 ChatClient
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder,
ToolCallbackProvider mcpToolCallbackProvider,
ChatMemory chatMemory) {
return builder
.defaultToolCallbacks(mcpToolCallbackProvider)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
}
这三行代码分别解决三个问题:
defaultToolCallbacks(...):把 MCP 发现的工具交给模型,模型才能发起getWeather调用;MessageChatMemoryAdvisor:按会话保存多轮对话,用户问完“今天”再问“明天”时模型能理解上下文;ChatClient:Spring AI 的统一聊天入口,屏蔽不同模型厂商的差异。
再补一个启动日志,方便确认工具发现是否成功:
@Bean
public ApplicationRunner mcpToolLogger(ToolCallbackProvider provider) {
return args -> {
ToolCallback[] callbacks = provider.getToolCallbacks();
log.info("MCP 客户端已连接,发现 {} 个工具:", callbacks.length);
Arrays.stream(callbacks).forEach(callback -> log.info(" - {}:{}",
callback.getToolDefinition().name(),
callback.getToolDefinition().description()));
};
}
启动后应该看到:
MCP 客户端已连接,发现 1 个工具:
- getWeather:查询指定城市在指定日期的天气情况。...
如果这里是 0 个工具,后面的对话一定不会调用天气能力,应该先排查 MCP 连接。
6.4 对话接口与系统提示词
/api/chat 接口的核心逻辑很短:
@PostMapping("/chat")
public ChatResponse chat(@RequestBody ChatRequest request) {
String conversationId = (request.conversationId() == null || request.conversationId().isBlank())
? UUID.randomUUID().toString()
: request.conversationId();
String answer = chatClient.prompt()
.system(systemPrompt())
.user(request.message())
.advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
return new ChatResponse(conversationId, answer);
}
系统提示词是“模型会不会乖乖调用工具”的关键。这里每次请求都动态生成,把真实的今天、明天、后天注入进去:
private String systemPrompt() {
LocalDate today = LocalDate.now();
String week = today.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.CHINA);
return """
你是“天气小助手”,一个基于 Spring AI + MCP 的中文天气助手。
当前日期:%s(%s)。
回答规则:
1. 当用户询问天气时,必须调用 getWeather 工具获取数据,禁止凭空编造天气。
2. 调用 getWeather 时:
- city:城市中文名;如果用户没有指定城市,默认使用“武汉”;
- date:yyyy-MM-dd 格式的日期。
3. 相对日期的换算基准:今天 = %s,明天 = %s,后天 = %s。
4. 拿到工具结果后,用简洁自然的中文总结天气,并注明“这是演示用的模拟数据”。
5. 如果工具提示不支持该城市,请如实告诉用户。
6. 如果用户只是打招呼或询问与天气无关的问题,可以正常聊天,不必调用工具。
""".formatted(today, week, today, today.plusDays(1), today.plusDays(2));
}
为什么要把日期写进提示词?
因为“今天”“明天”是相对日期,模型自己算容易错,尤其跨月、跨年时。把当前日期明确告诉它,再给出换算基准,能显著降低传错 date 的概率。
6.5 内置聊天页面
客户端还带了一个纯静态的 index.html,访问 http://localhost:8081 就能聊天,不需要额外启动前端工程。
页面会:
- 调用
/api/tools展示当前 MCP 工具; - 调用
/api/chat发送消息; - 用
crypto.randomUUID()生成conversationId; - 支持回车发送、Shift + Enter 换行、清空会话。
对于 Demo 来说,这比单独写一个 React 工程更省事,也方便分享给不懂前端的人直接体验。
七、跑起来:从 Key 到第一条天气回答
7.1 配置 DeepSeek Key
推荐用环境变量,避免把 Key 提交到 Git:
export DEEPSEEK_API_KEY=sk-你的真实Key
Windows PowerShell:
$env:DEEPSEEK_API_KEY="sk-你的真实Key"
也可以直接修改 mcp-client/src/main/resources/application.yml 里的 api-key。项目里预留的占位符是:
api-key: "${DEEPSEEK_API_KEY:sk-请填写你的DeepSeekAPIKey}"
7.2 编译
mvn clean package -DskipTests
会得到两个可执行 Jar:
mcp-server/target/mcp-server-1.0.0.jar
mcp-client/target/mcp-client-1.0.0.jar
7.3 先启动服务端
java -jar mcp-server/target/mcp-server-1.0.0.jar
服务端监听 8080,MCP 端点:
http://localhost:8080/mcp
7.4 再启动客户端
java -jar mcp-client/target/mcp-client-1.0.0.jar
客户端监听 8081。启动日志里确认工具发现成功:
MCP 客户端已连接,发现 1 个工具:
- getWeather:查询指定城市在指定日期的天气情况。...
Tomcat started on port 8081 (http)
7.5 提问
打开浏览器访问:
http://localhost:8081
输入:
今天武汉天气怎么样?
此时服务端控制台会打印:
【MCP 工具被调用】getWeather(city=武汉, date=2026-10-30)
页面最终返回类似:
武汉今天(2026-10-30)晴间多云,气温 22℃~28℃,体感 26℃,湿度 65%,东南风 2 级,空气质量良(AQI 72)。这是演示用的模拟数据。
也可以继续问“明天呢?”“后天会下雨吗?”,客户端会把相对日期换算成具体日期,再调用工具。
八、这条链路在协议层到底发生了什么
从代码看,好像只是 chatClient.prompt().call() 一行,但底层发生了很多事。
8.1 启动阶段:工具发现
MCP 客户端启动时,会向服务端发起初始化握手,然后调用 tools/list。服务端返回的工具定义大致如下:
{
"name": "getWeather",
"description": "查询指定城市在指定日期的天气情况。...",
"inputSchema": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名称,例如:武汉" },
"date": { "type": "string", "description": "查询日期,格式 yyyy-MM-dd" }
},
"required": ["city", "date"]
}
}
Spring AI 把这些定义转换成模型能理解的 Function Schema,交给 DeepSeek。
8.2 对话阶段:模型决定调用
用户提问后,客户端把“系统提示词 + 用户消息 + 工具定义”一起发给 DeepSeek。DeepSeek 判断需要查天气,返回类似:
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_xxx",
"type": "function",
"function": {
"name": "getWeather",
"arguments": "{\"city\":\"武汉\",\"date\":\"2026-10-30\"}"
}
}
]
}
注意:模型只负责“决定调用什么、参数是什么”,并不执行工具。真正执行的是 MCP 客户端。
8.3 工具执行:MCP tools/call
客户端收到 tool_calls 后,通过 MCP 协议向服务端发起调用:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "getWeather",
"arguments": {
"city": "武汉",
"date": "2026-10-30"
}
}
}
服务端执行 WeatherService.getWeather,返回:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "城市:武汉市\n日期:2026-10-30\n天气:晴间多云\n..."
}
],
"isError": false
}
}
8.4 最终回答:工具结果回传模型
客户端把工具结果作为一条 tool 消息追加到对话里,再次请求 DeepSeek。DeepSeek 这时已经拿到了真实数据,只需要把它总结成自然语言。
这就是为什么用户感觉“模型自己查了天气”,但实际上模型只做了两件事:决定调用工具 和 总结工具结果。
九、踩坑记录:那些文档里不一定写的细节
9.1 客户端启动时报连接失败
MCP 客户端默认 initialized: true,启动阶段就会连接服务端。如果先启动客户端、后启动服务端,客户端会初始化失败。
解决方式:先启动 mcp-server,再启动 mcp-client。如果顺序反了,重启客户端。
9.2 没有配置 Key,服务能启动但对话报错
spring.ai.deepseek.api-key 只要非空,Spring AI 就允许启动。占位符 sk-请填写你的DeepSeekAPIKey 也是非空字符串,所以服务能正常启动,但调用 DeepSeek 时会返回 401。
Demo 里的 ApiExceptionHandler 会把底层异常包装成 HTTP 502,并在 error 字段里带上原因,方便前端展示。排查时先看这个字段。
9.3 模型不调用工具,直接编天气
优先检查三件事:
- 客户端启动日志里是否发现
getWeather; /api/tools是否返回工具;- 工具描述和参数描述是否足够清楚。
如果工具描述写得太模糊,模型可能“觉得”自己知道答案,就不调用工具。系统提示词里明确写“天气问题必须调用工具”也有帮助。
9.4 “今天”被模型算错
大模型对相对日期不敏感,尤其是跨月、跨年。不要让它自己算,直接把当前日期和“今天/明天/后天”的换算基准写进系统提示词。
9.5 工具返回 JSON,模型反而总结得不好
工具结果不是越结构化越好。对 LLM 来说,一段字段清晰的纯文本通常比深层嵌套 JSON 更好读。这个 Demo 故意返回“城市/日期/天气/气温”这样的文本块,模型很容易提取。
9.6 Maven 下载依赖很慢
Spring AI 的依赖树不小,国内访问 Maven Central 可能很慢。可以在 ~/.m2/settings.xml 配置阿里云镜像:
<mirror>
<id>aliyun</id>
<mirrorOf>central</mirrorOf>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
如果某个 jar 下载损坏,删掉本地仓库对应目录后执行 mvn -U clean package -DskipTests 重试。
9.7 用 Mock 测试时,注意请求体编码
如果你像本文一样用 Python/Node 写一个假的 OpenAI 接口来验证链路,要注意:JDK HttpClient 在请求体长度未知时会使用 Transfer-Encoding: chunked。很多简易 Mock 只读 Content-Length,会解析出空 body,导致模型一直返回 tool_calls,客户端无限调用工具。
这不是 Spring AI 的问题,而是 Mock 没正确处理 chunked 编码。给 Mock 加上 chunked 解码即可。
十、从 Demo 到生产:还可以做什么
这个 Demo 只实现了最小闭环,真实项目里还可以继续扩展:
10.1 接入真实天气 API
替换 WeatherService.getWeather 的实现,调用和风天气、OpenWeatherMap、高德天气等 HTTP API。MCP 工具签名不变,客户端完全不需要改。
10.2 增加更多工具
例如:
getAirQuality(city, date):空气质量;getWeatherForecast(city, days):未来几天预报;getTravelAdvice(city, date):出行建议;sendWeatherAlert(...):极端天气提醒。
每个工具仍然是一个 @Tool 方法,注册到 ToolCallbackProvider 即可。
10.3 使用 Resources 和 Prompts
除了 Tools,MCP 还支持 Resources 和 Prompts。例如把“天气查询规范”做成 Prompt 模板,把“城市列表”做成 Resource,让客户端按需读取。
10.4 会话持久化
Demo 使用 InMemoryChatMemoryRepository,重启后会话丢失。生产环境可以换成 Redis、JDBC 等持久化实现。
10.5 鉴权、限流与可观测性
Streamable HTTP 服务部署到公网时,至少要考虑:
- MCP 端点鉴权(API Key / OAuth / 网关);
- 工具调用限流,避免被刷;
- 超时与重试;
- 工具调用日志、耗时、成功率指标;
- 敏感工具的人工确认(Human-in-the-loop)。
10.6 多 MCP Server 聚合
一个客户端可以配置多个 connections,同时连接天气、数据库、Git 等多个 MCP Server。Spring AI 会把所有工具聚合到同一个 ToolCallbackProvider 中,模型根据描述自行选择。
十一、总结
MCP 的价值,不是让大模型“多了一个函数”,而是把 AI 与外部能力之间的连接方式标准化:
- 工具提供方只需实现一次 MCP Server;
- 任何支持 MCP 的客户端都能发现并调用;
- 模型、工具、宿主应用可以各自演进,互不绑死。
Spring AI 则把 Java 接入 MCP 的门槛降到了几个注解和几行配置:
- 服务端:
@Tool+MethodToolCallbackProvider+spring-ai-starter-mcp-server-webmvc; - 客户端:
spring-ai-starter-mcp-client+ChatClient.defaultToolCallbacks(...); - 大模型:换成 DeepSeek,只需要一个 Starter 和一行
api-key。
如果你正在做 Java 侧的 AI 应用,不妨从这个天气 Demo 开始:先让链路跑通,再逐步把模拟数据换成真实业务能力。等工具越来越多,你会越来越体会到“协议标准化”带来的价值。
参考资料
- MCP 官方文档:https://modelcontextprotocol.io
- Spring AI 官方文档:https://docs.spring.io/spring-ai/reference/
- DeepSeek API 文档:https://api-docs.deepseek.com/
附:项目结构速查
spring-ai-mcp/
├── pom.xml
├── mcp-server/
│ ├── pom.xml
│ └── src/main/
│ ├── java/com/example/mcp/server/
│ │ ├── McpServerApplication.java
│ │ ├── config/McpToolConfig.java
│ │ └── weather/WeatherService.java
│ └── resources/application.yml
└── mcp-client/
├── pom.xml
└── src/main/
├── java/com/example/mcp/client/
│ ├── McpClientApplication.java
│ ├── config/ChatClientConfig.java
│ └── chat/ChatController.java
└── resources/
├── application.yml
└── static/index.html
启动顺序再强调一次:
# 1. 配置 Key
export DEEPSEEK_API_KEY=sk-你的真实Key
# 2. 编译
mvn clean package -DskipTests
# 3. 启动 MCP 服务端(8080)
java -jar mcp-server/target/mcp-server-1.0.0.jar
# 4. 启动 MCP 客户端(8081)
java -jar mcp-client/target/mcp-client-1.0.0.jar
然后打开 http://localhost:8081,问一句:
今天武汉天气怎么样?
评论区