Spring AI 2.0 学习指南企业级智能应用开发实践 环境准备 速查 ⌂ 全部教程
首页 / Spring AI 指南 / 第 8 章

CH 08MCP:模型上下文协议

第 7 章我们把工具写在了自己的工程里。但如果公司有 10 个系统、每个系统都想暴露工具给 AI 用呢? 难道把 10 个系统的代码都抄一遍?MCP(Model Context Protocol)就是答案:把工具变成跨进程、跨语言、可复用的标准服务, 就像当年 HTTP 统一了网页、JDBC 统一了数据库访问。

学习目标
掌握 MCP Server / Client 开发
前置要求
第 7 章
本章产出
MCP 文件 / 日程助手
建议学时
5–6 小时

8.1MCP 解决什么问题:工具的「USB 接口标准」

先看没有 MCP 时的困境:

❌ 没有 MCP 的世界 AI 应用 A ─┬─ 需要查订单 → 抄一份 OrderService 的代码 ├─ 需要查库存 → 抄一份 InventoryService 的代码 └─ 需要发邮件 → 抄一份 MailService 的代码 AI 应用 B ─┴─ 上面三份代码……再抄一遍 问题:代码重复、升级不同步、权限难管控、语言不通(Python 写的工具 Java 用不了) ✅ 有 MCP 的世界 订单系统 ──▶ MCP Server(订单)─┐ 库存系统 ──▶ MCP Server(库存)─┼─▶ AI 应用 A / B / C …… 通过标准协议发现并调用 邮件系统 ──▶ MCP Server(邮件)─┘ 每个系统只暴露一次,所有 AI 应用复用;语言无关(Java/Python/Node 都能实现)

MCP 与第 7 章 @Tool 的关系

对比第 7 章 @Tool(进程内)MCP(进程外)
部署形态与 AI 应用同一个 JVM独立进程 / 独立服务
复用性只能本应用用✅ 所有支持 MCP 的应用都能用
语言Java✅ 任意(Java/Python/Node/Go…)
权限与审计混在应用代码里✅ 可以独立做鉴权、限流、审计
调用开销本地方法调用,纳秒级进程间/网络通信,毫秒级
适用项目内部、轻度使用✅ 企业级、多系统共享、需要治理
一句话选型建议:自己项目内部的简单工具,用 @Tool 就够; 一旦出现「多个 AI 应用要用同一批工具」或「工具由别的团队维护」,就该上 MCP。 两者不冲突,可以混用——MCP 提供的工具在 Spring AI 里最终也会变成 ToolCallback, 和内嵌工具用同一套机制。

8.2架构与三大原语

角色职责本课程的对应实现
Host承载 AI 应用与用户界面的一方(你的 Spring Boot 应用 / Claude Desktop / IDE)我们的 springai-lab 主应用
ClientHost 内部的连接器,负责与某个 Server 建立 1:1 连接、协议握手、收发消息McpSyncClient / McpAsyncClient
Server暴露能力的一方,声明「我提供哪些工具 / 资源 / 提示词」我们写的 MCP Server 应用

Server 能暴露的三种能力(三大原语)

原语含义典型例子谁发起
Tools
最常用
可执行的「动作」,有参数、有返回值 查询订单、发送邮件、创建日程 模型决定
Resources 可读取的「数据」,像文件一样按 URI 访问 file:///docs/a.md、db://orders/2024001 应用决定
Prompts 预定义的「提示词模板」,可带参数 「代码审查模板」「周报生成模板」 用户/应用选择
初学阶段只关注 Tools。Resources 和 Prompts 在 Spring AI 里的封装相对简单, 生产项目里 95% 的场景都是用 Tools。本章的动手部分也只做 Tools,把机制吃透比贪多更有价值。

8.3三种传输方式:怎么连

