MCP 理论与实战

138 阅读19分钟

面向 Java 开发者的 MCP(Model Context Protocol)完整指南:先用「理论」把协议本质讲透,再用一个电商订单/商品查询 MCP Server 走完「提供工具 → 本地调用 → 第三方调用」全链路。

版本:Spring AI 1.1.2 · Spring AI Alibaba 1.1.2.2 · Spring Boot 3.5.9 · JDK 21+ 构建:Maven(spring-ai-alibaba-bom 统一版本管理) 模型:阿里云百炼(DashScope)通义千问 qwen-plus(客户端侧,服务端无需模型) 场景:电商「订单查询 / 商品搜索」


阅读地图(先花 3 分钟看这里)

这篇文章解决什么问题

你写了一个 Spring Boot 应用,想让大模型能「查数据库、调接口、操作文件」——但每个外部能力都单独写一遍适配,N 个应用 × M 个工具,适配代码爆炸。

MCP 就是干这个的:把「外部工具/数据源」统一成一套标准协议。工具方实现一次(Server),任何 AI 应用都能按协议调用(Client),一次实现、到处接入。这篇文章教你把这件事用 Spring AI 完整落地——从「协议是什么」到「怎么写 Server 暴露工具」,再到「本地和第三方两类客户端怎么调」。

写给谁

  • 有 Java / Spring Boot 基础,想给应用接入外部工具(数据库、GitHub、文件系统、第三方服务)的工程师;
  • 已经会 Function Calling(@Tool),想知道「MCP 和它什么关系、什么时候该用 MCP」的人;
  • 想把自己团队的内部系统封装成工具,供多个 AI 应用(Claude Desktop / Claude Code / 其它系统)复用的人。

不需要你懂协议细节,会写 Java 就行——Spring AI 已经把 JSON-RPC 握手、消息编解码全部封装好了。

一条主线 + 一个贯穿案例

整篇文章走一条主线——MCP 的「提供」与「调用」两侧:

提供服务端:业务逻辑 → @McpTool 注解 → 自动暴露为 MCP 工具(stdio / HTTP 两种传输)
调用客户端:Spring Boot / Claude Desktop / Claude Code → 通过协议发现工具 → LLM 自动调用

所有代码围绕同一个例子(后文反复用到):

一个电商 MCP Server 暴露两个工具:get_order(按订单号查订单)、search_products(按关键字搜商品)。你会在服务端用注解把它暴露出去,再在客户端让大模型「自己决定」调哪个工具来回答用户问题。

术语速查(精简版)

完整版见附录 · 术语表。先扫一眼,后面遇到不慌。

词一句话
MCP模型上下文协议:把「外部工具/数据源」统一成一套标准的 JSON-RPC 接口
MCP Server工具提供方:暴露 tools / resources / prompts
MCP Client工具调用方:连接 Server、发现并调用工具
MCP Host承载 AI 对话的宿主(Claude Desktop、你的 Spring Boot 应用)
tools / resources / prompts三大原语:可调用的函数 / 只读数据 / 提示词模板
stdio本地子进程传输(stdin/stdout 通信)
Streamable HTTP基于 HTTP 的远程传输,生产推荐
JSON-RPC 2.0MCP 的消息格式(请求 / 响应 / 通知)
Function Calling模型输出「调哪个函数 + 传什么参」,由应用执行

快速上手:15 分钟跑通最小闭环

目标:先看到结果,再回头理解原理。 这里只写「最小能跑通」的代码——一个 Server 暴露工具、一个 Client 让大模型调用它。完整版见第 5、6 章。

1. 准备环境

只需要一个云端 API Key(客户端调模型用),服务端连 Key 都不用:

# ① 阿里云百炼:创建 API Key(https://bailian.console.aliyun.com/  → API-KEY 管理)
# ② 写入环境变量(Windows 用 setx,Linux/macOS 用 export)
setx DASHSCOPE_API_KEY "sk-xxxxxxxx"

用 qwen-plus 跑通只要几分钱,首次开通一般有免费额度。

2. 服务端:一个注解暴露一个工具

新建 ecommerce-mcp-server 项目,pom 只加一个依赖(完整 pom 见第 5 章):

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

然后写这个工具类:

// 一个 @McpTool 注解,方法就变成了一个可被远程调用的 MCP 工具
@Component
public class EcommerceTools {

    public record OrderItem(String productId, String productName, int quantity, double price) {
    }

    public record Order(String orderId, String userId, String status, List<OrderItem> items, double totalAmount) {
    }

    @McpTool(name = "get_order", description = "根据订单号查询订单详情")
    public Order getOrder(@McpToolParam(description = "订单号") String orderId) {
        return new Order(orderId, "U1001", "已发货",
                List.of(new OrderItem("P1001", "华为 Mate 60 Pro", 1, 6999.00)), 6999.00);
    }
}

application.yml 一行启动为 HTTP MCP Server:

spring:
  ai:
    mcp:
      server:
        protocol: STREAMABLE   # 默认端点 POST /mcp

启动:mvn spring-boot:run,服务在 http://localhost:8080/mcp 等你调。

3. 客户端:让大模型自动调用它

新建 ecommerce-mcp-client 项目,pom 加两个依赖(模型 + MCP 客户端),application.yml 里「连上」刚才的 Server:

spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}
      chat:
        options:
          model: qwen-plus
    mcp:
      client:
        toolcallback:
          enabled: true                 # 关键:把 MCP 工具自动转成可被 LLM 调用的 ToolCallback
        streamable-http:
          connections:
            ecommerce:
              url: http://localhost:8080   # 连上 MCP Server

然后写一个 CommandLineRunner,把 MCP 工具挂到 ChatClient 上:

@Bean
CommandLineRunner demo(ChatClient.Builder builder, ToolCallbackProvider mcpTools) {
    return args -> {
        String answer = builder.build().prompt()
                .user("帮我查订单 2024090100000001 的物流状态")
                .toolCallbacks(mcpTools)      // 注入 MCP 工具,LLM 自己决定调不调、调哪个
                .call()
                .content();
        System.out.println(answer);
    };
}

