一、modelcontextprotocol:MCP 的官方大本营

1. 这个 GitHub 组织是什么

在 GitHub 搜索 MCP,前排结果基本都来自 modelcontextprotocol 组织——这是 Anthropic 主导成立的开放组织,专门维护 MCP 协议规范和各语言 SDK。

MCP 从设计之初就定位为开放标准:协议规范公开、SDK 全部 MIT 许可证、任何人都可以提 PR 贡献代码。这与 OpenAI 的 Function Call 形成对比——后者是 API 的一部分,由 OpenAI 单方面定义。MCP 的目标是成为大模型工具调用领域的通用协议。

2. 官方仓库全景

这个组织下有几个核心仓库,各自分工明确:

仓库

定位

说明

modelcontextprotocol

协议规范

MCP 协议的完整定义,所有接口、消息格式、行为约束都在这里。所有 SDK 都以这份规范为基准来实现

java-sdk

Java SDK

本文的主角,官方 Java 实现

typescript-sdk

TypeScript SDK

最早的 SDK 实现,也是参考实现(Reference Implementation)。TypeScript 版是第一个跑通协议全流程的

python-sdk

Python SDK

在 AI / ML 生态中使用最广的版本

servers

社区 MCP Server 集合

各种现成的 MCP Server:文件系统、GitHub、数据库、Web 搜索等

三个 SDK 面向不同的生态:TypeScript SDK 最早最全,是其他 SDK 的参照标准;Python SDK 在 AI 和数据科学领域用得最多;Java SDK 面向企业级应用,和 Spring AI 深度集成。

注:不同语言的 SDK 很多,本文仅列举三个核心语言举例。Java SDK 当前稳定版本是 1.1.0,由 Anthropic 团队维护。

二、Java SDK 全景:6 个模块各司其职

1. 模块总览

打开 Java SDK 的仓库,你会看到它不是一个单体模块项目,而是拆成了 6 个 Maven 模块:

模块

Maven ArtifactId

作用

mcp-bom

mcp-bom

BOM(Bill of Materials),统一管理所有模块的版本号,引入后不用逐个指定版本

mcp-core

mcp-core

核心实现,Client、Server、Transport、Schema 全部核心代码都在这里

mcp-json-jackson2

mcp-json-jackson2

Jackson 2.x 的 JSON 序列化实现

mcp-json-jackson3

mcp-json-jackson3

Jackson 3.x 的 JSON 序列化实现

mcp

mcp

便捷包,等于 mcp-core + mcp-json-jackson3,引入这一个就够了

mcp-test

mcp-test

测试工具和集成测试

大多数场景下,你只需要在 pom.xml 里引入一个依赖:


<dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp</artifactId> <version>1.1.0</version> </dependency>

这个 mcp 便捷包已经帮你打包了 mcp-core(核心实现)和 mcp-json-jackson3(JSON 序列化),拿来就能用。

2. 为什么 JSON 序列化要单独拆模块

你可能会好奇:JSON 序列化为什么不直接写在 mcp-core 里,还要单独拆两个模块出来?

原因是 Jackson 的版本冲突问题。Java 生态里 Jackson 几乎是 JSON 处理的事实标准,但 Jackson 存在大版本分裂:

  • Jackson 2.x:Spring Boot 2.x 时代的标配,目前仍有大量项目在用

  • Jackson 3.x:2025 年发布的新大版本,包名从 com.fasterxml.jackson 改成了 tools.jackson,不向后兼容

MCP SDK 把 JSON 序列化抽象成了 McpJsonMapper 接口,具体实现可以是 Jackson 2 或 Jackson 3。这样一来:

  • 你的项目如果用 Jackson 2,就引入 mcp-core + mcp-json-jackson2,不会和现有依赖冲突

  • 你的项目如果用 Jackson 3(或者新项目没有历史包袱),直接引入 mcp 便捷包,默认走 Jackson 3

这个设计在 Java 生态里很常见,Spring 框架对很多第三方库也是这么处理的——核心模块定义接口,具体实现按版本拆开。

3. Spring AI 集成模块去哪了

如果你翻过 Java SDK 早期版本(0.18.1 及之前),会发现仓库里还有两个模块:mcp-spring-webflux 和 mcp-spring-webmvc。但现在不见了。