传输方式原理优点缺点适用
stdio Host 以子进程方式启动 Server,通过标准输入输出通信 零网络配置、最简单、延迟极低 只能本机、Server 必须能被 Host 启动 本地工具
桌面应用
SSE
旧
HTTP 长连接,服务端通过 Server-Sent Events 推送 可跨网络、可被反向代理 需要维护长连接、部分代理不友好 早期实现
Streamable HTTP
推荐
标准 HTTP 请求 + 流式响应,无状态可扩展 ✅ 最容易部署到 K8s、可水平扩展、易做网关鉴权 需要独立部署一个服务 企业级
远程 MCP
课程建议路线:先用 stdio 把「Server 写出来 + Client 连上」跑通(10 分钟搞定,零网络问题), 再改成 Streamable HTTP 部署成独立服务。 直接上 HTTP 会让你在「连不上」的问题上浪费大量时间,而那不是本章要学的重点。

8.4写一个 MCP Server

① 依赖(建议独立工程)

MCP Server 通常做成独立应用,不要和主应用混在一起。

<!-- 新建工程 mcp-order-server,pom 片段 -->
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>4.1.1</version>
</parent>

<dependencies>
  <!-- 方案 A:stdio 模式(最简,用于本地联调) -->
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server</artifactId>
  </dependency>

  <!-- 方案 B:WebMVC / Streamable HTTP 模式(用于远程服务) -->
  <!--
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    </dependency>
  -->

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc</artifactId>
  </dependency>
</dependencies>

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

② stdio 模式:只需一个配置文件 + 一个工具类

# src/main/resources/application.yml(本地联调用 stdio)
spring:
  main:
    # ★ stdio 模式下必须关闭 Web 容器,否则启动日志会污染协议流(stdout 就是通信通道)
    web-application-type: none
  ai:
    mcp:
      server:
        name: order-mcp-server        # 服务名,客户端会看到
        version: 1.0.0
        # ★ 2.0 变更:显式打开 stdio。Streamable HTTP 已成为默认传输,
        #    所以本地用 stdio 时必须显式声明,否则 server 不会走标准输入输出。
        stdio: true
        # 执行类型:SYNC(同步)/ ASYNC(异步,返回 Mono/Flux)
        type: SYNC
        # 是否把工具变更通知给客户端
        tool-change-notification: true
2.0 把默认传输换成了 Streamable HTTP。官方说明是「Streamable HTTP is now the default, replacing the deprecated SSE transport」,STDIO 保留给本地进程式集成。
所以在 2.0 里:
  • 本地联调:显式设 spring.ai.mcp.server.stdio=true(如上)。
  • 远程部署(推荐形态):换成 spring-ai-starter-mcp-server-webmvc, 用 spring.ai.mcp.server.protocol=STREAMABLE;也有 STATELESS 变体, 适合放在反向代理后面做水平扩展(代价是失去 MCP 的双向通信能力)。
  • SSE:已被标记为 deprecated,新项目不要再选它。
// McpOrderServerApplication.java
@SpringBootApplication
public class McpOrderServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(McpOrderServerApplication.class, args);
    }
}
// OrderMcpTools.java —— 与第 7 章的 @Tool 写法完全一样!
@Component
public class OrderMcpTools {

    /**
     * ★ 2.0 变更:服务端暴露 MCP 能力要用 @McpTool / @McpToolParam,
     *   而不是给本地工具用的 @Tool / @ToolParam。两者语义不同:
     *   @Tool     = 让本应用的模型调用(第 7 章)
     *   @McpTool  = 通过 MCP 协议暴露给外部客户端
     *   包路径(2.0 起 MCP 注解已并入 Spring AI 本体):
     *     org.springframework.ai.mcp.annotation.McpTool
     *     org.springframework.ai.mcp.annotation.McpToolParam
     */
    @McpTool(description = """
            查询指定订单号的当前状态与预计发货时间。
            当用户询问发货情况、物流状态时调用。
            不要用于查询退款进度或账户余额。
            """)
    public OrderStatus queryOrderStatus(
            @McpToolParam(description = "订单号,纯数字字符串,例如 2024001")
            String orderNo) {
        // 这里可以是真实数据库查询。教学演示用模拟数据。
        return switch (orderNo) {
            case "2024001" -> new OrderStatus(orderNo, "未发货", "2024-03-08");
            case "2024002" -> new OrderStatus(orderNo, "已发货", "-");
            default -> new OrderStatus(orderNo, "NOT_FOUND", "-");
        };
    }