💡 ToolCallbackProvider mcpTools 不用自己写 @Bean 注册:MCP 场景下,这个 Bean 由 spring-ai-starter-mcp-client 的自动装配帮你生成,直接注入即可。前提:① 引入了 mcp-client starter;② yml 配好了 MCP Server 连接;③ spring.ai.mcp.client.toolcallback.enabled=true(默认开启)。

4. 运行,预期输出

先启动服务端,再启动客户端:

# 终端 1:启动 Server
cd ecommerce-mcp-server && mvn spring-boot:run

# 终端 2:启动 Client
cd ecommerce-mcp-client && mvn spring-boot:run
订单 2024090100000001 当前状态为「已发货」,包含 1 件商品:华为 Mate 60 Pro,金额 6999.00 元。

看到这个,你已经跑通了 MCP 的完整链路:Server 用 @McpTool 暴露工具 → Client 通过协议发现工具 → 大模型「看懂」工具描述后自己决定调 get_order → 拿到结果再组织语言回答。


第一部分 · 理论篇

第 1 章 MCP 是什么、解决什么问题

1.1 什么是 MCP:从「N × M」到「N + M」

先抛开「协议」这种大词,用大白话讲清楚 MCP 到底省了什么力气。

没有 MCP:两两配对、各写一遍。 假设你有 3 个 AI 应用,要接 4 个外部工具(数据库、GitHub、搜索、文件)。每个应用想用每个工具,都得单独写一段「怎么连这个工具」的适配代码,而且各写各的、互不通用——3 × 4 = 12 段适配代码。

有了 MCP:两边各遵守一次标准。 工具侧,每个工具只实现一次 MCP(4 次);应用侧,每个应用只支持一次 MCP(3 次)。一共 3 + 4 = 7 次。而且应用和工具越多、差距越悬殊——10 个应用 × 10 个工具,前者要 100 段、后者只要 20 次,差 5 倍。这就是「N × M 降到 N + M」。画出来看:

没有 MCP(每个应用 × 每个工具都要单独适配)
  应用A ──► 数据库A    应用A ──► GitHub
  应用B ──► 数据库A    应用B ──► GitHub
  应用C ──► 数据库A    应用C ──► GitHub
        (N × M 条连接,每条都要单独开发)

有了 MCP(统一协议,一次实现到处接入)
  应用A ─┐
  应用B ─┼── MCP 协议 ──► 数据库 MCP Server / GitHub MCP Server / ...
  应用C ─┘
        (N + M 条连接,工具侧只实现一次)

MCP(Model Context Protocol,模型上下文协议)是 Anthropic 于 2024 年底开源的开放标准协议,它把「AI 应用获取外部上下文和调用工具」这件事,抽象成一套统一的接口规范。

一句话类比:MCP 之于 AI 应用与工具,就像 USB-C 之于设备与配件——不是某个厂商的私有接口,而是所有厂商共同遵守的通用标准。一个设备(客户端)用 USB-C,就能插任意 USB-C 配件(工具/服务端),而不必为每个配件买转接头。

MCP 的核心价值,就四条:

  • 解耦:工具提供方(Server)和工具消费方(Client)各自独立演进,通过协议对接;
  • 跨语言:协议基于 JSON-RPC 2.0,Java / Python / TypeScript 客户端都能调同一个 Server;
  • 复用:社区已有数千个现成 MCP Server(GitHub、PostgreSQL、Slack、文件系统、浏览器等),按统一协议接入成本极低;
  • 标准化上下文:不止「调函数」,还统一了「读资源」「取提示词模板」的上下文供给方式。

1.2 MCP 与 Function Calling 的关系

这是最容易混淆的一点,先在这里讲清楚。两者不冲突,而且终点是同一条链路:

Function CallingMCP
工具定义位置应用内部,@Tool 方法外部/第三方服务,独立的 Server 进程
接入方式编译期就在应用里运行时通过协议发现、握手、调用
生命周期与应用同生共死独立部署、独立升级、可被多应用共享
典型场景应用自己的业务函数数据库、GitHub、外部物流/仓储系统等

💡 在 Spring AI 里,两者的终点是同一件事:MCP 工具经 ToolCallbackProvider 自动转成和 @Tool 完全一样的 ToolCallback,最终都参与同一次 Function Calling。换句话说,MCP 是把「外部的工具」统一成「应用内可调用的工具」的标准桥梁。

1.3 什么时候该用 MCP(选型)

不是所有工具都要上 MCP,先判断:

场景建议
工具只被自己一个应用用,逻辑简单直接用 @Tool(Function Calling),别上 MCP
工具要被多个应用 / 多语言 / 跨团队复用上 MCP,一次实现到处接入
接入第三方现成能力(GitHub、数据库、搜索)上 MCP,社区有现成 Server 直接连
想把内部系统封装成「标准工具」对外开放上 MCP,独立部署、独立升级、带鉴权

一句话:MCP 的价值在「复用」和「跨边界」。单应用内部的自有函数,@Tool 更省事;要共享、要接第三方、要跨语言,就上 MCP。

1.4 生态现状

MCP 发布后迅速成为事实标准,OpenAI、Google、微软、Anthropic 等都已支持。Spring AI 从 1.0 起深度集成 MCP,到 1.1.2 已支持 tools / resources / prompts 三类原语,以及 stdio / SSE / Streamable HTTP 三种传输。本文后续全部基于 Spring AI 1.1.2 展开。


第 2 章 架构与核心概念

本章解决什么问题:把 MCP 的「黑盒」打开,让你看懂它由哪几层组成——三类角色、三大原语、三种传输、一种消息格式。这是后面读 Spring AI 封装、排查问题的基础。

2.1 Client–Host–Server 三层架构

MCP 官方定义了三类角色:

┌─────────────────────────────────────────────────────────────┐
│  MCP Host(宿主,如 Claude Desktop、你的 Spring Boot 应用)    │
│                                                             │
│    ┌──────────────────────────────────────────────────┐     │
│    │  MCP Client(客户端,1:1 连接一个 Server)         │     │
│    │  · 发起握手、协商能力                              │     │
│    │  · 发请求调工具 / 读资源 / 取提示词                  │     │
│    └──────────────────────────┬───────────────────────┘     │
└───────────────────────────────┼─────────────────────────────┘
                                │ JSON-RPC 2.0(传输层:stdio / HTTP)
                                ▼
                    ┌───────────────────────┐
                    │  MCP Server(服务端)    │
                    │  · 暴露 tools           │
                    │  · 暴露 resources       │
                    │  · 暴露 prompts         │
                    └───────────────────────┘
  • Host:承载 AI 对话的宿主应用(Claude Desktop、你的 Spring Boot 应用)。它内部可以管理多个 Client。
  • Client:与单个 Server 维持 1:1 连接,负责握手、收发消息。
  • Server:提供工具/资源/提示词的独立进程或服务。

