面试知识库

主题 10 · MCP 协议与 Agent 互操作#

定位:从 MCP 协议规范到多客户端互操作——一个 MCP Server 如何同时服务 IDE 插件、Claude Desktop 和自研 Agent


一、通用知识#

1.1 核心概念与原理#

MCP 四层架构

MCP 协议分四层,自底向上:

  • Transport 层(通信传输):支持三种模式——stdio(本地进程标准流,零网络开销)、SSE(Server-Sent Events 长连接,浏览器友好)、Streamable HTTP(无状态 HTTP POST + 可选流式返回,生产首选)。
  • Tool 层(工具注册与调用):每个工具声明 name、description、inputSchema,客户端连接后通过 tools/list 自动发现。这是 MCP 使用频率最高的层——90% 的场景都是 Tool。
  • Resource 层(资源访问):暴露文件/数据库/代码仓库等结构化资源,客户端可以直接读取。
  • Prompt 层(提示词模板共享):Server 可以共享预定义的 prompt 模板,客户端复用。

面试怎么讲:

“MCP 就是 LLM 的 USB-C——标准化接口让任何 AI 客户端都能发现和调用你的工具,不用每个客户端单独适配。协议分四层:Transport → Tool → Resource → Prompt,实际落地最核心的是 Tool 层。”

MCP vs REST API 本质区别

两者的核心差异体现在三个维度:

维度REST APIMCP
发现机制人读 Swagger 文档,手写调用代码客户端 tools/list 自动获取 schema,模型原生理解
适配成本每个 AI 客户端写一套适配层一次暴露、N 端即插即用
消费者面向开发者面向模型

面试怎么讲:

“REST 是’你知道有什么端点就调什么’,MCP 是’自动发现 + 结构化调用 + 模型原生理解’。一个 MCP Server 上线后,Cursor、Claude Desktop、Continue、Cline 等任何支持 MCP 的客户端都能即插即用——零适配代码。”

Server vs Client 角色

  • Server(工具提供方):暴露 tools/resources/prompts,处理 tools/list(工具发现)和 tools/call(工具调用)请求。不关心谁在调用——只校验鉴权、验证参数。
  • Client(AI 客户端):连接 Server,发现可用工具,将工具 schema 注入 LLM 上下文。不关心工具怎么实现——只关心 schema 是什么、返回什么。
  • 一对多架构:一个 Server 可被多个 Client 同时连接(Cursor + Claude Desktop + 自定义 Agent 同时调用同一个 DocMind MCP Server)。

面试怎么讲:

“这种解耦让工具提供方和 AI 客户端可以独立迭代——Server 加了新工具,旧客户端不受影响,新客户端下次 tools/list 自动发现。”

MCP vs A2A

两者的关系是互补不竞争,解决的是不同层次的问题:

  • MCP(Agent-to-Tool):解决”AI 客户端怎么发现和调用你的工具”,核心是 Tool Discovery + JSON-RPC 调用。类比:给 Agent 装上手(能操作工具)。
  • A2A(Agent-to-Agent):Google 2025 年提出,解决”Agent 之间怎么协作”,核心是 Agent Card(能力自描述)+ Task/Message(任务分配和状态同步)。类比:给 Agent 装上嘴(能跟其他 Agent 说话)。

面试怎么讲:

“DocMind 目前只需要 MCP——外部 AI 客户端调用我们的检索工具,不是两个 Agent 在对话协作。如果以后要做多 Agent 场景(比如 DocMind Agent 和一个代码分析 Agent 协作),那才需要 A2A。先做 MCP 暴露工具能力,A2A 等真有需求时再引入。”

MCP 工程三原则

  1. 工具原子化——每个工具做一件事,粒度越细模型选择越准。DocMind 把语义检索和关键词检索拆成两个独立工具(doc_search vs keyword_search),而不是一个”搜索”工具加 mode 参数——因为模型在理解”这两个工具分别擅长什么”的时候比理解”一个工具有两种 mode”更准确。

  2. 幂等只读——检索类工具不产生副作用,调错了最多浪费一次算力,不会搞坏数据。这也是为什么 store_memory 被 agentic 循环白名单排除——写操作在 tool-calling 循环中风险太高。

  3. 向后兼容——新增工具不影响旧客户端(旧客户端不认识的工具会被忽略),schema 变更用 optional 参数保持兼容。

面试怎么讲:

“这三条听起来简单,但在实际接入 Cursor 和 Claude Desktop 时每条都踩过坑才沉淀下来的——工具不够原子导致模型选错、写操作工具被 LLM 误触发、schema 不兼容导致旧客户端报错。”

MCP 生态现状

面试怎么讲:

“截至 2025 年中,MCP 已经被主流 AI IDE 和客户端采纳:Claude Desktop 原生支持、Cursor 和 Continue 作为 IDE 插件支持、Cline 和 Windsurf 也跟进了。Server 端实现最成熟的是 TypeScript SDK(Anthropic 官方维护),Python SDK 次之,Java 生态靠 Spring AI 的 MCP Server Starter 补位。Spring AI 的优势是和 Spring Boot 生态天然集成——Security、Web、actuator 全部复用,劣势是版本迭代比 TS/Python SDK 慢一拍。对于 Java 后端团队来说,Spring AI MCP 是当前最务实的选择。”

MCP 工具调用的完整生命周期

