Spring AI 1.x 系列【47】 MCP Annotations 模块

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-client
  • spring-ai-starter-mcp-client-webflux
  • spring-ai-starter-mcp-server
  • spring-ai-starter-mcp-server-webflux
  • spring-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 集成

自动配置会完成以下工作:

  1. 扫描所有带有 MCP 客户端注解的 Bean
  2. 创建对应的声明规范
  3. 将其注册到 MCP 客户端
  4. 同时支持同步和异步实现
  5. 支持多客户端与客户端专属处理器
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 统一请求上下文(推荐)

使用 McpSyncRequestContextMcpAsyncRequestContext,同时兼容有状态和无状态操作:

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)。使用 McpSyncRequestContextMcpAsyncRequestContext 的方法在无状态模式下会被忽略。

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 --- 可能为 null
  • McpSyncServerExchange / McpAsyncServerExchange --- 服务端交换上下文
  • McpTransportContext --- 无状态传输上下文
  • CallToolRequest --- 工具请求(仅工具方法)

7.7 最佳实践

  1. 使用 McpMeta 传递上下文信息 ,始终对元数据值做 null 检查
  2. 进度令牌做 null 检查后再使用
  3. 选择合适的上下文 --- 统一用 McpSyncRequestContext/McpAsyncRequestContext,仅需轻量上下文时用 McpTransportContext,最简单场景完全省略
  4. 能力检查 --- 使用双向操作前先检查能力支持:
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}&current=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);
    }
}

相关推荐
心枢AI研习社1 小时前
我问了claude目前最强大的模型fable 5这个问题?
人工智能·agent·claude
湘美书院--湘美谈教育1 小时前
湘美谈教育AI系列经验集锦:赋能整理聊斋志异大寓言
大数据·人工智能·深度学习·神经网络·机器学习
不知名的老吴1 小时前
线程的生命周期之线程同步
java·开发语言·jvm
宜昌未来智慧谷1 小时前
WWDC 2026开发者视角解读:Siri独立App的技术架构与第三方AI模型接入机制
人工智能·架构·apple·wwdc·gemini
协享科技1 小时前
Spring Boot 与 Go 双服务架构实践:从单体拆分到通信设计
java·人工智能·spring boot·后端·架构·golang·ai编程
piglet121381 小时前
把搜索调到 Claude.ai 的水准
前端·人工智能
Linlingu1 小时前
openClaw不能操作我的电脑提示没有权限如何解决?
人工智能·windows·办公自动化·数字员工·小龙虾
snpgroupcn2 小时前
SNP亮相2026思爱普中国峰会,助力企业加速数据价值兑现
人工智能
IT乐手2 小时前
Anthropic 为何限制中国大陆使用 Claude?
人工智能