📌 通信是双向的。客户端可以调 Server 的工具,Server 也可以反向请求客户端的能力(如 sampling 让宿主调用 LLM)。不过日常开发 99% 的场景只用到「客户端调 Server」,本文也只讲这个方向。

2.2 三大原语:tools / resources / prompts

原语作用关键方法Spring AI 1.1.2 支持
tools可执行的功能(查订单、查数据库、写文件)tools/list、tools/call✅ 完整支持(自动转 ToolCallback)
resources只读数据源(文件内容、数据库 schema)resources/list、resources/read⚠️ 需手动用 McpSyncClient 读取
prompts可复用的提示词模板prompts/list、prompts/get⚠️ 需手动用 McpSyncClient 读取

💡 tools 是「一等公民」,resources / prompts 是「二等公民」:Spring AI 1.1.2 把 tools 自动转成 ToolCallback 参与 Function Calling;而 resources 和 prompts 没有自动集成,需要手动用 McpSyncClient 读取。第 6 章会分别演示两种用法。

2.3 传输层:stdio / Streamable HTTP / SSE

「传输」就是客户端和服务端之间用哪根「管子」传 JSON 消息。三种管子,从「近」到「远」:

传输机制适用场景备注
stdio本地子进程,通过 stdin/stdout 通信本地工具、开发调试最简单;进程生命周期由客户端管理
Streamable HTTPHTTP POST + 可选 SSE 流远程服务、生产环境1.1 新增的新一代传输,生产推荐
SSEHTTP + Server-Sent Events远程服务(旧方案)逐步被 Streamable HTTP 取代

逐个用大白话拆开:

  • stdio(本机当面说):客户端把 Server 当成本地程序直接「启动」(java -jar xxx.jar),靠程序的标准输入/输出(命令行里 print 出来、键盘敲进去的内容)来回传 JSON,根本不走网络。进程由客户端拉起、也由它关掉——所以只适合本机、单人调试。

  • SSE(旧广播,两条线):走网络。服务端先开一条 HTTP 长连接,专门「单向推送」消息给你(Server-Sent Events);客户端要反向发消息,得走另一个 HTTP 端点。等于两条线各管一个方向,长连接对负载均衡/反向代理不友好、断线重连也麻烦——已被淘汰,别主动用。

  • Streamable HTTP(一问一答,一条线):走网络,但只用一个端点(默认 POST /mcp)。客户端每次发一个请求,服务端按需回:内容少就回一个 JSON,要连发多条消息就回一个 SSE 流。等于普通 HTTP 一问一答,天然适配网关/代理/CDN,还能断线重连——生产首选。

选型建议:

  • 本地工具、单人调试 → stdio(最省事,连网络都不走);
  • 部署到服务器、供多应用/多用户调用 → Streamable HTTP(本文服务端实战以此为主)。

2.4 JSON-RPC 2.0 消息层

MCP 在传输层之上统一使用 JSON-RPC 2.0 作为消息格式。三种消息类型:

类型是否带 id是否期待响应说明
Request✅(非 null)✅期待一个带相同 id 的响应
Response✅(与请求相同 id)—包含 result 或 error(二者只存其一)
Notification❌❌单向通知,无响应

三条真实报文示例(对照第 3 章会看到完整握手流程):

// 1. Request:客户端请求「初始化」
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": { "name": "ecommerce-mcp-client", "version": "1.0.0" }
  }
}

// 2. Response:服务端返回能力
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": {}, "resources": {}, "prompts": {} },
    "serverInfo": { "name": "ecommerce-mcp-server", "version": "1.0.0" }
  }
}

// 3. Notification:客户端通知「我已初始化完成」(无 id、无响应)
{ "jsonrpc": "2.0", "method": "notifications/initialized" }

标准 JSON-RPC 错误码:-32700 解析错误、-32600 非法请求、-32601 方法不存在、-32602 参数非法、-32603 内部错误;MCP 另加 -32002 资源不存在等。


第 3 章 生命周期与一次完整调用

3.1 连接状态机

一次 MCP 连接从建立到关闭,经历四个状态:

Created ──(客户端发 initialize)──► Initialized ──(客户端发 notifications/initialized)──► Ready ──(关闭)──► Shutdown
  1. Created:连接建立(stdio 进程拉起 / HTTP 握手建立),尚未初始化;
  2. Initialized:客户端发 initialize,服务端返回能力;
  3. Ready:客户端发 notifications/initialized 确认,进入就绪态,可正常收发业务消息;
  4. Shutdown:关闭连接(stdio 关闭 stdin/stdout,HTTP 断开)。

3.2 能力协商(capabilities)

initialize 的响应里,服务端声明自己能提供什么(capabilities),客户端据此决定调用策略:

能力含义
tools提供可调用工具
resources提供只读资源
prompts提供提示词模板
logging支持日志通知
sampling(客户端侧)客户端允许服务端反向请求 LLM 采样

Spring AI 1.1.2 服务端默认开启 tools / resources / prompts 三项能力。

3.3 完整消息流(一次工具调用)

把第 2.4 节的三条报文串起来,就是一次「连接 → 发现工具 → 调工具」的完整时序:

客户端                                          服务端
   │── initialize ────────────────────────────►  │  (1) 握手,协商协议版本与能力
   │◄── initialize 响应(含 capabilities)────── │
   │── notifications/initialized ─────────────►  │  (2) 就绪
   │                                             │
   │── tools/list ────────────────────────────►  │  (3) 发现:有哪些工具
   │◄── 工具列表(name + description + inputSchema)│
   │                                             │
   │── tools/call (name=get_order, args=...) ─►  │  (4) 调用
   │◄── 结果(content 数组,isError 标志)─────── │
   │                                             │
   │── 关闭连接 ───────────────────────────────►  │  (5) 结束

对应 tools/call 的请求与响应报文:

// 请求
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "get_order",
    "arguments": { "orderId": "2024090100000001" }
  }
}

// 响应
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      { "type": "text", "text": "{\"orderId\":\"2024090100000001\",\"status\":\"已发货\",...}" }
    ],
    "isError": false
  }
}

💡 理论小结:MCP = JSON-RPC 2.0(消息层)+ tools/resources/prompts(原语)+ stdio/HTTP(传输层)+ initialize 握手(生命周期)。记住这条公式,后面看 Spring AI 的封装就不会觉得黑盒了。


第二部分 · 实战篇

第 4 章 项目准备

4.1 环境与版本矩阵

组件版本说明
JDK21+maven.compiler.release=21,record / 模式匹配开箱即用
Spring Boot3.5.9与 Spring AI 1.1.x 一一对应
Spring AI1.1.2MCP 核心(由 Alibaba BOM 锁定)
Spring AI Alibaba1.1.2.2跟踪 Spring AI 1.1.2,客户端调模型用
构建Mavenspring-ai-alibaba-bom 统一管理版本
模型阿里百炼 Qwen(qwen-plus)仅客户端需要,需自备百炼 API Key

💡 一个关键认知:MCP Server 只「提供工具」,不需要模型、不需要 API Key;模型和 Key 都在 Client 端。这正是「解耦」的体现——工具方和模型方彻底分离。

4.2 工程结构(两个项目)

本文实战拆成服务端和客户端两个 Maven 项目,职责清晰:

docker-hello/mcp/
├── ecommerce-mcp-server/                # ① MCP 服务端:暴露订单/商品工具(无模型依赖)
│   ├── pom.xml
│   └── src/main/
│       ├── java/com/example/mcp/
│       │   ├── EcommerceMcpServerApplication.java
│       │   ├── model/Product.java        # 商品(record)
│       │   ├── model/Order.java          # 订单(record)
│       │   ├── model/OrderItem.java      # 订单明细(record)
│       │   ├── service/EcommerceService.java   # 内存数据 + 查询逻辑
│       │   └── mcp/
│       │       ├── EcommerceTools.java    # @McpTool 工具
│       │       ├── EcommerceResources.java# @McpResource 资源
│       │       └── EcommercePrompts.java  # @McpPrompt 提示词
│       └── resources/application.yml
│
└── ecommerce-mcp-client/                # ② MCP 客户端:Spring Boot 应用消费上面的工具
    ├── pom.xml
    └── src/main/
        ├── java/com/example/mcp/EcommerceMcpClientApplication.java
        └── resources/application.yml

4.3 数据模型与「内存数据」约定

为了让读者无需数据库即可运行,本文用内存 List 种子数据承载订单/商品;生产环境把它换成 MyBatis-Plus + PostgreSQL(对应《AI 应用 - 电商智能客服》里的 Product / Order 业务表),MCP 层代码零改动。

// model/Product.java
public record Product(String id, String name, String category, double price, int stock) {}

// model/Order.java
public record Order(String orderId, String userId, String status,
                    List<OrderItem> items, double totalAmount) {}

// model/OrderItem.java
public record OrderItem(String productId, String productName, int quantity, double price) {}

第 5 章 MCP Server:提供工具(核心)

5.1 pom.xml

服务端用 WebMVC starter(支持 SSE / Streamable HTTP),只需一个依赖,不需要模型、不需要 API Key:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>ecommerce-mcp-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ecommerce-mcp-server</name>

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <spring-boot.version>3.5.9</spring-boot.version>
        <spring-ai.version>1.1.2</spring-ai.version>
    </properties>

    <dependencies>
        <!-- MCP Server:WebMVC 传输(SSE / Streamable HTTP) -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <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>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
            </plugin>
        </plugins>
    </build>
</project>

💡 服务端三种 starter 对照:spring-ai-starter-mcp-server(stdio)、...-server-webmvc(SSE / Streamable HTTP)、...-server-webflux(响应式)。spring-ai-starter-mcp-server-webmvc 会自动引入 spring-ai-mcp-annotations,@McpTool 等注解开箱即用。

5.2 application.yml

spring:
  ai:
    mcp:
      server:
        name: ecommerce-mcp-server      # 可选,服务端标识
        version: 1.0.0
        protocol: STREAMABLE            # 传输协议:STREAMABLE / SSE / STATELESS(不填默认 SSE)

protocol 决定服务端用哪种 HTTP 传输,可选三种值(不填默认 SSE):

协议传输方式会话适用场景
STREAMABLE单端点 POST /mcp,可回单个 JSON 或 SSE 流有状态(Mcp-Session-Id)生产推荐,对 LB / 代理 / CDN 友好
SSE两个端点:SSE 长连接 + POST 消息端点有状态长连接旧项目、需兼容
STATELESS同 STREAMABLE无状态,请求各自独立微服务、serverless、水平扩容
  • STREAMABLE(Streamable HTTP)是 MCP 2025-03-26 规范引入的新一代传输,取代 SSE:单个 POST /mcp 端点,客户端每次发一个 JSON-RPC 请求,服务端按需回单个 JSON 响应、或对「需要多条消息」的场景回 SSE 流。
  • STATELESS 是 Streamable HTTP 的无状态变体:不维护会话、每次请求自包含,请求可打到任意实例,适合多副本水平扩容和 serverless。
  • SSE 是旧传输(两个端点 + 长连接),对负载均衡不友好,不填 protocol 时默认走它;新项目直接用 STREAMABLE。

本文服务端统一用 STREAMABLE,端点即默认的 POST /mcp。

5.3 启动类与业务服务

// EcommerceMcpServerApplication.java
package com.example.mcp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class EcommerceMcpServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EcommerceMcpServerApplication.class, args);
    }
}
// service/EcommerceService.java —— 内存数据 + 查询逻辑(生产替换为 MyBatis-Plus)
package com.example.mcp.service;