从 Client 连接到工具执行完成的完整链路:

  1. 连接:Client 通过 Transport 层连接 Server(Streamable HTTP 就是一个 POST 请求)
  2. 握手:Client 发 initialize 请求,Server 返回协议版本和能力声明
  3. 工具发现:Client 发 tools/list,Server 返回所有工具的 name/description/inputSchema
  4. 注入上下文:Client 把工具信息注入 LLM 的 system prompt 或 tool-calling 协议
  5. 用户提问:用户输入问题,LLM 决定调用某个工具
  6. 工具调用:Client 发 tools/call,body 包含 tool name 和 arguments
  7. 执行:Server 路由到对应的 @Tool 方法,执行业务逻辑,返回 result
  8. 继续生成:Client 把工具结果喂给 LLM,继续生成最终回答

面试怎么讲:

“整个流程中 Server 是无状态的——每个 tools/call 独立处理,不需要维护会话。这也是为什么 Streamable HTTP 比 SSE 更适合生产——完全无状态,天然支持负载均衡。”

MCP 在 Java 生态的定位

面试怎么讲:

“MCP 的 SDK 生态目前 TypeScript 最成熟(Anthropic 官方维护,支持 stdio/SSE/Streamable HTTP 全部 Transport),Python 次之(社区活跃度高),Java 主要靠 Spring AI 的 MCP Server Starter。Spring AI 的好处是不需要直接操作 JSON-RPC——MethodToolCallbackProvider@Tool 方法自动转成 MCP 工具定义,spring-ai-starter-mcp-server-webmvc 自动启动 HTTP 端点处理 tools/listtools/call。劣势是 Spring AI 版本还在快速迭代,API 可能会变。但对于已经用 Spring Boot 的项目来说,这是最低摩擦的接入方式——不需要引入新的框架或语言。“

1.2 业界主流方案对比(表格形式)#

Agent 互操作协议对比

协议机制优势劣势适用场景
MCP (Anthropic)JSON-RPC 2.0 + Tool Discovery标准化、生态广(Cursor/Claude Desktop/Continue 等均支持)协议较新、Java SDK 生态弱于 Python/TS多客户端工具暴露
A2A (Google)Agent Card + Task/MessageAgent 间能力发现 + 任务协作生态未成熟、规范仍在迭代多 Agent 互操作场景
Custom REST + OpenAPIHTTP + Swagger 文档成熟稳定、无额外依赖每个 AI 客户端需手写适配层传统 API 集成
gRPCProtocol Buffers + 双向流高性能、强类型约束学习成本高、浏览器不友好内部微服务通信
OpenAI Plugins (deprecated)REST manifest + OAuth早期开放市场尝试已废弃、被 GPTs/Actions 替代历史参考

MCP Transport 模式对比

传输机制连接模式优势劣势适用场景
stdio进程标准流(stdin/stdout)本地进程零网络开销、延迟最低只能本地、不可跨网络本地 IDE 插件(Cursor 本地模式)
SSE (Server-Sent Events)HTTP 长连接单向推送有状态长连接浏览器原生兼容、实时推送有状态不利负载均衡、连接断开需重连Web 客户端、开发调试
Streamable HTTPHTTP POST + 可选流式返回无状态请求/响应可负载均衡/CDN 友好、容器扩缩容友好较新、部分客户端未支持生产部署首选

1.3 关键论文与技术要点#

论文/规范年份核心贡献面试一句话
MCP Specification (Anthropic)2024标准化 LLM-工具交互协议,四层架构”LLM 的 USB-C——连接标准化”
A2A Protocol (Google)2025Agent 间协作协议,Agent Card + Task 模型”Agent Card 是 Agent 的名片”
JSON-RPC 2.0 Specification2010轻量级远程过程调用协议,MCP 底层传输”无状态、轻量、双向——MCP 的骨架”
OpenAPI 3.1 Specification2021REST API 描述标准”MCP 之前的’工具发现’标准——人能读但模型读不好”
Anthropic Tool Use Documentation2024结构化工具定义最佳实践”工具描述 = 给模型的 prompt,示例比说明文字有效 10 倍”

Tool Description 最佳实践(重点展开)

面试怎么讲:

“MCP 工具的 description 本质上是给模型看的 prompt。写好 description 比写好代码更重要——代码写差了功能不对,description 写差了模型根本不会调你的工具或者传错参数。几条实战经验:第一,description 开头写’做什么’而不是’是什么’——语义向量文档检索:根据语义相似度从知识库中检索最相关的文档切片,适用于通用知识查询、概念解释等场景这是一个文档检索工具 好得多。第二,参数描述加具体示例——尤其是复杂类型(Map、List、嵌套对象),模型对示例的理解远强于类型说明。第三,描述使用场景和不使用场景——适用于通用知识查询、概念解释等场景 帮助模型区分这个工具和 keyword_search 的适用边界。第四,保持简洁——description 会占用 system prompt 的 token 窗口,6 个工具每个 200 字就是 1200 字,太冗长会挤压其他上下文。”

JSON-RPC 2.0 与 MCP 的关系

面试怎么讲:

“MCP 的底层传输协议用的是 JSON-RPC 2.0——一个 2010 年就有的轻量级 RPC 标准。为什么选它?因为足够简单:请求就是 {jsonrpc: '2.0', method: 'tools/list', params: {}, id: 1},响应就是 {jsonrpc: '2.0', result: [...], id: 1}。没有复杂的握手协议、没有二进制序列化、没有版本协商——任何语言都能轻松实现。MCP 在 JSON-RPC 之上定义了标准的 method 名(tools/list, tools/call, resources/list 等)和消息格式,相当于在通用的 RPC 框架上建了一套领域协议。这和 gRPC 用 Protocol Buffers 不同——JSON-RPC 牺牲一些性能换来了极低的接入门槛。“

1.4 常见面试问答#

Q1:MCP 和 REST API 根本区别是什么?

A:三个维度的本质差异。第一是发现机制——REST 靠人读 Swagger 文档写代码调用,MCP 靠 tools/list 让客户端自动获取所有工具 schema,模型原生理解每个工具干什么、参数是什么类型。REST 的 OpenAPI 规范虽然也是机器可读的,但它是设计给代码生成器用的(生成 HTTP 客户端代码),不是给 LLM 用的——模型不会自己去读一个 Swagger JSON 再决定调什么。第二是标准化——REST 每家 API 格式不一样(有的 JSON 有的 XML,有的 camelCase 有的 snake_case,错误码各不相同),MCP 统一了 JSON-RPC 2.0 + inputSchema/outputSchema,所有工具长一个样。第三是生态——一个 MCP Server 上线后,Cursor/Claude Desktop/Continue/Cline 等十几个 AI 客户端即插即用,而 REST 每接一个新客户端都要写适配代码。本质区别一句话:MCP 把工具接口从”面向开发者”升级到了”面向模型”。

Q2:为什么不直接暴露 REST 让 AI 客户端调?

A:可以但效率低,核心差异在适配成本。每个 AI 客户端需要单独写一个适配层去理解你的 REST API——端点叫什么、参数怎么传、响应怎么解析、错误码什么含义。有些客户端(如 Cursor)甚至需要你写一个 manifest 文件描述你的 API 才能让模型理解。MCP 把这些全标准化了——你只需要给每个工具写 @Tool 注解声明 name/description/schema,Spring AI 自动注册为 MCP 工具,客户端连上就能用。我们算过:如果为 Cursor、Claude Desktop、Continue 三个客户端分别写 REST 适配层,每个大约 200 行胶水代码 + 文档维护。MCP 方案零适配代码。而且未来每新增一个客户端,MCP 是零边际成本,REST 是线性增长。

Q3:A2A 和 MCP 什么关系?什么时候用哪个?

A:互补不竞争,解决的是不同层次的问题。MCP 是 Agent-to-Tool 协议——解决”AI 客户端怎么发现和调用你的工具”,核心是 Tool Discovery + JSON-RPC 调用。A2A 是 Agent-to-Agent 协议——解决”两个自主 Agent 之间怎么协作”,核心是 Agent Card(能力自描述)+ Task/Message(任务分配和状态同步)。用类比来说:MCP 是给 Agent 装上手(能操作工具),A2A 是给 Agent 装上嘴(能跟其他 Agent 说话协商任务)。当前阶段大多数场景只需要 MCP——单 Agent + N 个工具覆盖绝大部分需求。A2A 适用于更复杂的多 Agent 场景,比如一个知识检索 Agent 和一个代码分析 Agent 需要协作完成跨领域任务——DocMind 当前不需要,但如果未来要做”RAG Agent + 数据分析 Agent”联合问答,A2A 就是连接它们的协议层。实际项目中的建议是先做 MCP 暴露工具能力,A2A 等真有多 Agent 协作需求时再引入。

Q4:Transport 怎么选?

A:看部署环境和客户端类型。本地 IDE 插件(如 Cursor 本地模式、VS Code Continue)用 stdio——进程间通信零网络开销,延迟最低,但只能本地、不可跨网络。Web 客户端或开发调试用 SSE——浏览器原生支持 EventSource API、实时推送友好,但 SSE 是有状态长连接,Nginx 默认配置会在 60s 超时断开,容器扩缩容时已有连接不会自动迁移,需要客户端做重连逻辑。生产部署用 Streamable HTTP——每次请求独立无状态,可以过 Nginx 负载均衡、K8s HPA 容器扩缩容不需要粘性会话,CDN 也友好(GET 请求可缓存)。DocMind 的演进路径就是 SSE → Streamable HTTP:最初用 SSE,发现容器重启时大量长连接断开引发重连风暴,Claude Desktop 偶发断连。切到 Streamable HTTP 后完全消除了这些问题。

Q5:MCP 工具发现机制具体怎么工作?

A:标准流程三步。第一步,Client 连接 MCP Server 后发送 tools/list JSON-RPC 请求。第二步,Server 返回一个数组,每个元素包含工具的 name(如 “doc_search”)、description(“语义向量文档检索:根据语义相似度从知识库中检索最相关的文档切片”)、inputSchema(JSON Schema 描述参数类型和约束,如 query: string, topK: integer, kbIds: array<long>)。第三步,Client 拿到这些后注入 LLM 的 system prompt 或 tool-calling 协议中,模型就知道有哪些工具可用、每个工具的参数怎么填。关键细节:description 不只是给人看的注释,它是模型决定”什么时候调这个工具”的核心依据——写得好不好直接影响工具选择准确率。新增工具后,客户端下次 tools/list 自动获取——零配置发现,这就是为什么叫”即插即用”。

