28-SpringAIMCP完整接入

O泡李华 6

Spring AI 怎样接入 MCP?连接、发现、工具与授权

配置一个 MCP 地址不等于业务工具已经安全可用。传输、初始化、工具发现、参数与用户授权分别决定调用能否成功;任何一层被混淆,都会让调试停留在含混的失败信息里。

Spring AI 接入片段使用 Java 17、Spring Boot 3.5.3 与 Spring AI 1.1.8;协议与第三方平台示例另行标明适用范围。

MCP 协议接入

1. 先理解消息与生命周期

固定协议版本,并区分 stdio、旧 SSE 与 Streamable HTTP。

消息格式与传输方式是不同层次

MCP 使用 JSON-RPC 2.0 表达请求、响应和通知。请求通常具有 id,用于把响应与请求关联;通知没有请求编号,也不等待对应响应。

stdio 与 HTTP 负责把这些消息送到对方。更换传输不改变工具调用的业务含义,但会改变进程管理、认证、断线与会话处理方式。

下面是一个工具调用请求的消息示例,假设已经完成初始化:

{
  "jsonrpc": "2.0",
  "id": 12,
  "method": "tools/call",
  "params": {
    "name": "getMyOrderStatus",
    "arguments": {"orderNo": "ORDER001"}
  }
}

id 用于协议关联,并不自动成为业务幂等键。断线后换一个请求编号,也不意味着可以安全重复创建工单。

stdio 为什么不能把日志写到 stdout?

stdio 模式通常由客户端启动服务端子进程,通过标准输入发送消息,通过标准输出接收消息。协议消息按规定的行格式传输。

如果 Server 在标准输出打印启动横幅、调试日志或 SQL,客户端可能把它当作协议消息解析。日志应该走标准错误或其他日志输出渠道。某个服务“命令行能启动”,不等于它的标准输出符合协议。

stdio 的优势是本地配置直观,不必为每个本地工具建立 HTTP 入口。代价是客户端承担进程启动、工作目录、参数、退出清理等管理。相对路径由实际进程目录解释,不能假定与配置文件目录相同。

本地进程也可能访问网络、文件或数据库。采用 stdio 并不会自动限制它的权限,能力范围仍由运行账户、工具代码和执行环境决定。

Streamable HTTP 如何传递消息?

2025-06-18 版使用 Streamable HTTP 支持远程通信。客户端向统一 MCP 端点发送 POST,服务端根据协议返回 JSON 或 SSE 消息流;GET 可用于建立服务端消息流,并不是要求所有服务都提供无限长 GET 连接。传输规范。

它并不要求必须使用 HTTP/2。SSE 是一种响应编码与推送方式,也不能据此把 Streamable HTTP 等同于旧版 HTTP+SSE 传输。

服务端可以在初始化时建立会话,并返回 Mcp-Session-Id。后续请求按规范携带会话标识。这个值负责协议会话关联,不能替代认证凭证,更不能用它直接确定业务租户。

代理需要正确转发请求头和流式响应;如果代理缓存 SSE,用户可能看到工具调用长时间没有进展,但服务端实际上已经完成处理。

初始化时到底协商什么?

客户端先声明协议版本、客户端信息和能力;服务端返回选择的协议版本、服务端信息和能力。客户端确认兼容后发送初始化完成通知,然后进入正常交互。

Client → initialize(version, capabilities, clientInfo)
Server → result(version, capabilities, serverInfo)
Client → notifications/initialized
Client → tools/list
Client → tools/call

服务器应用版本与协议版本是不同概念。例如工具服务的 1.0.0 描述它自己的发布版本,不能代替 2025-06-18 这样的协议版本标识。

SDK 经常代办初始化与请求头处理。出现不兼容时,应该检查 SDK 实际支持的版本和协商日志,而不是手动覆盖一个字段强行伪装兼容。

旧 SSE 与断线恢复的边界

旧版 HTTP+SSE 使用不同端点组织消息,新版用统一端点的 Streamable HTTP。需要兼容旧服务时,客户端应明确选择旧传输,不要将 /sse 地址填入新传输配置后期待自动转换。

断线恢复也不是默认的“从最后一个 Token 接着来”。能否恢复,取决于服务端是否提供事件标识、保存事件和相应恢复能力。会话过期后可能需要重新初始化。

网络断开后要同时考虑两件事:协议状态是否还有效,业务操作是否已经发生。重新初始化可以修复协议状态,无法回滚已经提交的数据库事务。写工具需要独立的执行记录与幂等策略。