从 1.0.0 版本开始,这两个 Spring 集成模块迁移到了 Spring AI 2.0 中。Maven 坐标也变了:

迁移前(SDK 仓库)

迁移后(Spring AI)

io.modelcontextprotocol.sdk:mcp-spring-webflux

org.springframework.ai:mcp-spring-webflux

io.modelcontextprotocol.sdk:mcp-spring-webmvc

org.springframework.ai:mcp-spring-webmvc

这也是为什么你用 Spring AI MCP Server 时,引入的是 org.springframework.ai 的依赖,而不是 io.modelcontextprotocol.sdk。

这个拆分的逻辑很清楚:SDK 本身保持轻量,只负责 MCP 协议的纯 Java 实现,不绑定任何框架;Spring 集成由 Spring AI 团队维护,和 Spring Boot 的自动配置、生命周期管理深度绑定。各管各的,互不耦合。

三、SDK 核心架构:四层分明

了解完模块划分,接下来看 SDK 内部的代码结构。整个 mcp-core 模块的代码分成四层,从下往上依次是:Schema 层、Transport 层、Session 层、Client/Server 层。每一层的职责很清晰,从下往上看:Schema 层定义协议里有哪些消息类型,Transport 层负责把消息送达,Session 层管理一次完整的连接会话,Client/Server 层是开发者直接打交道的 API。

1. Schema 层:协议的字典

McpSchema 是整个 SDK 里最大的一个类——2786 行代码,全是数据结构定义。它就像一本字典,规定了 Client 和 Server 之间能说哪些话、每句话的格式是什么。

之前讲 MCP 协议规范 JSON-RPC 2.0 那篇文章里,大家已经知道 MCP 底层用 JSON-RPC 2.0 通信。McpSchema 做的事情就是把协议规范里定义的所有消息类型,都变成了 Java 的 Record 类(Java 16+ 的不可变数据类)。

挑几个你最常接触的类型:

类型

对应的协议操作

说明

McpSchema.Tool

工具定义

包含工具名称、描述、参数的 JSON Schema

McpSchema.CallToolRequest

tools/call 请求

Client 调用工具时发的请求,包含工具名和参数

McpSchema.CallToolResult

tools/call 响应

Server 执行工具后返回的结果

McpSchema.Resource

资源定义

包含资源 URI、名称、描述、MIME 类型

McpSchema.Prompt

Prompt 模板定义

包含模板名称、描述、参数列表

McpSchema.InitializeRequest

initialize 请求

连接建立时的握手请求

McpSchema.InitializeResult

initialize 响应

握手响应,包含 Server 的能力声明

这些类型最终会被包装成 JSON-RPC 的 Request / Response 在 Client 和 Server 之间传输。比如 Client 调用工具时,实际发出的 JSON-RPC 消息长这样:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "getUserAnnualLeave",
    "arguments": { "employeeId": "E001" }
  }
}

其中 params 部分就是 McpSchema.CallToolRequest 对象序列化后的结果。

2. Transport 层:消息的快递公司

Transport 层的职责很单一:把 JSON-RPC 消息从一端送到另一端。它不关心消息内容是什么(工具调用还是资源读取),只管送达。

SDK 提供了三种 Transport 实现,可以理解为三家不同的快递公司,各有各的配送方式:

2.1 Stdio Transport

通过进程的 stdin / stdout 传输消息。MCP Server 作为子进程启动,Client(Host 应用)通过标准输入输出和它通信。每条消息是一行 JSON 文本,用换行符分隔。

你在 Claude Desktop 里配置本地 MCP Server 时,用的就是这种方式。Claude Desktop 启动你的 Java 进程,然后通过 stdin 发请求、从 stdout 读响应。

关键类:

  • 客户端:StdioClientTransport

  • 服务端:StdioServerTransportProvider

2.2 SSE Transport

基于 Server-Sent Events 的传输。如果你读过之前的 SSE 系列文章,对这个应该不陌生。

SSE 本身是单向的(只能 Server 向 Client 推送),所以 MCP 的 SSE Transport 实际上走了两条路:

  • Server → Client:通过 SSE 长连接推送事件(响应、通知)

  • Client → Server:通过单独的 HTTP POST 请求发送(请求)