    @McpTool(description = "列出某个客户最近 5 笔订单的订单号与状态")
    public List<OrderStatus> listRecentOrders(
            @McpToolParam(description = "客户姓名") String customerName) {
        return List.of(
                new OrderStatus("2024001", "未发货", "2024-03-08"),
                new OrderStatus("2023129", "已完成", "-"));
    }

    public record OrderStatus(String orderNo, String status, String promisedDate) {}
}

③ 关键一步:让 Server 发现你的工具

2.0 起这一步变得很简单:不用写任何配置类。 只要工具类是个 Spring bean 且方法上标了 @McpTool, MCP Server 的自动配置会通过注解扫描把它自动注册出去:

@Component          // ★ 必须是 Spring bean,否则扫描不到
public class OrderMcpTools {

    @McpTool(description = "查询指定订单号的当前状态与预计发货时间")
    public OrderStatus queryOrderStatus(
            @McpToolParam(description = "订单号,例如 2024001") String orderNo) {
        // …… 业务逻辑
    }
}
// 就这样,不需要 @Configuration,不需要 ToolCallbackProvider。

注解扫描的包范围可以配置(默认扫应用主类所在包及其子包)。如果你确实需要程序化注册 (例如工具来自第三方对象、或要按环境动态决定暴露哪些),再退回到 Provider 写法:

@Configuration
public class McpServerConfig {

    @Bean
    public ToolCallbackProvider orderToolsProvider(OrderMcpTools orderMcpTools) {
        // 把 bean 里的 @Tool 方法收集成工具(注意:这里是本地 @Tool 语义,
        // 适用于"用代码手工装配 MCP Server"的场景)
        return MethodToolCallbackProvider.builder()
                .toolObjects(orderMcpTools)
                .build();
    }
}
2.0 还给了「控制暴露面」的开关。MCP Server 现在支持过滤哪些工具对外可见—— 用一个 McpToolFilter bean 按服务标识、工具名或描述筛选。 这在多租户或分级授权的场景里很关键:不要把内部管理类工具无意中暴露给所有客户端。 另外还有 spring.ai.mcp.server.expose-mcp-client-tools 控制是否把本应用作为客户端拿到的工具再对外暴露。
stdio 模式的三个致命坑:
  1. 忘了 web-application-type: none:容器启动会打印日志到 stdout, 而 stdout 正是 MCP 的通信通道,协议立刻被污染,客户端报「无法解析消息」。
  2. 在工具方法里 System.out.println:同样是污染协议流。 调试请一律用 log(输出到 stderr 或文件)。
  3. 启动太快退出:stdio 模式下 Server 必须保持运行等待输入, 如果 main 里没有任何阻塞机制,进程会直接结束。用 Spring Boot 的常驻模式即可(不要手动 System.exit)。

8.5写一个 MCP Client(连上 Server)

① 主应用加依赖

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

② 声明要连接的 Server 列表

创建 src/main/resources/mcp-servers.json:

{
  "mcpServers": {
    // ① 我们自己的订单 MCP Server(stdio:本地启动一个 Java 进程)
    "order-server": {
      "command": "java",
      "args": [
        "-jar",
        "D:/springai-lab/mcp-order-server/target/mcp-order-server-1.0.0.jar"
      ]
    },

    // ② 官方文件系统 Server(Node 实现,演示跨语言复用)
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "D:/springai-lab/data"          // ★ 授予访问的目录白名单
      ]
    },

    // ③ 远程 Streamable HTTP Server(生产形态)
    "remote-weather": {
      "url": "http://localhost:8081/mcp",
      "type": "STREAMABLE",
      "headers": {
        "Authorization": "Bearer ${MCP_TOKEN}"
      }
    }
  }
}
# ★ 2.0 推荐写法:直接在 yml 里声明每个 stdio 连接,不再需要外部 JSON 文件
spring:
  ai:
    mcp:
      client:
        enabled: true
        name: springai-lab
        version: 1.0.0
        # 一次性初始化所有 Server 连接(不等第一个请求才连)
        initialized: true
        # 把 MCP 工具包装成 ToolCallback(但要显式传给 ChatClient,见 8.6 节)
        toolcallback:
          enabled: true
        stdio:
          connections:
            # ---- 我们自己的订单 MCP Server ----
            order-server:
              command: java
              args:
                - -jar
                - D:/springai-lab/mcp-order-server/target/mcp-order-server-1.0.0.jar
            # ---- 官方文件系统 Server(Node 实现) ----
            filesystem:
              command: npx
              args:
                - -y
                - "@modelcontextprotocol/server-filesystem"
                - D:/springai-lab/data          # ★ 授权目录白名单
          # 单次调用超时
          request-timeout: 60s