Q6:MCP 安全怎么做?

A:MCP 协议本身不定义鉴权——安全由实现方自行加。实践中分三层:Transport 层加 TLS 加密传输(HTTPS),防中间人窃听。应用层加身份认证——MVP 用 API-Key(简单、机器客户端友好),生产升级 OAuth 2.1 资源服务器。工具层做权限控制——不是所有认证用户都能调所有工具,需要根据角色或 scope 限制可用工具集。另外两个容易忽略的点:第一,API-Key 比较必须用 constant-time 算法(如 MessageDigest.isEqual),否则攻击者可以通过响应时间差异逐位猜测密钥——普通 String.equals 是逐字符比较,匹配越多耗时越长,侧信道就泄露了。第二,Filter Chain 顺序很重要——MCP 鉴权 filter 必须在 JWT filter 之前,否则 MCP 请求会被 JWT filter 先拦截返回 401(MCP 客户端不带 JWT)。


二、DocMind 实践#

30 秒口述版#

“DocMind 用 Spring AI 的 MCP Server 对外暴露了 6 个工具端点,让 Cursor 和 Claude Desktop 等外部 AI 客户端能即插即用地调用我们的检索能力。核心设计有三个亮点:第一是零配置注册——MethodToolCallbackProvider 自动扫描 @Tool 注解生成 MCP schema,40 行配置代码替代了预估 2000+ 行的自研方案。第二是安全层——McpApiKeyAuthFilterMessageDigest.isEqual() 做 constant-time 比较防时序攻击,过滤器链顺序 McpApiKey → JWT → UsernamePassword 确保 MCP 请求和普通用户请求各走各的鉴权路径。第三是 Transport 选型——从 SSE 切到 Streamable HTTP 解决了长连接不利于负载均衡的问题。实测 Cursor 自动发现 6 个工具后首次调用 5/6 参数正确,唯一出错的 filters Map 参数通过在 @ToolParam 描述中加 JSON 示例修复——工具描述本质是给模型看的 prompt。“

详细展开#

Situation:

DocMind 的检索工具(语义检索、关键词检索、Web 搜索、记忆召回、知识库元信息查询)已经通过 @Tool 注解标准化,内部 agentic 循环和 Worker 通过 Java 直调使用。但随着 AI IDE 和 AI 客户端生态的发展,外部接入需求出现了:开发者希望在 Cursor IDE 中直接查询 DocMind 知识库辅助编码,运营团队希望在 Claude Desktop 中调用 DocMind 检索能力做内容审核。如果为每个客户端写一套 REST API 适配层——Cursor 需要一套 manifest + handler,Claude Desktop 需要另一套,未来的 Continue/Cline 又需要新的适配——维护成本随客户端数量线性增长,且每次工具变更需要同步更新所有适配层。需要一个”一次暴露、多端消费”的标准化方案。