关键类:

  • 客户端:HttpClientSseClientTransport(基于 JDK 内置的 HttpClient,不依赖第三方 HTTP 库)

  • 服务端:HttpServletSseServerTransportProvider(基于 Jakarta Servlet)

2.3 Streamable HTTP Transport

SSE Transport 的进化版。双向都走 HTTP,Client 的请求和 Server 的响应都可以流式传输,不需要维持一个额外的 SSE 长连接,按需建立连接即可。

这是 MCP 协议较新引入的传输方式,也是官方推荐的远程传输方案。

关键类:

  • 客户端:HttpClientStreamableHttpTransport

  • 服务端:HttpServletStreamableServerTransportProvider

2.4 三种 Transport 怎么选

维度

Stdio

SSE

Streamable HTTP

通信方式

stdin / stdout

SSE 推送 + HTTP POST

双向 HTTP 流

适用范围

仅本地

本地 + 远程

本地 + 远程

网络要求

无(进程间通信)

HTTP

HTTP

实现复杂度

生产就绪度

适合本地开发

可用于生产

推荐生产使用

典型场景

Claude Desktop 本地工具

传统 Web 环境

企业级远程部署

简单来说:本地开发调试用 Stdio,生产环境远程部署用 Streamable HTTP,SSE 作为兼容性较好的过渡方案。

3. Session 层:对话的窗口

Transport 只管送消息,但一次完整的 MCP 通信不只是单条消息的收发——Client 和 Server 建立连接后,需要先握手(initialize),交换各自的能力声明(我支持哪些功能、你支持哪些功能),然后才能正式收发请求。

Session 层管理的就是这整个连接生命周期。它负责三件事:

  1. 维护连接状态:是否已初始化、是否已关闭

  2. 请求-响应配对:每个 JSON-RPC 请求有一个 id,Session 要把收到的响应按 id 路由回对应的请求(因为可能同时有多个请求在等待响应)

  3. 处理通知消息:JSON-RPC 除了请求-响应,还有通知(Notification)——不需要响应的单向消息,比如工具列表变更通知

关键类:McpClientSession(客户端会话)、McpServerSession(服务端会话)。

4. Client/Server 层:开发者直接打交道的 API

最上层就是你实际使用的 API 了。SDK 提供了 Builder 模式来创建 Client 和 Server,API 设计简洁清晰。

4.1 McpServer

McpServer 是服务端的入口类,提供两个静态方法:

  • McpServer.sync(transportProvider) ——创建同步 Server

  • McpServer.async(transportProvider) ——创建异步 Server

通过 Builder 链式调用注册工具、资源、Prompt:


McpSyncServer server = McpServer.sync(transportProvider)
    .serverInfo(new McpSchema.Implementation("my-server", "1.0.0"))
    .tool(
        new McpSchema.Tool("getUserAnnualLeave", "查询员工剩余年假天数", jsonSchema),
        (exchange, request) -> {
            String employeeId = request.arguments().get("employeeId").toString();
            String result = "员工 " + employeeId + " 剩余年假:5 天";
            return new McpSchema.CallToolResult(
                List.of(new McpSchema.TextContent(result)), false
            );
        }
    )
    .build();

4.2 McpClient

McpClient 是客户端的入口类,同样提供同步和异步两个版本:


McpSyncClient client = McpClient.sync(transport)
    .clientInfo(new McpSchema.Implementation("my-client", "1.0.0"))
    .build();

// 建立连接,完成握手
client.initialize();

// 发现 Server 端有哪些工具
McpSchema.ListToolsResult toolsResult = client.listTools();
for (McpSchema.Tool tool : toolsResult.tools()) {
    System.out.println("工具:" + tool.name() + " - " + tool.description());
}

// 调用工具
Map<String, Object> args = Map.of("employeeId", "E001");
McpSchema.CallToolResult result = client.callTool(
    new McpSchema.CallToolRequest("getUserAnnualLeave", args)
);

// 关闭连接
client.closeGracefully();

4.3 同步 vs 异步怎么选

