深入理解 AI Agent · MCP 子系列 #02:MCP Server 开发实战—从工具注册到无状态新规范

导读 :上一篇文章(MCP-01)从协议层面拆解了 MCP 的消息格式、传输层设计和生命周期管理。本篇进入实战环节------如何用 Spring AI 搭建一个生产级的 MCP Server。文章将结合 Dream-SaaS 项目中的真实踩坑经验,覆盖工具注册的两种方案、Server 架构分层设计、参数校验与错误处理,并重点解读 2026 年 7 月 28 日发布的 MCP 无状态新规范------这是 MCP 协议自发布以来最大的一次架构变更,直接影响了 Server 端的开发方式。


一、工具注册:两种方案的选型与踩坑

MCP Server 的核心工作就是"把工具暴露出去"。Spring AI 提供了两种工具注册方式,各有适用场景。在 Dream-SaaS 项目的开发过程中,两种方式都用过,也踩了不少坑。

1.1 方案 A:@McpTool + annotation-scanner(声明式)

这是 Spring AI 为 MCP 专门设计的注解方案。在方法上标注 @McpTool,参数标注 @McpToolParam,框架启动时通过 annotation-scanner 自动扫描并注册为 MCP 工具:

bash 复制代码
@Component
public class McpService {

    @McpTool(name = "hello", description = "一个Mcp Server 的测试方法")
    public String hello(
            @McpToolParam(description = "请求的类型") String type,
            @McpToolParam(description = "请求的名称") String name) {
        return new TestTool().Hello(type, name);
    }
}

配合 YAML 配置启用扫描:

bash 复制代码
spring:
  ai:
    mcp:
      server:
        enabled: true
        annotation-scanner:
          enabled: true
        type: sync
        protocol: streamable
        streamable-http:
          mcp-endpoint: /mcp

这种方式的优点是简洁直观:注解即文档,配置即启用。适合工具数量有限、逻辑相对简单的场景。缺点是工具类必须被 Spring 扫描到,且注解与 MCP 协议强耦合------如果后续想把同一个方法同时作为普通 REST 接口暴露,或者同时注册到 Spring AI 的 Agent 工具链中,需要额外处理。

1.2 方案 B:@Tool + McpToolContributor(模块化)

Dream-SaaS 项目最终采用的方案。工具方法使用 Spring AI 通用的 @Tool 注解,再通过实现 McpToolContributor 接口将工具对象贡献给 MCP Server:

bash 复制代码
@Component
public class McpService {

    @Tool(name = "hello", description = "一个Mcp Server 的测试方法")
    public String hello(
            @ToolParam(description = "请求的类型") String type,
            @ToolParam(description = "请求的名称") String name) {
        return new TestTool().Hello(type, name);
    }
}
bash 复制代码
@Component
public class TestToolContributor implements McpToolContributor {

    @Autowired
    private McpService mcpService;

    @Override
    public String moduleId() {
        return "test";
    }

    @Override
    public Object[] toolObjects() {
        return new Object[] { mcpService };
    }
}

这种方案的核心优势在于模块化 。每个业务模块实现自己的 McpToolContributor,通过 moduleId() 标识归属,toolObjects() 返回包含 @Tool 方法的 Bean 实例。Spring AI 框架在启动时聚合所有 Contributor,统一注册到 MCP Server。

在 Dream-SaaS 项目中,我们有十几个业务模块(代码审查、文档搜索、数据库查询、API 网关调用等),每个模块各自实现 Contributor。当工具数量超过 50 个后,这种分而治之的方式远比在一个大类上堆注解要清晰------每个模块的工具定义、参数校验、错误处理都在各自的模块内完成。

此外,@Tool 是 Spring AI 的通用注解。同一个工具方法可以同时被 MCP Server 暴露给外部 Client,也可以被本地的 Agent 工具链直接调用------无需为 MCP 和非 MCP 场景维护两套代码。

1.3 两种方案的对比

对比维度 方案 A:@McpTool + Scanner 方案 B:@Tool + Contributor
注解类型 @McpTool / @McpToolParam @Tool / @ToolParam
注册方式 annotation-scanner 自动扫描 McpToolContributor 手动聚合
模块化支持 弱,所有工具平铺 强,按 moduleId 分模块
复用性 MCP 专用注解,难复用 @Tool 通用,可同时用于 Agent 和 MCP
适用场景 工具数量少、逻辑简单 多模块、大型项目
配置复杂度 低(一个 enabled 开关) 中(需要实现 Contributor 接口)