Action:

  1. MCP Server 接入(零配置注册): 引入 spring-ai-starter-mcp-server-webmvc 依赖。McpToolsConfig 中用 MethodToolCallbackProvider.builder().toolObjects(docSearchTool, kbMetaTool, keywordSearchTool, webSearchTool, memoryTool) 传入 5 个 tool bean,框架自动扫描每个 bean 上的所有 @Tool 注解方法(MemoryTool 有 recallMemory 和 storeMemory 两个 @Tool 方法,所以 5 bean 产出 6 个 MCP 工具),从方法签名生成 JSON Schema(参数类型、required/optional),从 @ToolParam 注解提取参数描述,注册为 MCP 标准工具。application.yml 配置 spring.ai.mcp.server.enabled=truetype=SYNC(同步调用),protocol=STREAMABLE(Streamable HTTP 传输),name=DocMind MCP Serverinstructions 写明工具能力概述供客户端 UI 展示。整个过程不需要手写任何 JSON-RPC handler、不需要手写 schema JSON——Spring AI 框架全部自动生成,新增工具只需要写 @Tool 方法。

  2. API-Key 鉴权层: McpApiKeyAuthFilter 继承 OncePerRequestFilter,拦截逻辑分三步。第一步路径匹配:isMcpEndpoint() 检查 URI 是否是 /mcp/mcp/**/sse/sse/**——只有 MCP 端点才校验 API-Key,其他路径放行给 JWT filter。第二步密钥校验:从 X-API-Key 请求头取值,用 MessageDigest.isEqual() 做 constant-time 比较防时序侧信道——String.equals 的实现是逐字符比较,第一个字符不匹配就返回 false(耗时短),全匹配才返回 true(耗时长),攻击者可以通过响应时间逐位猜测密钥。MessageDigest.isEqual 固定遍历两个 byte 数组的全部长度,耗时恒定不泄露信息。第三步认证注入:校验通过创建 ROLE_MCP_CLIENTUsernamePasswordAuthenticationToken 注入 SecurityContext;不通过则不设置上下文,交给 Spring Security 的 authenticationEntryPoint 统一返回 401(不是在 filter 里直接返回错误,避免绕过 Security 的异常处理链)。SecurityConfig 中过滤器链顺序:addFilterBefore(mcpApiKeyAuthFilter, JwtAuthenticationFilter.class) + addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) → 最终链序:McpApiKey → JWT → UsernamePassword。这个顺序保证 MCP 请求先走 API-Key 校验(通过后不再走 JWT),普通用户请求跳过 API-Key(无 MCP 路径匹配)直接走 JWT。

  3. MCP Console 管理面板: McpConsoleController 提供 4 个端点——GET /api/mcp/tools(列表,按 name 升序)、PUT /api/mcp/tools/{id}/status(启用/禁用,只允许 active/disabled 两个状态值)、GET /api/mcp/stats(调用统计汇总:总调用次数、平均延迟、Top 工具、各工具明细)、POST /api/mcp/tools/{id}/test(真实调用测试连通性——用最小参数 ping 每个工具,如 docSearchTool.searchDocs("连通性测试", 1, null),返回延迟和 sampleOutput)。工具注册信息持久化在 mcp_tool_registry 表中,包含 call_count 和 avg_latency_ms 字段,每次工具调用后更新。

  4. Cursor 接入实测: 在 Cursor 的 mcp.json 中配置 DocMind MCP Server URL + API-Key。接入过程:Cursor 启动后自动发送 tools/list 获取 6 个工具,在 Cursor Agent 模式下输入”在 DocMind 知识库中搜索 Spring Security 配置相关内容”,Agent 自动选择 doc_search 工具并传入 {query: "Spring Security 配置", topK: 5, kbIds: null}——参数完全正确。5/6 工具首次调用参数正确。唯一出错的是 keyword_searchfilters 参数(Java Map<String, Object> 类型),Cursor Agent 传了字符串 "category=技术文档" 而非 JSON 对象 {"category": "技术文档"},导致 Jackson 反序列化失败抛 InvalidFormatException。根因分析:JSON Schema 中 Map<String, Object> 会被映射为 additionalProperties: true 的 object 类型,但模型在没有示例时倾向于把参数简化为字符串。修复方案是在 @ToolParam 的 description 中加入 JSON 示例 过滤条件,JSON对象格式,例如 {"category": "技术文档"}——本质上 MCP 的工具描述就是给模型看的 prompt,示例比纯类型说明有效得多。

  5. Transport 选型迭代: 最初用 SSE(浏览器兼容、实时推送),但 SSE 是有状态长连接,Nginx 负载均衡需要粘性会话,容器重启时大量连接断开引发重连风暴。切到 Streamable HTTP 后每次请求独立无状态,可过 LB/CDN,容器扩缩容无感知。Claude Desktop 在 SSE 下偶发断连,重连后 Client 重新发 tools/list 获取工具列表——这个过程验证了 MCP 无状态工具发现的健壮性,但频繁断连重连影响用户体验。切到 Streamable HTTP 后稳定性显著提升,再无断连问题。

  6. 双路复用架构: 所有 @Tool Bean 同时服务两条路径——内部 agentic 循环通过 ToolCallingManager 直接调用(AgentToolContext ThreadLocal 捕获结果),外部 MCP 客户端通过 HTTP 端点调用。AgentToolContext.isActive() 区分调用来源:内部调用时 kbIds 由用户会话上下文强制覆盖(防 LLM 幻觉 kbIds 突破用户勾选范围),外部调用时 kbIds 取客户端传参(当前 MVP 阶段已加 API-Key 鉴权,生产需从 auth context 注入用户 ID 校验 kbIds 归属)。store_memory 工具只在 MCP 外部端点暴露,agentic 循环的 toolCallbacks 白名单硬排除——防止 LLM 在 tool-calling 循环中误触发写入操作。

Result:

MCP Server 上线后实现了”一次暴露、多端消费”的目标。Cursor 和 Claude Desktop 均在 5 分钟内完成接入配置(配置 URL + API-Key → 自动发现 6 个工具 → 开始使用)。唯一需要修复的是 keyword_search 的 Map 参数问题,修复后 6/6 工具参数准确率 100%。MCP Console 管理面板在日常运维中的价值是:不需要查日志就能知道每个工具的健康状态和调用频率,新版本部署后一键测试所有工具连通性。

6 个 MCP 工具端点一览:

工具名Bean 类功能Agentic 循环内部MCP 外部暴露
doc_searchDocSearchTool语义向量检索白名单内暴露
keyword_searchKeywordSearchToolBM25 关键词检索白名单内暴露
web_searchWebSearchToolTavily Web 搜索白名单内暴露
recall_memoryMemoryTool跨会话记忆召回白名单内暴露
store_memoryMemoryTool记忆写入白名单硬排除暴露
kb_metaKbMetaTool知识库元信息查询白名单硬排除暴露

注意:agentic 循环白名单暴露 4 个只读工具(doc_search/keyword_search/web_search/recall_memory)+ executeCode 沙箱计算工具(sandbox.enabled 默认关),store_memorykb_meta 硬排除防止 LLM 误触发。executeCode 是第 7 个 @Tool,但仅内部——不加入 McpToolsConfig,不在上面 6 个 MCP 端点内(对外跑任意代码安全面太大)。MCP 外部端点仍暴露 6 个——外部客户端是人在驱动,风险由调用方承担。

已知安全边界与生产加固路径:

风险点MVP 现状生产加固方案
工具调用鉴权/mcp/**/sse 由 McpApiKeyAuthFilter 校验 X-API-Key升级 OAuth 2.1 资源服务器 + RFC 8707
kbIds 权限穿透外部调用可传任意 kbIds(已非匿名)从 auth context 注入用户 ID,校验 kbIds 归属
Memory 读写无隔离外部可传任意 userIduserId 从 auth context 注入,禁止外部指定
Web Search 配额外部调用直接消耗 Tavily API 额度接入 Bucket4j / Redis 令牌桶限流