import com.example.mcp.model.Order;
import com.example.mcp.model.OrderItem;
import com.example.mcp.model.Product;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class EcommerceService {

    private final List<Product> products = List.of(
            new Product("P1001", "华为 Mate 60 Pro", "手机", 6999.00, 128),
            new Product("P1002", "iPhone 15 Pro", "手机", 8999.00, 64),
            new Product("P2001", "MacBook Air M3", "笔记本", 9499.00, 32),
            new Product("P3001", "AirPods Pro 2", "耳机", 1899.00, 200)
    );

    private final List<Order> orders = List.of(
            new Order("2024090100000001", "U1001", "已发货",
                    List.of(new OrderItem("P1001", "华为 Mate 60 Pro", 1, 6999.00)), 6999.00),
            new Order("2024090100000002", "U1002", "待付款",
                    List.of(new OrderItem("P3001", "AirPods Pro 2", 2, 1899.00)), 3798.00)
    );

    /** 按订单号查询 */
    public Optional<Order> getOrder(String orderId) {
        return orders.stream().filter(o -> o.orderId().equals(orderId)).findFirst();
    }

    /** 按关键字搜索商品(匹配名称或分类) */
    public List<Product> searchProducts(String keyword) {
        return products.stream()
                .filter(p -> p.name().contains(keyword) || p.category().contains(keyword))
                .toList();
    }

    /** 全部商品(作为 resource 暴露) */
    public List<Product> allProducts() {
        return products;
    }
}

5.4 暴露工具:@McpTool + @McpToolParam

这是本文与 Function Calling 教程里「@Tool」的关键区别——MCP 原生注解 @McpTool 提供更精确的参数级描述:

// mcp/EcommerceTools.java
package com.example.mcp.mcp;

import com.example.mcp.model.Order;
import com.example.mcp.model.Product;
import com.example.mcp.service.EcommerceService;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.mcp.annotation.McpToolParam;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class EcommerceTools {

    private final EcommerceService service;

    public EcommerceTools(EcommerceService service) {
        this.service = service;
    }

    @McpTool(name = "get_order", description = "根据订单号查询订单详情(含状态、商品明细、金额)")
    public Order getOrder(@McpToolParam(description = "订单号") String orderId) {
        return service.getOrder(orderId)
                .orElseThrow(() -> new IllegalArgumentException("订单不存在:" + orderId));
    }

    @McpTool(name = "search_products", description = "按关键字搜索商品(匹配名称或分类)")
    public List<Product> searchProducts(@McpToolParam(description = "搜索关键字,如「手机」「耳机」") String keyword) {
        return service.searchProducts(keyword);
    }
}

要点:

  • description 不是注释,是「给 LLM 看的说明书」——LLM 靠它决定「什么时候调这个工具、传什么参数」。写清楚「入参含义 + 返回什么」,工具被正确调用的概率才高。
  • name 省略时默认用方法名;这里显式给蛇形命名,便于跨语言客户端识别。
  • 方法签名里的 Java 类型(record、List、基本类型)会自动生成 JSON Schema 作为工具的 inputSchema,无需手写。

📌 @McpTool vs @Tool:Spring AI 1.1.2 也支持把带 @Tool 的方法经服务端 tool-callback-converter(默认 true)自动转成 MCP 工具。但推荐直接用 @McpTool——它是 MCP 原生注解,支持参数级 @McpToolParam 描述,语义更精确。

5.5 资源与提示词:三类原语齐活

// mcp/EcommerceResources.java —— 只读资源
package com.example.mcp.mcp;

import com.example.mcp.service.EcommerceService;
import org.springaicommunity.mcp.annotation.McpResource;
import org.springframework.stereotype.Component;

import java.util.stream.Collectors;

@Component
public class EcommerceResources {

    private final EcommerceService service;

    public EcommerceResources(EcommerceService service) {
        this.service = service;
    }

    @McpResource(name = "product_catalog", uri = "product://catalog",
                 description = "当前在售商品目录(JSON 列表)", mimeType = "application/json")
    public String productCatalog() {
        return service.allProducts().stream()
                .map(p -> String.format("{\"id\":\"%s\",\"name\":\"%s\",\"category\":\"%s\",\"price\":%.2f,\"stock\":%d}",
                        p.id(), p.name(), p.category(), p.price(), p.stock()))
                .collect(Collectors.joining(",", "[", "]"));
    }
}
// mcp/EcommercePrompts.java —— 提示词模板
package com.example.mcp.mcp;

import org.springaicommunity.mcp.annotation.McpPrompt;
import org.springaicommunity.mcp.annotation.McpArg;
import org.springframework.stereotype.Component;

@Component
public class EcommercePrompts {

    @McpPrompt(name = "order_refund_prompt",
               description = "生成售后/退货场景的客服话术提示词")
    public String orderRefundPrompt(@McpArg(name = "orderId", description = "订单号") String orderId) {
        return "用户要申请退货,涉及订单号 " + orderId + "。请用礼貌、专业的客服语气,说明退货流程和注意事项。";
    }
}

💡 提示词内容来自方法返回值:@McpPrompt 没有 template 属性,模板就是方法拼好返回的字符串(参数用 @McpArg 声明、从方法入参拿到)。

💡 服务端声明、客户端手动取:@McpResource / @McpPrompt 只是服务端用来「对外声明我有哪些资源 / 提示词模板」的注解,只负责声明,不负责客户端怎么用。客户端这边,tools 有 toolcallback.enabled=true 会自动包装成 ToolCallback 参与 Function Calling;但 resources / prompts 没有这种自动包装,客户端必须用 McpSyncClient 的 listResources() / readResource() / listPrompts() / getPrompt() 手动取(见 6.5 节)。

5.6 stdio 模式(本地调用)

本地用 Claude Desktop / Claude Code 直连时,把进程作为子进程拉起的 stdio 模式最省事。改用 stdio starter 并关闭一切控制台输出(否则会污染 JSON 报文):

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
spring.ai.mcp.server.stdio=true
spring.main.banner-mode=off
spring.main.web-application-type=none
logging.pattern.console=

打包后 java -jar target/ecommerce-mcp-server-0.0.1-SNAPSHOT.jar 即可被客户端以 stdio 方式拉起。

5.7 安全提醒(重要)

Streamable HTTP / SSE 默认暴露的是一个无鉴权的 JSON-RPC 端点。换句话说,只要能访问到 http://你的机器:8080/mcp,任何人都能:

端点方法别人能干什么
tools/list枚举你暴露了哪些工具(信息泄露)
tools/call直接调用你的工具——查订单、搜商品、甚至写操作
resources/read读你暴露的资源
prompts/get取你的提示词模板