2.0 的配置写法变了两处,抄旧代码会不生效:
  • 从「外部 JSON 文件 + servers-configuration」改为 yml 里的 stdio.connections.<名字>.command/args。 上面的 mcp-servers.json 仍可作为备选方案保留,但以 connections 为准。
  • 远程 Server 用 url + type(STREAMABLE / STATELESS)声明, 通过 headers 带鉴权信息。
排查口诀:连不上就先看配置键是否被识别——用 IDEA 展开 spring.ai.mcp.client 看当前版本支持哪些键,比翻文档快。
配置键名在各版本间有微调。如果启动时报 mcp-servers.json 找不到或连接没建立, 优先用 IDEA 的 yml 自动补全把 spring.ai.mcp.client 展开看一遍当前版本支持的键, 再对照官方文档。这类「配置漂移」在新兴协议里很常见,属于正常现象。

③ 用代码直接操作 MCP 客户端(了解机制)

@Service
public class McpInspectService {

    /**
     * 容器里每个已配置的 Server 都会有一个 McpSyncClient bean。
     * 用 List 注入可以拿到全部。
     */
    private final List<McpSyncClient> clients;

    public McpInspectService(List<McpSyncClient> clients) {
        this.clients = clients;
    }

    /** 列出所有 Server 提供的工具——调试时最先跑这个 */
    public void listAllTools() {
        for (McpSyncClient client : clients) {
            // 服务器信息(名字、版本)
            var info = client.getServerInfo();
            log.info("=== MCP Server: {} v{} ===",
                    info.name(), info.version());

            // 工具清单
            var tools = client.listTools();
            tools.tools().forEach(t ->
                    log.info("  工具: {}  描述: {}  参数: {}",
                            t.name(), t.description(), t.inputSchema()));
        }
    }

    /** 手工调用一个工具(不经过模型)——用于验证 Server 端逻辑是否正确 */
    public Object callToolManually(String toolName, Map<String, Object> args) {
        for (McpSyncClient client : clients) {
            boolean has = client.listTools().tools().stream()
                    .anyMatch(t -> t.name().equals(toolName));
            if (has) {
                var result = client.callTool(
                        new McpSchema.CallToolRequest(toolName, args));
                return result.content();
            }
        }
        throw new IllegalArgumentException("未找到工具:" + toolName);
    }
}
「先手工调通,再交给模型」是 MCP 调试的黄金顺序。 listAllTools() 确认工具被发现了,callToolManually() 确认 Server 端逻辑正确, 最后才接入模型。这样出问题时你能立刻判断是「连接问题」「Server 问题」还是「模型选择问题」。

8.6把 MCP 工具交给模型:和第 7 章无缝衔接

开启 spring.ai.mcp.client.toolcallback.enabled=true 后, Spring AI 会把所有 MCP Server 提供的工具自动包装成 ToolCallback, 然后你就能像使用本地 @Tool 一样把它们交给模型。

@Configuration
public class McpChatClientConfig {

