Spring AI Agent 漫画实战
第 08 话 PROTOCOL

工具界的 USB-C:MCP 协议与分布式能力 把工具从"一个 Java 方法"升级成"一个可被任意 Agent 发现的服务"

上一话的 @Tool 有个硬伤:工具与 Agent 必须在同一个 JVM 里。 MCP 就是把这层耦合拆掉的开放协议 —— 它让"能力"变成可独立部署、可被发现、可跨语言复用的服务。

老 K
又一个协议?我这十年学的协议还少吗。这不就是把工具包成 RPC,然后……多了个 JSON-RPC 的马甲?
小 S
马甲不重要,生态位才重要。MCP 定的是「AI 应用 ↔ 能力提供方」之间的公共契约。有了它:
你写的 Jira/数据库/内部系统工具,不用改一行代码就能被任何支持 MCP 的客户端(包括别人的 Agent)调用。

一、先解决 N×M 集成爆炸

没有 MCP 时的世界:N 个 AI 应用 × M 个数据源 = N×M 套适配器。有了 MCP:N + M。

没有 MCP:N × M 条连线 Agent A Agent B Agent C MySQL Jira K8s 9 条适配器,每条都要各自维护 有了 MCP:N + M 条连线 Agent A Agent B Agent C MCP 协议 JSON-RPC 2.0 + 标准能力模型 MySQL / Jira / K8s Server 每方只实现一次,之后自由组合

二、三个角色 + 三类能力 + 三种传输

ROLES

三角色

  • Host:AI 应用本体(你的 Spring Boot 服务)
  • Client:Host 内的连接器,1 个 Client 对 1 个 Server
  • Server:能力提供方,独立进程
CAPABILITY

三类能力

  • Tools:可被模型调用的函数(最常用)
  • Resources:可被读取的数据(文件、记录)
  • Prompts:服务端预置的提示词模板

进阶:Sampling(服务端反向请求模型补全)、Roots、Elicitation(向用户追问)。

TRANSPORT

三种传输

  • stdio:本地子进程,最常用
  • SSE / Streamable HTTP:远程服务
  • (早期 WebSocket,已边缘化)
传输方式怎么连适用代价
stdioClient 启动一个本地子进程,用 stdin/stdout 收发 JSON-RPC 消息本地工具、命令行工具、桌面客户端(最广泛)进程生命周期管理;日志不能打 stdout;难水平扩展
SSE / HTTPHTTP POST 发请求 + SSE 推流远程共享服务、团队级能力中台需要鉴权、TLS、网关;断线重连
同进程直连不跨网络(就是第 06 话的 @Tool)高性能、强事务、需要共享 Spring 上下文无法跨语言/跨团队复用

三、写一个 MCP Server(Spring Boot 版)

// pom: spring-ai-starter-mcp-server-webmvc(或 -webflux)

@Service
public class K8sTools {

    @Tool(description = "查询指定命名空间下处于非 Running 状态的 Pod 列表,返回 Pod 名称、状态与重启次数") //!hl
    public List<PodInfo> listAbnormalPods(
            @ToolParam(description = "Kubernetes 命名空间,如 prod、staging") String namespace) {
        return k8sService.listAbnormal(namespace);
    }

    @Tool(description = "获取某个 Pod 最近 N 行日志,用于排障") //!hl
    public String podLogs(
            @ToolParam(description = "Pod 名称") String pod,
            @ToolParam(description = "命名空间") String namespace,
            @ToolParam(description = "日志行数,默认 200", required = false) Integer lines) {
        return k8sService.tailLogs(namespace, pod, lines == null ? 200 : lines);
    }
}

@Configuration
class McpServerConfig {
    @Bean
    ToolCallbackProvider k8sToolProvider(K8sTools k8sTools) {   //!hl
        return MethodToolCallbackProvider.builder()
                .toolObjects(k8sTools)
                .build();
    }
}
# application.yml(Server 端)
spring:
  ai:
    mcp:
      server:
        name: ops-mcp-server        //!hl
        version: 1.0.0
        type: SYNC                  # SYNC 或 ASYNC
        # stdio 模式(本地被拉起时用)
        stdio: true
        # 若用 HTTP/SSE 模式:
        # sse-message-endpoint: /mcp/message
        # capabilities:
        #   tool: true
        #   resource: true
        #   prompt: true
头号陷阱