而这些工具背后往往是订单、商品、用户数据(PII)。HTTP 模式裸暴露 ≈ 把内部能力大门敞开。生产前至少做四件事:

  1. 传输:stdio 只在本机进程间通信、不开端口,相对安全(本地调试首选);要走网络就上 HTTPS,别用明文 HTTP。
  2. 鉴权:网关层加 API Key / OAuth / JWT,或用 Spring Security 保护 /mcp 端点;别靠「URL 猜不到」这种隐蔽式安全。
  3. 最小权限:只 @McpTool 暴露必要方法;写操作/删除类工具要么不暴露,要么单独加鉴权 + 审计;对入参做校验。
  4. 留痕:记录每次 tools/call 的入参/出参,便于审计与排查。

一句话:HTTP 模式的 MCP Server 默认等于「无鉴权的远程函数调用接口」,上线必须 HTTPS + 鉴权 + 最小权限,缺一不可。(完整生产清单见第 8 章)

下面给一个最简示例:用 Spring Security 给 /mcp 加 HTTP Basic 鉴权。

<!-- 服务端 pom 增加安全依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
// mcp/McpSecurityConfig.java
package com.example.mcp.mcp;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class McpSecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable)          // JSON-RPC POST 无浏览器会话,关掉 CSRF
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/mcp").authenticated()     // 只锁 MCP 端点,其余放行
                .anyRequest().permitAll())
            .httpBasic(Customizer.withDefaults())            // 用户名/密码
            .build();
    }

    @Bean
    UserDetailsService users() {
        return new InMemoryUserDetailsManager(
            User.withUsername("mcp")
                .password("{noop}secret")                    // 生产换成 BCryptPasswordEncoder
                .roles("MCP")
                .build());
    }
}

要点:

  • csrf(...).disable() 必须关:MCP 走 JSON-RPC POST,没有浏览器会话,默认的 CSRF 防护会直接挡住无 token 的 POST。
  • {noop} 是「明文密码」标记,仅演示用;生产用 new BCryptPasswordEncoder() 并去掉 {noop} 前缀。
  • 客户端要带上凭证:Server 加锁后,第 6 章的客户端连接时也要在 header 里带上用户名/密码(HTTP Basic),否则握手会被 401 拒掉。
  • 想要 API Key / OAuth:把 .httpBasic(...) 换成自定义 OncePerRequestFilter 校验 X-API-Key header,或 .oauth2ResourceServer(oauth2 -> oauth2.jwt(...)) 校验 JWT,机制相同、只是换一种凭证。

第 6 章 MCP Client:调用工具(本地 + 第三方)

6.1 先看清:客户端能调哪几类 Server

「调用 MCP 工具」这件事,本质只有两个维度——工具是谁的(本地自己的 / 第三方社区的)× 用什么传输连(stdio / HTTP)。组合起来就是四类:

本地(自己写的 Server)第三方(社区现成 Server)
stdiojava -jar 拉起自己的 jarnpx 拉起 filesystem 等社区 Server
Streamable HTTPhttp://localhost:8080 连自己的服务远程 URL https://xxx/mcp 连别人部署的

💡 好消息:在 Spring AI 里,四类调用方式完全统一——不管本地还是第三方、stdio 还是 HTTP,对业务代码来说都只是「一段连接配置」,最终都变成 ToolCallback,挂在同一个 ChatClient 上。下面用一份配置把四类都演示出来。

6.2 pom.xml

客户端需要「MCP 客户端 starter + 大模型 starter」:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>ecommerce-mcp-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ecommerce-mcp-client</name>

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <spring-boot.version>3.5.9</spring-boot.version>
        <spring-ai-alibaba.version>1.1.2.2</spring-ai-alibaba.version>
    </properties>

    <!-- 不用 parent,改用 BOM 统一管理版本:Spring Boot + Spring AI Alibaba 各导入一份 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- Spring AI Alibaba BOM 内已含 Spring AI BOM,MCP 客户端等 Spring AI 依赖的版本也由它统一管理 -->
            <dependency>
                <groupId>com.alibaba.cloud.ai</groupId>
                <artifactId>spring-ai-alibaba-bom</artifactId>
                <version>${spring-ai-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- Web:提供 REST 接口(内含内嵌 Tomcat,Servlet 容器) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 云端模型:百炼 Qwen(DashScope 原生集成,不需要走 OpenAI 兼容端点) -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
        </dependency>
        <!-- MCP Client:把 MCP 工具自动转成 ToolCallback -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-client</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
            </plugin>
        </plugins>
    </build>
</project>

6.3 application.yml(同时接本地 + 第三方)

下面是完整的连接配置,四类各演示一个(实际用哪个就留哪个,别全开):

spring:
  ai:
    # ① 模型:百炼 DashScope(只客户端需要)
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}       # 百炼 API Key(环境变量注入,勿硬编码)
      chat:
        options:
          model: qwen-plus                # 默认模型;复杂工具调用可切 qwen-max

    # ② MCP 客户端
    mcp:
      client:
        type: SYNC                        # Servlet 应用用 SYNC;WebFlux 应用用 ASYNC
        initialized: true                 # 启动时就握手并加载工具(默认懒连接,见下方说明)
        toolcallback:
          enabled: true                   # 关键:把 MCP 工具自动转成 ToolCallback

        # 传输一:stdio(本地子进程)
        stdio:
          connections:
            ecommerce-local:              # ① 本地:拉起自己打包的 ecommerce server jar
              command: java
              args:
                - -jar
                - D:/devworkspace/docker-hello/mcp/ecommerce-mcp-server/target/ecommerce-mcp-server-0.0.1-SNAPSHOT.jar
            filesystem:                   # ② 第三方:社区 filesystem MCP server(需 Node.js)
              command: npx
              args:
                - -y
                - "@modelcontextprotocol/server-filesystem"
                - "D:/tmp/mcp-files"

        # 传输二:Streamable HTTP(远程服务)
        streamable-http:
          connections:
            ecommerce-remote:             # ③ 本地 HTTP:连自己部署的 Streamable HTTP server
              url: http://localhost:8080
            # ④ 第三方远程:把 url 换成别人部署的 MCP 服务地址即可,机制完全一样
            # third-party:
            #   url: https://your-company-mcp.example.com/mcp

