1. 概述
Spring AI MCP Annotations 模块为 MCP 服务端和客户端提供了基于注解的方法处理能力。通过简洁、声明式的 Java 注解方式,简化了 MCP 服务端方法和客户端处理器的创建与注册。
本库构建于 MCP Java SDK 之上,提供更高层次的、基于注解的编程模型:
- 使用声明式注解创建和注册
MCP操作处理器 - 减少样板代码,提高可维护性
- 同时支持
MCP服务端和客户端开发
2. 架构概览
2.1 服务端注解
| 注解 | 用途 |
|---|---|
@McpTool |
实现 MCP 工具,自动生成 JSON Schema |
@McpResource |
通过 URI 模板提供资源访问 |
@McpPrompt |
生成提示消息 |
@McpComplete |
提供自动补全功能 |
2.2 客户端注解
| 注解 | 用途 |
|---|---|
@McpLogging |
处理日志消息通知 |
@McpSampling |
处理 LLM 采样/补全请求 |
@McpElicitation |
处理信息收集请求 |
@McpProgress |
处理长任务进度通知 |
@McpToolListChanged |
处理工具列表变更通知 |
@McpResourceListChanged |
处理资源列表变更通知 |
@McpPromptListChanged |
处理提示词列表变更通知 |
注意: 所有
MCP客户端注解都必须包含clients参数来关联特定的MCP客户端连接,该值必须与application.properties中配置的连接名称匹配。
2.3 特殊参数与注解
| 参数/注解 | 说明 |
|---|---|
McpSyncRequestContext |
同步操作的统一请求上下文接口,支持有状态和无状态模式,提供日志、进度、采样、信息收集等便捷方法。自动注入,排除在 JSON Schema 之外 |
McpAsyncRequestContext |
异步版本,返回 Mono 响应式类型,其余与 McpSyncRequestContext 相同 |
McpTransportContext |
轻量级传输上下文,用于无状态操作;不支持双向通信(roots、elicitation、sampling) |
@McpProgressToken |
标记方法参数以接收进度令牌,自动注入并排除在 JSON Schema 之外 |
McpMeta |
访问 MCP 请求、通知和结果中的元数据,自动注入 |
3. 快速入门
3.1 依赖
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-annotations</artifactId>
</dependency>
使用以下任一 MCP Boot Starter 时,MCP Annotations 会自动包含:
spring-ai-starter-mcp-clientspring-ai-starter-mcp-client-webfluxspring-ai-starter-mcp-serverspring-ai-starter-mcp-server-webfluxspring-ai-starter-mcp-server-webmvc
3.2 配置
注解扫描默认启用,可通过以下属性配置:
客户端:
yaml
spring:
ai:
mcp:
client:
annotation-scanner:
enabled: true
服务端:
yaml
spring:
ai:
mcp:
server:
annotation-scanner:
enabled: true
3.3 快速示例
服务端 --- 计算器工具:
java
@Component
public class CalculatorTools {
@McpTool(name = "add", description = "将两个数相加")
public int add(
@McpToolParam(description = "第一个数", required = true) int a,
@McpToolParam(description = "第二个数", required = true) int b) {
return a + b;
}
@McpTool(name = "multiply", description = "将两个数相乘")
public double multiply(
@McpToolParam(description = "第一个数", required = true) double x,
@McpToolParam(description = "第二个数", required = true) double y) {
return x * y;
}
}
客户端 --- 日志处理器:
java
@Component
public class LoggingHandler {
@McpLogging(clients = "my-server")
public void handleLoggingMessage(LoggingMessageNotification notification) {
System.out.println("收到日志: " + notification.level() +
" - " + notification.data());
}
}
Spring Boot 自动配置会检测并注册带注解的 Bean。
4. 客户端注解详解
4.1 @McpLogging
处理 MCP 服务端的日志通知。
java
@Component
public class LoggingHandler {
@McpLogging(clients = "my-mcp-server")
public void handleLoggingMessage(LoggingMessageNotification notification) {
System.out.println("收到日志: " + notification.level() +
" - " + notification.data());
}
// 支持按参数分别接收
@McpLogging(clients = "my-mcp-server")
public void handleLoggingWithParams(LoggingLevel level, String logger, String data) {
System.out.println(String.format("[%s] %s: %s", level, logger, data));
}
}
4.2 @McpSampling
处理 MCP 服务端的 LLM 采样请求。
java
@Component
public class SamplingHandler {
// 同步实现
@McpSampling(clients = "llm-server")
public CreateMessageResult handleSamplingRequest(CreateMessageRequest request) {
String response = generateLLMResponse(request);
return CreateMessageResult.builder()
.role(Role.ASSISTANT)
.content(new TextContent(response))
.model("gpt-4")
.build();
}
// 异步实现
@McpSampling(clients = "llm-server")
public Mono<CreateMessageResult> handleAsyncSampling(CreateMessageRequest request) {
return Mono.fromCallable(() -> {
String response = generateLLMResponse(request);
return CreateMessageResult.builder()
.role(Role.ASSISTANT)
.content(new TextContent(response))
.model("gpt-4")
.build();
}).subscribeOn(Schedulers.boundedElastic());
}
}
4.3 @McpElicitation
处理信息收集请求,用于向用户获取补充信息。
java
@Component
public class ElicitationHandler {
@McpElicitation(clients = "interactive-server")
public ElicitResult handleElicitationRequest(ElicitRequest request) {
Map<String, Object> userData = presentFormToUser(request.requestedSchema());
if (userData != null) {
return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
} else {
return new ElicitResult(ElicitResult.Action.DECLINE, null);
}
}
// 异步实现
@McpElicitation(clients = "interactive-server")
public Mono<ElicitResult> handleAsyncElicitation(ElicitRequest request) {
return Mono.fromCallable(() -> {
Map<String, Object> userData = asyncGatherUserInput(request);
return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
}).timeout(Duration.ofSeconds(30))
.onErrorReturn(new ElicitResult(ElicitResult.Action.CANCEL, null));
}
}
4.4 @McpProgress
处理长时间运行操作的进度通知。
java
@Component
public class ProgressHandler {
@McpProgress(clients = "my-mcp-server")
public void handleProgressNotification(ProgressNotification notification) {
double percentage = notification.progress() * 100;
System.out.println(String.format("进度: %.2f%% - %s",
percentage, notification.message()));
}
// 按参数分别接收
@McpProgress(clients = "my-mcp-server")
public void handleProgressWithDetails(
String progressToken, double progress,
Double total, String message) {
if (total != null) {
System.out.println(String.format("[%s] %.0f/%.0f - %s",
progressToken, progress, total, message));
} else {
System.out.println(String.format("[%s] %.2f%% - %s",
progressToken, progress * 100, message));
}
}
}
4.5 @McpToolListChanged
处理服务端工具列表变更通知。
java
@Component
public class ToolListChangedHandler {
@McpToolListChanged(clients = "tool-server")
public void handleToolListChanged(List<McpSchema.Tool> updatedTools) {
System.out.println("工具列表已更新: " + updatedTools.size() + " 个工具可用");
toolRegistry.updateTools(updatedTools);
for (McpSchema.Tool tool : updatedTools) {
System.out.println(" - " + tool.name() + ": " + tool.description());
}
}
}
4.6 @McpResourceListChanged
处理服务端资源列表变更通知。
java
@Component
public class ResourceListChangedHandler {
@McpResourceListChanged(clients = "resource-server")
public void handleResourceListChanged(List<McpSchema.Resource> updatedResources) {
System.out.println("资源已更新: " + updatedResources.size());
resourceCache.clear();
for (McpSchema.Resource resource : updatedResources) {
resourceCache.register(resource);
}
}
}
4.7 @McpPromptListChanged
处理服务端提示词列表变更通知。
java
@Component
public class PromptListChangedHandler {
@McpPromptListChanged(clients = "prompt-server")
public void handlePromptListChanged(List<McpSchema.Prompt> updatedPrompts) {
System.out.println("提示词已更新: " + updatedPrompts.size());
promptCatalog.updatePrompts(updatedPrompts);
}
}
4.8 Spring Boot 集成
自动配置会完成以下工作:
- 扫描所有带有
MCP客户端注解的Bean - 创建对应的声明规范
- 将其注册到
MCP客户端 - 同时支持同步和异步实现
- 支持多客户端与客户端专属处理器
java
@SpringBootApplication
public class McpClientApplication {
public static void main(String[] args) {
SpringApplication.run(McpClientApplication.class, args);
}
}
@Component
public class MyClientHandlers {
@McpLogging(clients = "my-server")
public void handleLogs(LoggingMessageNotification notification) { /* ... */ }
@McpSampling(clients = "my-server")
public CreateMessageResult handleSampling(CreateMessageRequest request) { /* ... */ }
@McpProgress(clients = "my-server")
public void handleProgress(ProgressNotification notification) { /* ... */ }
}
4.9 配置属性
yaml
spring:
ai:
mcp:
client:
type: SYNC # 或 ASYNC
annotation-scanner:
enabled: true
sse:
connections:
my-server: # 连接名 → 注解中 clients 参数的值
url: http://localhost:8080
tool-server: # 另一个 clients 值
url: http://localhost:8081
stdio:
connections:
local-server: # 又一个 clients 值
command: /path/to/mcp-server
args:
- --mode=production
clients参数值必须与配置中的连接名称匹配。
5. 服务端注解详解
5.1 @McpTool
标记方法为 MCP 工具实现,自动生成 JSON Schema。
基础用法:
java
@Component
public class CalculatorTools {
@McpTool(name = "add", description = "将两个数相加")
public int add(
@McpToolParam(description = "第一个数", required = true) int a,
@McpToolParam(description = "第二个数", required = true) int b) {
return a + b;
}
}
高级特性(annotations 元信息):
java
@McpTool(name = "calculate-area",
description = "计算矩形面积",
annotations = McpTool.McpAnnotations(
title = "矩形面积计算器",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true
))
public AreaResult calculateRectangleArea(
@McpToolParam(description = "宽度", required = true) double width,
@McpToolParam(description = "高度", required = true) double height) {
return new AreaResult(width * height, "平方单位");
}
带请求上下文:
java
@McpTool(name = "process-data", description = "带请求上下文处理数据")
public String processData(
McpSyncRequestContext context,
@McpToolParam(description = "待处理数据", required = true) String data) {
context.info("正在处理: " + data);
context.progress(p -> p.progress(0.5).total(1.0).message("处理中..."));
context.ping();
return "已处理: " + data.toUpperCase();
}
动态 Schema 支持:
java
@McpTool(name = "flexible-tool", description = "动态 Schema 处理")
public CallToolResult processDynamic(CallToolRequest request) {
Map<String, Object> args = request.arguments();
String result = "已处理 " + args.size() + " 个动态参数";
return CallToolResult.builder().addTextContent(result).build();
}
5.2 @McpResource
通过 URI 模板提供资源访问。
java
@Component
public class ResourceProvider {
@McpResource(uri = "config://{key}", name = "配置项",
description = "提供配置数据")
public String getConfig(String key) {
return configData.get(key);
}
// 带请求上下文
@McpResource(uri = "data://{id}", name = "数据资源",
description = "带请求上下文的资源")
public ReadResourceResult getData(McpSyncRequestContext context, String id) {
context.info("访问资源: " + id);
context.ping();
String data = fetchData(id);
return new ReadResourceResult(List.of(
new TextResourceContents("data://" + id, "text/plain", data)));
}
}
5.3 @McpPrompt
生成 AI 交互的提示消息。
java
@Component
public class PromptProvider {
@McpPrompt(name = "greeting", description = "生成问候消息")
public GetPromptResult greeting(
@McpArg(name = "name", description = "用户姓名", required = true) String name) {
String message = "你好," + name + "!今天有什么可以帮你的?";
return new GetPromptResult("问候",
List.of(new PromptMessage(Role.ASSISTANT, new TextContent(message))));
}
// 可选参数
@McpPrompt(name = "personalized-message", description = "生成个性化消息")
public GetPromptResult personalizedMessage(
@McpArg(name = "name", required = true) String name,
@McpArg(name = "age", required = false) Integer age,
@McpArg(name = "interests", required = false) String interests) {
// 根据参数构建个性化消息
}
}
5.4 @McpComplete
为提示词提供自动补全功能。
java
@Component
public class CompletionProvider {
@McpComplete(prompt = "city-search")
public List<String> completeCityName(String prefix) {
return cities.stream()
.filter(city -> city.toLowerCase().startsWith(prefix.toLowerCase()))
.limit(10)
.toList();
}
@McpComplete(prompt = "code-completion")
public CompleteResult completeCode(String prefix) {
List<String> completions = generateCodeCompletions(prefix);
return new CompleteResult(
new CompleteResult.CompleteCompletion(
completions, completions.size(), hasMoreCompletions));
}
}
5.5 异步支持
所有服务端注解均支持 Reactor 响应式实现:
java
@Component
public class AsyncTools {
@McpTool(name = "async-fetch", description = "异步获取数据")
public Mono<String> asyncFetch(
@McpToolParam(description = "URL", required = true) String url) {
return Mono.fromCallable(() -> fetchFromUrl(url))
.subscribeOn(Schedulers.boundedElastic());
}
@McpResource(uri = "async-data://{id}", name = "异步数据")
public Mono<ReadResourceResult> asyncResource(String id) {
return Mono.fromCallable(() -> {
String data = loadData(id);
return new ReadResourceResult(List.of(
new TextResourceContents("async-data://" + id, "text/plain", data)));
});
}
}
5.6 Spring Boot 集成
java
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}
@Component
public class MyMcpTools {
// @McpTool 注解的方法
}
@Component
public class MyMcpResources {
// @McpResource 注解的方法
}
自动配置会:扫描带注解的 Bean → 创建声明规范 → 注册到 MCP 服务端 → 根据配置适配同步/异步实现。
5.7 配置属性
yaml
spring:
ai:
mcp:
server:
type: SYNC # 或 ASYNC
annotation-scanner:
enabled: true
6. 有状态与无状态实现
6.1 统一请求上下文(推荐)
使用 McpSyncRequestContext 或 McpAsyncRequestContext,同时兼容有状态和无状态操作:
java
public record UserInfo(String name, String email, int age) {}
@McpTool(name = "unified-tool", description = "统一请求上下文工具")
public String unifiedTool(
McpSyncRequestContext context,
@McpToolParam(description = "输入", required = true) String input) {
// 日志
context.info("处理: " + input);
// 进度
context.progress(50);
// 心跳
context.ping();
// 能力检查后使用
if (context.elicitEnabled()) {
StructuredElicitResult<UserInfo> elicitResult = context.elicit(UserInfo.class);
// 处理用户输入...
}
if (context.sampleEnabled()) {
CreateMessageResult samplingResult = context.sample("生成回复");
// 处理采样结果...
}
return "已处理";
}
6.2 简单操作(无上下文)
最简单的方式,省略所有上下文参数:
java
@McpTool(name = "simple-add", description = "简单加法")
public int simpleAdd(
@McpToolParam(description = "第一个数", required = true) int a,
@McpToolParam(description = "第二个数", required = true) int b) {
return a + b;
}
6.3 轻量级无状态(McpTransportContext)
仅需最小传输上下文时使用:
java
@McpTool(name = "stateless-tool", description = "无状态工具")
public String statelessTool(
McpTransportContext context,
@McpToolParam(description = "输入", required = true) String input) {
// 仅传输级上下文,不支持双向操作
return "已处理: " + input;
}
注意: 无状态服务端不支持双向操作(roots、elicitation、sampling)。使用
McpSyncRequestContext或McpAsyncRequestContext的方法在无状态模式下会被忽略。
6.4 方法过滤规则
框架根据服务端类型和方法特征自动过滤注解方法,被过滤的方法会输出警告日志。
| 服务端类型 | 接受的方法 | 过滤的方法 |
|---|---|---|
| Sync Stateful | 非响应式返回 + 双向上下文 | 响应式返回(Mono/Flux) |
| Async Stateful | 响应式返回 + 双向上下文 | 非响应式返回 |
| Sync Stateless | 非响应式返回 + 无双向上下文 | 响应式返回 或 双向上下文参数 |
| Async Stateless | 响应式返回 + 无双向上下文 | 非响应式返回 或 双向上下文参数 |
最佳实践:
- 保持方法与服务端类型一致(同步服务器用同步方法,异步服务器用异步方法)
- 将有状态与无状态实现分离到不同类中
- 启动时检查日志中的过滤警告
- 使用正确的上下文:有状态用
McpSyncRequestContext/McpAsyncRequestContext,无状态用McpTransportContext
7. 特殊参数详解
7.1 McpMeta
访问 MCP 请求、通知和结果中的元数据。自动注入,永不返回 null。
java
@McpTool(name = "contextual-tool", description = "带元数据的工具")
public String processWithContext(
@McpToolParam(description = "输入数据", required = true) String data,
McpMeta meta) {
String userId = (String) meta.get("userId");
String userRole = (String) meta.get("userRole");
if ("admin".equals(userRole)) {
return processAsAdmin(data, userId);
} else {
return processAsUser(data, userId);
}
}
7.2 @McpProgressToken
接收进度令牌,参数类型为 String,可能为 null。
java
@McpTool(name = "long-operation", description = "带进度的长任务")
public String performLongOperation(
@McpProgressToken String progressToken,
@McpToolParam(description = "操作名称", required = true) String operation,
@McpToolParam(description = "时长(秒)", required = true) int duration,
McpSyncServerExchange exchange) {
if (progressToken != null) {
for (int i = 1; i <= duration; i++) {
Thread.sleep(1000);
double progress = (double) i / duration;
exchange.progressNotification(new ProgressNotification(
progressToken, progress, 1.0,
String.format("处理中... %d%%", (int)(progress * 100))));
}
}
return "操作 " + operation + " 已完成";
}
7.3 McpSyncRequestContext / McpAsyncRequestContext
统一请求上下文,支持有状态和无状态操作,提供高级功能。
同步版本:
java
public record UserInfo(String name, String email, int age) {}
@McpTool(name = "advanced-tool", description = "完整服务端能力工具")
public String advancedTool(
McpSyncRequestContext context,
@McpToolParam(description = "输入", required = true) String input) {
context.info("处理: " + input);
context.ping();
context.progress(50);
if (context.elicitEnabled()) {
StructuredElicitResult<UserInfo> elicitResult = context.elicit(
e -> e.message("需要补充信息"), UserInfo.class);
if (elicitResult.action() == ElicitResult.Action.ACCEPT) {
UserInfo userInfo = elicitResult.structuredContent();
}
}
if (context.sampleEnabled()) {
CreateMessageResult samplingResult = context.sample(
s -> s.message("处理: " + input)
.modelPreferences(pref -> pref.modelHints("gpt-4")));
}
return "已处理";
}
异步版本:
java
@McpTool(name = "async-advanced-tool", description = "异步完整能力工具")
public Mono<String> asyncAdvancedTool(
McpAsyncRequestContext context,
@McpToolParam(description = "输入", required = true) String input) {
return context.info("异步处理: " + input)
.then(context.progress(25))
.then(context.ping())
.flatMap(v -> context.elicitEnabled()
? context.elicitation(UserInfo.class)
.map(userInfo -> "为用户处理: " + userInfo.name())
: Mono.just("处理中..."))
.flatMap(msg -> context.sampleEnabled()
? context.sampling("处理: " + input).map(result -> "完成: " + result)
: Mono.just("完成: " + msg));
}
7.4 McpTransportContext
轻量级传输上下文,仅用于无状态操作。
java
@McpTool(name = "stateless-tool", description = "无状态工具")
public String statelessTool(
McpTransportContext context,
@McpToolParam(description = "输入", required = true) String input) {
return "以无状态模式处理: " + input;
}
7.5 CallToolRequest
提供完整的工具请求访问,支持运行时动态 Schema。
java
@McpTool(name = "dynamic-tool", description = "动态 Schema 工具")
public CallToolResult processDynamicSchema(CallToolRequest request) {
Map<String, Object> args = request.arguments();
StringBuilder result = new StringBuilder("已处理:\n");
for (Map.Entry<String, Object> entry : args.entrySet()) {
result.append(" ").append(entry.getKey())
.append(": ").append(entry.getValue()).append("\n");
}
return CallToolResult.builder().addTextContent(result.toString()).build();
}
7.6 参数注入规则
自动注入的参数(不参与 JSON Schema 生成):
McpMeta--- 永不返回null@McpProgressToken String--- 可能为nullMcpSyncServerExchange/McpAsyncServerExchange--- 服务端交换上下文McpTransportContext--- 无状态传输上下文CallToolRequest--- 工具请求(仅工具方法)
7.7 最佳实践
- 使用
McpMeta传递上下文信息 ,始终对元数据值做null检查 - 进度令牌做
null检查后再使用 - 选择合适的上下文 --- 统一用
McpSyncRequestContext/McpAsyncRequestContext,仅需轻量上下文时用McpTransportContext,最简单场景完全省略 - 能力检查 --- 使用双向操作前先检查能力支持:
java
@McpTool(name = "capability-aware", description = "能力感知工具")
public String capabilityAware(
McpSyncRequestContext context,
@McpToolParam(description = "数据", required = true) String data) {
if (context.elicitEnabled()) {
var result = context.elicit(UserInfo.class);
}
if (context.sampleEnabled()) {
var samplingResult = context.sample("处理: " + data);
}
return "已处理";
}
8. 完整示例
8.1 计算器服务端
java
@SpringBootApplication
public class CalculatorServerApplication {
public static void main(String[] args) {
SpringApplication.run(CalculatorServerApplication.class, args);
}
}
@Component
public class CalculatorTools {
@McpTool(name = "add", description = "两数相加")
public double add(
@McpToolParam(description = "第一个数", required = true) double a,
@McpToolParam(description = "第二个数", required = true) double b) {
return a + b;
}
@McpTool(name = "subtract", description = "两数相减")
public double subtract(
@McpToolParam(description = "第一个数", required = true) double a,
@McpToolParam(description = "第二个数", required = true) double b) {
return a - b;
}
@McpTool(name = "multiply", description = "两数相乘")
public double multiply(
@McpToolParam(description = "第一个数", required = true) double a,
@McpToolParam(description = "第二个数", required = true) double b) {
return a * b;
}
@McpTool(name = "divide", description = "两数相除")
public double divide(
@McpToolParam(description = "被除数", required = true) double dividend,
@McpToolParam(description = "除数", required = true) double divisor) {
if (divisor == 0) {
throw new IllegalArgumentException("除数不能为零");
}
return dividend / divisor;
}
@McpTool(name = "calculate-expression", description = "计算复杂数学表达式")
public CallToolResult calculateExpression(
CallToolRequest request, McpSyncRequestContext context) {
Map<String, Object> args = request.arguments();
String expression = (String) args.get("expression");
context.info("计算: " + expression);
try {
double result = evaluateExpression(expression);
return CallToolResult.builder()
.addTextContent("结果: " + result).build();
} catch (Exception e) {
return CallToolResult.builder()
.isError(true)
.addTextContent("错误: " + e.getMessage()).build();
}
}
}
配置:
yaml
spring:
ai:
mcp:
server:
name: calculator-server
version: 1.0.0
type: SYNC
protocol: SSE
capabilities:
tool: true
resource: true
prompt: true
completion: true
8.2 文档处理服务端
java
@Component
public class DocumentServer {
private final Map<String, Document> documents = new ConcurrentHashMap<>();
@McpResource(uri = "document://{id}", name = "文档",
description = "访问已存储的文档")
public ReadResourceResult getDocument(String id, McpMeta meta) {
Document doc = documents.get(id);
if (doc == null) {
return new ReadResourceResult(List.of(
new TextResourceContents("document://" + id, "text/plain", "文档未找到")));
}
String accessLevel = (String) meta.get("accessLevel");
if ("restricted".equals(doc.getClassification()) && !"admin".equals(accessLevel)) {
return new ReadResourceResult(List.of(
new TextResourceContents("document://" + id, "text/plain", "拒绝访问")));
}
return new ReadResourceResult(List.of(
new TextResourceContents("document://" + id, doc.getMimeType(), doc.getContent())));
}
@McpTool(name = "analyze-document", description = "分析文档内容")
public String analyzeDocument(
McpSyncRequestContext context,
@McpToolParam(description = "文档ID", required = true) String docId,
@McpToolParam(description = "分析类型", required = false) String type) {
Document doc = documents.get(docId);
if (doc == null) return "文档未找到";
String progressToken = context.request().progressToken();
if (progressToken != null) {
context.progress(p -> p.progress(0.0).total(1.0).message("开始分析"));
}
String result = performAnalysis(doc, type != null ? type : "summary");
if (progressToken != null) {
context.progress(p -> p.progress(1.0).total(1.0).message("分析完成"));
}
return result;
}
@McpPrompt(name = "document-summary", description = "生成文档摘要提示")
public GetPromptResult documentSummaryPrompt(
@McpArg(name = "docId", required = true) String docId,
@McpArg(name = "length", required = false) String length) {
// 生成摘要提示...
}
}
8.3 MCP 客户端处理器
java
@Component
public class ClientHandlers {
private final Logger logger = LoggerFactory.getLogger(ClientHandlers.class);
private final ChatModel chatModel;
public ClientHandlers(@Lazy ChatModel chatModel) {
this.chatModel = chatModel;
}
@McpLogging(clients = "server1")
public void handleLogging(LoggingMessageNotification notification) {
switch (notification.level()) {
case ERROR -> logger.error("[MCP] {} - {}", notification.logger(), notification.data());
case WARNING -> logger.warn("[MCP] {} - {}", notification.logger(), notification.data());
case INFO -> logger.info("[MCP] {} - {}", notification.logger(), notification.data());
default -> logger.debug("[MCP] {} - {}", notification.logger(), notification.data());
}
}
@McpSampling(clients = "server1")
public CreateMessageResult handleSampling(CreateMessageRequest request) {
List<Message> messages = request.messages().stream()
.map(msg -> msg.role() == Role.USER
? new UserMessage(((TextContent) msg.content()).text())
: AssistantMessage.builder()
.content(((TextContent) msg.content()).text()).build())
.toList();
ChatResponse response = chatModel.call(new Prompt(messages));
return CreateMessageResult.builder()
.role(Role.ASSISTANT)
.content(new TextContent(response.getResult().getOutput().getText()))
.model(request.modelPreferences().hints().get(0).name())
.build();
}
@McpProgress(clients = "server1")
public void handleProgress(ProgressNotification notification) {
progressTracker.update(
notification.progressToken(), notification.progress(),
notification.total(), notification.message());
}
@McpToolListChanged(clients = "server1")
public void handleServer1ToolsChanged(List<McpSchema.Tool> tools) {
logger.info("Server1 工具已更新: {} 个", tools.size());
toolRegistry.updateServerTools("server1", tools);
}
}
配置:
yaml
spring:
ai:
mcp:
client:
type: SYNC
initialized: true
request-timeout: 30s
annotation-scanner:
enabled: true
sse:
connections:
server1:
url: http://localhost:8080
stdio:
connections:
local-tool:
command: /usr/local/bin/mcp-tool
args:
- --mode=production
8.4 异步工具服务端
java
@Component
public class AsyncDataProcessor {
@McpTool(name = "fetch-data", description = "从外部源获取数据")
public Mono<DataResult> fetchData(
@McpToolParam(description = "数据源URL", required = true) String url,
@McpToolParam(description = "超时秒数", required = false) Integer timeout) {
Duration timeoutDuration = Duration.ofSeconds(timeout != null ? timeout : 30);
return WebClient.create().get().uri(url).retrieve()
.bodyToMono(String.class)
.map(data -> new DataResult(url, data, System.currentTimeMillis()))
.timeout(timeoutDuration)
.onErrorReturn(new DataResult(url, "获取数据出错", 0L));
}
@McpTool(name = "process-stream", description = "处理数据流")
public Flux<String> processStream(
McpAsyncRequestContext context,
@McpToolParam(description = "条目数", required = true) int count) {
String progressToken = context.request().progressToken();
return Flux.range(1, count)
.delayElements(Duration.ofMillis(100))
.flatMap(i -> {
if (progressToken != null) {
double progress = (double) i / count;
return context.progress(p -> p.progress(progress).total(1.0)
.message("处理条目 " + i))
.thenReturn("已处理条目 " + i);
}
return Mono.just("已处理条目 " + i);
});
}
}
8.5 无状态工具
java
@Component
public class StatelessTools {
@McpTool(name = "format-text", description = "格式化文本")
public String formatText(
@McpToolParam(description = "待格式化文本", required = true) String text,
@McpToolParam(description = "格式类型", required = true) String format) {
return switch (format.toLowerCase()) {
case "uppercase" -> text.toUpperCase();
case "lowercase" -> text.toLowerCase();
case "reverse" -> new StringBuilder(text).reverse().toString();
default -> text;
};
}
@McpTool(name = "validate-json", description = "验证 JSON")
public CallToolResult validateJson(
McpTransportContext context,
@McpToolParam(description = "JSON 字符串", required = true) String json) {
try {
new ObjectMapper().readTree(json);
return CallToolResult.builder()
.addTextContent("有效的 JSON")
.structuredContent(Map.of("valid", true)).build();
} catch (Exception e) {
return CallToolResult.builder()
.addTextContent("无效 JSON: " + e.getMessage())
.structuredContent(Map.of("valid", false, "error", e.getMessage())).build();
}
}
}
8.6 多模型采样(Sampling)
此示例展示服务端使用 MCP Sampling 从多个 LLM 提供商生成内容。
服务端 --- 天气+诗歌:
java
@Service
public class WeatherService {
private final RestClient restClient = RestClient.create();
@McpTool(description = "获取指定位置的温度(摄氏度)")
public String getTemperature2(McpSyncServerExchange exchange,
@McpToolParam(description = "纬度") double latitude,
@McpToolParam(description = "经度") double longitude) {
WeatherResponse weatherResponse = restClient.get()
.uri("https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t=temperature_2m",
latitude, longitude)
.retrieve().body(WeatherResponse.class);
StringBuilder openAiPoem = new StringBuilder();
StringBuilder anthropicPoem = new StringBuilder();
if (exchange.getClientCapabilities().sampling() != null) {
var messageRequestBuilder = McpSchema.CreateMessageRequest.builder()
.systemPrompt("You are a poet!")
.messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
new McpSchema.TextContent("请根据此天气数据写一首诗..."))));
// 请求 OpenAI
var openAiRequest = messageRequestBuilder
.modelPreferences(ModelPreferences.builder().addHint("openai").build()).build();
CreateMessageResult openAiResponse = exchange.createMessage(openAiRequest);
openAiPoem.append(((McpSchema.TextContent) openAiResponse.content()).text());
// 请求 Anthropic
var anthropicRequest = messageRequestBuilder
.modelPreferences(ModelPreferences.builder().addHint("anthropic").build()).build();
CreateMessageResult anthropicResponse = exchange.createMessage(anthropicRequest);
anthropicPoem.append(((McpSchema.TextContent) anthropicResponse.content()).text());
}
return "OpenAI 诗歌:\n" + openAiPoem + "\n\nAnthropic 诗歌:\n" + anthropicPoem;
}
}
客户端 --- 按模型路由:
java
@Service
public class McpClientHandlers {
@Autowired
Map<String, ChatClient> chatClients;
@McpSampling(clients = "server1")
public CreateMessageResult samplingHandler(CreateMessageRequest llmRequest) {
var userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text();
String modelHint = llmRequest.modelPreferences().hints().get(0).name();
ChatClient hintedChatClient = chatClients.entrySet().stream()
.filter(e -> e.getKey().contains(modelHint))
.findFirst().orElseThrow().getValue();
String response = hintedChatClient.prompt()
.system(llmRequest.systemPrompt())
.user(userPrompt).call().content();
return CreateMessageResult.builder()
.content(new McpSchema.TextContent(response)).build();
}
}
8.7 与 Spring AI 集成
将 MCP 工具与 Spring AI 的函数调用集成:
java
@RestController
@RequestMapping("/chat")
public class ChatController {
private final ChatModel chatModel;
private final SyncMcpToolCallbackProvider toolCallbackProvider;
public ChatController(ChatModel chatModel,
SyncMcpToolCallbackProvider toolCallbackProvider) {
this.chatModel = chatModel;
this.toolCallbackProvider = toolCallbackProvider;
}
@PostMapping
public ChatResponse chat(@RequestBody ChatRequest request) {
ToolCallback[] mcpTools = toolCallbackProvider.getToolCallbacks();
Prompt prompt = new Prompt(
request.getMessage(),
ChatOptionsBuilder.builder().withTools(mcpTools).build());
return chatModel.call(prompt);
}
}
@Component
public class WeatherTools {
@McpTool(name = "get-weather", description = "获取当前天气")
public WeatherInfo getWeather(
@McpToolParam(description = "城市名", required = true) String city,
@McpToolParam(description = "单位(metric/imperial)", required = false) String units) {
String unit = units != null ? units : "metric";
return weatherService.getCurrentWeather(city, unit);
}
}