代码锚点#

类/方法路径职责
McpToolsConfig.mcpToolCallbackProvider()config/McpToolsConfig.javaMethodToolCallbackProvider 注册 5 个对外 @Tool Bean(不含仅内部的 CodeExecTool)
McpApiKeyAuthFilter.doFilterInternal()security/McpApiKeyAuthFilter.java/mcp + /sse 路径 API-Key 鉴权
McpApiKeyAuthFilter.constantTimeEquals()同上MessageDigest.isEqual 防时序侧信道
SecurityConfig.filterChain()config/SecurityConfig.javaMcpApiKey → JWT → UsernamePassword 过滤器链顺序
McpConsoleControllercontroller/McpConsoleController.javaMCP 管理面板:列表/启禁/统计/测试 4 个端点
DocSearchTool.searchDocs()mcp/DocSearchTool.java@Tool 语义检索,AgentToolContext 双路分流
MemoryTool.recallMemory()mcp/MemoryTool.java@Tool 记忆召回,resolveUserId 安全策略
application.yml (MCP section)src/main/resources/application.ymlMCP Server 启用/协议/名称/说明配置

量化数据#

指标数值基线来源
MCP 工具端点6 个(5 Bean / 6 @Tool 方法)McpToolsConfig
支持 TransportStreamable HTTP最初 SSEapplication.yml
Cursor 首次调用准确率5/6 工具参数正确(83%)实测
修复后调用准确率6/6(100%)83%@ToolParam 加 JSON 示例
API-Key 鉴权延迟<1ms(constant-time 比较)McpApiKeyAuthFilter
MCP Console 功能4 个端点(list/status/stats/test)McpConsoleController
过滤器链层数3 层(McpApiKey → JWT → UsernamePassword)SecurityConfig
客户端接入时间<5 分钟(配置 URL + API-Key → 发现工具 → 使用)Cursor/Claude Desktop 实测
McpToolsConfig 代码量40 行自研预估 2000+ 行Spring AI 自动化 vs 手工

三、追问应对#

面试官想听到的信号#

  • 协议层理解深度:不只是”MCP 能调工具”,而是能讲清四层架构(Transport/Tool/Resource/Prompt)、三种 Transport 选型权衡、Tool Discovery 的 tools/list → 注入 LLM 上下文的完整流程
  • 多客户端实测经验:真的接入过 Cursor/Claude Desktop,能讲出参数误传(Map 类型 → 字符串)、长连接断开(SSE 在容器重启时的重连风暴)等真实问题和修复过程,不是纸上谈兵
  • 安全意识:API-Key + constant-time 比较 + filter chain ordering——不是”加个鉴权就完了”,而是理解时序攻击原理(String.equals vs MessageDigest.isEqual)、过滤器顺序(McpApiKey 必须在 JWT 前面)、以及 MVP 到生产的加固路径
  • MCP vs REST vs A2A 清晰定位:不混淆概念——MCP 是 Agent-to-Tool(工具发现+调用),A2A 是 Agent-to-Agent(能力发现+任务协作),REST 是传统集成。能说出各自适用场景和互补关系
  • 双路复用的架构思维:同一套 @Tool Bean 同时服务内部 agentic 循环和外部 MCP 客户端,AgentToolContext.isActive() 区分内外调用路径,kbIds 安全策略在两条路径下有不同处理——不是两套代码,而是一鱼两吃

追问预判与应答#

Q1:MCP Server 和普通 REST API 有什么本质区别?

A:核心区别是”面向模型”vs”面向开发者”。REST API 的消费者是人——开发者读 Swagger 文档,理解端点含义,手写调用代码,写错了编译期 / 运行期报错,靠人来修。MCP 的消费者是模型——客户端连接后通过 tools/list 自动获取所有工具的 name/description/inputSchema,注入 LLM 的 tool-calling 协议,模型原生理解每个工具该怎么调、参数该传什么。这意味着新增一个工具只需要加 @Tool 注解并写好 description,不需要写 API 文档、不需要每个客户端写适配代码。description 的质量直接决定模型调用准确率——本质上工具描述就是给模型看的 prompt,所以我们在 @ToolParam 描述里加了 JSON 示例、使用场景说明,效果立竿见影。另一个区别是错误处理:REST 靠状态码(404/500),MCP 靠 JSON-RPC 的 error 对象(code + message),模型能理解错误原因并自动调整策略(比如换一个工具或修改参数重试)。

Q2:Cursor 的 filters 参数问题怎么解决的?有什么通用教训?

A:问题是 keyword_searchfilters 参数类型是 Map<String, Object>,JSON Schema 会把它映射为 type: object, additionalProperties: true——这对模型来说信息量太少,不知道该传什么键值对。Cursor Agent 传了一个字符串 "category=技术文档" 导致 Jackson 反序列化失败。修复方案是在 @ToolParam 的 description 中加入具体 JSON 示例——过滤条件,JSON对象格式,例如 {"category": "技术文档"}。这背后的通用教训是:MCP 工具描述本质是给模型看的 prompt,模型对”示例”的理解远强于对”类型说明”的理解。这跟 few-shot prompting 是一个道理——告诉模型”请传 Map 类型”它可能不理解,但给它一个 JSON 示例它立刻就知道格式了。修复后 Cursor 对 filters 参数的调用准确率从 0% 提到 100%。推广到整体:我们后来给所有 @ToolParam 的 description 都做了一轮优化——加示例值、加适用场景说明、加约束条件(如 “topK 建议 5-20”),整体工具调用准确率明显提升。这个经验也适用于任何 tool-calling 场景,不限于 MCP。