排查时先保存非敏感元信息:传输方式、端点、协议版本、HTTP 状态、JSON-RPC 方法和错误码。仅有最终异常字符串,往往不足以区分代理故障与服务端业务错误。

2. Spring AI Client 怎样配置和排查

依赖、端点和工具集合按 Spring AI 1.1.8 阅读。

MCP Client 在应用中承担什么?

客户端负责与 MCP Server 建立协议会话,协商能力,获取工具描述,发送调用请求并处理结果。它并不等于聊天模型。模型决定提出什么工具请求,客户端完成协议通信,业务服务决定请求是否有权执行。

先用固定参数手动调用一个只读工具,再把它提供给模型,可以分别排查通信和模型选择。如果手动调用已经失败,模型生成更多解释也无法修复服务端的路径或权限配置。

依赖与 HTTP 连接配置

在已有 Boot 项目中导入 Spring AI 1.1.8 BOM,并加入客户端 starter。下面是 pom.xml 中两个对应片段,保留项目原有 Boot parent 和其他依赖:

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

在 application.properties 配置一个 Streamable HTTP 连接:

spring.ai.mcp.client.name=business-assistant
spring.ai.mcp.client.version=1.0.0
spring.ai.mcp.client.type=SYNC
spring.ai.mcp.client.request-timeout=10s
spring.ai.mcp.client.streamable-http.connections.orders.url=http://localhost:8090
spring.ai.mcp.client.streamable-http.connections.orders.endpoint=/mcp

1.1.8 的配置把服务基础地址与协议端点分开。这里预期访问的是 http://localhost:8090/mcp,不要在基础地址里再重复拼一次 /mcp。客户端的 version 是应用版本标识,不是协议版本开关。对应版本配置说明。

服务端若要求认证,还需要通过该版本支持的传输定制能力传入凭证。上面的配置没有实现认证,也不能据此直接访问受保护的业务工具。

按连接、初始化、发现、调用逐层检查

连接成功只代表能到达端点。接下来客户端发送 initialize,协商协议版本和能力,然后完成初始化通知。只有服务端声明支持工具,才能按协议查询工具列表。

可以把检查结果分成四列:

层次 需要确认的内容 常见问题
传输 地址、端口、TLS、认证 404、证书错误、401
初始化 协议兼容、双方能力 版本不支持、初始化超时
发现 工具名称和输入结构 名称变更、分页未取全
执行 参数、权限、业务结果 参数无效、无权查询、下游失败

工具列表可能分页,工具也可能动态变化。应用不应永久相信启动时保存的一份工具列表。刷新策略和变化通知要结合 SDK 支持的能力实现。

工具可发现,不代表全部交给模型

一个服务端可能提供查询和管理工具。助手只需要查询订单,就应该过滤掉删除、导出全量数据等工具。工具暴露越多,模型选择的复杂度越高,授权和审计也越难维护。

多个 Server 可能存在同名工具。需要建立“连接标识、服务端工具名、模型可见名称”的映射,避免日志里一个 search 无法判断到底查询了哪个系统。

接入聊天应用时,工具回调只是执行入口,仍需要应用限制调用次数、超时和结果大小。不能因为 starter 提供了自动配置,就假设预算、租户传播和重复执行都已经解决。

区分协议错误与工具业务错误

格式错误、未知方法等属于协议层问题;工具执行后返回“没有权限”或“订单不存在”,属于工具结果。MCP 工具结果可以表达执行错误,客户端应保留这一信息,不要把错误文本当成订单事实。

超时尤其需要谨慎:客户端没收到响应,不能证明服务端没有执行。只读查询通常可以受控重试;创建工单等操作应先查询执行记录,不能直接复制请求再次执行。

日志应记录连接名、方法、工具名、请求标识、结果分类和耗时。凭证、完整业务数据和模型上下文不宜原样打印。对用户返回的错误应说明可恢复方式,例如“查询暂时失败,可以稍后重试”,并附可用于排查的请求编号。

3. Server 怎样封装业务能力

模型参数不决定当前用户,服务身份也不代替对象授权。

工具接口应该比内部接口更窄

内部订单实体可能包含手机号、地址、成本和内部备注。助手回答“是否发货”只需要状态、更新时间和物流摘要。工具返回一个专用 DTO,可以缩小泄漏范围,也使模型更容易理解结果。

工具名称应体现能力,例如 getMyOrderStatus;描述说明适用场景与限制,例如“只查询当前登录用户的订单状态,不执行取消或退款”。描述帮助模型选择,实际限制由服务器代码执行。

参数只保留业务必需项。tenantId、角色和当前用户不能由模型自行填写,因为这些参数正是授权的依据。它们来自经过认证并验证的服务器上下文。