1.4 踩坑记录

在 Dream-SaaS 的开发过程中,工具注册环节踩了几个典型的坑,记录如下:

坑 1:@Tool ≠ @McpTool。 annotation-scanner 只扫描 @McpTool,不扫描 @Tool。如果开启了 annotation-scanner 但用了 @Tool 注解,工具不会被注册,且没有报错------属于"静默失败",排查成本很高。最终我们在启动日志中加了工具注册汇总,快速发现"注册了 0 个工具"的异常。

坑 2:同一工具名不能双开两种方案。 如果同一个工具名(如 "hello")同时出现在 @McpTool(通过 scanner 注册)和 McpToolContributor 中,启动时会抛出 IllegalArgumentException。两种方案只能选一种,或者确保工具名不冲突。

坑 3:toolObjects() 返回的是 Bean 而非 Method。 这是一个容易混淆的点------toolObjects() 需要返回包含工具方法的对象实例(Bean),而不是方法引用。框架会在这些 Bean 上扫描 @Tool 注解来提取工具定义。如果返回 null 或空数组,不会报错,只是该 Contributor 的工具不会被注册。

坑 4:同进程 Server + Client 自连不稳定。 在测试环境中,如果 MCP Server 和 MCP Client 跑在同一个 Spring Boot 进程中(同一个端口),会出现连接不稳定的问题。具体表现为偶尔超时、偶尔返回空结果。建议测试时使用两个进程、两个端口,模拟真实的部署拓扑。


二、Server 架构分层设计

一个生产级的 MCP Server 不是"写几个 @Tool 方法就上线"。当工具数量超过 20 个、接入多个 LLM 应用后,需要清晰的分层架构来保证可维护性。以下是 Dream-SaaS 项目中总结的四层架构模型:

bash 复制代码
┌──────────────────────────────────────────────────────────┐
│  协议层(Protocol Layer)                                  │
│  MCP 协议适配、JSON-RPC 编解码、传输层选择                    │
├──────────────────────────────────────────────────────────┤
│  路由层(Routing Layer)                                   │
│  工具注册表、请求分发、moduleId 路由                          │
├──────────────────────────────────────────────────────────┤
│  工具层(Tool Layer)                                      │
│  参数校验、权限检查、限流、日志审计                            │
├──────────────────────────────────────────────────────────┤
│  执行层(Execution Layer)                                 │
│  业务逻辑实现、外部服务调用、结果封装                          │
└──────────────────────────────────────────────────────────┘

2.1 协议层

由 Spring AI 框架自动处理。配置 spring.ai.mcp.server.protocol=streamable 后,框架会自动启用 Streamable HTTP 传输,处理 JSON-RPC 消息的编解码、请求/响应匹配、错误格式化。

完整的 Server 端配置示例:

bash 复制代码
server:
  port: 8091

spring:
  ai:
    mcp:
      server:
        enabled: true
        name: dream-saas-mcp-server
        version: 1.0.0
        type: sync
        protocol: streamable
        streamable-http:
          mcp-endpoint: /mcp
        annotation-scanner:
          enabled: false    # 使用 McpToolContributor 方案时关闭
        tool-info:
          # 工具描述是否包含参数的完整 Schema
          include-full-schema: true

开发者无需关心协议细节------这也是选择 Spring AI 而非从零实现 MCP 协议栈的主要原因。

2.2 路由层

路由层负责将 tools/call 请求分发到正确的工具方法。方案 B 中的 McpToolContributor 就是路由层的一部分------框架根据 moduleId 和工具名构建注册表。

在大型项目中,可以进一步扩展路由层,实现高级路由功能:

bash 复制代码
@Component
public class AdvancedToolRouter {

    private final Map<String, ToolHandler> registry = new ConcurrentHashMap<>();

    public void register(String moduleId, String toolName, ToolHandler handler) {
        String key = moduleId + ":" + toolName;
        registry.put(key, handler);
    }