SDK 提供了同步和异步两套 API,分别对应 McpSyncClient / McpAsyncClient 和 McpSyncServer / McpAsyncServer。

异步 API 基于 Project Reactor(一个响应式编程库),返回 Mono<T> 和 Flux<T> 类型。如果你用过 Spring WebFlux,对这套东西应该不陌生。

实际上同步 API 内部就是对异步 API 的 .block() 封装——调用异步方法然后阻塞等待结果。所以两套 API 的底层实现是同一套代码。

怎么选:

  • 传统 Spring MVC 项目(阻塞式):用同步 API,简单直观

  • Spring WebFlux 项目(响应式)或需要处理大量并发连接:用异步 API

  • 不确定:用同步,等遇到性能瓶颈再切异步

四、实战:用纯 SDK 构建 MCP Server

光看架构图不够直观,下面用纯 SDK(不依赖 Spring AI)来搭一个 MCP Server,实现两个工具:getUserAnnualLeave(查年假)和 getOrderStatus(查订单状态)。和之前 Spring AI 版本做个对比,你就能感受到 Spring AI 帮你省了多少事。

1. Maven 依赖

不需要 Spring Boot,只要一个依赖:


<dependencies> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp</artifactId> <version>1.1.0</version> </dependency> </dependencies>

2. 完整代码

import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;

import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;

public class EnterpriseMcpServer {

    public static void main(String[] args) throws InterruptedException {
        // 1. 创建 Transport:使用 Stdio 方式(通过 stdin/stdout 通信)
        StdioServerTransportProvider transportProvider = new StdioServerTransportProvider();

        // 2. 构建 Server,注册工具
        McpSyncServer server = McpServer.sync(transportProvider)
            .serverInfo(new McpSchema.Implementation("enterprise-server", "1.0.0"))
            // 注册工具:查年假
            .tool(
                buildAnnualLeaveTool(),
                (exchange, request) -> handleAnnualLeave(request)
            )
            // 注册工具:查订单状态
            .tool(
                buildOrderStatusTool(),
                (exchange, request) -> handleOrderStatus(request)
            )
            .build();

        System.err.println("Enterprise MCP Server 已启动,等待 Client 连接...");

        // 3. 注册关闭钩子,进程退出时优雅关闭 Server
        Runtime.getRuntime().addShutdownHook(new Thread(server::close));

        // 4. 阻塞主线程,保持进程存活
        // build() 内部的 stdin 监听线程是守护线程,如果 main 方法结束,JVM 会直接退出
        // 所以必须手动阻塞主线程,让 Server 持续运行
        new CountDownLatch(1).await();
    }

    // ========== 工具定义 ==========

    /**
     * 构建查年假工具的定义
     * 需要手动拼 JSON Schema 来描述参数
     */
    private static McpSchema.Tool buildAnnualLeaveTool() {
        // 参数的 JSON Schema
        String inputSchema = """
            {
                "type": "object",
                "properties": {
                    "employeeId": {
                        "type": "string",
                        "description": "员工工号,如 E001"
                    }
                },
                "required": ["employeeId"]
            }
            """;

        return new McpSchema.Tool(
            "getUserAnnualLeave",
            "查询员工剩余年假天数,包括总天数、已使用天数、剩余天数",
            McpSchema.JsonSchema.fromJson(inputSchema)
        );
    }

    /**
     * 构建查订单状态工具的定义
     */
    private static McpSchema.Tool buildOrderStatusTool() {
        String inputSchema = """
            {
                "type": "object",
                "properties": {
                    "orderId": {
                        "type": "string",
                        "description": "订单编号,如 ORD-20260301-001"
                    }
                },
                "required": ["orderId"]
            }
            """;

        return new McpSchema.Tool(
            "getOrderStatus",
            "查询订单的物流状态和详细信息",
            McpSchema.JsonSchema.fromJson(inputSchema)
        );
    }

    // ========== 工具处理函数 ==========

    private static McpSchema.CallToolResult handleAnnualLeave(McpSchema.CallToolRequest request) {
        // 从请求中提取参数
        String employeeId = request.arguments().get("employeeId").toString();

        // 实际项目中这里调用 HR 系统查询
        String result = String.format(
            "员工 %s 的年假信息:总年假 15 天,已使用 10 天,剩余 5 天", employeeId
        );

        return new McpSchema.CallToolResult(
            List.of(new McpSchema.TextContent(result)),
            false  // isError = false,表示执行成功
        );
    }