    /**
     * ★ 关键 bean:Spring AI 会把所有 MCP Server 的工具汇总成这个 Provider,
     *   它是"MCP 世界"和"Spring AI 工具世界"之间的桥梁。
     *
     * ★★ 2.0 最容易踩的坑:MCP 工具【不会】自动注册到 ChatClient 上。
     *    框架刻意这么设计——避免一个应用无意间把上百个外部工具全塞给模型。
     *    你必须显式地把它传给 .defaultTools(...) 或 .tools(...),就像下面这样。
     */
    @Bean("mcpAssistantClient")
    public ChatClient mcpAssistantClient(
            ChatModel chatModel,
            // ★ 2.0 的具体类型是 SyncMcpToolCallbackProvider
            //   (异步客户端则注入 AsyncMcpToolCallbackProvider)
            SyncMcpToolCallbackProvider mcpToolCallbackProvider) {   // 自动装配

        // 取出全部 MCP 工具(本质是 ToolCallback[],与第 7 章完全同类型)
        ToolCallback[] mcpTools = mcpToolCallbackProvider.getToolCallbacks();

        // 打印出来确认连上了几个工具——启动日志里能看到
        org.slf4j.LoggerFactory.getLogger(McpChatClientConfig.class)
                .info("已从 MCP Server 加载 {} 个工具", mcpTools.length);

        return ChatClient.builder(chatModel)
                .defaultSystem("""
                        你是"智汇"智能助手,可以访问文件系统和订单系统。

                        规则:
                        1. 涉及真实数据(文件、订单)时必须调用工具。
                        2. 需要读取或写入文件时,先确认路径在允许范围内。
                        3. 工具报错时,向用户说明原因并给出建议。
                        """)
                .defaultToolCallbacks(mcpTools)     // ★ 注册 MCP 工具
                .build();
    }
}
这就是 MCP 最漂亮的地方:对模型和业务代码来说,MCP 工具和本地工具没有任何区别。 模型看到的都是「工具名 + 描述 + 参数 schema」,调用链路都是 ToolCallback。 你甚至可以混用:.defaultTools(localTools) 再 .defaultToolCallbacks(mcpTools), 模型根本分不出哪个是本地的、哪个是远程的。这就是协议标准化的力量。

8.7完整示例:MCP 文件助手

做一个小助手:既能用我们自己写的订单 MCP Server,也能用官方文件系统 MCP Server(跨语言复用)。

springai-lab/ # 主应用(MCP Host + Client) ├── src/main/java/com/example/springai/ch08/ │ ├── McpChatClientConfig.java # 装配带 MCP 工具的 ChatClient │ ├── McpInspectService.java # 工具清单 / 手工调用(8.5 节) │ ├── FileAssistantService.java # 业务服务 │ └── McpAssistantController.java # HTTP 接口 └── src/main/resources/ └── mcp-servers.json # MCP Server 清单(8.5 节) mcp-order-server/ # 独立工程(MCP Server) ├── src/main/java/.../McpOrderServerApplication.java ├── src/main/java/.../OrderMcpTools.java ├── src/main/java/.../McpServerConfig.java └── src/main/resources/application.yml

① 先构建并验证 Server

# 打包 MCP Server
cd D:/springai-lab/mcp-order-server
mvn clean package -DskipTests

# 手工测试:往它的 stdin 里发一条 MCP 初始化消息(最原始的验证方式)
# 更推荐用官方 Inspector(见 8.8 节),可视化操作
java -jar target/mcp-order-server-1.0.0.jar

② 业务服务(含安全边界)

@Slf4j
@Service
public class FileAssistantService {

    private final ChatClient mcpAssistantClient;

    public FileAssistantService(
            @Qualifier("mcpAssistantClient") ChatClient client) {
        this.mcpAssistantClient = client;
    }

    public String ask(String question) {
        // 把当前用户身份放进 ToolContext(虽然 MCP Server 目前没用它,
        // 但这是标准做法,未来 Server 端做鉴权时会用到)
        return mcpAssistantClient.prompt()
                .user(question)
                .toolContext(Map.of(
                        "userId", SecurityContextHolder.currentUserId(),
                        "tenantId", SecurityContextHolder.currentTenantId()))
                .call()
                .content();
    }
}

③ 接口(含一个运维接口,用于排查工具注册情况)