    public ToolHandler resolve(String toolName) {
        // 优先精确匹配,其次模糊匹配
        ToolHandler handler = registry.get(toolName);
        if (handler == null) {
            // 尝试跨模块查找
            handler = registry.values().stream()
                .filter(h -> h.supports(toolName))
                .findFirst()
                .orElse(null);
        }
        return handler;
    }
}

这种扩展路由可以实现按租户路由(不同租户看到不同的工具集)、按环境路由(灰度/生产)等功能。

2.3 工具层

工具层是参数校验、权限检查和日志审计的统一入口。在 Dream-SaaS 项目中,我们实现了一个通用的工具调用拦截器:

bash 复制代码
@Component
public class ToolCallInterceptor {

    private static final Logger log = LoggerFactory.getLogger(ToolCallInterceptor.class);

    public String intercept(String toolName, Map<String, Object> args,
                            Callable<String> delegate) throws Exception {
        long start = System.currentTimeMillis();
        log.info("[MCP-TOOL] invoke: {} args={}", toolName, truncate(args, 200));

        try {
            String result = delegate.call();
            long cost = System.currentTimeMillis() - start;
            log.info("[MCP-TOOL] success: {} cost={}ms result={}",
                toolName, cost, truncate(result, 200));
            return result;
        } catch (Exception e) {
            long cost = System.currentTimeMillis() - start;
            log.error("[MCP-TOOL] failed: {} cost={}ms error={}",
                toolName, cost, e.getMessage());
            throw e;
        }
    }

    private String truncate(Object obj, int maxLen) {
        String s = String.valueOf(obj);
        return s.length() > maxLen ? s.substring(0, maxLen) + "..." : s;
    }
}

日志关键字 [MCP-TOOL] 方便 grep 过滤。工具层的价值在于:业务逻辑开发者不需要关心横切关注点,只需专注实现工具功能。

2.4 执行层

执行层是真正的业务逻辑。每个工具方法调用具体的业务服务------数据库查询、外部 API 调用、文件操作等。执行层返回的结果需要封装为 MCP 协议要求的格式:

bash 复制代码
@Tool(name = "searchDocs", description = "搜索项目文档,返回最匹配的文档片段")
public String searchDocs(
        @ToolParam(description = "搜索关键词") String query,
        @ToolParam(description = "返回结果数量", required = false) Integer topK) {

    int k = (topK != null) ? topK : 5;
    List<DocResult> results = docSearchService.search(query, k);

    // MCP 工具返回值会自动封装为 content 数组
    return results.stream()
        .map(r -> String.format("【%s】\n%s\n(相关度: %.2f)",
            r.getTitle(), r.getContent(), r.getScore()))
        .collect(Collectors.joining("\n---\n"));
}

Spring AI 会自动将 String 返回值封装为 {"type": "text", "text": "..."} 格式,开发者不需要手动构造 JSON。


三、参数校验与错误处理

MCP Server 的参数校验比传统 REST API 更关键------因为调用方不是人类,而是 LLM。LLM 生成的参数可能类型不匹配、缺少必填字段、甚至传入完全无关的参数。

3.1 JSON Schema 自动校验

Spring AI 会根据 @ToolParam@McpToolParam 注解自动生成 JSON Schema,并在 tools/list 响应中返回给 Client:

bash 复制代码
{
  "name": "searchDocs",
  "description": "搜索项目文档,返回最匹配的文档片段",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "搜索关键词"
      },
      "topK": {
        "type": "integer",
        "description": "返回结果数量",
        "default": 5
      }
    },
    "required": ["query"]
  }
}

LLM 根据这个 Schema 生成参数,框架在工具调用前会自动做一轮 Schema 校验。如果参数不合法,直接返回 -32602(Invalid params)错误,不会进入业务逻辑。

3.2 增强校验:自定义校验器

对于复杂的业务校验(如"query 长度不超过 500 字符"、"topK 必须在 1-50 之间"),可以在工具方法内做前置校验:

bash 复制代码
@Tool(name = "searchDocs", description = "搜索项目文档")
public String searchDocs(
        @ToolParam(description = "搜索关键词") String query,
        @ToolParam(description = "返回数量") Integer topK) {

    // 前置校验
    if (query == null || query.isBlank()) {
        throw new McpToolException(-32602, "query 不能为空");
    }
    if (query.length() > 500) {
        throw new McpToolException(-32602, "query 长度不能超过500字符");
    }
    if (topK != null && (topK < 1 || topK > 50)) {
        throw new McpToolException(-32602, "topK 必须在1-50之间");
    }

    // 业务逻辑
    return docSearchService.search(query, topK != null ? topK : 5);
}

3.3 结构化错误码设计

在 Dream-SaaS 项目中,我们设计了一套结构化的错误码体系,与 MCP 协议的 JSON-RPC 错误码对应:

错误码 错误类型 触发场景
-32602 参数校验失败 类型不匹配、必填字段缺失、枚举值越界
-32603 内部执行错误 业务异常、外部服务超时、数据库连接失败
-32001 工具不存在 请求了未注册的工具名
-32002 权限不足 当前用户/租户无权调用该工具
-32003 资源限制 请求频率超限、并发数超限、配额耗尽

错误响应的 data 字段携带结构化信息,方便 Client 端做自动化的错误处理:

bash 复制代码
{
  "jsonrpc": "2.0",
  "id": "req-42",
  "error": {
    "code": -32602,
    "message": "参数校验失败: topK 必须在1-50之间",
    "data": {
      "field": "topK",
      "value": 100,
      "constraint": "range[1,50]",
      "suggestion": "将 topK 设置为 1 到 50 之间的整数"
    }
  }
}

data.suggestion 字段是专门为 LLM 设计的------当 LLM 收到这个错误时,可以根据 suggestion 自动修正参数并重试,而不需要将错误传递给用户。


四、MCP 7/28 无状态新规范:协议级架构变更

2026 年 7 月 28 日,MCP 规范发布了一次重大更新。这次更新的核心方向是全面无状态化------移除 initialize 握手、取消协议层会话机制、让每个请求自包含。这是 MCP 自发布以来最大的一次架构变更。

4.1 变更详情

移除 initialize 握手和 Mcp-Session-Id

旧规范要求 Client 在首次连接时发送 initialize 请求完成握手,Server 分配 Mcp-Session-Id,后续所有请求都必须携带这个 Session ID。这意味着 Client 和 Server 之间形成了有状态的绑定关系。

新规范直接移除了这一机制。没有握手,没有 Session ID。Client 可以直接发送 tools/listtools/call 请求,无需预先建立会话。

每个请求自包含

旧规范中,客户端信息(名称、版本、能力)在握手时一次性传递。新规范要求每个请求在 _meta 字段中携带协议版本、客户端身份和能力声明:

bash 复制代码
{
  "jsonrpc": "2.0",
  "id": "req-1",
  "method": "tools/call",
  "params": {
    "name": "searchDocs",
    "arguments": { "query": "MCP 规范" }
  },
  "_meta": {
    "protocolVersion": "2026-07-28",
    "clientInfo": {
      "name": "dream-saas-agent",
      "version": "2.0.0"
    },
    "capabilities": {
      "tools": {}
    }
  }
}

这意味着每个请求都是独立的、自描述的------Server 不需要维护任何客户端状态。

新增 HTTP Header

新规范引入了三个关键 Header:

Header 用途 示例值
MCP-Protocol-Version 协议版本 2026-07-28
Mcp-Method 请求方法名 tools/call
Mcp-Name 工具名称 searchDocs

这些 Header 使得网关和负载均衡器无需解析 JSON Body 就能做出路由决策。例如 Nginx 可以基于 Mcp-Method 做差异化路由:

bash 复制代码
# 工具调用请求路由到计算密集型后端
if ($http_mcp_method = "tools/call") {
    proxy_pass http://mcp-compute-backend;
}

# 工具列表请求路由到轻量级后端
if ($http_mcp_method = "tools/list") {
    proxy_pass http://mcp-meta-backend;
}
工具列表缓存

新规范为 tools/list 等响应引入了协议级的缓存控制:

bash 复制代码
{
  "jsonrpc": "2.0",
  "id": "list-1",
  "result": {
    "tools": [...],
    "_meta": {
      "ttlMs": 300000,
      "cacheScope": "global"
    }
  }
}
  • ttlMs:缓存有效期(毫秒),300000 = 5 分钟
  • cacheScope:缓存作用域,global 表示所有 Client 共享缓存,client 表示按 Client 隔离