    private static McpSchema.CallToolResult handleOrderStatus(McpSchema.CallToolRequest request) {
        String orderId = request.arguments().get("orderId").toString();

        // 实际项目中这里调用订单系统查询
        String result = String.format(
            "订单 %s 状态:已发货,快递单号 SF1234567890,预计明天送达", orderId
        );

        return new McpSchema.CallToolResult(
            List.of(new McpSchema.TextContent(result)),
            false
        );
    }
}

3. 代码解读

对照代码,你可以看到用纯 SDK 构建 MCP Server 需要做四件事:

第一步:创建 Transport。这里用 StdioServerTransportProvider,表示通过 stdin/stdout 和 Client 通信。如果要远程部署,换成 HttpServletSseServerTransportProvider 或 HttpServletStreamableServerTransportProvider。

第二步:构建工具定义。这是最费劲的部分——你需要手动拼 JSON Schema 来描述每个工具的参数。参数名、参数类型、参数描述、是否必填,都要自己写。对比一下 Function Call 那篇文章里用 Gson 手动构建 JSON Schema 的代码,结构是一样的,只是 SDK 提供了 McpSchema.Tool 这个类来承载。

第三步:编写处理函数。每个工具需要一个处理函数,接收 McpSchema.CallToolRequest(包含工具名和参数),返回 McpSchema.CallToolResult(包含执行结果)。

第四步:阻塞主线程保活。这一步容易被忽略。build() 方法返回后不会阻塞主线程,而 SDK 内部监听 stdin 的线程是守护线程(Daemon Thread)——守护线程不会阻止 JVM 退出。如果 main 方法执行完就结束了,JVM 会立刻退出,Server 根本来不及处理任何请求。所以必须用 new CountDownLatch(1).await() 手动阻塞主线程,让进程一直活着。同时注册 ShutdownHook,在进程被终止时(比如 Ctrl+C)优雅关闭 Server。

注意 System.err.println 而不是 System.out.println。因为 Stdio Transport 用 stdout 传输 JSON-RPC 消息,如果你往 stdout 打日志,会和协议消息混在一起,Client 就解析不了了。日志输出要走 stderr。

4. 对比:SDK 直接写 vs Spring AI 注解

同一个查年假工具,两种方式的代码量对比:

Spring AI 方式——3 行搞定:

@McpTool(description = "查询员工剩余年假天数,包括总天数、已使用天数、剩余天数")
public String getUserAnnualLeave(
        @McpToolParam(description = "员工工号,如 E001") String employeeId) {
    return String.format("员工 %s 的年假信息:总年假 15 天,已使用 10 天,剩余 5 天", employeeId);
}

SDK 方式——需要 30+ 行:


// 1. 手动拼 JSON Schema(10+ 行)
String inputSchema = """
    {
        "type": "object",
        "properties": {
            "employeeId": {
                "type": "string",
                "description": "员工工号,如 E001"
            }
        },
        "required": ["employeeId"]
    }
    """;
McpSchema.Tool tool = new McpSchema.Tool(
    "getUserAnnualLeave",
    "查询员工剩余年假天数,包括总天数、已使用天数、剩余天数",
    McpSchema.JsonSchema.fromJson(inputSchema)
);

// 2. 手动编写处理函数,手动提取参数(5+ 行)
BiFunction<McpSyncServerExchange, McpSchema.CallToolRequest, McpSchema.CallToolResult> handler =
    (exchange, request) -> {
        String employeeId = request.arguments().get("employeeId").toString();
        String result = String.format("员工 %s 的年假信息:...", employeeId);
        return new McpSchema.CallToolResult(
            List.of(new McpSchema.TextContent(result)), false
        );
    };

// 3. 注册到 Server Builder
McpServer.sync(transport).tool(tool, handler).build();