几个要点:

配置作用
toolcallback.enabled: true最关键的开关:MCP 工具自动转成 ToolCallback,否则工具发现不了
initialized: true启动时立刻握手并加载工具(默认是懒连接,首次调工具才连)
type: SYNCServlet 应用用 SYNC,WebFlux 用 ASYNC
stdio.connections.<name>.command/argsstdio 用「命令 + 参数」拉起子进程

⚠️ Windows 下 stdio 拉 npx 的坑:Windows 的 npx 其实是 npx.cmd 批处理脚本,不能被 ProcessBuilder 直接启动。上面 command: npx 在 Windows 会报错,要改成:

command: cmd.exe
args:
  - /c
  - npx
  - -y
  - "@modelcontextprotocol/server-filesystem"
  - "D:/tmp/mcp-files"

Linux / macOS 则直接用 command: npx 即可。

6.4 LLM 自动调用 MCP 工具

接入后,ToolCallbackProvider 里已经装好了所有 MCP 工具(本地 + 第三方的),直接挂到 ChatClient 上即可,LLM 会自己决定要不要调、调哪个:

// EcommerceMcpClientApplication.java
package com.example.mcp;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class EcommerceMcpClientApplication {

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

    @Bean
    CommandLineRunner demo(ChatClient.Builder builder, ToolCallbackProvider mcpTools) {
        return args -> {
            String answer = builder.build().prompt()
                    .user("帮我查订单 2024090100000001 的物流状态,另外有没有 1000 块以下的耳机?")
                    .toolCallbacks(mcpTools)   // 注入 MCP 工具(含 get_order / search_products)
                    .call()
                    .content();
            System.out.println(answer);
        };
    }
}

💡 运行顺序:先启动服务端,再启动客户端。若用 stdio 方式,客户端会自动拉起 server 进程,无需手动启动服务端。

💡 懒连接 vs 急连接:默认 MCP 客户端在第一次真正调用工具时才建立连接、完成握手。上面的 initialized: true 改为启动时就连接并加载工具——好处是启动阶段就能发现配置错误,坏处是启动变慢。开发期建议开 true 快速暴露问题。

6.5 手动 McpSyncClient:读 resources / prompts

自动转 ToolCallback 只覆盖 tools;要读 resources / prompts,用 McpSyncClient:

import org.springframework.ai.mcp.client.McpClient;
import org.springframework.ai.mcp.client.McpSyncClient;
import org.springframework.ai.mcp.client.transport.HttpClientStreamableHttpTransport;
import org.springframework.ai.mcp.spec.McpSchema;

// 1. 建 Streamable HTTP 传输,连到 MCP server
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport
        .builder("http://localhost:8080")
        .build();

// 2. 建同步客户端并初始化握手
McpSyncClient client = McpClient.sync(transport)
        .requestTimeout(java.time.Duration.ofSeconds(60))
        .build();
client.initialize();

// 3. 列工具 / 调工具
McpSchema.ListToolsResult tools = client.listTools();
McpSchema.CallToolResult result = client.callTool(
        McpSchema.CallToolRequest.builder("get_order")
                .arguments(java.util.Map.of("orderId", "2024090100000001"))
                .build());

// 4. 读资源(自动转 ToolCallback 覆盖不到的能力)
McpSchema.ListResourcesResult resources = client.listResources();
// 读某个资源:client.readResource(McpSchema.ReadResourceRequest.builder("resource://uri").build());

// 5. 取提示词
McpSchema.ListPromptsResult prompts = client.listPrompts();

// 6. 用完关闭
client.closeGracefully();

6.6 多 server 重名处理

接入多个 MCP server 时(本地 + 第三方),工具可能互相重名或与本地 @Tool 重名。Spring AI 提供:

  • McpToolNamePrefixGenerator:给 MCP 工具统一加前缀(如 ecommerce_get_order);
  • McpToolFilter:按需过滤掉不想要的工具。

6.7 连接加了鉴权的 Server

第 5.7 节给 Server 加了鉴权后,客户端连接就要带凭证。注意:1.1.2 的声明式配置(spring.ai.mcp.client.streamable-http.connections.*)只支持 url,不支持 header / 用户名密码,所以要连加了锁的 Server,得走编程式 McpSyncClient,用 customizeRequest / customizeClient 带上凭证。

方式一:API Key(自定义 header)

McpSyncClient client = McpClient.sync(
        HttpClientStreamableHttpTransport.builder("http://localhost:8080")
                .customizeRequest(req -> req.header("X-API-Key", "secret"))   // 加 API Key 头
                .build())
        .requestTimeout(java.time.Duration.ofSeconds(60))
        .build();
client.initialize();

方式二:HTTP Basic(用户名 / 密码)

McpSyncClient client = McpClient.sync(
        HttpClientStreamableHttpTransport.builder("http://localhost:8080")
                .customizeClient(c -> c.authenticator(new java.net.Authenticator() {
                    @Override
                    protected java.net.PasswordAuthentication getPasswordAuthentication() {
                        return new java.net.PasswordAuthentication("mcp", "secret".toCharArray());
                    }
                }))
                .build())
        .build();
client.initialize();

⚠️ 代价:编程式 McpSyncClient 是「手动调用」——不像声明式那样自动把工具包进 ToolCallback 挂到 ChatClient,你得自己 listTools() / callTool()(见 6.5)。1.1.2 里「声明式自动包装」和「带凭证」目前是二选一。


第 7 章 其它客户端调用(Claude Desktop / Claude Code)

本章解决什么问题:MCP 最大的价值在于「跨应用、跨语言」。同一个 Server,除了第 6 章的 Spring Boot 客户端,还能被 Claude Desktop / Claude Code 等任何支持 MCP 的应用直接调用。本章演示这两种最常见的接入方式。

7.1 Claude Desktop

编辑 Claude Desktop 的配置文件 claude_desktop_config.json:

方式 A:stdio(本地拉起 jar)

{
  "mcpServers": {
    "ecommerce": {
      "command": "java",
      "args": ["-jar", "D:/devworkspace/docker-hello/mcp/ecommerce-mcp-server/target/ecommerce-mcp-server-0.0.1-SNAPSHOT.jar"]
    }
  }
}

方式 B:Streamable HTTP(远程服务)

