Lzh on GitHub

MCP 服务器端注解

MCP 服务器端注解提供了一种使用 Java 注解实现 MCP 服务器功能的声明式方式。这些注解简化了工具、资源、提示和补全处理器的创建。

MCP 服务器端注解提供了一种使用 Java 注解实现 MCP 服务器功能的声明式方式。这些注解简化了工具、资源、提示和补全处理器的创建。

服务器端注解

@McpTool

@McpTool 注解用于标记方法为 MCP 工具实现,并支持自动生成 JSON schema。

基础用法:

@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 = "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, "平方单位");
}

使用请求上下文

工具可以访问请求上下文以执行高级操作:

@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 支持

工具可以接受 CallToolRequest 来处理运行时 schema:

@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();
}

进度跟踪

工具可以接收进度 token,用于跟踪长时间运行的操作:

@McpTool(name = "long-task", description = "长时间任务进度跟踪")
public String performLongTask(
        McpSyncRequestContext context,
        @McpToolParam(description = "任务名称", required = true) String taskName) {

    String progressToken = context.request().progressToken();

    if (progressToken != null) {
        context.progress(p -> p.progress(0.0).total(1.0).message("任务开始"));

        // 执行任务...

        context.progress(p -> p.progress(1.0).total(1.0).message("任务完成"));
    }

    return "任务 " + taskName + " 已完成";
}

@McpResource

@McpResource 注解用于通过 URI 模板访问资源。

基础用法

@Component
public class ResourceProvider {

    @McpResource(
        uri = "config://{key}",
        name = "Configuration",
        description = "提供配置数据")
    public String getConfig(String key) {
        return configData.get(key);
    }
}

使用 ReadResourceResult

@McpResource(
    uri = "user-profile://{username}",
    name = "User Profile",
    description = "提供用户资料信息")
public ReadResourceResult getUserProfile(String username) {
    String profileData = loadUserProfile(username);

    return new ReadResourceResult(List.of(
        new TextResourceContents(
            "user-profile://" + username,
            "application/json",
            profileData)
    ));
}

使用请求上下文

@McpResource(
    uri = "data://{id}",
    name = "Data Resource",
    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)
    ));
}

@McpPrompt

@McpPrompt 注解用于为 AI 交互生成提示消息(prompt)。

基本用法

@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) {

    StringBuilder message = new StringBuilder();
    message.append("你好,").append(name).append("\n\n");

    if (age != null) {
        message.append("你已经 ").append(age).append(" 岁了,");
        // 添加与年龄相关的内容
    }

    if (interests != null && !interests.isEmpty()) {
        message.append("你对 ").append(interests).append(" 感兴趣");
        // 添加与兴趣相关的内容
    }

    return new GetPromptResult(
        "个性化消息",
        List.of(new PromptMessage(Role.ASSISTANT, new TextContent(message.toString())))
    );
}

@McpComplete

@McpComplete 注解用于为提示(prompt)提供自动补全功能。

基本用法

@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();
    }
}

根据用户输入的前缀 prefix,返回最多 10 个匹配的城市名称,实现简单的自动补全。

使用 CompleteRequest.CompleteArgument

@McpComplete(prompt = "travel-planner")
public List<String> completeTravelDestination(CompleteRequest.CompleteArgument argument) {
    String prefix = argument.value().toLowerCase();
    String argumentName = argument.name();

    // 根据参数名称提供不同的补全结果
    if ("city".equals(argumentName)) {
        return completeCities(prefix);
    } else if ("country".equals(argumentName)) {
        return completeCountries(prefix);
    }

    return List.of();
}

根据 argument 中的参数名和输入值提供不同类型的补全,例如城市或国家。

使用 CompleteResult

@McpComplete(prompt = "code-completion")
public CompleteResult completeCode(String prefix) {
    List<String> completions = generateCodeCompletions(prefix);

    return new CompleteResult(
        new CompleteResult.CompleteCompletion(
            completions,
            completions.size(),  // 总数
            hasMoreCompletions   // 是否还有更多补全结果
        )
    );
}

返回完整的补全结果对象,包括匹配列表、总数量以及是否还有更多可补全项。

无状态与有状态实现

统一请求上下文(推荐)