stdio 模式下,你的应用绝对不能往 stdout 打印任何东西。 Spring Boot 默认日志输出到控制台(stdout),会直接把 JSON-RPC 报文污染成「半行日志 + 半行 JSON」, 表现为连接随机断开、工具调用莫名失败。
解决:stdio 模式启动时把日志重定向到 stderr 或文件(logging.pattern.console= + 文件 appender,或启动脚本里 2>&1 之外单独处理)。 这个坑能让人排查一整天。

四、Client 端:接进来就能用

# application.yml(Client 端)
spring:
  ai:
    mcp:
      client:
        stdio:
          connections:
            ops-server:                       //!hl
              command: java                    //!hl
              args: ["-jar", "/opt/ops-mcp-server.jar"]   //!hl
              env:
                KUBECONFIG: /etc/kube/config
        sse:
          connections:
            jira-server:
              url: https://mcp.internal.example.com/jira
              # 需要鉴权时走 header / 网关
@Bean
ChatClient opsAgent(ChatClient.Builder builder,
                    ToolCallbackProvider mcpTools) {   //!hl 由 MCP Starter 自动装配进来
    return builder
            .defaultSystem("你是运维排障助手,先取证再下结论,禁止臆测。")
            .defaultToolCallbacks(mcpTools)             //!hl
            .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory()).build())
            .build();
}

// 也支持外部配置文件,与 Claude Desktop 同款格式:
// spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json
源码解剖 · 一次 MCP 调用经历了什么
  1. initialize:Client 与 Server 握手,交换协议版本与能力(支持哪些 tool/resource/prompt)。
  2. tools/list:Client 拉取工具定义(JSON Schema),缓存起来。
  3. 包装:Spring AI 把每个远程工具包装成一个 ToolCallback,通过 ToolCallbackProvider 暴露给 ChatClient。从这一步起,与本地 @Tool 完全同构,模型根本不知道工具在哪个进程。
  4. tools/call:模型产生 toolCall → 框架通过 McpSyncClient / McpAsyncClient 发 JSON-RPC → 拿回结果 → 转成 ToolResponseMessage 回填。
  5. notifications/tools/list_changed:Server 工具变更时主动通知,Client 刷新缓存 —— 这就是动态发现。

注意第 3 步:MCP 工具与本地工具在 ChatClient 眼里是同一种东西。这意味着你可以自由混用 —— 本地 @Tool 做高频、强事务的操作,MCP 做跨系统、跨语言的能力。

五、选型:本地 @Tool 还是 MCP?

维度本地 @ToolMCP Server
部署耦合同 JVM,改工具要重启 Agent独立进程,可独立发布与扩缩容
语言只能 Java任意语言(Python/Go/TS 都能写 Server)
复用范围本应用整个公司 / 整个生态(含第三方客户端)
性能方法调用,微秒级进程/网络开销,毫秒到百毫秒级
事务与上下文可直接参与 Spring 事务、共享 SecurityContext跨进程,需自行传递身份与幂等键
可观测天然进入现有 APM需额外埋点(跨进程 traceId 传递)
动态发现不支持(编译期确定)支持(tools/list + 变更通知)
决策规则

同一个应用内、强事务、高频调用 → 用 @Tool。
要被多个 Agent 复用、要跨语言、要独立发布、或本来就是别人的系统 → 用 MCP。
不要为了"上协议"而上协议 —— 一次本地方法调用被拆成跨进程 RPC,是把简单问题复杂化。

六、安全边界(MCP 放大了风险)

RISK 1

命令注入

stdio 的 command / args 绝不能拼接用户输入。否则用户一句恶意提问就能让你的服务器执行任意命令。 command 必须是配置里的常量。

RISK 2

权限放大

MCP Server 往往以"服务账号"运行,权限远大于请求它的用户。必须在 Server 侧再做一次用户级鉴权, 把调用者身份随请求透传,不能只看"来自内网"。

RISK 3

返回内容污染

工具返回的内容(网页、日志、Jira 描述)可能包含提示词注入。把外部内容当不可信输入,必要时做净化与隔离提示。

RISK 4

工具膨胀

连上 5 个 Server 就是 80 个工具 → token 暴涨 + 选择准确率暴跌。按场景裁剪工具集,只挂载当前任务需要的 Server。

踩坑清单
小 S
一句话:MCP 解决的不是"能不能调",而是"谁来维护这个适配器"。协议的价值永远在生态,不在语法。
← 上一话:RAG 目录 下一话:多 Agent 编排 →