Spring AI 入门教程(五):Model Context Protocol (MCP)
📖 前言
在之前的教程中,我们已经学习了 Spring AI 的基础概念、聊天模型、函数调用等核心功能。今天,我们将深入探索 Spring AI 生态系统中一个非常重要的协议:MCP(Model Context Protocol)。
MCP 是一个开放协议,用于标准化应用程序与 AI 模型之间的上下文交互。通过 MCP,我们可以将工具(Tools)、资源(Resources)和提示模板(Prompts)以统一的方式暴露给 AI 模型,使 AI 应用更加模块化和可扩展。
在本篇教程中,我们将通过完整的代码示例,学习如何:
- ✅ 构建 MCP Server,提供天气查询、系统信息和问候模板服务
- ✅ 构建 MCP Client,连接并消费 MCP Server 的能力
- ✅ 将 Resources 和 Prompts 转换为 Tools,让 LLM 动态调用
- ✅ 实现 Tools、Resources、Prompts 的综合应用场景
🎯 什么是 MCP?
MCP(Model Context Protocol) 是由 Anthropic 提出的开放协议,旨在标准化 AI 应用与外部数据源、工具和服务的交互方式。
MCP 的三大核心能力
- Tools(工具):允许 AI 模型执行操作,如查询天气、计算数据等
- Resources(资源):提供数据访问接口,如读取文件、查询系统信息等
- Prompts(提示模板):预定义的提示模板,支持参数化生成个性化内容
Spring AI MCP 架构
┌─────────────────────────────────────────┐
│ MCP Client (Spring AI) │
│ ┌──────────┬──────────┬──────────────┐ │
│ │ Tools │Resources │ Prompts │ │
│ └──────────┴──────────┴──────────────┘ │
└──────────────┬──────────────────────────┘
│ Streamable-HTTP / SSE
┌──────────────┴──────────────────────────┐
│ MCP Server (Spring AI) │
│ ┌──────────┬──────────┬──────────────┐ │
│ │@McpTool │@McpRes. │ @McpPrompt │ │
│ └──────────┴──────────┴──────────────┘ │
└─────────────────────────────────────────┘
🏗️ 项目结构
本教程包含两个独立的 Spring Boot 应用:
spring-ai-demo/
├── pom.xml # 父 POM(依赖管理)
├── spring-ai-mcp-server/ # MCP 服务器(端口 8100)
│ ├── pom.xml
│ └── src/main/java/cn/dianyu/ai/mcp/server/
│ ├── McpServerDemoApplication.java
│ ├── McpWeatherTools.java # 天气查询工具
│ ├── SystemInfoResource.java # 系统信息资源
│ └── GreetingPrompt.java # 问候提示模板
│
└── spring-ai-mcp-client/ # MCP 客户端(端口 8200)
├── pom.xml
└── src/main/java/cn/dianyu/ai/mcp/client/
├── McpClientDemoApplication.java
├── ChatController.java # 聊天控制器
├── McpResourceToolConfig.java # Resource 转 Tool 配置
└── McpPromptToolConfig.java # Prompt 转 Tool 配置
父 POM 说明
项目的根目录 pom.xml 作为父 POM,负责统一的依赖管理和版本控制:
<project>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.14</version>
</parent>
<groupId>cn.dianyu.ai</groupId>
<artifactId>spring-ai-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>spring-ai-mcp-server</module>
<module>spring-ai-mcp-client</module>
</modules>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.1.6</spring-ai.version>
</properties>
<!-- 公共依赖(所有子模块共享) -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- DeepSeek 模型支持 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
<!-- 智谱 AI 模型支持 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-zhipuai</artifactId>
</dependency>
</dependencies>
<!-- 依赖管理:统一 Spring AI 版本 -->
<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>
</project>
父 POM 的关键作用:
- 统一版本管理:通过
<properties>定义 Spring AI 版本(1.1.6),所有子模块使用相同版本 - 依赖管理:通过
<dependencyManagement>导入 Spring AI BOM,确保依赖版本一致性 - 公共依赖:将 Web、DeepSeek、智谱 AI 等通用依赖放在父 POM,子模块无需重复声明
- 模块化构建:通过
<modules>声明子模块,支持一次性构建整个项目
子模块 POM 简化:
由于父 POM 已经管理了大部分依赖,子模块的 pom.xml 非常简洁:
<!-- spring-ai-mcp-server/pom.xml -->
<project>
<parent>
<groupId>cn.dianyu.ai</groupId>
<artifactId>spring-ai-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-ai-mcp-server</artifactId>
<dependencies>
<!-- 只需声明 MCP Server 特有依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>
</project>
<!-- spring-ai-mcp-client/pom.xml -->
<project>
<parent>
<groupId>cn.dianyu.ai</groupId>
<artifactId>spring-ai-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-ai-mcp-client</artifactId>
<dependencies>
<!-- 只需声明 MCP Client 特有依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
</dependencies>
</project>
优势:
- ✅ 避免版本冲突:所有模块使用统一的 Spring AI 版本
- ✅ 简化子模块配置:子模块只需关注自己的特有依赖
- ✅ 便于升级:修改父 POM 中的版本号即可升级所有模块
- ✅ 符合 Maven 最佳实践:采用父子 POM 结构
🚀 第一部分:构建 MCP Server
1.1 添加依赖
在 spring-ai-mcp-server/pom.xml 中添加 MCP Server Starter:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
支持的传输协议:
spring-ai-starter-mcp-server- STDIO 传输spring-ai-starter-mcp-server-webmvc- WebMVC(SSE/Streamable-HTTP/Stateless)spring-ai-starter-mcp-server-webflux- WebFlux(响应式)
1.2 配置文件
application.yaml 关键配置:
server:
port: 8100
spring:
ai:
mcp:
server:
enabled: true
protocol: STREAMABLE # 使用 Streamable-HTTP 协议
name: my-spring-ai-mcp-server
version: 1.0.0
type: SYNC # 同步服务器类型
instructions: "该服务器提供天气查询工具、系统信息资源和问候提示"
# 启用所有能力
capabilities:
resource: true
tool: true
prompt: true
completion: true
# 变更通知
resource-change-notification: true
tool-change-notification: true
prompt-change-notification: true
# Streamable-HTTP 端点
streamable-http:
mcp-endpoint: /mcp
keep-alive-interval: 30s
# 启用注解自动扫描
annotation-scanner:
enabled: true
zhipuai:
api-key: your-api-key
chat:
enabled: true
options:
model: glm-4.5-air
temperature: 0.7
1.3 创建 MCP Tools
使用 @McpTool 注解定义工具方法:
@Component
public class McpWeatherTools {
private final Random random = new Random();
/**
* 获取指定城市的当前天气
*/
@McpTool(name = "mcp_get_current_weather",
description = "获取指定城市的当前天气信息")
public String getCurrentWeather(
@McpToolParam(description = "城市名称,例如:北京、上海、广州",
required = true) String cityName) {
String[] conditions = {"晴", "多云", "小雨", "大雨", "雷阵雨", "雪"};
String condition = conditions[random.nextInt(conditions.length)];
int temperature = random.nextInt(35) - 5;
int humidity = random.nextInt(60) + 30;
return String.format("【%s】当前天气:%s,温度:%d°C,湿度:%d%%,更新时间:%s",
cityName, condition, temperature, humidity, LocalDateTime.now());
}
/**
* 获取天气预报(未来多天)
*/
@McpTool(name = "mcp_get_weather_forecast",
description = "获取指定城市的天气预报(未来多天)")
public String getWeatherForecast(
@McpToolParam(description = "城市名称", required = true) String cityName,
@McpToolParam(description = "预报天数(1-7天),默认5天",
required = false) Integer days) {
if (days == null || days < 1) days = 5;
if (days > 7) days = 7;
StringBuilder forecast = new StringBuilder();
forecast.append(String.format("【%s】未来%d天天气预报:\n", cityName, days));
String[] conditions = {"晴", "多云", "小雨", "大雨", "雷阵雨", "雪"};
for (int i = 1; i <= days; i++) {
String condition = conditions[random.nextInt(conditions.length)];
int highTemp = random.nextInt(15) + 20;
int lowTemp = highTemp - random.nextInt(10) - 5;
forecast.append(String.format(" 第%d天:%s,%d°C ~ %d°C\n",
i, condition, lowTemp, highTemp));
}
return forecast.toString();
}
/**
* 比较两个城市的天气
*/
@McpTool(name = "mcp_compare_weather",
description = "比较两个城市的天气情况")
public String compareWeather(
@McpToolParam(description = "第一个城市名称", required = true) String city1,
@McpToolParam(description = "第二个城市名称", required = true) String city2) {
String weather1 = getCurrentWeather(city1);
String weather2 = getCurrentWeather(city2);
return String.format("天气对比:\n%s\n%s", weather1, weather2);
}
/**
* 获取空气质量指数
*/
@McpTool(name = "mcp_get_air_quality",
description = "获取指定城市的空气质量指数(AQI)")
public Map<String, Object> getAirQuality(
@McpToolParam(description = "城市名称", required = true) String cityName) {
Map<String, Object> result = new HashMap<>();
int aqi = random.nextInt(200) + 10;
String level;
String color;
if (aqi <= 50) {
level = "优";
color = "绿色";
} else if (aqi <= 100) {
level = "良";
color = "黄色";
} else if (aqi <= 150) {
level = "轻度污染";
color = "橙色";
} else {
level = "中度污染";
color = "红色";
}
result.put("city", cityName);
result.put("aqi", aqi);
result.put("level", level);
result.put("color", color);
result.put("timestamp", LocalDateTime.now().toString());
return result;
}
}
关键点:
- 使用
@Component标记为 Spring Bean @McpTool定义工具名称和描述@McpToolParam定义参数及其描述,支持required属性- 支持返回字符串或结构化数据(Map、List 等)
- Spring Boot 会自动扫描并注册这些工具
1.4 创建 MCP Resources
使用 @McpResource 注解定义资源访问:
@Component
public class SystemInfoResource {
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 获取系统信息资源
* 支持的 infoType:memory、cpu、system、jvm、all
*/
@McpResource(
uri = "system://{infoType}",
name = "系统信息",
description = "获取系统运行时信息,支持 memory、cpu、system、jvm、all 等类型"
)
public String getSystemInfo(String infoType) {
try {
Map<String, Object> systemInfo = new HashMap<>();
switch (infoType.toLowerCase()) {
case "memory":
systemInfo.put("type", "memory");
systemInfo.putAll(getMemoryInfo());
break;
case "cpu":
systemInfo.put("type", "cpu");
systemInfo.putAll(getCpuInfo());
break;
case "system":
systemInfo.put("type", "system");
systemInfo.putAll(getSystemInfo());
break;
case "jvm":
systemInfo.put("type", "jvm");
systemInfo.putAll(getJvmInfo());
break;
case "all":
systemInfo.put("type", "all");
systemInfo.put("memory", getMemoryInfo());
systemInfo.put("cpu", getCpuInfo());
systemInfo.put("system", getSystemInfo());
systemInfo.put("jvm", getJvmInfo());
break;
default:
systemInfo.put("error", "不支持的信息类型: " + infoType);
systemInfo.put("supportedTypes",
new String[]{"memory", "cpu", "system", "jvm", "all"});
}
systemInfo.put("timestamp", LocalDateTime.now().toString());
return objectMapper.writeValueAsString(systemInfo);
} catch (Exception e) {
throw new RuntimeException("无法生成系统信息: " + infoType, e);
}
}
private Map<String, Object> getMemoryInfo() {
Map<String, Object> memoryInfo = new HashMap<>();
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
memoryInfo.put("heapUsed",
memoryMXBean.getHeapMemoryUsage().getUsed() / (1024 * 1024) + " MB");
memoryInfo.put("heapMax",
memoryMXBean.getHeapMemoryUsage().getMax() / (1024 * 1024) + " MB");
memoryInfo.put("nonHeapUsed",
memoryMXBean.getNonHeapMemoryUsage().getUsed() / (1024 * 1024) + " MB");
return memoryInfo;
}
private Map<String, Object> getCpuInfo() {
Map<String, Object> cpuInfo = new HashMap<>();
OperatingSystemMXBean osMXBean =
ManagementFactory.getOperatingSystemMXBean();
cpuInfo.put("availableProcessors", osMXBean.getAvailableProcessors());
cpuInfo.put("systemLoadAverage", osMXBean.getSystemLoadAverage());
return cpuInfo;
}
private Map<String, Object> getSystemInfo() {
Map<String, Object> sysInfo = new HashMap<>();
sysInfo.put("osName", System.getProperty("os.name"));
sysInfo.put("osVersion", System.getProperty("os.version"));
sysInfo.put("osArch", System.getProperty("os.arch"));
sysInfo.put("userName", System.getProperty("user.name"));
sysInfo.put("userHome", System.getProperty("user.home"));
return sysInfo;
}
private Map<String, Object> getJvmInfo() {
Map<String, Object> jvmInfo = new HashMap<>();
jvmInfo.put("javaVersion", System.getProperty("java.version"));
jvmInfo.put("javaVendor", System.getProperty("java.vendor"));
jvmInfo.put("javaHome", System.getProperty("java.home"));
jvmInfo.put("vmName", System.getProperty("java.vm.name"));
jvmInfo.put("startTime", ManagementFactory.getRuntimeMXBean().getStartTime());
jvmInfo.put("uptime", ManagementFactory.getRuntimeMXBean().getUptime() + " ms");
return jvmInfo;
}
}
关键点:
- URI 模板支持动态参数:
system://{infoType} - 返回 JSON 格式的结构化数据
- 适合提供实时数据、文件内容、数据库查询结果等
- 资源通过 URI 访问,客户端可以动态读取
1.5 创建 MCP Prompts
使用 @McpPrompt 注解定义提示模板:
@Component
public class GreetingPrompt {
/**
* 生成个性化问候提示
*/
@McpPrompt(
name = "greeting",
description = "生成个性化的问候语,支持多种语言"
)
public String generateGreeting(
@McpArg(name = "name",
description = "要问候的姓名或称呼",
required = true) String name,
@McpArg(name = "language",
description = "语言类型:zh-中文, en-英文",
required = false) String language) {
if (name == null || name.trim().isEmpty()) name = "朋友";
if (language == null || language.trim().isEmpty()) language = "zh";
if ("en".equalsIgnoreCase(language)) {
return String.format("Hello %s! How can I assist you today?", name);
} else {
return String.format("你好,%s!今天我能帮你什么?", name);
}
}
/**
* 生成商务问候提示
*/
@McpPrompt(
name = "business_greeting",
description = "生成专业的商务问候语"
)
public String generateBusinessGreeting(
@McpArg(name = "recipientName",
description = "收件人姓名",
required = true) String recipientName,
@McpArg(name = "senderName",
description = "发件人姓名",
required = true) String senderName) {
if (recipientName == null || recipientName.trim().isEmpty())
recipientName = "尊敬的客户";
if (senderName == null || senderName.trim().isEmpty())
senderName = "客服代表";
return String.format(
"尊敬的 %s:\n\n您好!我是 %s,很高兴为您服务。请问有什么可以帮助您的吗?",
recipientName, senderName
);
}
/**
* 生成节日祝福提示
*/
@McpPrompt(
name = "festival_greeting",
description = "生成节日祝福问候语"
)
public String generateFestivalGreeting(
@McpArg(name = "recipientName",
description = "收件人姓名",
required = true) String recipientName,
@McpArg(name = "festival",
description = "节日名称:春节、中秋、圣诞、元旦等",
required = true) String festival) {
if (recipientName == null || recipientName.trim().isEmpty())
recipientName = "朋友";
if (festival == null || festival.trim().isEmpty())
festival = "节日";
return String.format(
"亲爱的 %s:\n\n祝您%s快乐!愿您在新的一年里身体健康、工作顺利、万事如意!",
recipientName, festival
);
}
}
关键点:
@McpPrompt定义提示模板名称和描述@McpArg定义参数及其是否必需- 适合生成邮件模板、对话开场白、特定场景的提示等
- 提示模板可以被客户端重复使用,支持参数化定制
1.6 启动 MCP Server
@SpringBootApplication
public class McpServerDemoApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerDemoApplication.class, args);
}
}
启动后,MCP Server 将在 http://localhost:8100/mcp 提供服务。
验证服务器:
# 检查服务器是否启动
curl http://localhost:8100/mcp
🔌 第二部分:构建 MCP Client
2.1 添加依赖
在 spring-ai-mcp-client/pom.xml 中添加 MCP Client Starter:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
支持的传输协议:
spring-ai-starter-mcp-client- STDIO、HTTP/SSE、Streamable-HTTP(基于 JDK HttpClient)spring-ai-starter-mcp-client-webflux- WebFlux 传输(推荐生产环境)
2.2 配置文件
application.yaml 关键配置:
server:
port: 8200
spring:
ai:
mcp:
server:
enabled: false # 客户端不需要启用 Server
client:
enabled: true
name: my-mcp-client
version: 1.0.0
type: SYNC
request-timeout: 30s
# 启用工具回调集成
toolcallback:
enabled: true
# 启用注解扫描
annotation-scanner:
enabled: true
# 连接到本地 MCP Server
streamable-http:
connections:
local-server:
url: http://localhost:8100
endpoint: /mcp
zhipuai:
api-key: your-api-key
chat:
enabled: true
options:
model: glm-4.5-air
temperature: 0.7
2.3 基础聊天控制器
@RestController
@RequestMapping("/chat")
public class ChatController {
private final ZhiPuAiChatModel chatModel;
private final SyncMcpToolCallbackProvider toolCallbackProvider;
private final List<McpSyncClient> mcpSyncClients;
public ChatController(ZhiPuAiChatModel chatModel,
SyncMcpToolCallbackProvider toolCallbackProvider,
List<McpSyncClient> mcpSyncClients) {
this.chatModel = chatModel;
this.toolCallbackProvider = toolCallbackProvider;
this.mcpSyncClients = mcpSyncClients;
}
/**
* 基础聊天:使用 MCP Tools
*/
@GetMapping
public ChatResponse chat(String message) {
// 获取 MCP 工具作为 Spring AI 函数回调
ToolCallback[] mcpTools = toolCallbackProvider.getToolCallbacks();
ChatClient chatClient = ChatClient.create(chatModel);
Prompt prompt = new Prompt(message, ChatOptions.builder().build());
// 使用自定义的工具名称(由 Resource/Prompt 转换而来)
return chatClient.prompt(prompt)
.toolNames("system_info_reader", "greeting_generator",
"business_greeting_generator", "festival_greeting_generator")
.toolCallbacks(toolCallbackProvider)
.call()
.chatResponse();
}
}
测试示例:
# 使用天气工具
curl "http://localhost:8200/chat?message=北京的天气怎么样?"
# 使用系统信息工具
curl "http://localhost:8200/chat?message=当前系统的内存使用情况如何?"
2.4 访问 MCP Resources
/**
* 获取所有可用的 MCP Resources 列表
*/
@GetMapping("/resources")
public McpSchema.ListResourceTemplatesResult getResources() {
if (mcpSyncClients.isEmpty()) {
throw new IllegalStateException("没有可用的 MCP 客户端连接");
}
return mcpSyncClients.get(0).listResourceTemplates();
}
/**
* 读取指定的 MCP Resource
*/
@GetMapping("/resource/read")
public McpSchema.ReadResourceResult readResource(@RequestParam String uri) {
if (mcpSyncClients.isEmpty()) {
throw new IllegalStateException("没有可用的 MCP 客户端连接");
}
McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest(uri);
return mcpSyncClients.get(0).readResource(request);
}
测试示例:
# 获取所有资源模板
curl http://localhost:8200/chat/resources
# 读取系统内存信息
curl "http://localhost:8200/chat/resource/read?uri=system://memory"
# 读取所有系统信息
curl "http://localhost:8200/chat/resource/read?uri=system://all"
返回示例:
{
"type": "memory",
"heapUsed": "128 MB",
"heapMax": "4096 MB",
"nonHeapUsed": "45 MB",
"timestamp": "2026-06-16T10:30:00"
}
2.5 使用 MCP Prompts
/**
* 获取所有可用的 MCP Prompts 列表
*/
@GetMapping("/prompts")
public McpSchema.ListPromptsResult getPrompts() {
if (mcpSyncClients.isEmpty()) {
throw new IllegalStateException("没有可用的 MCP 客户端连接");
}
return mcpSyncClients.get(0).listPrompts();
}
/**
* 使用 MCP Prompt 生成消息
*/
@PostMapping("/prompt/use")
public String usePrompt(@RequestParam String promptName,
@RequestParam(required = false) String name,
@RequestParam(required = false) String language) {
if (mcpSyncClients.isEmpty()) {
throw new IllegalStateException("没有可用的 MCP 客户端连接");
}
// 构建参数 Map
Map<String, Object> arguments = new HashMap<>();
if (name != null) arguments.put("name", name);
if (language != null) arguments.put("language", language);
McpSchema.GetPromptRequest request =
new McpSchema.GetPromptRequest(promptName, arguments);
McpSchema.GetPromptResult result =
mcpSyncClients.get(0).getPrompt(request);
// 将生成的提示内容发送给 LLM
ChatClient chatClient = ChatClient.create(chatModel);
String promptMessage = result.messages().get(0).content().toString();
return chatClient.prompt(promptMessage).call().content();
}
测试示例:
# 获取所有提示模板
curl http://localhost:8200/chat/prompts
# 使用问候提示模板
curl -X POST "http://localhost:8200/chat/prompt/use?promptName=greeting&name=张三&language=zh"
# 使用商务问候模板
curl -X POST "http://localhost:8200/chat/prompt/use?promptName=business_greeting&recipientName=李总&senderName=王经理"
# 使用节日祝福模板
curl -X POST "http://localhost:8200/chat/prompt/use?promptName=festival_greeting&recipientName=赵先生&festival=春节"
🔄 第三部分:将 Resources 和 Prompts 转换为 Tools
这是一个高级技巧!通过将 Resources 和 Prompts 包装成 Spring AI Tools,我们可以让 LLM 自主决定何时调用它们,而不是硬编码调用逻辑。
3.1 Resource 转 Tool
创建 McpResourceToolConfig.java:
@Component
public class McpResourceToolConfig {
private final List<McpSyncClient> mcpSyncClients;
private static final Logger log = LoggerFactory.getLogger(McpResourceToolConfig.class);
public McpResourceToolConfig(List<McpSyncClient> mcpSyncClients) {
this.mcpSyncClients = mcpSyncClients;
}
/**
* 系统信息资源读取工具
* 将 Resource 访问转换为 Spring AI Tool
*/
@Bean("system_info_reader")
@Description("读取系统运行时信息,支持 memory、cpu、system、jvm、all 等类型")
public Function<SystemInfoRequest, SystemInfoResponse> systemInfoReader() {
log.info("注册系统信息读取工具: systemInfoReader");
return request -> {
log.info("读取系统信息: infoType={}", request.infoType());
if (mcpSyncClients.isEmpty()) {
return new SystemInfoResponse("错误:没有可用的 MCP 客户端连接", false);
}
try {
// 构建资源 URI
String resourceUri = "system://" + request.infoType();
// 调用 MCP 客户端读取资源
McpSchema.ReadResourceRequest readRequest =
new McpSchema.ReadResourceRequest(resourceUri);
McpSchema.ReadResourceResult result =
mcpSyncClients.get(0).readResource(readRequest);
// 提取资源内容
String content = result.contents().stream()
.findFirst()
.map(contentItem -> {
if (contentItem instanceof McpSchema.TextResourceContents textContent) {
return textContent.text();
}
return contentItem.toString();
})
.orElse("资源内容为空");
log.info("成功读取系统信息: {}", request.infoType());
return new SystemInfoResponse(content, true);
} catch (Exception e) {
log.error("读取系统信息失败: infoType={}", request.infoType(), e);
return new SystemInfoResponse("读取失败: " + e.getMessage(), false);
}
};
}
/**
* 系统信息请求 DTO
*/
public record SystemInfoRequest(String infoType) {}
/**
* 系统信息响应 DTO
*/
public record SystemInfoResponse(String content, boolean success) {}
}
工作原理:
- 创建一个 Spring Bean,类型为
Function<Request, Response> - 使用
@Bean("system_info_reader")指定工具名称 - 使用
@Description提供工具描述(LLM 根据描述决定是否调用) - 在函数内部调用
McpSyncClient.readResource()读取资源 - LLM 可以根据用户需求自主调用这个工具
3.2 Prompt 转 Tool
创建 McpPromptToolConfig.java:
@Component
public class McpPromptToolConfig {
private final List<McpSyncClient> mcpSyncClients;
private static final Logger log = LoggerFactory.getLogger(McpPromptToolConfig.class);
public McpPromptToolConfig(List<McpSyncClient> mcpSyncClients) {
this.mcpSyncClients = mcpSyncClients;
}
/**
* 通用问候语生成工具
*/
@Bean("greeting_generator")
@Description("生成个性化问候语,支持中英文,参数:name-姓名, language-语言(zh/en)")
public Function<GreetingRequest, GreetingResponse> greetingGenerator() {
log.info("注册问候语生成工具: greetingGenerator");
return request -> {
log.info("生成问候语: name={}, language={}", request.name(), request.language());
if (mcpSyncClients.isEmpty()) {
return new GreetingResponse("错误:没有可用的 MCP 客户端连接", false);
}
try {
// 构建参数 Map
Map<String, Object> arguments = new HashMap<>();
if (request.name() != null && !request.name().isEmpty()) {
arguments.put("name", request.name());
}
if (request.language() != null && !request.language().isEmpty()) {
arguments.put("language", request.language());
}
// 调用 MCP 客户端获取提示
McpSchema.GetPromptRequest promptRequest =
new McpSchema.GetPromptRequest("greeting", arguments);
McpSchema.GetPromptResult result =
mcpSyncClients.get(0).getPrompt(promptRequest);
// 提取提示消息内容
String content = result.messages().stream()
.findFirst()
.map(message -> message.content().toString())
.orElse("提示内容为空");
log.info("成功生成问候语: {}", request.name());
return new GreetingResponse(content, true);
} catch (Exception e) {
log.error("生成问候语失败: name={}", request.name(), e);
return new GreetingResponse("生成失败: " + e.getMessage(), false);
}
};
}
/**
* 商务问候语生成工具
*/
@Bean("business_greeting_generator")
@Description("生成商务问候语,参数:recipientName-收件人姓名, senderName-发件人姓名")
public Function<BusinessGreetingRequest, BusinessGreetingResponse> businessGreetingGenerator() {
log.info("注册商务问候语生成工具: businessGreetingGenerator");
return request -> {
log.info("生成商务问候语: recipientName={}, senderName={}",
request.recipientName(), request.senderName());
if (mcpSyncClients.isEmpty()) {
return new BusinessGreetingResponse("错误:没有可用的 MCP 客户端连接", false);
}
try {
Map<String, Object> arguments = new HashMap<>();
if (request.recipientName() != null && !request.recipientName().isEmpty()) {
arguments.put("recipientName", request.recipientName());
}
if (request.senderName() != null && !request.senderName().isEmpty()) {
arguments.put("senderName", request.senderName());
}
McpSchema.GetPromptRequest promptRequest =
new McpSchema.GetPromptRequest("business_greeting", arguments);
McpSchema.GetPromptResult result =
mcpSyncClients.get(0).getPrompt(promptRequest);
String content = result.messages().stream()
.findFirst()
.map(message -> message.content().toString())
.orElse("提示内容为空");
log.info("成功生成商务问候语");
return new BusinessGreetingResponse(content, true);
} catch (Exception e) {
log.error("生成商务问候语失败", e);
return new BusinessGreetingResponse("生成失败: " + e.getMessage(), false);
}
};
}
/**
* 节日祝福生成工具
*/
@Bean("festival_greeting_generator")
@Description("生成节日祝福问候语,参数:recipientName-收件人姓名, festival-节日名称")
public Function<FestivalGreetingRequest, FestivalGreetingResponse> festivalGreetingGenerator() {
log.info("注册节日祝福生成工具: festivalGreetingGenerator");
return request -> {
log.info("生成节日祝福: recipientName={}, festival={}",
request.recipientName(), request.festival());
if (mcpSyncClients.isEmpty()) {
return new FestivalGreetingResponse("错误:没有可用的 MCP 客户端连接", false);
}
try {
Map<String, Object> arguments = new HashMap<>();
if (request.recipientName() != null && !request.recipientName().isEmpty()) {
arguments.put("recipientName", request.recipientName());
}
if (request.festival() != null && !request.festival().isEmpty()) {
arguments.put("festival", request.festival());
}
McpSchema.GetPromptRequest promptRequest =
new McpSchema.GetPromptRequest("festival_greeting", arguments);
McpSchema.GetPromptResult result =
mcpSyncClients.get(0).getPrompt(promptRequest);
String content = result.messages().stream()
.findFirst()
.map(message -> message.content().toString())
.orElse("提示内容为空");
log.info("成功生成节日祝福");
return new FestivalGreetingResponse(content, true);
} catch (Exception e) {
log.error("生成节日祝福失败", e);
return new FestivalGreetingResponse("生成失败: " + e.getMessage(), false);
}
};
}
// DTO 定义
public record GreetingRequest(String name, String language) {}
public record GreetingResponse(String content, boolean success) {}
public record BusinessGreetingRequest(String recipientName, String senderName) {}
public record BusinessGreetingResponse(String content, boolean success) {}
public record FestivalGreetingRequest(String recipientName, String festival) {}
public record FestivalGreetingResponse(String content, boolean success) {}
}
3.3 为什么需要转换?
传统方式(硬编码):
// 开发者需要手动判断何时调用 Resource/Prompt
if (userAsksForSystemInfo()) {
readResource("system://memory");
}
转换后的方式(LLM 自主决策):
// LLM 根据用户意图自主决定调用哪个工具
ChatClient chatClient = ChatClient.create(chatModel);
return chatClient.prompt(userMessage)
.toolCallbacks(toolCallbackProvider) // 包含所有转换后的工具
.call()
.chatResponse();
优势:
- ✅ LLM 可以根据自然语言理解自主选择合适的工具
- ✅ 减少硬编码的条件判断逻辑
- ✅ 更符合 AI Agent 的设计理念
- ✅ 提高系统的灵活性和可扩展性
🎨 第四部分:综合应用场景
4.1 高级聊天控制器
创建一个同时使用 Tools、Resources 和 Prompts 的高级聊天接口:
/**
* 综合示例:同时使用 Tools、Resources 和 Prompts
*/
@PostMapping("/advanced")
public ChatResponse advancedChat(@RequestBody AdvancedChatRequest request) {
// 1. 获取工具
ToolCallback[] mcpTools = toolCallbackProvider.getToolCallbacks();
// 2. 可选:先读取某个资源作为上下文
String enhancedMessage = request.getMessage();
if (request.getResourceUri() != null && !mcpSyncClients.isEmpty()) {
try {
McpSchema.ReadResourceRequest resourceRequest =
new McpSchema.ReadResourceRequest(request.getResourceUri());
McpSchema.ReadResourceResult resourceResult =
mcpSyncClients.get(0).readResource(resourceRequest);
// 将资源内容添加到用户消息中
enhancedMessage = request.getMessage() + "\n\n参考资源: " + resourceResult.contents();
} catch (Exception e) {
// 资源读取失败时继续执行
System.err.println("读取资源失败: " + e.getMessage());
}
}
// 3. 可选:使用提示模板
if (request.getPromptName() != null && !mcpSyncClients.isEmpty()) {
try {
McpSchema.GetPromptRequest promptRequest =
new McpSchema.GetPromptRequest(request.getPromptName(), request.getPromptArgs());
McpSchema.GetPromptResult promptResult =
mcpSyncClients.get(0).getPrompt(promptRequest);
enhancedMessage = promptResult.messages().get(0).content().toString();
} catch (Exception e) {
System.err.println("使用提示模板失败: " + e.getMessage());
}
}
// 4. 调用 LLM
ChatClient chatClient = ChatClient.create(chatModel);
Prompt prompt = new Prompt(enhancedMessage, ChatOptions.builder().build());
return chatClient.prompt(prompt)
.toolCallbacks(mcpTools)
.call()
.chatResponse();
}
/**
* 高级聊天请求 DTO
*/
@Data
public static class AdvancedChatRequest {
private String message;
private String resourceUri; // 可选:资源 URI
private String promptName; // 可选:提示模板名称
private Map<String, Object> promptArgs; // 可选:提示模板参数
}
测试示例:
curl -X POST http://localhost:8200/chat/advanced \
-H "Content-Type: application/json" \
-d '{
"message": "请帮我分析当前系统状态",
"resourceUri": "system://all",
"promptName": "greeting",
"promptArgs": {
"name": "管理员",
"language": "zh"
}
}'
4.2 实际应用场景
场景 1:智能客服系统
// 用户提问:"我需要给张总发送一封商务邮件"
// LLM 自动调用:
// 1. business_greeting_generator(recipientName="张总", senderName="客服")
// 2. 生成完整的商务邮件内容
场景 2:系统监控助手
// 用户提问:"当前服务器的负载情况如何?"
// LLM 自动调用:
// 1. system_info_reader(infoType="cpu")
// 2. system_info_reader(infoType="memory")
// 3. 综合分析并给出建议
场景 3:节日营销自动化
// 用户提问:"春节快到了,帮我给客户王先生写个祝福"
// LLM 自动调用:
// 1. festival_greeting_generator(recipientName="王先生", festival="春节")
// 2. 生成个性化的节日祝福
🔧 第五部分:进阶配置与最佳实践
5.1 同步 vs 异步服务器
同步服务器(SYNC):
spring:
ai:
mcp:
server:
type: SYNC # 默认
- 适用于传统的请求-响应模式
- 仅注册同步的 MCP 注解方法
- 简单易用,适合大多数场景
异步服务器(ASYNC):
spring:
ai:
mcp:
server:
type: ASYNC
- 适用于响应式应用程序
- 仅注册异步的 MCP 注解方法
- 非阻塞操作,高吞吐量
5.2 协议选择
Streamable-HTTP(推荐):
spring:
ai:
mcp:
server:
protocol: STREAMABLE
- 替代旧的 SSE 传输
- 支持 HTTP POST 和 GET
- 可选择使用 SSE 流式传输
无状态服务器(STATELESS):
spring:
ai:
mcp:
server:
protocol: STATELESS
- 不维护会话状态
- 适合微服务架构和云原生部署
- 简化部署模型
5.3 工具名称前缀生成
当连接多个 MCP 服务器时,可能出现工具名称冲突。可以使用自定义前缀生成器:
@Component
public class CustomToolNamePrefixGenerator implements McpToolNamePrefixGenerator {
@Override
public String prefixedToolName(McpConnectionInfo connectionInfo, Tool tool) {
// 使用服务器名称作为前缀
String serverName = connectionInfo.initializeResult().serverInfo().name();
return serverName + "_" + tool.name();
}
}
5.4 工具过滤
选择性包含/排除工具:
@Component
public class CustomMcpToolFilter implements McpToolFilter {
@Override
public boolean test(McpConnectionInfo connectionInfo, McpSchema.Tool tool) {
// 排除实验性工具
if (tool.description() != null &&
tool.description().contains("experimental")) {
return false;
}
return true;
}
}
5.5 客户端定制
自定义客户端行为:
@Component
public class CustomMcpSyncClientCustomizer implements McpSyncClientCustomizer {
@Override
public void customize(String serverConfigurationName, McpClient.SyncSpec spec) {
// 自定义请求超时
spec.requestTimeout(Duration.ofSeconds(30));
// 添加工具变更监听
spec.toolsChangeConsumer((List<McpSchema.Tool> tools) -> {
System.out.println("工具列表已更新: " + tools.size() + " 个工具可用");
});
// 添加日志消息处理
spec.loggingConsumer((McpSchema.LoggingMessageNotification log) -> {
System.out.println("收到日志: " + log.level() + " - " + log.data());
});
}
}
🎓 总结
通过本篇教程,我们全面学习了 Spring AI MCP 的核心概念和实践应用:
核心要点回顾
-
MCP 三大能力:
- Tools:执行操作
- Resources:数据访问
- Prompts:提示模板
-
Server 开发:
- 使用
@McpTool、@McpResource、@McpPrompt注解 - 支持同步/异步两种模式
- 支持多种传输协议(STDIO、SSE、Streamable-HTTP)
- 使用
-
Client 开发:
- 自动发现和使用 Server 暴露的能力
- 可以将 Resources 和 Prompts 转换为 Tools
- 支持工具过滤和前缀生成
-
最佳实践:
- 优先使用 Streamable-HTTP 协议
- 生产环境使用 WebFlux 传输
- 合理配置超时和重试策略
- 做好日志记录和监控
参考资源
更多推荐




所有评论(0)