可以使用 McpSyncRequestContextMcpAsyncRequestContext 提供统一接口,适用于有状态和无状态操作:

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) {

    // 访问请求及其元数据
    String progressToken = context.request().progressToken();

    // 使用便捷方法进行日志记录
    context.info("正在处理: " + input);

    // 进度通知(注意客户端需在请求中设置 progress token 才能接收进度更新)
    context.progress(50); // 简单百分比进度

    // 向客户端发送 ping
    context.ping();

    // 使用前检查功能支持情况
    if (context.elicitEnabled()) {
        // 请求用户输入(仅在有状态模式下有效)
        StructuredElicitResult<UserInfo> elicitResult = context.elicit(UserInfo.class);
        if (elicitResult.action() == ElicitResult.Action.ACCEPT) {
            // 使用用户提供的数据
        }
    }

    if (context.sampleEnabled()) {
        // 请求 LLM 采样(仅在有状态模式下有效)
        CreateMessageResult samplingResult = context.sample("生成响应");
        // 使用采样结果
    }

    return "使用统一上下文处理完成";
}

简单操作(无上下文)

对于简单操作,可以完全省略上下文参数:

@McpTool(name = "simple-add", description = "简单加法")
public int simpleAdd(
        @McpToolParam(description = "第一个数字", required = true) int a,
        @McpToolParam(description = "第二个数字", required = true) int b) {
    return a + b;
}

直接对两个必填参数 ab 执行加法操作,无需使用请求上下文。

轻量无状态(使用 McpTransportContext)

对于只需最小传输上下文的无状态操作:

@McpTool(name = "stateless-tool", description = "无状态工具,带传输上下文")
public String statelessTool(
        McpTransportContext context,
        @McpToolParam(description = "输入内容", required = true) String input) {
    // 仅访问传输层上下文
    // 无双向操作(如根操作、用户输入请求、采样)
    return "已处理: " + input;
}
无状态服务器不支持双向操作:

因此,在无状态模式下,使用 McpSyncRequestContextMcpAsyncRequestContext 的方法会被忽略。

按服务器类型的方法过滤

MCP 注解框架会根据服务器类型和方法特性自动筛选被注解的方法。这样可以确保每种服务器配置只注册合适的方法。对于每个被筛选的方法,框架都会记录一条警告日志,以便调试使用。

同步与异步过滤

同步服务器(Synchronous Servers)