Q3:API-Key 鉴权够安全吗?生产怎么加固?

A:MVP 阶段够用——理由是 DocMind 暴露的工具以只读为主(5/6 是检索类),store_memory 虽然是写操作但只写 Redis 记忆条目风险有限,API-Key 已经防住了匿名访问。实现上也注意了细节:constant-time 比较防时序攻击、Filter Chain 顺序确保 MCP 请求不被 JWT filter 误拦截。但生产环境四个问题必须加固。第一步升级 OAuth 2.1 资源服务器 + RFC 8707 资源指示符,让每个 MCP 客户端有独立的 client_id 和 scope,可以做细粒度授权(比如某客户端只能调 doc_search 不能调 web_search)。第二步解决 kbIds 权限穿透——当前外部调用持 API-Key 可传任意 kbIds,意味着一个客户端可以检索所有知识库,生产需要从 auth context 注入用户 ID,工具层校验 kbIds 归属(复用对话层的 KbAccessGuard 逻辑)。第三步 Memory 读写隔离——当前外部可传任意 userId 读写记忆,生产需要 userId 从 auth context 强制注入。第四步 Web Search 加速率限制——外部调用直接消耗 Tavily API 额度,恶意客户端可以耗尽配额,需要 Bucket4j/Redis 令牌桶按客户端限流。

Q4:MCP 协议的版本兼容怎么做?

A:三条原则。第一,新增工具不影响旧客户端——Client 只调用它 tools/list 里认识的工具,新增的工具会出现在列表里但不被旧客户端使用,这是天然的向前兼容。第二,删除工具要分步——先在 description 里标 deprecated 提示”此工具将在 X 版本移除,请迁移到 Y 工具”,过渡期后再移除。如果直接删除,正在使用该工具的客户端会调用失败但不会崩溃——MCP Client 通常会把工具调用失败当做一次不成功的 tool call 返回给模型,模型会自行调整策略。第三,Schema 变更用 optional 参数——新增参数设 required=false 并给默认值,旧客户端不传也不报错。MCP 的这套兼容策略和 REST API 的版本化(/v1、/v2)思路完全不同——没有 URL 版本号概念,靠工具级别的增量兼容,更灵活但也要求每个工具的接口设计从第一版就考虑可扩展性。

Q5:MCP 的 Resource 和 Prompt 层为什么没用?

A:不是不知道,是做了取舍后有意不用的。先说 Resource 层:它适合暴露文件系统、数据库连接、代码仓库等结构化资源让客户端直接读取——典型用法是让 Cursor 直接浏览项目文件树或让 Claude Desktop 读取数据库 schema。但 DocMind 的文档已经通过知识库切块 + 向量化处理了,用户需要的不是”原始文档”而是”语义相关的切片”,以 chunk 形式通过 Tool 层返回更合适。如果暴露 Resource 让客户端直接读 MinIO 原始 PDF,反而绕过了我们精心设计的切块 + 向量检索 + rerank 管线,检索质量会大幅退化。再说 Prompt 层:它适合共享提示词模板让客户端复用——比如一套标准的”RAG 问答 prompt 模板”。但 DocMind 的 prompt 是 PromptAssembler 内部管理的,模板跟检索质量强绑定——CRAG HIGH/MEDIUM/LOW/FALLBACK 四档分别对应不同模板,暴露给外部客户端反而破坏了这套分级逻辑。未来如果有”让外部 Agent 复用我们的 prompt 模板”的需求再开也不迟。

Q6:如果 MCP Server 挂了客户端怎么降级?

A:分两层看。Client 侧:MCP 客户端(Cursor/Claude Desktop)通常有 retry + timeout 机制,连接失败会重试。更重要的是这些客户端都有 fallback 能力——Cursor 自带代码搜索和 Codebase Indexing,MCP Server 不可用时退回自有能力,用户体验降级但不中断。Claude Desktop 在工具调用失败时会告知用户”工具暂时不可用”并基于已有上下文继续对话。Server 侧:DocMind 的 MCP 工具全部是只读操作——挂了不会丢数据、不会产生副作用,恢复后立刻可用。Streamable HTTP 的无状态特性也帮了忙——没有长连接需要维护,容器重启后下一次请求正常处理即可,不需要重建连接状态。如果要更进一步,可以在 MCP Server 前面加健康检查端点(/actuator/health),LB 发现不健康自动摘除,配合 K8s readinessProbe 实现零停机滚动更新。

Q7:为什么选 Spring AI MCP 而不是自己实现 JSON-RPC?