新增 server/discover RPC

作为 initialize 握手的替代,新规范提供了可选的 server/discover RPC 方法,用于能力发现。Client 可以选择调用它来获取 Server 的能力信息,也可以不调用------直接发送工具调用请求。这把"能力协商"从必须步骤变成了可选步骤。

4.2 新旧规范完整对比

维度 旧规范(2025-11-25) 新规范(2026-07-28)
连接模式 先握手,后会话绑定 无握手,直接请求
会话机制 强制携带 Mcp-Session-Id 无协议层会话
网关路由 需解析 JSON Body 读取 Mcp-Method 头即可
客户端信息 握手时一次性传递 每个请求 _meta 自包含
工具列表缓存 无原生支持 协议级 ttl + 作用域控制
负载均衡 必须粘性会话 轮询即可
长任务实现 SSE 长连接绑定实例 任务句柄轮询
授权 基础 OAuth,可选 PKCE 强制 OAuth 2.1 + PKCE
JSON Schema draft-07 2020-12
错误码 自定义扩展 回归 JSON-RPC 标准码(-32020~-32099 保留)
扩展机制 非正式 Tasks 成为正式扩展(io.modelcontextprotocol/tasks)
MCP Apps 服务端渲染 HTML 沙箱

4.3 对 Java Server 开发的影响

新规范对 Spring AI MCP Server 的开发有以下几个直接影响:

开发简化。 不再需要维护 Session 状态,Server 端代码更简洁。工具调用方法无需检查 Session ID 的合法性,无需处理 Session 过期和重连逻辑。Spring AI 框架层面也会相应简化------不再需要 McpSessionManager 之类的组件。

部署友好。 无状态 Server 可以直接部署在 Kubernetes 等容器编排平台上,任意 Pod 都可以处理任意请求,无需粘性会话配置。负载均衡从"必须一致性哈希"降级为"简单轮询",运维成本显著降低。

bash 复制代码
# Kubernetes 部署示例------无需 sessionAffinity
apiVersion: v1
kind: Service
metadata:
  name: mcp-server
spec:
  selector:
    app: mcp-server
  ports:
    - port: 8091
      targetPort: 8091
  # 不需要 sessionAffinity: ClientIP
  type: ClusterIP

网关简化。 通过 Mcp-MethodMcp-Name Header,API 网关可以在不解包 JSON Body 的情况下完成路由、限流和鉴权。这对于使用 Spring Cloud Gateway 或 Nginx 的场景非常实用。

缓存优化。 tools/list 的 ttl 机制使得 Client 端可以做协议级的缓存管理,减少不必要的网络请求。对于工具列表不经常变化的场景,可以设置较长的 ttl(如 300000ms),显著降低首次调用延迟。


五、从有状态迁移到无状态:实操建议

对于已经基于旧规范开发的 MCP Server,迁移到新规范需要关注以下几个关键点。

5.1 移除 Session 管理逻辑

如果 Server 代码中有 Session 存储(如 Redis 中的 Session 数据、内存中的 Session Map),需要评估哪些数据是真正需要持久化的(如长任务的状态),哪些是协议层 Session 的残留(如握手信息)。前者保留,后者移除。

bash 复制代码
// 旧代码------需要移除
public class OldMcpHandler {
    private final Map<String, McpSession> sessions = new ConcurrentHashMap<>();

    public void handleInitialize(String sessionId, InitParams params) {
        sessions.put(sessionId, new McpSession(params));
    }
}

// 新代码------无需 Session
public class NewMcpHandler {
    // 每个请求自包含 _meta,无需维护 sessions
    public void handleToolCall(JsonRpcRequest request) {
        ClientInfo client = request.getMeta().getClientInfo();
        // 直接使用请求中的信息
    }
}

5.2 迁移长任务实现

旧规范中长任务依赖 SSE 长连接绑定到特定实例。新规范改为任务句柄(Task Handle)模式:

bash 复制代码
旧模式:
Client ──── SSE长连接 ────► Server实例A(任务执行中...)

新模式:
Client ──── tools/call ──► Server实例A(返回任务句柄 task-123)
Client ──── 轮询状态 ────► Server实例B(通过任务存储查询进度)
Client ──── 获取结果 ────► Server实例C(任务完成,返回结果)

