MCP(Model Context Protocol)正成为 AI 工具连接的标准

1. 概述

大语言模型(LLM)只有在能读取真实数据、调用真实系统时才真正有用:查询订单、阅读工单、执行查询。以前,每个 AI 应用都要为不同的工具自己编写集成方式。Model Context Protocol(MCP)应运而生,用来规范这种连接方式。

本文讲解什么是 MCP 以及为什么它越来越受关注。然后你将用 Spring Boot 编写一个小型 MCP 服务器,配置客户端来调用它,并了解什么时候不应该使用 MCP。

2. MCP 是什么?

MCP 是一个开放协议。它定义了 AI 应用如何发现和调用外部能力的方式:工具、数据和预设提示词。你可以把 MCP 想象成 AI 的“USB 接口”。任何遵循标准的设备都能插入支持该标准的主机上运行。

2.1. 三个主要角色

  • Host:用户交互的应用程序,例如桌面上的 AI 助手、IDE 或内部聊天机器人。
  • Client:位于 host 内部的组件。每个 client 与一个 server 保持一一对应的连接。
  • Server:为 AI 提供能力的程序,例如读取订单数据库的服务器或访问文件系统的服务器。

2.2. 服务器端的三种“基础类型”

  • Tools:模型可以决定调用的函数,如 findOrder 或 cancelOrder。
  • Resources:只读数据,如文件内容或配置记录,被纳入上下文。
  • Prompts:预构建的提示词模板,用户可以选择使用。

2.3. 底层协议

MCP 使用 JSON-RPC 2.0。最常见的是两种传输方式:stdio(host 启动 server 作为子进程),和 HTTP(server 远程运行)。一个工具调用看起来像这样:

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "findOrder",
    "arguments": { "orderId": "ORD-1001" }
  }
}

基本的工作流程包括三个步骤:

  1. Client 发送 initialize 以交换协议版本和支持的能力。
  2. Client 调用 tools/list 获取工具列表,包括描述和参数的 JSON Schema。
  3. 当模型决定使用工具时,client 发送 tools/call 如上例,然后将结果返回给模型。

重要点:模型不直接调用服务器。Host 位于中间,所以 host 可以在运行敏感工具之前要求用户确认。

3. 为什么 MCP 越来越受关注?

3.1. N × M 问题

假设有 N 个 AI 应用和 M 个系统需要连接。没有通用标准,你就得编写 N × M 种集成方式。有了通用协议,每个应用只需支持 MCP 一次,每个系统只需一个 MCP 服务器。问题就变成了 N + M。

3.2. 职责分离

拥有订单系统的团队编写并运维自己的 MCP 服务器。做聊天机器人的团队只需指向那个服务器。业务逻辑、访问权限和审计日志都在正确的地方,不会被复制到每个提示词或每个应用中。

3.3. 不被绑定到单一模型提供商

每个 LLM 提供商的 function calling 都有自己的格式。MCP 位于这些格式之上。你今天写的服务器在更换模型或更换 host 时仍然可以使用。

3.4. 生态系统在不断增长

越来越多的 AI 助手、IDE 和框架支持 MCP。有多种语言的 SDK,其中 Java 有官方 SDK,Spring AI 内置集成。这是许多人认为 MCP 是工具连接标准的有力候选的原因。不过,该协议仍在不断演变,所以长期部署时需要关注规范。

4. 用 Spring Boot 编写 MCP 服务器

你将构建一个提供两个工具的服务器,用于查询和取消订单,通过 stdio 运行。

4.1. 依赖

Spring AI 提供了 MCP 服务器的 starter。导入 Spring AI 的 BOM,并使用与你的 Spring Boot 3.x 兼容的最新版本:

<properties>
    <java.version>21</java.version>
    <spring-ai.version><!-- Spring AI 当前版本 --></spring-ai.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server</artifactId>
    </dependency>
</dependencies>

Starter spring-ai-starter-mcp-server 支持 stdio 传输。如果想通过 HTTP 运行服务器,Spring AI 有用于 Web MVC 和 WebFlux 的专用 starter。