@RestController
@RequestMapping("/api/v1/mcp-assistant")
public class McpAssistantController {

    private final FileAssistantService assistantService;
    private final McpInspectService inspectService;

    public McpAssistantController(FileAssistantService a, McpInspectService i) {
        this.assistantService = a;
        this.inspectService = i;
    }

    @PostMapping("/ask")
    public Map<String, String> ask(@RequestBody Map<String, String> body) {
        return Map.of("answer", assistantService.ask(body.get("question")));
    }

    /**
     * 列出所有已连接的 MCP Server 及其工具。
     * ★ 强烈建议保留这个接口——它是排查"工具没注册上"的最快手段。
     * 生产环境记得加管理员权限。
     */
    @GetMapping("/tools")
    public Map<String, Object> listTools() {
        inspectService.listAllTools();
        return Map.of("ok", true, "message", "工具清单已打印到日志");
    }
}

④ 验证

# ① 先看工具有没有注册上
curl http://localhost:8080/api/v1/mcp-assistant/tools
# 检查日志,期望看到类似:
# === MCP Server: order-mcp-server v1.0.0 ===
#   工具: queryOrderStatus  描述: 查询指定订单号的当前状态与预计发货时间…
#   工具: listRecentOrders  描述: 列出某个客户最近 5 笔订单…
# === MCP Server: filesystem v0.6.2 ===
#   工具: read_file / write_file / list_directory / …

# ② 用远程 MCP 工具查订单(模型不知道这是远程的)
curl -X POST http://localhost:8080/api/v1/mcp-assistant/ask \
  -H "Content-Type: application/json" \
  -d '{"question":"订单 2024001 发货了吗?"}'
# 期望:答案来自 order-mcp-server 的工具返回

# ③ 用文件系统 MCP 工具(跨语言:Server 是 Node 写的)
curl -X POST http://localhost:8080/api/v1/mcp-assistant/ask \
  -H "Content-Type: application/json" \
  -d '{"question":"列出 D:/springai-lab/data 目录下的所有文件"}'
# 期望:模型调用 list_directory 工具并整理成清单

# ④ 越权测试:让文件助手读白名单外的目录
curl -X POST http://localhost:8080/api/v1/mcp-assistant/ask \
  -H "Content-Type: application/json" \
  -d '{"question":"帮我读一下 C:/Windows/System32/drivers/etc/hosts 文件"}'
# 期望:MCP Server 拒绝访问(因为白名单只授权了 D:/springai-lab/data)
# ★ 这验证了 MCP 的价值:权限约束在 Server 端,而不是靠模型自觉
第 ④ 步是本章最有价值的一个验证。 它直观地展示了 MCP 的安全模型:权限边界由 Server 端强制,与模型的行为无关。 对比第 7 章「工具实现内部做权限校验」,MCP 把这件事提升到了服务治理层面—— 可以由安全团队独立维护,而不是散落在各个 AI 应用的代码里。

8.8调试与常见坑

用官方 Inspector 调试 Server

# MCP Inspector 是一个网页版调试工具,能列出工具、手工填参数调用、看原始报文
npx @modelcontextprotocol/inspector java -jar target/mcp-order-server-1.0.0.jar

# 它会启动一个本地 Web 界面(通常是 http://localhost:6274)
# 在里面你可以:
#   1. 看到 Server 声明的全部工具与参数 schema
#   2. 手工填参数调用,验证 Server 端逻辑
#   3. 查看完整的 JSON-RPC 请求/响应报文
调试 MCP 的正确顺序(能省掉你 80% 的排查时间):
  1. 用 Inspector 直连 Server —— 确认 Server 本身没问题(工具能列出、能调用成功)。
  2. 用 listAllTools() 从客户端看 —— 确认连接建立、工具被发现。
  3. 用 callToolManually() 手工调 —— 确认客户端到 Server 的调用链路通。
  4. 最后才接模型 —— 如果前三步都过了但模型不用工具,那是「描述/提示词」的问题,不是 MCP 的问题。

常见坑排错表