建立 Server 的协议入口

在 Java 17、Boot 3.5.3 项目中导入 Spring AI 1.1.8 BOM,加入下面的依赖片段:

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

application.properties 中配置协议:

server.port=8090
spring.ai.mcp.server.name=order-query-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.type=SYNC
spring.ai.mcp.server.protocol=STREAMABLE

这个配置建立 MCP 传输入口,不包含业务认证实现。身份链路需要与 Spring Security 或现有可信认证组件结合,不能把网络可达等同于有权访问。1.1.8 Server 文档。

工具注册采用该版本支持的注解或 ToolCallback 适配方式。二者的参数上下文和异常映射需要核对对应 API;不要同时注册同一工具,造成重复名称。

先定义契约,再连接业务服务

下面是工具输入的 JSON Schema 设计片段,不是完整 tools/list 响应:

{
  "type": "object",
  "properties": {
    "orderNo": {
      "type": "string",
      "pattern": "^[A-Z0-9]{8,32}$",
      "description": "用户提供的订单编号"
    }
  },
  "required": ["orderNo"],
  "additionalProperties": false
}

Schema 能约束输入形状,不能替代服务端校验。服务器仍要验证长度、格式和查询权限。即使客户端没有执行 Schema 校验,业务入口也应保持同样边界。

业务调用流程可以写成以下伪代码。principal 表示认证模块提供的可信身份,不是模型参数:

验证 principal 有效且具有订单查询权限
校验 orderNo 格式
查询 tenantId = principal.tenantId
  且 ownerId = principal.userId
  且 orderNo = 输入编号的订单
不存在或不可访问 → 返回统一的不可查询结果
存在 → 映射为脱敏 OrderStatusView
记录授权结果、查询时间和请求编号

“不存在或不可访问”使用统一对外结果,可以减少通过编号探测订单是否存在的机会。内部审计仍应区分真实不存在与拒绝访问,便于排查。

服务身份与用户身份不能混淆

助手服务访问 MCP Server,可能先用应用凭证证明“这是允许访问的客户端”。但同一个助手服务会处理很多用户,应用凭证不能证明每次调用对应哪个人。

需要额外的受信任用户上下文:例如经过验证的用户令牌,或由可信服务签发且具有限定受众、有效期和范围的委托凭证。Server 验证后再建立用户与租户上下文。

不能简单相信一个普通 HTTP 头里的 X-User-Id。如果外部请求也能填写它,攻击者就能冒充其他用户。来源约束、签名验证和边界处清除伪造头部都是整体设计的一部分。

返回、超时与审计也属于工具设计

返回中加入 queriedAt 和状态版本,可以提醒模型该结果是查询时的快照。结果设置条数和大小上限,避免一条工具调用把大量敏感数据加入上下文。

异常分类至少区分参数无效、访问被拒、业务不可用和内部错误。内部堆栈只进入受保护日志,工具结果不应暴露 SQL、连接串或服务凭证。

后续增加写工具时,必须另行设计授权、用户确认、幂等和执行记录。工具的只读提示或注解只是元信息,不能作为访问控制保证。服务端要根据真实方法行为维护契约。

4. 设计判断与适用边界

接入先用固定参数验证只读工具,再让模型选择。把网络、协议和模型决策分开观察,保留连接标识与结果分类。工具列表只开放任务需要的能力,多个 Server 同名工具要建立清楚映射。

本地 ToolContext 不会自动形成远端可信用户委托。Server 验证身份与访问范围,返回脱敏 DTO。会话 ID、JSON-RPC 编号和业务幂等键分别管理,连接恢复后不无条件重放写操作。

5. 问答

问:MCP Client 与 ChatClient 是同一个东西吗?

答:不是。前者处理工具协议连接,后者组织模型调用。应用可以把 MCP 工具接入聊天流程。

问:HTTP 200 就能证明工具调用成功吗?

答:不能。还要解析协议响应及工具结果中的错误信息。

问:工具参数符合 Schema,是否就可以执行?

答:还需要业务规则和权限检查。结构合法与访问合法是两件事。

问:工具返回实体类省事吗?

答:容易暴露额外字段,也把内部结构绑定到外部契约。专用 DTO 更便于控制数据范围。

问:MCP 就是一个特殊 REST 接口吗?

答:它可以通过 HTTP 传输,但还有统一的消息、初始化和能力发现机制,不能只按普通业务路径理解。

问:stdio 是否更安全?

答:减少了远程入口,但进程仍具有实际运行权限。是否安全取决于权限和工具实现。

参考资料