注意: Spring AI 的 artifact 名称在不同的里程碑版本之间有过变化。请检查与你所选版本相应的文档。

4.2. Domain 和 Tool

public record Order(String id, String customerName, double total, String status) {}
@Service
public class OrderTools {

    private final Map<String, Order> orders = new ConcurrentHashMap<>(Map.of(
            "ORD-1001", new Order("ORD-1001", "Nguyen Van An", 350_000, "SHIPPING"),
            "ORD-1002", new Order("ORD-1002", "Tran Thi Binh", 120_000, "PENDING")
    ));

    @Tool(description = "按订单代码查询订单,返回客户名称、总金额和状态")
    public Order findOrder(@ToolParam(description = "订单代码,如 ORD-1001") String orderId) {
        Order order = orders.get(orderId);
        if (order == null) {
            throw new IllegalArgumentException("未找到订单 " + orderId);
        }
        return order;
    }

    @Tool(description = "取消订单。只能取消状态为 PENDING 的订单")
    public String cancelOrder(@ToolParam(description = "要取消的订单代码") String orderId) {
        Order order = findOrder(orderId);
        if (!"PENDING".equals(order.status())) {
            return "无法取消:订单状态为 " + order.status();
        }
        orders.put(orderId, new Order(order.id(), order.customerName(), order.total(), "CANCELLED"));
        return "已取消订单 " + orderId;
    }
}

两个注解发挥主要作用:

  • @Tool(包 org.springframework.ai.tool.annotation)将方法标记为工具。description 部分是模型读取以决定是否调用工具的内容。描述越清楚,模型选择越准确。
  • @ToolParam 描述每个参数。Spring AI 从参数类型和此描述生成 JSON Schema。

注意 cancelOrder 在违反业务规则时返回一条消息,而不是抛出异常。模型读取该消息并自然地向用户解释。

4.3. 将工具注册到 MCP 服务器

@SpringBootApplication
public class OrderMcpServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderMcpServerApplication.class, args);
    }

    @Bean
    ToolCallbackProvider orderToolProvider(OrderTools orderTools) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(orderTools)
                .build();
    }
}

MethodToolCallbackProvider 扫描 orderTools 中带有 @Tool 的方法。MCP 服务器的自动配置将查找所有 ToolCallbackProvider bean 并通过 tools/list 公开它们。

4.4. 配置 stdio

spring.ai.mcp.server.name=order-mcp-server
spring.ai.mcp.server.version=0.0.1
spring.ai.mcp.server.stdio=true

spring.main.web-application-type=none
spring.main.banner-mode=off
logging.pattern.console=
logging.file.name=./logs/order-mcp-server.log

对于 stdio,stdout 是 JSON-RPC 传输通道。任何其他打印到 stdout 的内容都会破坏协议,包括横幅和日志。因此上面的配置关闭了横幅、禁用了控制台日志并将日志转移到文件。

注意: 这是初次使用 stdio 编写 MCP 服务器时最常见的错误。甚至一行调试用的 System.out.println 也会导致客户端报告解析错误或断开连接。请使用 logger 将输出写到文件。

构建成 jar 文件:

./mvnw clean package -DskipTests

5. 从客户端连接

许多 host 通过包含 mcpServers 键的 JSON 文件配置服务器。文件的名称和位置取决于每个 host:

{
  "mcpServers": {
    "orders": {
      "command": "java",
      "args": ["-jar", "/opt/mcp/order-mcp-server.jar"]
    }
  }
}

启动时,host 像子进程一样运行命令 java -jar ...,然后发送 initialize 和 tools/list。你问:“订单 ORD-1002 还能取消吗?如果可以,请为我取消。“模型将调用 findOrder,然后是 cancelOrder。第二次调用返回的结果如下:

{"content":[{"type":"text","text":"已取消订单 ORD-1002"}],"isError":false}

通常 host 会在运行工具之前显示一个权限对话框。对于有副作用的工具(如 cancelOrder)应该保持这种行为。