现象原因解决
客户端报「无法解析消息 / Unexpected token」Server 的 stdout 被日志污染stdio 模式设 web-application-type: none;禁止 System.out.println;日志改到文件
Server 进程启动后立刻退出main 方法跑完就结束了用 Spring Boot 常驻;不要手动 exit
Connection refused / 连不上Server jar 路径错、端口错、Server 没启动用绝对路径;先手工 java -jar 跑一遍确认能起来
工具列表为空没提供 ToolCallbackProvider bean,或 toolcallback.enabled=false检查 McpServerConfig;检查客户端的 toolcallback.enabled
工具能列出但调用报错参数 schema 与接收类型不匹配用 Inspector 看参数 schema,核对 Java 方法签名
npx 相关 Server 连不上Node 未安装 / npx 首次下载慢 / 网络问题先手工执行 npx -y @modelcontextprotocol/server-filesystem 看是否能跑起来
远程 HTTP Server 401缺少 Authorization 头在 mcp-servers.json 的 headers 里配置 token
启动特别慢(每次几十秒)initialized: false,每个 Server 等到首次使用时才连接设 initialized: true,启动时一次性建立连接
模型不用 MCP 工具工具描述不清;或工具太多用 8.7 的 /tools 接口确认工具真的注册上了;减小工具数量
工具调用超时Server 端逻辑慢(如大文件读取)调大 request-timeout;Server 端做分页/限制

8.9自测题

Q1. MCP 相比进程内 @Tool 的核心优势是什么?什么情况下不需要 MCP?

核心优势:跨进程、跨语言、可复用、可独立治理(鉴权限流审计), 让工具从「应用的一部分」变成「企业的公共能力」。 不需要 MCP 的情况:工具只服务于自己这一个应用、数量少、不需要独立权限管控—— 这时候用 @Tool 更简单,且没有进程间通信开销。

Q2. Host、Client、Server 各自的职责是什么?

Host 是承载 AI 应用与用户界面的宿主程序(我们的 Spring Boot 应用); Client 是 Host 内部的连接器,与 Server 一对一连接,负责协议握手与消息收发; Server 是暴露能力(Tools/Resources/Prompts)的独立服务。

Q3. 为什么 stdio 模式下必须关闭 Web 容器、禁止 System.out?

因为 stdout 是 MCP 的通信通道,协议消息通过它传输。 Spring Boot 的启动日志、System.out.println 都会往 stdout 写内容, 导致客户端收到的不再是合法 JSON-RPC 消息,解析立刻失败。 解决办法:关闭 Web 容器、日志输出改到 stderr 或文件。

Q4. 从 MCP Server 拿到的工具,和本项目的 @Tool 方法,在传给模型时有区别吗?

没有区别。两者最终都被统一成 ToolCallback, 通过 .defaultTools(...) 或 .defaultToolCallbacks(...) 交给模型。 模型看到的只有「工具名 + 描述 + 参数 schema」,它无法区分工具是本地还是远程的。 这正是 MCP 的价值:在协议层统一,在实现层解耦。

Q5.【动手题】把第 7 章的「天气查询工具」改造成一个独立的 MCP Server,并用 Inspector 验证它能被外部调用。

步骤:① 新建工程,加 spring-ai-starter-mcp-server; ② 把 WeatherTools 类复制过来(代码几乎不用改); ③ 写一个 ToolCallbackProvider bean 注册它; ④ yml 里设 web-application-type: none 和 protocol: STDIO; ⑤ mvn package 后用 npx @modelcontextprotocol/inspector java -jar target/*.jar 连接; ⑥ 在 Inspector 界面里调用 queryWeather,确认返回正确。

本章验收标准:
  • 能画出 Host / Client / Server 的关系图,并说出三种传输方式的区别。
  • 能独立写一个 stdio 模式的 MCP Server,并用 Inspector 调通。
  • 能在主应用里配置 MCP Client,连上两个以上 Server(含一个 Node 实现的)。
  • MCP 工具能和本地 @Tool 混用,模型无感。
  • 能解释「为什么 MCP 的权限边界比写在提示词里更可靠」。