{
  "mcpServers": {
    "ecommerce": {
      "url": "http://localhost:8080/mcp"
    }
  }
}

重启 Claude Desktop 后,就能在对话里让它「查订单 2024090100000001」了。

7.2 Claude Code

方式 A:项目级 .mcp.json(放在项目根目录,团队共享)

{
  "mcpServers": {
    "ecommerce": {
      "command": "java",
      "args": ["-jar", "target/ecommerce-mcp-server-0.0.1-SNAPSHOT.jar"]
    }
  }
}

方式 B:claude mcp add 命令

claude mcp add ecommerce -- java -jar target/ecommerce-mcp-server-0.0.1-SNAPSHOT.jar

7.3 附录:curl 验证底层报文 & MCP Inspector

调试时可用 curl 直接打 Streamable HTTP 端点,确认协议层是否正常:

curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'

更推荐用官方 MCP Inspector(npx @modelcontextprotocol/inspector)图形化地做初始化、列工具、调工具的联调。


第三部分 · 最佳实践与排查

第 8 章 最佳实践

本章解决什么问题:从「能跑」到「能上线」。三条主线:生产安全、连接容错、第三方 Server 治理。

8.1 生产安全

  • HTTP 传输默认无鉴权,必须加 Spring Security / 网关层 API Key 或 OAuth;
  • 工具能力遵循最小权限:只暴露必要方法,写操作类工具务必加鉴权与审计;
  • 生产连远程 server 优先 Streamable HTTP;本地工具用 stdio。

8.2 连接与容错

  • 懒连接 vs 急连接:默认客户端首次调工具才建连接(启动慢不代表没连上);开发期设 initialized: true 快速暴露配置错误;
  • 设置合理的 requestTimeout,工具执行过长要有超时与降级;
  • listTools / listResources / listPrompts 支持分页(传 cursor,循环到 nextCursor 为空)。

8.3 第三方 MCP Server 治理

接入第三方 Server 时,因为它运行的是「别人的代码」,要格外谨慎:

关注点建议
信任边界优先用官方/大厂维护的 Server,社区 Server 先审代码再接入
权限最小化filesystem 这类 Server 只给它最小目录、只读权限
输入校验工具参数最终可能落库/落盘,服务端对参数做校验
可观测记录每次工具调用的入参/出参,便于审计与排错

第 9 章 高频坑速查

坑现象解法章节
stdio 日志污染客户端解析 JSON 失败关闭 banner / web / console 日志5.6
工具重名多个 server 工具冲突McpToolNamePrefixGenerator / McpToolFilter6.6
版本不对齐自动配置失效、Bean 缺失Spring AI 1.1.2 ↔ Spring Boot 3.5.x ↔ JDK 214.1
toolcallback 没开工具发现不了spring.ai.mcp.client.toolcallback.enabled=true6.3
Windows 拉 npx 失败stdio 连接报错command: cmd.exe + args: /c npx ...6.3
description 没写LLM 乱传参、不调工具认真写 @McpTool/@McpToolParam 的 description5.4
客户端启动慢/不连以为是没连上默认懒连接;initialized: true 改急连接6.4
HTTP 裸暴露工具被公网任意调用加 API Key / OAuth 鉴权5.7

附录 · 速查表

依赖速查

用途artifact
MCP Server(stdio)spring-ai-starter-mcp-server
MCP Server(WebMVC,SSE/Streamable HTTP)spring-ai-starter-mcp-server-webmvc
MCP Server(WebFlux)spring-ai-starter-mcp-server-webflux
MCP Clientspring-ai-starter-mcp-client
云端模型(百炼 DashScope)spring-ai-alibaba-starter-dashscope

配置 key 速查

配置作用
spring.ai.mcp.server.protocol服务端传输:STREAMABLE / SSE / STATELESS(默认 SSE)
spring.ai.mcp.server.stdiostdio 模式开关(stdio starter)
spring.ai.mcp.client.enabled启用 MCP 客户端
spring.ai.mcp.client.typeSYNC(Servlet)/ ASYNC(WebFlux)
spring.ai.mcp.client.initialized是否启动时握手加载工具(默认懒连接)
spring.ai.mcp.client.toolcallback.enabled自动把 MCP 工具转 ToolCallback
spring.ai.mcp.client.stdio.connections.<name>.command/argsstdio 连接(命令 + 参数)
spring.ai.mcp.client.{sse,streamable-http}.connections.<name>.urlHTTP 连接地址
spring.ai.dashscope.api-key百炼 API Key(客户端)
spring.ai.dashscope.chat.options.model模型名(如 qwen-plus)

注解速查

注解作用
@McpTool把方法暴露为 MCP 工具
@McpToolParam工具参数描述
@McpResource暴露只读资源
@McpPrompt暴露提示词模板(内容来自方法返回值,无 template 属性)
@McpArg提示词参数声明(@McpPrompt 方法的入参)

注:这些注解来自 org.springaicommunity:mcp-annotations(由 spring-ai-mcp-annotations 传递引入),包名是 org.springaicommunity.mcp.annotation。

术语表

词一句话
MCP模型上下文协议:把外部工具/数据源统一成一套 JSON-RPC 接口
MCP Server工具提供方,暴露 tools / resources / prompts
MCP Client工具调用方,连接 Server 并发现/调用工具
MCP Host承载 AI 对话的宿主(Claude Desktop、Spring Boot 应用)
tools可调用的函数,自动转 ToolCallback
resources只读数据源,需 McpSyncClient 手动读
prompts提示词模板,需 McpSyncClient 手动取
stdio本地子进程传输(stdin/stdout)
Streamable HTTP基于 HTTP 的远程传输,生产推荐
JSON-RPC 2.0MCP 的消息格式(Request / Response / Notification)
initialize握手请求,协商协议版本与能力
capabilities能力声明(tools / resources / prompts 等)
Function Calling模型输出「调哪个函数 + 传什么参」,由应用执行
ToolCallbackSpring AI 里「工具」的统一抽象,MCP 与 @Tool 的汇合点

一句口诀收官:MCP 用统一协议把「外部工具」变成「应用内工具」;Spring AI 里服务端用 @McpTool 暴露,客户端用 ToolCallbackProvider 接入,最终都汇入同一条 Function Calling 链路。