需要引入任务状态存储(如 Redis 或数据库),将任务进度与具体的 Server 实例解耦。

5.3 适配新 Header

如果使用了 API 网关,需要更新路由规则:

bash 复制代码
# 旧:解析 Body 中的 method 字段(需要 ngx_http_json_module)
# 新:直接读取 Header
location /mcp {
    # 基于 Header 路由
    proxy_set_header X-Mcp-Method $http_mcp_method;

    # 基于工具名做差异化限流
    limit_req_zone $http_mcp_name zone=per_tool:10m rate=10r/s;
    limit_req zone=per_tool burst=20;

    proxy_pass http://mcp-backend;
}

5.4 升级 JSON Schema 版本

新规范将 JSON Schema 从 draft-07 升级到 2020-12。主要变化包括:

  • $ref 现在可以与其它关键字共存(draft-07 中 $ref 会忽略同级其他关键字)
  • 新增 $defs 替代 definitions
  • 新增 prefixItems 替代旧版 tuple 类型的 items 数组
  • unevaluatedItemsunevaluatedProperties 提供更精确的校验

如果工具定义中使用了 2020-12 的新特性,需要同步更新 Server 端的 Schema 生成逻辑。Spring AI 的后续版本应该会跟进这一变更。

5.5 强化授权机制

新规范要求强制使用 OAuth 2.1 + PKCE,并且增加了 RFC 9207 的 iss 验证。同时,DCR(Dynamic Client Registration)被废弃,转向 CIMD(Client Identity Metadata Document)。