如果你想让你的 Spring 应用充当 client,Spring AI 也有 MCP 客户端 starter。这个 starter 将来自外部服务器的工具转换为可与 ChatClient 使用的工具。

6. 测试工具的行为

工具只是普通的 Java 方法,所以可以测试而不需要启动 MCP:

class OrderToolsTest {

    private final OrderTools tools = new OrderTools();

    @Test
    void shouldCancelPendingOrder() {
        String result = tools.cancelOrder("ORD-1002");

        assertThat(result).isEqualTo("已取消订单 ORD-1002");
        assertThat(tools.findOrder("ORD-1002").status()).isEqualTo("CANCELLED");
    }

    @Test
    void shouldRefuseToCancelShippingOrder() {
        assertThat(tools.cancelOrder("ORD-1001"))
                .contains("无法取消")
                .contains("SHIPPING");
    }

    @Test
    void shouldFailWhenOrderNotFound() {
        assertThatThrownBy(() -> tools.findOrder("ORD-9999"))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("ORD-9999");
    }
}

三个测试涵盖了成功流程、业务规则和订单未找到的情况。SDK 已经处理了协议部分。你需要确保的是业务逻辑返回正确的结果,并向模型提供易于理解的消息。

要进行端到端测试,可以使用 MCP inspector 工具。它们允许列出工具并手动调用。

7. 实际应用:需要注意什么

  • 按业务逻辑设计工具,而不是按数据表。 findOrder 比万能的 executeSql 工具更好。针对性强的工具既容易控制权限,也能帮助模型做出正确选择。
  • 最小化权限。 服务器应该使用只具有必需权限的账户。分离读取工具和写入工具,并考虑只在某些环境中启用写入工具。
  • 警惕提示词注入。 工具返回的数据(如用户输入的注释)可能包含试图控制模型的指令。不要让一个工具的结果自动触发危险操作,必须有用户确认。
  • 只使用可信服务器。 通过 stdio 运行的第三方 MCP 服务器是你机器上的一个进程,拥有你的权限。工具描述也可能被注入恶意内容。阅读源代码或仅使用来自你信任的来源。
  • 远程服务器的身份验证。 通过 HTTP 运行时,将服务器放在适当的身份验证机制后面,并记录所有工具调用的审计日志。
  • 保持工具数量适中。 每个工具都会消耗上下文来描述。工具太多会导致模型容易选错。

8. 什么时候不应该使用 MCP

  • 小型应用,只有一个模型,只有几个函数。 框架的直接 function calling(如 @Tool 直接用于 ChatClient)更简单。你不需要额外的进程和协议层。
  • 流程固定,不需要模型决策。 如果第 1 步总是调用 API A,第 2 步总是调用 API B,就直接写代码。不要把一个 if 语句就能解决的事情交给 LLM。
  • 需要极低延迟或高确定性。 每一轮模型 → 客户端 → 服务器都会增加延迟。模型的结果也不是完全确定的。
  • 不可逆的操作而没有人工审查。 转账或批量删除数据不应该作为自动工具暴露。如果必须有,一定要配合确认步骤。
  • 你需要一个供其他系统调用的 API。 MCP 是为 AI 设计的。在服务之间,REST、gRPC 或消息传递仍然是正确的选择。

9. 结论

MCP 解决了一个非常实际的问题:以通用方式将 AI 连接到工具和数据,而不是让每个应用自己编写方式。由于 host-client-server 模型、三个基础类型(tools/resources/prompts)和 JSON-RPC 基础,你编写的服务器可以在多个不同的 host 中重复使用。

使用 Spring AI,编写 MCP 服务器只需要几个 @Tool 注解、一个 ToolCallbackProvider bean 和正确的 stdio 配置。困难部分在别的地方:设计具体明确的工具、严格的权限控制、防范提示词注入。你还需要清醒地认识到哪些情况下普通代码仍然是更好的解决方案。

mcpaispring-aispring-boot技术趋势