差距一目了然。Spring AI 帮你做了三件事:

  1. 从方法签名自动生成 JSON Schema:方法参数名变成 Schema 的 properties,参数类型变成 type,@McpToolParam 的 description 变成参数描述,方法名变成工具名

  2. 自动把请求参数映射到方法参数:Client 传过来的 arguments 里的值,自动按名称匹配到方法参数上

  3. 自动把返回值包装成 CallToolResult:你返回一个 String,框架帮你包成 TextContent 再包成 CallToolResult

理解了这些,你就知道 @McpTool 注解不是魔法,而是对 SDK API 的工程封装。

五、实战:用纯 SDK 构建 MCP Client

Server 端写完了,再来看 Client 端。下面这段代码展示了一个 MCP Client 的完整生命周期:连接 Server → 发现工具 → 调用工具 → 关闭连接。

1. 完整代码


import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.spec.McpSchema;

import java.util.Map;

public class EnterpriseMcpClient {

    public static void main(String[] args) {
        // 1. 创建 Transport,指向 Server 进程
        // ServerParameters 定义了要启动的子进程命令
        StdioClientTransport transport = StdioClientTransport.builder("java")
            .args("-jar", "enterprise-mcp-server.jar")
            .build();

        // 2. 创建 Client
        McpSyncClient client = McpClient.sync(transport)
            .clientInfo(new McpSchema.Implementation("enterprise-client", "1.0.0"))
            .build();

        try {
            // 3. 建立连接,完成握手
            client.initialize();
            System.out.println("已连接到 MCP Server");

            // 4. 发现工具
            McpSchema.ListToolsResult toolsResult = client.listTools();
            System.out.println("Server 提供了 " + toolsResult.tools().size() + " 个工具:");
            for (McpSchema.Tool tool : toolsResult.tools()) {
                System.out.println("  - " + tool.name() + ":" + tool.description());
            }

            // 5. 调用工具:查年假
            McpSchema.CallToolResult leaveResult = client.callTool(
                new McpSchema.CallToolRequest(
                    "getUserAnnualLeave",
                    Map.of("employeeId", "E001")
                )
            );
            System.out.println("\n查年假结果:");
            for (McpSchema.Content content : leaveResult.content()) {
                if (content instanceof McpSchema.TextContent text) {
                    System.out.println("  " + text.text());
                }
            }

            // 6. 调用工具:查订单
            McpSchema.CallToolResult orderResult = client.callTool(
                new McpSchema.CallToolRequest(
                    "getOrderStatus",
                    Map.of("orderId", "ORD-20260301-001")
                )
            );
            System.out.println("\n查订单结果:");
            for (McpSchema.Content content : orderResult.content()) {
                if (content instanceof McpSchema.TextContent text) {
                    System.out.println("  " + text.text());
                }
            }

        } finally {
            // 7. 关闭连接
            client.closeGracefully();
            System.out.println("\n连接已关闭");
        }
    }
}

运行输出:


已连接到 MCP Server Server 提供了 2 个工具: - getUserAnnualLeave:查询员工剩余年假天数,包括总天数、已使用天数、剩余天数 - getOrderStatus:查询订单的物流状态和详细信息 查年假结果: 员工 E001 的年假信息:总年假 15 天,已使用 10 天,剩余 5 天 查订单结果: 订单 ORD-20260301-001 状态:已发货,快递单号 SF1234567890,预计明天送达 连接已关闭

2. 交互流程

Client 和 Server 之间的完整交互流程用时序图表示(原文图片无法获取,核心流程如下):

1. Client 启动 Server 子进程(Stdio 方式);2. Client 发送 initialize 请求,完成握手;3. Client 发送 listTools 请求,获取 Server 工具列表;4. Client 发送 callTool 请求,调用具体工具;5. Server 执行工具并返回结果;6. Client 关闭连接,Server 优雅退出。

整个流程和 HTTP 的请求-响应很像,只不过多了一个握手阶段。握手时双方交换能力声明——Server 告诉 Client 自己支持哪些功能(Tools?Resources?Prompts?),Client 也告诉 Server 自己支持哪些功能(比如是否支持 Sampling)。握手完成后才能正式通信。

3. 工具变更监听

在实际场景中,Server 端的工具列表可能会动态变化——比如运维人员通过管理后台新增了一个工具,或者某个工具因为故障被下线了。SDK 提供了 toolsChangeConsumer 来监听这种变化:


