Spring AI / Model Context Protocol (MCP) / MCP Annotations / MCP Annotations Examples
·
Spring AI
参考文档
模型上下文协议(MCP)
MCP 注解
MCP 注解示例
MCP 注解示例
本页提供了在 Spring AI 应用程序中使用 MCP 注解的全面示例。
完整应用示例
简单计算器服务器
一个提供计算器工具的 MCP 服务器完整示例:
@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();
}
}
}
配置:
spring:
ai:
mcp:
server:
name: calculator-server
version: 1.0.0
type: SYNC
protocol: SSE # 或 STDIO、STREAMABLE
capabilities:
tool: true
resource: true
prompt: true
completion: true
文档处理服务器
一个带有资源和提示词功能的文档处理服务器示例:
@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 ReadResourceResult.builder(List.of(
new TextResourceContents("document://" + id,
"text/plain", "未找到文档")
)).build();
}
// 从元数据中检查访问权限
String accessLevel = (String) meta.get("accessLevel");
if ("restricted".equals(doc.getClassification()) &&
!"admin".equals(accessLevel)) {
return ReadResourceResult.builder(List.of(
new TextResourceContents("document://" + id,
"text/plain", "访问被拒绝")
)).build();
}
return ReadResourceResult.builder(List.of(
new TextResourceContents("document://" + id,
doc.getMimeType(), doc.getContent())
)).build();
}
@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 analysisType = type != null ? type : "summary";
String result = performAnalysis(doc, analysisType);
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) {
Document doc = documents.get(docId);
if (doc == null) {
return GetPromptResult.builder(List.of(new PromptMessage(Role.SYSTEM,
TextContent.builder("未找到文档").build())))
.description("错误")
.build();
}
String promptText = String.format(
"请用%s总结以下文档:\n\n%s",
length != null ? length : "几个段落",
doc.getContent()
);
return GetPromptResult.builder(List.of(new PromptMessage(Role.USER, TextContent.builder(promptText).build())))
.description("文档摘要")
.build();
}
@McpComplete(prompt = "document-summary")
public List<String> completeDocumentId(String prefix) {
return documents.keySet().stream()
.filter(id -> id.startsWith(prefix))
.sorted()
.limit(10)
.toList();
}
}
带处理器的 MCP 客户端
一个带有各种处理器的完整 MCP 客户端应用程序:
@SpringBootApplication
public class McpClientApplication {
public static void main(String[] args) {
SpringApplication.run(McpClientApplication.class, args);
}
}
@Component
public class ClientHandlers {
private final Logger logger = LoggerFactory.getLogger(ClientHandlers.class);
private final ProgressTracker progressTracker = new ProgressTracker();
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());
break;
case WARNING:
logger.warn("[MCP] {} - {}", notification.logger(), notification.data());
break;
case INFO:
logger.info("[MCP] {} - {}", notification.logger(), notification.data());
break;
default:
logger.debug("[MCP] {} - {}", notification.logger(), notification.data());
}
}
@McpSampling(clients = "server1")
public CreateMessageResult handleSampling(CreateMessageRequest request) {
// 使用 Spring AI ChatModel 进行采样
List<Message> messages = request.messages().stream()
.map(msg -> {
if (msg.role() == Role.USER) {
return new UserMessage(((TextContent) msg.content()).text());
} else {
return AssistantMessage.builder()
.content(((TextContent) msg.content()).text())
.build();
}
})
.toList();
ChatResponse response = chatModel.call(new Prompt(messages));
return CreateMessageResult.builder(Role.ASSISTANT,
response.getResult().getOutput().getText(),
request.modelPreferences().hints().get(0).name())
.build();
}
@McpElicitation(clients = "server1")
public ElicitResult handleElicitation(ElicitRequest request) {
// 在实际应用中,这里会显示一个 UI 对话框
Map<String, Object> userData = new HashMap<>();
logger.info("请求交互: {}", request.message());
// 根据模式模拟用户输入
Map<String, Object> schema = request.requestedSchema();
if (schema != null && schema.containsKey("properties")) {
Map<String, Object> properties = (Map<String, Object>) schema.get("properties");
properties.forEach((key, value) -> {
// 在实际应用中,会提示用户输入每个字段
userData.put(key, getDefaultValueForProperty(key, value));
});
}
return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
}
@McpProgress(clients = "server1")
public void handleProgress(ProgressNotification notification) {
progressTracker.update(
notification.progressToken(),
notification.progress(),
notification.total(),
notification.message()
);
// 更新 UI 或发送 WebSocket 通知
broadcastProgress(notification);
}
@McpToolListChanged(clients = "server1")
public void handleServer1ToolsChanged(List<McpSchema.Tool> tools) {
logger.info("Server1 工具已更新:{} 个工具可用", tools.size());
// 更新工具注册表
toolRegistry.updateServerTools("server1", tools);
// 通知 UI 刷新工具列表
eventBus.publish(new ToolsUpdatedEvent("server1", tools));
}
@McpResourceListChanged(clients = "server1")
public void handleServer1ResourcesChanged(List<McpSchema.Resource> resources) {
logger.info("Server1 资源已更新:{} 个资源可用", resources.size());
// 清除该服务器的资源缓存
resourceCache.clearServer("server1");
// 注册新资源
resources.forEach(resource ->
resourceCache.register("server1", resource));
}
}
配置:
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
异步示例
异步工具服务器
@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);
});
}
@McpResource(uri = "async-data://{id}", name = "异步数据")
public Mono<ReadResourceResult> getAsyncData(String id) {
return Mono.fromCallable(() -> loadDataAsync(id))
.subscribeOn(Schedulers.boundedElastic())
.map(data -> ReadResourceResult.builder(List.of(
new TextResourceContents("async-data://" + id,
"application/json", data)
)).build());
}
}
异步客户端处理器
@Component
public class AsyncClientHandlers {
@McpSampling(clients = "async-server")
public Mono<CreateMessageResult> handleAsyncSampling(CreateMessageRequest request) {
return Mono.fromCallable(() -> {
// 为 LLM 准备请求
String prompt = extractPrompt(request);
return prompt;
})
.flatMap(prompt -> callLLMAsync(prompt))
.map(response -> CreateMessageResult.builder(Role.ASSISTANT, response, "gpt-4")
.build())
.timeout(Duration.ofSeconds(30));
}
@McpProgress(clients = "async-server")
public Mono<Void> handleAsyncProgress(ProgressNotification notification) {
return Mono.fromRunnable(() -> {
// 更新进度跟踪
updateProgressAsync(notification);
})
.then(broadcastProgressAsync(notification))
.subscribeOn(Schedulers.parallel());
}
@McpElicitation(clients = "async-server")
public Mono<ElicitResult> handleAsyncElicitation(ElicitRequest request) {
return showUserDialogAsync(request)
.map(userData -> {
if (userData != null && !userData.isEmpty()) {
return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
} else {
return new ElicitResult(ElicitResult.Action.DECLINE, null);
}
})
.timeout(Duration.ofMinutes(5))
.onErrorReturn(new ElicitResult(ElicitResult.Action.CANCEL, null));
}
}
无状态服务器示例
@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 "title" -> toTitleCase(text);
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 {
JsonMapper mapper = new JsonMapper();
mapper.readTree(json);
return CallToolResult.builder()
.addTextContent("有效的 JSON")
.structuredContent(Map.of("valid", true))
.build();
} catch (JacksonException e) {
return CallToolResult.builder()
.addTextContent("无效的 JSON: " + e.getMessage())
.structuredContent(Map.of("valid", false, "error", e.getMessage()))
.build();
}
}
@McpResource(uri = "static://{path}", name = "静态资源")
public String getStaticResource(String path) {
// 简单的无状态资源
return loadStaticContent(path);
}
@McpPrompt(name = "template", description = "模板提示词")
public GetPromptResult templatePrompt(
@McpArg(name = "template", required = true) String templateName,
@McpArg(name = "variables", required = false) String variables) {
String template = loadTemplate(templateName);
if (variables != null) {
template = substituteVariables(template, variables);
}
return GetPromptResult.builder(List.of(new PromptMessage(Role.USER, TextContent.builder(template).build())))
.description("模板: " + templateName)
.build();
}
}
使用多个 LLM 提供商的 MCP 采样
此示例演示了如何使用 MCP 采样从多个 LLM 提供商生成创意内容,展示了基于注解的方法在服务器和客户端实现中的应用。
采样服务器实现
服务器提供了一个天气工具,使用 MCP 采样从不同的 LLM 提供商生成诗歌。
此示例直接使用
McpSyncServerExchange以便对低级 MCP API 进行细粒度控制。对于更简单的情况,请使用McpSyncRequestContext,它提供了更高级、更方便的接口(例如context.sampleEnabled()、context.sample(...)、context.info(...))。
@Service
public class WeatherService {
private final RestClient restClient = RestClient.create();
public record WeatherResponse(Current current) {
public record Current(LocalDateTime time, int interval, double temperature_2m) {
}
}
@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 openAiWeatherPoem = new StringBuilder();
StringBuilder anthropicWeatherPoem = new StringBuilder();
// 发送日志通知
exchange.loggingNotification(LoggingMessageNotification.builder(LoggingLevel.INFO, "开始采样")
.build());
// 检查客户端是否支持采样
if (exchange.getClientCapabilities().sampling() != null) {
var samplingMessages = List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
McpSchema.TextContent.builder(
"请写一首关于这个天气预报的诗(温度单位为摄氏度)。使用 Markdown 格式:\n "
+ new JsonHelper().toJson(weatherResponse)).build()));
var messageRequestBuilder = McpSchema.CreateMessageRequest.builder(samplingMessages, 500)
.systemPrompt("你是一位诗人!");
// 向 OpenAI 请求诗歌
var openAiLlmMessageRequest = messageRequestBuilder
.modelPreferences(ModelPreferences.builder().addHint("openai").build())
.build();
CreateMessageResult openAiLlmResponse = exchange.createMessage(openAiLlmMessageRequest);
openAiWeatherPoem.append(((McpSchema.TextContent) openAiLlmResponse.content()).text());
// 向 Anthropic 请求诗歌
var anthropicLlmMessageRequest = messageRequestBuilder
.modelPreferences(ModelPreferences.builder().addHint("anthropic").build())
.build();
CreateMessageResult anthropicAiLlmResponse = exchange.createMessage(anthropicLlmMessageRequest);
anthropicWeatherPoem.append(((McpSchema.TextContent) anthropicAiLlmResponse.content()).text());
}
exchange.loggingNotification(LoggingMessageNotification.builder(LoggingLevel.INFO, "完成采样")
.build());
// 合并结果
String responseWithPoems = "OpenAI 关于天气的诗:" + openAiWeatherPoem.toString() + "\n\n" +
"Anthropic 关于天气的诗:" + anthropicWeatherPoem.toString() + "\n"
+ new JsonHelper().toJson(weatherResponse);
return responseWithPoems;
}
}
采样客户端实现
客户端根据模型提示将采样请求路由到相应的 LLM 提供商:
@Service
public class McpClientHandlers {
private static final Logger logger = LoggerFactory.getLogger(McpClientHandlers.class);
@Autowired
Map<String, ChatClient> chatClients;
@McpProgress(clients = "server1")
public void progressHandler(ProgressNotification progressNotification) {
logger.info("MCP 进度: [{}] 进度: {} 总计: {} 消息: {}",
progressNotification.progressToken(), progressNotification.progress(),
progressNotification.total(), progressNotification.message());
}
@McpLogging(clients = "server1")
public void loggingHandler(LoggingMessageNotification loggingMessage) {
logger.info("MCP 日志: [{}] {}", loggingMessage.level(), loggingMessage.data());
}
@McpSampling(clients = "server1")
public CreateMessageResult samplingHandler(CreateMessageRequest llmRequest) {
logger.info("MCP 采样: {}", llmRequest);
// 提取用户提示和模型提示
var userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text();
String modelHint = llmRequest.modelPreferences().hints().get(0).name();
// 根据模型提示查找合适的 ChatClient
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(Role.ASSISTANT, response, modelHint)
.build();
}
}
客户端应用设置
在客户端应用程序中注册 MCP 工具和处理器:
@SpringBootApplication
public class McpClientApplication {
public static void main(String[] args) {
SpringApplication.run(McpClientApplication.class, args).close();
}
@Bean
public CommandLineRunner predefinedQuestions(OpenAiChatModel openAiChatModel,
ToolCallbackProvider mcpToolProvider) {
return args -> {
ChatClient chatClient = ChatClient.builder(openAiChatModel)
.defaultTools(mcpToolProvider)
.build();
String userQuestion = """
阿姆斯特丹现在的天气如何?
请包含所有 LLM 提供商的创意响应。
在其他提供商的响应之后,添加一首综合所有其他提供商诗歌的诗。
""";
System.out.println("> 用户: " + userQuestion);
System.out.println("> 助手: " + chatClient.prompt(userQuestion).call().content());
};
}
}
配置
服务器配置
# 服务器 application.properties
spring.ai.mcp.server.name=mcp-sampling-server-annotations
spring.ai.mcp.server.version=0.0.1
spring.ai.mcp.server.protocol=STREAMABLE
spring.main.banner-mode=off
客户端配置
# 客户端 application.properties
spring.application.name=mcp
spring.main.web-application-type=none
# 为多个模型禁用默认的聊天客户端自动配置
spring.ai.chat.client.enabled=false
# API 密钥
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY}
# 使用 stateless-http 传输的 MCP 客户端连接
spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080
# 禁用工具回调以防止循环依赖
spring.ai.mcp.client.toolcallback.enabled=false
演示的关键功能
- 多模型采样:服务器使用模型提示从多个 LLM 提供商请求内容
- 基于注解的处理器:客户端使用
@McpSampling、@McpLogging和@McpProgress注解 - 无状态 HTTP 传输:使用可流式传输协议进行通信
- 创意内容生成:从不同模型生成关于天气数据的诗歌
- 统一响应处理:将来自多个提供商的响应合并为单个结果
示例输出
运行客户端时,您将看到类似如下的输出:
> 用户: 阿姆斯特丹现在的天气如何?
请包含所有 LLM 提供商的创意响应。
在其他提供商的响应之后,添加一首综合所有其他提供商诗歌的诗。
> 助手:
OpenAI 关于天气的诗:
**阿姆斯特丹的冬日低语**
*温度:4.2°C*
在阿姆斯特丹的怀抱中,运河映照着天空,
4.2 度的轻柔寒意飘过...
Anthropic 关于天气的诗:
**运河边的沉思**
*当前条件:4.2°C*
在自行车休憩的水道旁,
冬日的空气考验着阿姆斯特丹...
天气数据:
{
"current": {
"time": "2025-01-23T11:00",
"interval": 900,
"temperature_2m": 4.2
}
}
与 Spring AI 集成
展示 MCP 工具与 Spring AI 函数调用集成的示例:
@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) {
// 将 MCP 工具作为 Spring AI 函数回调获取
ToolCallback[] mcpTools = toolCallbackProvider.getToolCallbacks();
// 创建带有 MCP 工具的提示词
Prompt prompt = new Prompt(
request.getMessage(),
ChatOptionsBuilder.builder()
.withTools(mcpTools)
.build()
);
// 调用带有 MCP 工具的聊天模型
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";
// 调用天气 API
return weatherService.getCurrentWeather(city, unit);
}
@McpTool(name = "get-forecast", description = "获取天气预报")
public ForecastInfo getForecast(
@McpToolParam(description = "城市名称", required = true) String city,
@McpToolParam(description = "天数(1-7)", required = false) Integer days) {
int forecastDays = days != null ? days : 3;
return weatherService.getForecast(city, forecastDays);
}
}
其他资源
更多推荐



所有评论(0)