A:成本差异巨大,核心是”自动化 vs 手工”。Spring AI 的 MethodToolCallbackProvider 自动扫描 @Tool 注解,从方法签名生成 JSON Schema(参数类型、required/optional),从 @ToolParam 注解生成参数描述,从返回类型生成 outputSchema——整个过程零配置,加一个新工具只需要写 @Tool 方法就行。自己实现 JSON-RPC 需要什么?至少五层:手写每个工具的 JSON-RPC handler(解析 params、调用业务逻辑、包装 result)、手写每个工具的 inputSchema/outputSchema JSON(还要保证和代码同步更新,一旦方法签名改了忘了改 schema 就出 bug)、手写参数反序列化逻辑(JSON → Java 类型映射,Map<String, Object> 这种泛型类型尤其麻烦)、手写 tools/list 的聚合返回(遍历所有已注册工具生成列表)、手写 Transport 层(Streamable HTTP 的请求分发 + 可选流式返回 + 错误码映射)。粗估自研需要 2000+ 行代码且每加一个工具就要改 handler + schema 两处。Spring AI 方案只需要一个 McpToolsConfig 配置类 40 行——投入产出比差了 50 倍。而且 Spring AI 跟 Spring Security、Spring Web 天然集成——鉴权层直接复用现有 Filter Chain,不需要在 JSON-RPC handler 里自己写鉴权逻辑。当然,如果你不用 Spring 生态,TypeScript SDK 是更好的选择。

Q8:MCP Console(管理面板)有什么实际价值?

A:三个场景,解决的是”工具治理”问题。第一,线上监控:GET /api/mcp/stats 聚合所有工具的调用次数、平均延迟、Top 工具——一眼看出哪个工具被调最多(通常是 doc_search)、哪个延迟最高(通常是 web_search,因为依赖外部 Tavily API)、哪个出错最多。这些数据持久化在 mcp_tool_registry 表里,可以做趋势分析。第二,运维操作:PUT /api/mcp/tools/{id}/status 一键禁用某个有问题的工具——典型场景是 Tavily API 限额耗尽时临时禁用 web_search,避免外部客户端调用报错;或者发现某个工具有 bug 需要紧急下线,一键 disabled 不需要重新部署。第三,测试验证:POST /api/mcp/tools/{id}/test 用最小参数真实调用工具验证连通性——用 searchDocs("连通性测试", 1, null) 这种最小 ping 参数,验证 embedding API、Milvus、Redis 等下游服务都活着。部署后一键检查所有工具连通性,不需要启动完整的 AI 客户端来测。这个面板的设计思路是:MCP 工具不是暴露出去就不管了,需要运维手段来管理生命周期。

Q9:MCP 工具描述(description)怎么写才能让模型调得准?

A:这个问题的本质是”怎么给模型写一段好的 prompt 让它理解你的工具”。几条经验。第一,description 开头用动词描述功能而不是名词定义——语义向量文档检索:根据语义相似度从知识库中检索最相关的文档切片这是一个文档检索工具 好得多,模型更容易理解工具的动作和产出。第二,参数描述必须加具体示例——尤其是 Map、List、嵌套对象等复杂类型。我们在 @ToolParam 的 description 里加了 JSON 示例后,Cursor 对 filters 参数的调用准确率从 0% 提到 100%,这就是 few-shot 的力量。第三,描述适用场景和不适用场景——比如 doc_search 写”适用于通用知识查询、概念解释等场景”,keyword_search 写”适用于精确关键词匹配、术语检索等场景”,帮助模型区分两个工具的适用边界。第四,保持简洁——工具描述会占用 LLM 的上下文窗口,6 个工具每个 200 字就是 1200 字,太冗长会挤压其他上下文。经验法则是每个工具描述控制在 50-100 字以内。

Q10:如果要支持更多外部 AI 客户端(比如 20 个),MCP Server 的架构需要怎么调整?

A:当前单实例 MCP Server 就能撑。原因是 MCP 的 Streamable HTTP 模式下每次调用是独立 HTTP 请求,和普通 REST API 没区别——Spring Boot 的 Tomcat 线程池(默认 200)就能处理相当大的并发。真正需要调整的是三个方面:第一,水平扩容——MCP Server 是无状态的(Streamable HTTP 无连接维护),可以直接加实例挂在 Nginx/K8s Service 后面。第二,限流分层——不同客户端的调用频率差异大(Cursor 用户可能每分钟调用 10 次,而一个自动化 Agent 可能每分钟 1000 次),需要按 client_id 做差异化限流。第三,工具版本管理——20 个客户端不可能同时升级,需要支持工具的灰度发布——比如新版 doc_search_v2 先暴露给测试客户端,验证无误后再让所有客户端的 tools/list 返回。这可以通过 mcp_tool_registry 表的 status 字段 + 客户端白名单实现。

反击引导#

  • “MCP 暴露的工具和 agentic 循环里用的工具是同一套 @Tool Bean——一鱼两吃的双路设计是工具层最核心的架构决策,包括 AgentToolContext ThreadLocal 怎么区分内外调用、kbIds 安全策略怎么分流。“(引导至 Story 09 工具系统设计)
  • “API-Key 鉴权只是 MCP 端点的第一层,完整的安全体系包括 16 处硬约束 + 6 类软约束的防御纵深——从 SafetyGuard 紧急词到 kbIds 权限穿透到 store_memory 白名单排除。“(引导至 Story 13 安全约束体系)
  • “Transport 从 SSE 切到 Streamable HTTP 只是冰山一角,整个系统有 10 层独立降级路径——每层都有确定性兜底,MCP Server 不可用只是其中一个故障域。“(引导至 Story 08 全链路降级)