McpSyncClient client = McpClient.sync(transport)
    .clientInfo(new McpSchema.Implementation("my-client", "1.0.0"))
    .toolsChangeConsumer(tools -> {
        System.out.println("工具列表已更新,当前工具数量:" + tools.size());
        for (McpSchema.Tool tool : tools) {
            System.out.println("  - " + tool.name());
        }
    })
    .build();

当 Server 端发出工具列表变更通知时,这个回调会被自动触发。在 Spring AI 注解方式中,这个监听是自动处理的,你不需要手动注册。

注意:Server 需要主动发送通知,Client 才能感知到变化。如果 Server 端没有实现通知机制,这个回调不会触发。SDK 内部收到变更通知后会自动获取最新工具列表,然后传给回调。

六、Transport 机制详解

前面在架构分层里简单介绍了三种 Transport,这里展开说说它们各自的通信原理。

1. Stdio:进程间的传纸条

Stdio Transport 的原理最简单——两个进程之间通过标准输入输出传递消息。优点是简单、安全(不暴露网络端口)。缺点是只能本地通信,而且进程的生命周期管理比较复杂——Host 要负责启动 Server 子进程、监控进程状态、在不需要时杀掉进程。

适合的场景:本地开发调试、Claude Desktop / Cursor 集成本地工具。

2. SSE:基于事件流的远程通信

SSE Transport 用于远程场景,通信走 HTTP 协议。和之前 SSE 系列文章里讲的一样,SSE 本身只支持服务端向客户端单向推送,所以 MCP 的 SSE Transport 实际上是两条路:

  • Client 先通过 GET 请求建立 SSE 长连接,Server 通过这条连接推送响应和通知。

  • Client 发请求时走单独的 HTTP POST。

优点是基于标准 HTTP,穿越防火墙和反向代理方便,SSE 自带断线重连机制。缺点是 Client → Server 方向不是流式的,每次要发一个完整的 HTTP POST 请求。

3. Streamable HTTP:双向流式通信

Streamable HTTP 是 MCP 协议较新推出的传输方式,简化了 SSE Transport 的双通道架构:

  • Client 的请求通过 HTTP POST 发送,响应体可以是普通 JSON(简单请求),也可以是 SSE 流(需要流式返回或服务器主动推送时)

  • 不需要预先建立 SSE 长连接,按需建立连接

  • Server 可以在响应头里返回 Session ID,Client 后续请求带上这个 ID 来关联会话

相比 SSE Transport,Streamable HTTP 更简洁——不需要维护一个额外的 SSE 长连接通道,每个 POST 请求的响应本身就可以是流式的。

这是 MCP 协议目前主推的远程传输方式,适合企业级生产环境部署。

七、Spring AI 与官方 SDK 的关系

1. 层次关系

你用 Spring AI 写的 MCP Server,底层的依赖链条是:Spring AI MCP Starter → 官方 Java SDK(mcp-core 等模块)→ JDK / Jackson 等基础依赖。

2. Spring AI 在 SDK 之上做了什么

具体来说,Spring AI MCP Starter 在官方 SDK 之上封装了三件事:

  1. 注解驱动的工具注册:扫描所有标注了 @McpTool / @McpResource / @McpPrompt 的 Bean 和方法,通过反射读取方法签名和注解属性,自动转换为 SDK 的 McpSchema.Tool / McpSchema.Resource / McpSchema.Prompt 对象,再调用 SDK 的 Builder API 注册到 Server 上。

  2. 自动配置 Transport:根据你引入的 Starter 依赖自动选择 Transport 实现:

引入的 Starter

自动配置的 Transport

spring-ai-starter-mcp-server

StdioServerTransportProvider

spring-ai-starter-mcp-server-webmvc

HttpServletSseServerTransportProvider

spring-ai-starter-mcp-server-webflux

WebFlux 版 Transport

  1. Spring Boot 生命周期管理:Server 的启动和关闭与 Spring 容器的生命周期绑定——容器启动时自动构建并启动 MCP Server,容器关闭时自动优雅关闭 Server。你不需要手动写 server.build() 和 server.closeGracefully()。

3. 什么时候该用 SDK,什么时候用 Spring AI