如果 Server 之前的授权是可选的或使用了非标准的鉴权方式,迁移时需要统一到 OAuth 2.1 标准。具体步骤:

  1. 接入标准 OAuth 2.1 授权服务器(如 Keycloak、Auth0)
  2. 实现 PKCE 流程(code_challenge + code_verifier
  3. 验证 Token 中的 iss 字段(RFC 9207 要求)
  4. 迁移 DCR 配置到 CIMD 格式

六、工具组合模式

MCP Server 暴露的是单个工具,但在实际场景中,LLM Agent 往往需要组合多个工具来完成复杂任务。以下是三种常见的工具组合模式。

6.1 Chain 模式(链式调用)

工具按顺序执行,前一个工具的输出作为后一个工具的输入:

bash 复制代码
搜索文档 → 提取关键信息 → 生成摘要 → 发送通知

这种模式适合有明确先后依赖关系的场景。Agent 在规划时确定工具调用顺序,逐步执行。Spring AI 的 Agent 框架原生支持这种模式------LLM 的 ReAct 循环本身就是 Chain 模式。

6.2 DAG 模式(有向无环图)

多个工具可以并行执行,结果在某个节点汇合:

bash 复制代码
         ┌→ 代码扫描 ─────┐
文档分析 ─┤                ├→ 综合报告
         └→ 依赖分析 ─────┘

DAG 模式适合可以并行的独立子任务。Agent 框架需要支持识别哪些工具可以并行调用。在实际实现中,可以通过 CompletableFuture 在 Java 端实现并行调用:

bash 复制代码
public String generateReport(String projectId) {
    CompletableFuture<DocAnalysis> docs = CompletableFuture.supplyAsync(
        () -> docAnalysisTool.analyze(projectId));
    CompletableFuture<CodeScan> code = CompletableFuture.supplyAsync(
        () -> codeScanTool.scan(projectId));
    CompletableFuture<DepAnalysis> deps = CompletableFuture.supplyAsync(
        () -> depAnalysisTool.analyze(projectId));

    // 等待所有并行任务完成
    CompletableFuture.allOf(docs, code, deps).join();

    return reportGenerator.merge(docs.join(), code.join(), deps.join());
}

注意:这种并行逻辑应该在单个 MCP 工具内部实现,而不是依赖 Agent 框架的并行调用能力。从 MCP 协议的角度看,这只是一个工具调用------内部的并行对协议层透明。

6.3 Sub-Agent 模式

将复杂的工具调用任务委托给一个子 Agent,由子 Agent 自主决定调用哪些工具、以什么顺序调用。主 Agent 只关心最终结果。

这种模式适合"分析代码质量"这类需要多步判断的复杂任务。子 Agent 有自己的 System Prompt 和工具集,可以独立完成多轮工具调用。

在 Dream-SaaS 项目中,Sub-Agent 模式用于"代码审查"场景------主 Agent 将代码审查任务委托给专门的审查 Sub-Agent,Sub-Agent 内部调用代码扫描工具、复杂度分析工具、安全漏洞检测工具等,最终返回一份结构化的审查报告。


七、权限控制与超时管理

7.1 权限控制:RBAC + OPA

MCP Server 暴露的工具可能有权限限制------某些工具只对特定角色开放。实现方式有两种:

方案一:RBAC 注解

bash 复制代码
@Tool(name = "executeQuery", description = "执行 SQL 查询")
@RequiresRole("admin")
public String executeQuery(@ToolParam(description = "SQL语句") String sql) {
    return jdbcTemplate.queryForList(sql).toString();
}

在工具层拦截器中检查权限,不通过则返回 -32002 错误。

方案二:OPA(Open Policy Agent)集中式策略

将权限策略外置到 OPA,Server 在执行工具前调用 OPA 做策略检查。这种方式适合策略复杂、需要动态调整的场景:

bash 复制代码
public class OpaAuthzInterceptor {
    private final OpaClient opaClient;

    public boolean authorize(String toolName, String userId, Map<String, Object> context) {
        OpaRequest request = OpaRequest.builder()
            .input(Map.of(
                "tool", toolName,
                "user", userId,
                "context", context
            ))
            .build();
        return opaClient.evaluate("mcp/authz/allow", request).getResult();
    }
}

7.2 超时管理

MCP 工具调用的超时管理比传统 RPC 更复杂------LLM 可能在一次推理中调用多个工具,每个工具的执行时间差异很大(文档搜索 100ms,数据库查询 2s,外部 API 调用 10s+)。

建议分层设置超时

超时维度 推荐值 说明
单工具调用超时 根据工具特性(5s~60s) 在工具元数据中配置
Agent 单轮推理超时 30s~120s 包含所有工具调用时间
MCP Client 连接超时 10s 建立连接的上限
MCP 请求超时 与工具超时对齐 单个 JSON-RPC 请求的超时
bash 复制代码
@Tool(name = "slowApi", description = "调用外部 API(耗时较长)")
@ToolTimeout(value = 30, unit = TimeUnit.SECONDS)
public String slowApi(@ToolParam(description = "请求参数") String param) {
    return externalApiClient.call(param, Duration.ofSeconds(25));
}

7.3 熔断策略

当某个工具的后端服务不可用时,应该快速失败而不是长时间等待。推荐使用 Resilience4j 实现熔断:

bash 复制代码
@CircuitBreaker(name = "externalApi", fallbackMethod = "externalApiFallback")
@Tool(name = "externalApi", description = "调用外部 API")
public String externalApi(@ToolParam(description = "请求参数") String param) {
    return externalApiClient.call(param);
}

public String externalApiFallback(String param, Exception e) {
    return "服务暂时不可用,请稍后重试。(熔断器已触发)";
}

熔断配置建议:

bash 复制代码
resilience4j:
  circuitbreaker:
    instances:
      externalApi:
        slidingWindowSize: 10        # 滑动窗口大小
        failureRateThreshold: 50     # 失败率阈值 50%
        waitDurationInOpenState: 30s # 熔断开启后等待时间
        permittedNumberOfCallsInHalfOpenState: 3

八、Client 集成与验收

8.1 Spring AI MCP Client 配置

配置 MCP Client 连接 Server:

bash 复制代码
spring:
  ai:
    mcp:
      client:
        enabled: true
        streamable-http:
          connections:
            dream-saas-server:
              url: http://localhost:8091
              endpoint: /mcp

在 Agent 中使用 MCP 工具:

bash 复制代码
@RestController
public class AgentController {

    @Autowired
    private McpSyncClient mcpClient;

    @Autowired
    private ChatClient chatClient;

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        // 获取 MCP Server 的工具列表
        List<Tool> tools = mcpClient.listTools();

        // 将工具注入 ChatClient
        return chatClient.prompt()
            .user(message)
            .tools(tools)
            .call()
            .content();
    }
}