配置方式:spring.ai.mcp.server.type=SYNC 同步服务器使用同步提供者,特点如下:

  • 接受 非响应式返回类型的方法:
    • 原始类型(intdoubleboolean
    • 对象类型(StringInteger、自定义 POJO)
    • MCP 类型(CallToolResultReadResourceResultGetPromptResultCompleteResult
    • 集合类型(List<String>Map<String, Object>
  • 过滤掉 响应式返回类型的方法:
    • Mono<T>
    • Flux<T>
    • Publisher<T>

示例代码:

@Component
public class SyncTools {

    @McpTool(name = "sync-tool", description = "同步工具")
    public String syncTool(String input) {
        // 此方法将在同步服务器上被注册
        return "已处理: " + input;
    }

    @McpTool(name = "async-tool", description = "异步工具")
    public Mono<String> asyncTool(String input) {
        // 此方法将在同步服务器上被过滤掉
        // 并记录警告日志
        return Mono.just("已处理: " + input);
    }
}

异步服务器(Asynchronous Servers)

配置方式:spring.ai.mcp.server.type=ASYNC,异步服务器使用异步提供者,特点如下:

  • 接受 响应式返回类型的方法:
    • Mono<T>(用于单结果)
    • Flux<T>(用于流式结果)
    • Publisher<T>(通用响应式类型)
  • 过滤掉 非响应式返回类型的方法:
    • 原始类型
    • 对象类型
    • 集合类型
    • MCP 结果类型

示例代码:

@Component
public class AsyncTools {

    @McpTool(name = "async-tool", description = "异步工具")
    public Mono<String> asyncTool(String input) {
        // 此方法将在异步服务器上被注册
        return Mono.just("已处理: " + input);
    }

    @McpTool(name = "sync-tool", description = "同步工具")
    public String syncTool(String input) {
        // 此方法将在异步服务器上被过滤掉
        // 并记录警告日志
        return "已处理: " + input;
    }
}

有状态与无状态过滤

有状态服务器(Stateful Servers)

有状态服务器支持双向通信,并接受以下方法:

  • 双向上下文参数
    • McpSyncRequestContext(用于同步操作)
    • McpAsyncRequestContext(用于异步操作)
    • McpSyncServerExchange(遗留同步操作)
    • McpAsyncServerExchange(遗留异步操作)
  • 支持的双向操作
    • roots() — 访问根目录
    • elicit() — 请求用户输入
    • sample() — 请求 LLM 采样

示例代码:

@Component
public class StatefulTools {

    @McpTool(name = "interactive-tool", description = "支持双向操作的工具")
    public String interactiveTool(
            McpSyncRequestContext context,
            @McpToolParam(description = "输入内容", required = true) String input) {

        // 此方法将在有状态服务器上被注册
        // 可使用 elicitation、sampling、roots 等功能
        if (context.sampleEnabled()) {
            var samplingResult = context.sample("生成响应");
            // 处理采样结果...
        }

        return "使用上下文处理完成";
    }
}

无状态服务器(Stateless Servers)

无状态服务器针对简单的请求-响应模式进行了优化,并具有以下特点:

  • 过滤掉带双向上下文参数的方法:
    • 使用 McpSyncRequestContext 的方法会被跳过
    • 使用 McpAsyncRequestContext 的方法会被跳过
    • 使用 McpSyncServerExchange 的方法会被跳过
    • 使用 McpAsyncServerExchange 的方法会被跳过
    • 每个被过滤的方法都会记录警告日志
  • 接受的方法
    • McpTransportContext(轻量级无状态上下文)
    • 完全没有上下文参数的方法
    • 仅包含普通 @McpToolParam 参数的方法
  • 不支持双向操作
    • roots() — 不可用
    • elicit() — 不可用
    • sample() — 不可用

示例代码:

@Component
public class StatelessTools {

    @McpTool(name = "simple-tool", description = "简单无状态工具")
    public String simpleTool(@McpToolParam(description = "输入内容") String input) {
        // 此方法将在无状态服务器上被注册
        return "已处理: " + input;
    }

    @McpTool(name = "context-tool", description = "带传输上下文的工具")
    public String contextTool(
            McpTransportContext context,
            @McpToolParam(description = "输入内容") String input) {
        // 此方法将在无状态服务器上被注册
        return "已处理: " + input;
    }

    @McpTool(name = "bidirectional-tool", description = "带双向上下文的工具")
    public String bidirectionalTool(
            McpSyncRequestContext context,
            @McpToolParam(description = "输入内容") String input) {
        // 此方法将在无状态服务器上被过滤掉
        // 并记录警告日志
        return "已使用采样处理";
    }
}

过滤总结

服务器类型可接受的方法被过滤的方法
同步有状态(Sync Stateful)非响应式返回类型 + 双向上下文参数响应式返回类型(Mono/Flux)
异步有状态(Async Stateful)响应式返回类型(Mono/Flux) + 双向上下文参数非响应式返回类型
同步无状态(Sync Stateless)非响应式返回类型 + 无双向上下文响应式返回类型 或 带双向上下文参数
异步无状态(Async Stateless)响应式返回类型(Mono/Flux) + 无双向上下文非响应式返回类型 或 带双向上下文参数
方法过滤最佳实践:
  1. 保持方法与服务器类型一致
  • 同步服务器使用同步方法
  • 异步服务器使用异步方法
  1. 将有状态与无状态实现分开
  • 放在不同类中,便于维护和理解
  1. 启动时检查日志
  • 留意被过滤方法的警告日志
  1. 使用正确的上下文
  • 有状态:McpSyncRequestContext / McpAsyncRequestContext
  • 无状态:McpTransportContext
  1. 同时支持有状态和无状态部署时进行测试
  • 确保方法在两种模式下都能按预期工作

异步支持

所有服务器注解均支持使用 Reactor 的异步实现:

@Component
public class AsyncTools {

    @McpTool(name = "async-fetch", description = "异步获取数据")
    public Mono<String> asyncFetch(
            @McpToolParam(description = "URL", required = true) String url) {

        return Mono.fromCallable(() -> {
            // 模拟异步操作
            return 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)
            ));
        }).delayElements(Duration.ofMillis(100));
    }
}

Spring Boot 集成

通过 Spring Boot 的自动配置,被注解的 Bean 会被自动检测并注册:

@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 注解方法
}

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

  1. 扫描带有 MCP 注解的 Bean
  2. 创建相应的规范(specifications)
  3. 将它们注册到 MCP 服务器
  4. 根据配置处理同步(sync)和异步(async)实现

配置属性

配置服务器注解扫描器:

spring:
  ai:
    mcp:
      server:
        type: SYNC  # 可选 SYNC 或 ASYNC
        annotation-scanner:
          enabled: true

附加资源