大多数 Java 项目直接用 Spring AI 就够了——省事,约定优于配置,和 Spring Boot 生态无缝集成。

需要用纯 SDK 的场景:

3. 什么时候该用 SDK,什么时候用 Spring AI

大多数 Java 项目直接用 Spring AI 就够了——省事,约定优于配置,和 Spring Boot 生态无缝集成。

需要用纯 SDK 的场景:

场景描述

选择建议

非 Spring Boot 项目

如纯 Java 桌面应用、Android 端嵌入 MCP Client、传统 Java Web 项目(非 Spring 体系),只能使用纯 SDK 手动构建 Client/Server

自定义 Transport 实现

需要自定义通信方式(如 WebSocket、MQ 等),需基于 SDK 的 Transport 接口扩展,Spring AI 封装较深,不便于自定义

深度定制化需求

如自定义工具注册逻辑、会话管理规则、JSON 序列化方式等,需直接操作 SDK 核心 API,突破 Spring AI 的封装限制

参与 MCP 生态贡献

开发 MCP Server 模板、扩展 SDK 功能等,需基于官方 SDK 进行开发,了解协议底层实现

八、常见问题与排查思路

1. 连接失败:Client 无法连接到 Server

常见原因及排查步骤:

  • Transport 类型不匹配:Client 用 Stdio Transport,Server 用 SSE Transport,需确保双方 Transport 一致,比如都用 Stdio 或都用 Streamable HTTP。

  • Server 未正常启动:检查 Server 进程是否存活,Stdio 方式可查看进程列表,远程方式可通过 curl 测试 Server 端口是否可访问。

  • 日志冲突(Stdio 方式):Server 往 stdout 输出日志,导致 Client 解析 JSON 失败,需将 Server 日志切换到 stderr(如 System.err.println)。

  • 握手失败:检查 Client 和 Server 的 SDK 版本是否一致(建议均使用 1.1.0 稳定版),版本不兼容会导致 initialize 请求响应异常。

2. 工具调用失败:Client 调用工具返回错误

核心排查方向:

  • 参数不匹配:检查 Client 传递的参数名、类型是否与 Server 工具定义的 JSON Schema 一致,比如参数名拼写错误、类型为 String 却传了 Number。

  • JSON 序列化异常:检查项目中 Jackson 版本是否与 SDK 兼容,使用 Jackson 2.x 需引入 mcp-json-jackson2,Jackson 3.x 需引入 mcp-json-jackson3,避免版本冲突。

  • 工具处理函数异常:Server 端工具处理逻辑抛出异常,需在处理函数中添加异常捕获,返回 isError = true 的 CallToolResult,便于 Client 排查。

3. 性能瓶颈:高并发场景下响应缓慢

优化方案:

  • 使用异步 API:高并发场景下替换同步 Client/Server 为异步版本(McpAsyncClient / McpAsyncServer),基于 Project Reactor 提升并发处理能力。

  • 选择合适的 Transport:远程部署优先使用 Streamable HTTP Transport,相比 SSE 减少长连接维护开销,提升传输效率。

  • 复用会话:避免频繁创建和关闭 Client 会话,一个会话可处理多次工具调用,减少握手开销。

九、总结

MCP 官方 Java SDK 是 Spring AI MCP 功能的底层基石,它的核心价值在于实现了 MCP 协议的纯 Java 标准化实现,脱离具体框架束缚,适配不同 Java 生态场景。

整篇文章从「是什么」(modelcontextprotocol 组织与 SDK 仓库)、「有什么」(6 个 Maven 模块)、「怎么工作」(四层核心架构)、「怎么用」(纯 SDK 实战 Client/Server)、「怎么选」(SDK 与 Spring AI 的取舍)五个维度,拆解了 Java SDK 的核心细节。

理解 SDK 的底层实现,不仅能帮你快速排查 Spring AI 封装带来的问题,更能让你在需要定制化、脱离 Spring 框架时,灵活运用 MCP 协议构建工具调用能力。

最后记住:Spring AI 是「懒人工具」,帮你快速落地;官方 SDK 是「底层基石」,帮你掌控细节。根据项目场景选择合适的方式,才能高效利用 MCP 协议的价值。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