8.2 验收方法

MCP Server 开发完成后,可以通过以下步骤验收:

步骤一:直接调用 tools/list

bash 复制代码
curl -X POST http://localhost:8091/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "test-1",
    "method": "tools/list"
  }'

预期返回所有注册的工具列表,包含名称、描述和参数 Schema。

步骤二:调用单个工具

bash 复制代码
curl -X POST http://localhost:8091/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "test-2",
    "method": "tools/call",
    "params": {
      "name": "hello",
      "arguments": {"type": "greeting", "name": "World"}
    }
  }'

预期返回正确的工具执行结果。

步骤三:异常参数测试

bash 复制代码
curl -X POST http://localhost:8091/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "test-3",
    "method": "tools/call",
    "params": {
      "name": "hello",
      "arguments": {}
    }
  }'

预期返回 -32602 错误(缺少必填参数)。

步骤四:LLM 集成测试

通过 Spring AI ChatClient 发送自然语言请求,验证 LLM 能正确选择并调用工具:

bash 复制代码
用户: "帮我搜索一下关于 MCP 协议的文档"
预期: LLM 调用 searchDocs 工具,返回搜索结果

步骤五:性能压测

使用 JMeter 或 Gatling 对 MCP Server 做压力测试,验证在并发场景下的稳定性和响应时间。建议的压测指标:

指标 目标值
tools/list P99 < 50ms
tools/call P99(简单工具) < 200ms
tools/call P99(复杂工具) < 5s
错误率 < 0.1%
最大并发 根据业务需求(通常 50-200)

总结

本篇从实战角度全面拆解了 MCP Server 的开发要点:

  • 工具注册:@McpTool + Scanner 适合简单场景,@Tool + McpToolContributor 适合多模块大型项目
  • 架构分层:协议层→路由层→工具层→执行层,横切关注点在工具层统一处理
  • 参数校验:JSON Schema 自动校验 + 结构化错误码,降低 LLM 生成错误参数的影响
  • 无状态新规范:移除握手和会话,每个请求自包含,支持轮询负载均衡和协议级缓存
  • 工具组合:Chain / DAG / Sub-Agent 三种模式,适配不同复杂度的业务场景
  • 权限与容错:RBAC/OPA 权限控制 + 超时管理 + 熔断策略
  • 验收流程:从 tools/list 到 LLM 集成测试的完整验收链路

MCP 7/28 无状态新规范是一次方向性的调整------从"有状态的会话协议"转向"无状态的请求协议"。这个转变与 Web 从 SOAP 到 REST 的演进逻辑一致:简单、可扩展、对基础设施友好。对于 Java 开发者来说,这意味着更少的样板代码、更灵活的部署方式、更好的云原生兼容性。


下一篇预告:MCP-03《MCP Client 集成与多工具编排》,将从 Client 视角出发,讲解如何在 Spring AI 应用中集成多个 MCP Server、实现工具组合编排的高级模式,以及生产环境中的监控告警和性能调优。

--- 深入理解 AI Agent · MCP 子系列 · 第 2 篇 ---

有问题评论区见,欢迎交流~

相关推荐
一心只读圣贤书2 小时前
AI 辅助前端空状态体验治理:从无数据页面到可行动引导
前端·人工智能
大虾别跑2 小时前
ai-news-2026-08-11-evening
人工智能
阿星AI工作室2 小时前
12个Codex实战技巧:配置调优→会话分工→上下文防丢,一套流程吃透
人工智能
宇的出海纪元2 小时前
独立开发做竞品分析踩坑实录:从手动摸排到自动化监控
人工智能·团队开发·产品经理·个人开发
lailai04102 小时前
课后作业PPT制作的效率
人工智能
科技风向标go2 小时前
2026户外太阳能监控怎么选不踩坑?户外(格行AOV+黑光)、工程(海康大华)、生态(小米萤石)——三大派系技术路线全解析
大数据·人工智能·智能家居·监控·户外安防
武子康2 小时前
上下文装不下以后:Pi Compaction 怎样压缩历史,又会丢掉什么
人工智能·llm·agent
菜冻鱼2 小时前
Python-sklearn-特征工程
开发语言·人工智能·python·机器学习·numpy·matplotlib·sklearn