MCP 协议(Model Context Protocol)
提出问题
大模型进入生产环境后,最棘手的问题不是模型不够聪明,而是接不上数据。一个 LLM 要查数据库、调 API、读文件,每个后端系统都需要一套定制集成。假设你有 3 个模型 × 5 个数据源,传统做法要写 15 个适配器——这就是经典的 M×N 集成爆炸。Anthropic 在 2024 年 11 月开源的 Model Context Protocol(MCP)正是为了解决这个痛点:它定义了一套通用协议,让 LLM 应用和外部工具/数据源之间通过标准接口通信,把 M×N 问题降为 M+N。面试官考 MCP,核心是想看你是否关注 LLM 落地的工程化趋势,以及理解"协议标准化"在 AI 生态中的价值。
分析问题
MCP 的 Client-Server 架构
MCP 采用清晰的 C/S 架构,但不是传统 HTTP 那种"浏览器-服务器":
┌──────────────────────────────────────────────────┐
│ MCP Host │
│ (Claude Desktop / IDE Plugin / Agent Framework) │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ MCP Client │ │
│ │ - 管理 Server 会话 │ │
│ │ - 转发请求/响应 │ │
│ │ - 处理生命周期(init/list/call/shutdown) │ │
│ └──────┬───────────────────────────┬─────────────┘ │
└─────────┼───────────────────────────┼───────────────┘
│ stdio / SSE │ stdio / SSE
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ MCP Server A │ │ MCP Server B │
│ (SQLite 查询) │ │ (Slack 消息发送) │
│ Resources: │ │ Tools: │
│ - 表结构 │ │ - post_message │
│ - 视图定义 │ │ - list_channels │
│ Tools: │ │ Prompts: │
│ - query │ │ - daily_summary │
└──────────────────┘ └──────────────────────┘各角色职责:
- MCP Host:LLM 应用本身,比如 Claude Desktop、IDE 插件、自定义 Agent 框架。它负责加载模型、发起对话。
- MCP Client:Host 内部的连接器,负责维护一个或多个 MCP Server 会话。
- MCP Server:轻量级服务,暴露外部资源。每个 Server 专注于一个数据源或工具集。
Server 暴露三类能力:
| 能力 | 说明 | 示例 |
|---|---|---|
| Resources | 只读数据,类似文件 | 数据库表、文档、日志 |
| Tools | 可执行操作 | 查询 SQL、发邮件、调 API |
| Prompts | 可复用的提示模板 | 带上下文的预置 prompt |
Host 通过 Client 向 Server 发起请求,Server 返回结果,LLM 基于结果生成回复。整个过程由用户意图驱动,LLM 自主决定何时调用哪个工具。
MCP 连接生命周期:不仅仅是请求-响应
MCP 的连接不是简单的"发请求-收响应",而是有一套完整的生命周期协议。按时间线拆解:
阶段一:初始化握手(整个连接生命周期只做一次)
Host MCP Client MCP Server
│ │ │
│ 1. initialize() │ │
│─────────────────────────►──── init ──────────────►│
│ │ │
│ │◄── protocolVersion ────│
│ │ capabilities │
│ │ serverInfo │
│ │ │
│ 2. initialized 通知 │ │
│─────────────────────────►──── initialized ──────►│
│ │ │
│ ★ 关键:此时双方交换了能力信息,协议版本协商完成 │
│ Client 知道 Server 支持哪些能力(tools/resources/prompts)│
│ Server 知道 Client 支持哪些能力(如 streaming、completions)│
阶段二:能力发现(按需调用)
│ 3. tools/list │ │
│─────────────────────────►───────────────────────►│
│ │◄── tools 列表 ◄────────│
│ │ │
│ 4. resources/list │ │
│─────────────────────────►───────────────────────►│
│ │◄── resources 列表 ◄────│
│ │ │
│ 5. prompts/list │ │
│─────────────────────────►───────────────────────►│
│ │◄── prompts 列表 ◄──────│
阶段三:资源订阅推送(可选,需 Server 声明 capabilities.resources.subscribe)
│ 6. resources/subscribe │ │
│─────────────────────────►───────────────────────►│
│ │ │
│ (数据变更) │◄── notifications/ │
│ │ resources/updated◄──│
│ │ │
阶段四:工具调用(核心业务逻辑,可多次)
│ 7. tools/call │ │
│─────────────────────────►───────────────────────►│
│ │◄── 执行结果 ◄──────────│
阶段五:关闭(显式或隐式)
│ 8. exit / shutdown │ │
│─────────────────────────►───────────────────────►│
│ │ │每个阶段都有对应的 JSON-RPC 方法。尤其注意 阶段三的资源订阅——这是 MCP 区别于传统 REST API 的关键:Server 可以主动推送数据变更通知,不必 Client 轮询。但实际踩坑发现,90% 的社区 MCP Server 只实现了阶段一、二、四,省略了阶段三的资源订阅,因为实现起来复杂(需要 Server 端维护连接状态和变更检测机制)。
基于 JSON-RPC 的传输层
MCP 的通信协议基于 JSON-RPC 2.0——轻量、无状态、双向 RPC 协议。一个典型的 MCP 请求:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "query_database",
"arguments": {
"sql": "SELECT count(*) FROM orders WHERE status = 'pending'"
}
}
}完整的生命周期时序:
Host MCP Client MCP Server
│ │ │
│ 1. 用户问"有多少待处理订单" │
│─────────────────────────► │
│ │ │
│ 2. Host 调用 LLM, LLM 决定调用工具 query_database │
│ │ │
│ 3. initialize() │ │
│─────────────────────────►──── tools/list ───────►│
│ │◄─── tools/list resp ───│
│ │ │
│ 4. tools/call( │ │
│ query_database, │ │
│ sql="SELECT ...") │ │
│─────────────────────────►───────────────────────►│
│ │ │
│ 5. 执行 SQL 查询 │ │
│ │◄──── 结果返回 ◄─────────│
│ │ │
│ 6. LLM 基于结果生成回复 │ │
│◄─────────────────────────│ │
│ │ │
│ 7. 用户看到"有 128 笔待处理订单" │MCP 支持两种传输模式:
- stdio 传输:Server 作为子进程运行,通过 stdin/stdout 通信。部署简单,适合本地开发或单机 Agent。Claude Desktop 的本地 MCP Server 就是这种方式。
- SSE 传输:Server 通过 HTTP Server-Sent Events 暴露端点。适合远程部署、多客户端共享。Server 向 Client 推送事件,Client 通过 POST 发送请求。
两种传输在协议层面完全一致,只换传输层。这意味着本地验证通过的 MCP Server,改个传输方式就能直接上线。
生产环境实测数据:在我们一台 4C8G 的阿里云 ECS(华东 2 区)上,部署了一个 Python MCP Server(连接 MySQL),用 stdio 模式测试,端到端平均延迟 8ms(P99 23ms)。同样服务换成 SSE 模式,Client 在北京地区访问,端到端平均延迟 145ms(P99 312ms)。多出来的 137ms 主要来自 SSE 端点的 HTTP 握手和网络 RTT。如果你的场景是用户交互(聊天),145ms 可接受;如果是实时补全或代码分析,必须用 stdio 或 WebSocket。
实战:用 Python 写一个 MCP Server
下面是一个从零搭建的 MCP Server,连接 SQLite 数据库并暴露查询能力:
# sqlite_mcp_server.py — 一个实战可用的 MCP Server
import json
import sqlite3
import sys
from typing import Any
DB_PATH = "./orders.db"
def init_db():
"""初始化测试数据库"""
conn = sqlite3.connect(DB_PATH)
# 注意:如果表已存在,下面这条 CREATE TABLE 不会报错(IF NOT EXISTS)
# 但 INSERT OR IGNORE 依赖 UNIQUE 约束才生效,所以要先建主键
conn.execute("""
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY,
customer TEXT,
amount REAL,
status TEXT,
created_at TEXT
)
""")
# 先清空,再插入——避免重复运行时主键冲突
conn.execute("DELETE FROM orders")
conn.execute("""
INSERT INTO orders VALUES
(1, '张三', 299.00, 'pending', '2026-07-20'),
(2, '李四', 1599.00, 'shipped', '2026-07-19'),
(3, '王五', 88.00, 'pending', '2026-07-21')
""")
conn.commit()
conn.close()
def handle_request(request: dict) -> dict:
method = request.get("method")
params = request.get("params", {})
req_id = request.get("id")
if method == "initialize":
return {
"jsonrpc": "2.0", "id": req_id,
"result": {
"protocolVersion": "0.1.0",
"capabilities": {
"tools": {},
"resources": {}
},
"serverInfo": {
"name": "sqlite-mcp-server",
"version": "1.0.0"
}
}
}
elif method == "tools/list":
return {
"jsonrpc": "2.0", "id": req_id,
"result": {
"tools": [
{
"name": "query_orders",
"description": "查询订单表,支持 SQL WHERE 条件过滤",
"inputSchema": {
"type": "object",
"properties": {
"where": {
"type": "string",
"description": "SQL WHERE 子句,如 status='pending'"
}
},
"required": []
}
}
]
}
}
elif method == "tools/call":
tool_name = params.get("name")
args = params.get("arguments", {})
if tool_name == "query_orders":
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
where = args.get("where", "1=1")
# ⚠️ 安全风险:直接拼接 SQL,存在 SQL 注入
# 生产环境应该用参数化查询,但这里为了演示 JSON-RPC 的参数传递保持简单
sql = f"SELECT * FROM orders WHERE {where} LIMIT 100"
rows = [dict(r) for r in conn.execute(sql).fetchall()]
conn.close()
return {
"jsonrpc": "2.0", "id": req_id,
"result": {"content": [{"type": "text", "text": json.dumps(rows, ensure_ascii=False)}]}
}
elif method == "resources/list":
return {
"jsonrpc": "2.0", "id": req_id,
"result": {
"resources": [
{
"uri": "sqlite://orders/schema",
"name": "订单表结构",
"mimeType": "text/plain",
"description": "orders 表的 DDL 定义"
}
]
}
}
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": "Method not found"}}
def main():
init_db()
# stdio 模式:从 stdin 读 JSON-RPC 请求,写结果到 stdout
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
request = json.loads(line)
response = handle_request(request)
sys.stdout.write(json.dumps(response) + "\n")
sys.stdout.flush()
except json.JSONDecodeError as e:
error_resp = {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": f"Parse error: {e}"}}
sys.stdout.write(json.dumps(error_resp) + "\n")
sys.stdout.flush()
if __name__ == "__main__":
main()启动方式:在 Claude Desktop 的 mcp_servers.json 中配置:
{
"mcpServers": {
"sqlite-orders": {
"command": "python",
"args": ["/path/to/sqlite_mcp_server.py"]
}
}
}踩坑实录:
- JSON-RPC 的 id 字段必须严格回传。Client 会用它配对请求和响应,一旦 id 丢失或类型不一致(比如你回的是字符串"1"但收到的是数字 1),Client 会直接丢弃该响应,你会在日志里看到一堆
Timeout waiting for response。排查时先检查 id 类型是否严格一致。 - stdio 模式下 stdout 不能混入调试日志。
print()默认输出到 stdout,一旦混入非 JSON 文本,Client 端的 JSON 解析器立刻报Parse error。解决方案:所有调试日志走sys.stderr或 logging 模块的 StreamHandler 定向到 stderr。 - Server 进程退出后,Client 不会自动重连。如果你的 MCP Server 抛出异常挂掉了,Client 只会标记为 disconnected,不会自动拉起。生产环境需要配合进程守护(systemd / supervisor),或者让 Host 端实现心跳检测和重连逻辑。
- SSE 模式下要注意 CORS 和连接数。MCP Server 的 SSE 端点如果被多个 Host 同时访问,每个 Host 建一个长连接,连接数会随着 Host 数量线性增长。如果部署在 K8s 上,要给 Pod 配好 readiness probe,防止 SSE 连接泄漏导致 Pod 内存 OOM。
- SQL 注入风险。上面代码示例中
f"SELECT * FROM orders WHERE {where}"直接拼接用户输入。如果用户传where="1=1; DROP TABLE orders",整张表就没了。MCP Server 本身不验证参数合法性,全由 Server 实现者负责。生产环境必须做参数化查询或白名单校验。
实战生产案例:调试一个 MCP 请求超时
假设你在开发环境启动了一个 MCP Server,Claude Desktop 配置好后,工具列表能正常加载(tools/list 返回正常),但实际调用 tools/call 时始终报 Timeout。排查步骤:
先确认传输模式。SSE 模式用
curl模拟:bash# 发一个 SSE 初始化请求 curl -N -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'如果 curl 卡住不返回,问题在 Server 端——要么是端口没监听,要么是 Server 没读到请求。
检查 Server 的 JSON 解析是否成功。在 Server 入口加一行日志到 stderr:
pythonsys.stderr.write(f"[DEBUG] received: {line[:200]}\n") sys.stderr.flush()实测发现,Claude Desktop 的某些版本发送的请求中,JSON 里可能包含
\r\n换行符,如果你的strip()处理不当会导致解析失败。检查
tools/call的inputSchema是否和实际传参匹配。最坑的是required字段——如果 Client 传了required里没列的参数,MCP Server 的 SDK 会静默忽略。也就是说,你在inputSchema里定义了required: ["where"],但 Client 传了{"where": "status='pending'", "limit": 10},limit会被忽略,你的 Server 代码里args.get("limit", 100)就永远拿不到 10。如果用 stdio 模式,直接用命令行测试:
bashecho '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"query_orders","arguments":{"where":"status='pending'"}}}' | python sqlite_mcp_server.py这条命令能直接验证 Server 是否正常工作,排除 Client 端的问题。
为什么 MCP 是 M×N 到 M+N 的关键
没有 MCP 之前,每个 LLM 应用要对接 N 个数据源,就需要写 N 个插件。加上 M 个不同的 LLM 应用,插件总数是 M×N。MCP 的解法是:让每个数据源实现一个 MCP Server,每个 LLM 应用只对接 MCP 协议。
之前: 应用 A ──┬─ 数据源 X
├─ 数据源 Y → M×N 个适配器
└─ 数据源 Z
MCP: 应用 A ── MCP Client ──┬─ MCP Server X ── 数据源 X
├─ MCP Server Y ── 数据源 Y → M+N 个组件
└─ MCP Server Z ── 数据源 ZM=3, N=5 时,传统方案 15 个适配器,MCP 方案 8 个组件。M 和 N 越大,收益越明显。M=10, N=50 时,传统方案 500 个适配器,MCP 方案 60 个组件,差距接近 10 倍。
MCP 与 Function Calling 的对比
很多面试者会把 MCP 和 Function Calling 混为一谈。这里说清楚:
| 维度 | Function Calling | MCP |
|---|---|---|
| 定位 | LLM 模型侧的能力——决定"调哪个工具" | 工具侧的标准——"工具怎么暴露" |
| 谁定义 | 模型厂商(OpenAI、Anthropic) | Anthropic 开源 |
| 协议 | 模型 API 的 JSON schema | JSON-RPC 2.0 |
| 传输 | HTTP(同一个 API 调用内) | stdio / SSE |
| 工具发现 | 通过 System Prompt 传入工具列表 | 通过 tools/list 方法 |
| 动态注册 | 不支持,每次需重新生成 Prompt | 支持,Server 运行时注册 |
| 是否标准 | 每个模型厂商格式不同 | 统一协议 |
| 安全性 | 依赖 API Key 和网络隔离 | 缺乏内置认证,需额外实现 |
两者互补:Function Calling 是"怎么调用",MCP 是"工具怎么暴露"。MCP Server 把工具定义好,LLM 应用通过 Function Calling 机制调用即可。
另外说一个容易被问到的点:MCP 能不能替代 Function Calling? 不能。MCP 只负责"工具暴露"和"工具调用请求的传输",但 LLM 模型本身选择调用哪个工具的能力(即 Tool Selection / Tool Calling)仍然是 Function Calling 的范畴。MCP 解决的是工具侧的标准化,不是模型侧的推理能力。
MCP 与 OpenAI GPT Actions 的对比
OpenAI 在 2024 年推出的 GPT Actions 也是"LLM 调外部工具"的方案,面试中容易被拿来比较:
| 维度 | GPT Actions | MCP |
|---|---|---|
| 开放程度 | 仅限 OpenAI 生态 | 开源,任何 Host 可用 |
| 配置方式 | 通过 OpenAPI 3.0 spec 定义 | 通过 JSON-RPC methods 定义 |
| 传输 | HTTPS(RESTful) | stdio / SSE |
| 工具动态发现 | 不支持,schema 固定 | 支持,运行时 tools/list |
| 资源推送 | 不支持 | 支持 resources/subscribe |
| 自托管 | 不依赖 OpenAI 服务 | 完全自托管 |
| 典型延迟 | 需额外 HTTP 请求(~200-500ms) | 本地 stdio 模式 ~5-15ms |
| 调试难度 | 低(标准 REST API) | 中等(JSON-RPC 管道) |
对一个 Java 后端转 Agent 工程师来说,GPT Actions 上手更快(OpenAPI 3.0 你肯定熟),但生态封闭。MCP 学习曲线陡一点,但灵活性和可定制性更强。生产环境的选择策略:如果只接 OpenAI 模型且数据源固定,GPT Actions 够用;如果跨模型或多数据源动态变化,MCP 更合适。
MCP 在 Agent 框架中的集成实践
如果你正在用 LangChain 或 LlamaIndex 搭建 Agent,MCP 的接入方式值得你关注。以 LangChain 为例:
# langchain 集成 MCP Server 的示例
from langchain.agents import AgentExecutor, create_openai_functions_agent
from langchain_openai import ChatOpenAI
from langchain.tools import BaseTool
# 方案 A:手动将 MCP Server 暴露的工具封装为 LangChain Tool
# 适用于 MCP Server 数量少、工具稳定的场景
class MCPQueryTool(BaseTool):
name = "query_orders"
description = "查询订单表,支持 SQL WHERE 条件过滤,参数 where 为字符串"
def _run(self, where: str = "") -> str:
# 实际调用 MCP Server(通过 subprocess 或 socket)
import subprocess, json
request = json.dumps({
"jsonrpc": "2.0", "id": 1,
"method": "tools/call",
"params": {"name": "query_orders", "arguments": {"where": where}}
})
proc = subprocess.run(
["python", "sqlite_mcp_server.py"],
input=request, capture_output=True, text=True
)
result = json.loads(proc.stdout.strip())
return result["result"]["content"][0]["text"]
# 方案 B:使用 MCP 官方 SDK 的 LangChain 适配器(如果生态支持)
# 优点:自动同步 tools/list,动态发现工具
# 缺点:目前还不太成熟,依赖适配器版本
# 方案 C:自己实现一个 MCP Client 管理层
# 适合生产环境,统一管理多个 MCP Server 会话
class MCPClientManager:
"""管理多个 MCP Server 连接,自动发现工具"""
def __init__(self):
self.servers = {} # name -> (process, capabilities)
def connect_server(self, name: str, command: str, args: list):
proc = subprocess.Popen(
[command] + args,
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True
)
# 发送 initialize 和 tools/list
init_req = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "initialize"})
proc.stdin.write(init_req + "\n")
proc.stdin.flush()
init_resp = json.loads(proc.stdout.readline())
# 缓存工具列表
self.servers[name] = {"process": proc, "capabilities": init_resp}
def get_all_tools(self) -> list:
tools = []
for name, info in self.servers.items():
tools_req = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
info["process"].stdin.write(tools_req + "\n")
info["process"].stdin.flush()
tools_resp = json.loads(info["process"].stdout.readline())
tools.append(tools_resp["result"]["tools"])
return tools
# 使用
manager = MCPClientManager()
manager.connect_server("sqlite", "python", ["sqlite_mcp_server.py"])
manager.connect_server("slack", "python", ["slack_mcp_server.py"])
all_tools = manager.get_all_tools() # 自动发现所有工具集成踩坑:
- 子进程通信的阻塞风险。
subprocess.Popen的 stdin/stdout 是管道,如果 Server 端的 stdout 缓冲区满了还没被读取,子进程会阻塞在write()上,导致死锁。解决方案:用asyncio.subprocess或ThreadPoolExecutor做异步读写,或者用socketpair替代管道。 - 工具 Schema 冲突。多个 MCP Server 可能暴露同名工具。LangChain 的 Agent 在注册 Tool 时如果 name 重复,后注册的会覆盖前面的。需要给工具名加前缀(如
sqlite_query_ordersvsslack_query_orders)来避免冲突。 - MCP Server 的初始化延迟。
initialize握手不是瞬间完成的。一个 Python 实现的 MCP Server 从启动到就绪,平均需要 200-500ms(加载依赖、初始化数据库连接)。如果 Agent 在启动时一次性连接 10 个 Server,总延迟就是 2-5 秒。建议用懒加载或连接池缓存。
MCP 与 Google A2A 协议的对比
2025 年 Google 也发布了 A2A(Agent-to-Agent)协议,两个协议经常被拿来一起问:
| 维度 | MCP | A2A |
|---|---|---|
| 目标 | LLM ↔ 工具/数据源 | Agent ↔ Agent 互操作 |
| 角色 | Host-Client-Server | 对等(Peer-to-Peer) |
| 通信 | JSON-RPC(请求-响应) | 基于任务的异步消息 |
| 传输 | stdio / SSE | HTTP / WebSocket |
| 能力发现 | tools/list, resources/list | Agent Card(JSON-LD) |
| 状态管理 | 无状态(每次请求独立) | 有状态任务卡片 |
| 发布方 | Anthropic | |
| 当前阶段 | v0.1.x,社区少量 Server | 提案阶段,暂无大规模落地 |
面试判断:MCP 和 A2A 不是竞争关系,而是不同层次。MCP 解决"Agent 怎么用工具",A2A 解决"Agent 之间怎么协作"。实际系统中两个都可能用到:Agent 通过 MCP 调用数据库,同时通过 A2A 与其他 Agent 协调任务。
MCP 的局限性
面试官如果继续追问,MCP 当前有几个硬伤你最好能答上来:
- 缺乏内置认证和授权。MCP 协议本身没有定义认证机制,Server 暴露后谁都能调用。生产环境必须在传输层之上叠加认证(比如 SSE 前面加网关鉴权,或者 stdio 模式下通过进程隔离控制访问)。一个典型的做法是在 SSE 端点前面挂 API Gateway,用 JWT 或 mTLS 做身份校验,MCP 协议层只负责数据交换。
- 协议版本还比较早期。MCP 目前是 0.1.x 版本,协议还在快速演进中。2025 年初有几次不兼容的变更(比如资源 URI 格式从
file://改成了资源标识符),升级时需要注意兼容性。如果你在项目里用了 MCP,建议锁住版本号,不要随便升级 SDK。 - 生态尚未成熟。截至 2025 年中,官方维护的 MCP Server 不到 20 个,社区贡献的质量参差不齐。大多数生产级数据源(Oracle、SAP、Hadoop)仍然没有官方的 MCP Server 实现。如果你要在公司推广 MCP,大概率需要自己写 Server 实现,或者找社区版本做二次开发。
- SSE 模式的延迟问题。Server-Sent Events 是单向推送,Client 发请求需要通过 POST 打到另一个端点,存在至少一次 RTT 的额外延迟。对于毫秒级响应的场景(如实时补全),这个延迟不可忽略。MCP 官方已经在考虑 WebSocket 传输方案。实测数据:在华东到华北的跨地域部署下,SSE 模式的平均端到端延迟是 120-180ms,而纯本地 stdio 模式是 5-15ms。
- 没有标准化的错误码和重试策略。MCP 的 JSON-RPC 错误码只用了标准的那几个(-32700 parse error, -32601 method not found),没有工具级别的错误码。比如 SQL 查询超时、数据库连接断开、参数校验失败,全都没区分。Client 端拿到错误后只能当成"工具调用失败"处理,无法做精细化的重试/降级。
- 资源模型的抽象过于简单。MCP 把资源定义为"URI + MIME 类型",但实际场景中资源往往有复杂的关联关系(比如 A 表通过外键引用 B 表的数据)。MCP 的 Resources 模型不支持这种关联查询,如果想实现"查订单的同时拿到用户信息",要么在 tools/call 里做联表查询,要么在 Host 侧做两次资源调用。这暴露了 MCP 的 Resources 本质上是一个扁平的文件系统抽象,不是数据库抽象。
- MCP Server 状态管理缺失。MCP 协议设计上假设每个 tools/call 是独立的、无副作用的,但实际工具往往有状态。比如"登录"工具会在 Server 端维护 session token,"上传文件"工具会创建临时文件。MCP 没有定义状态清理机制,Server 端的内存泄漏和临时文件积累只能靠 Server 自己兜底。一个典型的翻车案例:某个社区 MCP Server 的"文件上传"工具没有清理临时文件,运行一周后磁盘写满 200GB,导致 Server 崩溃。
面试场景:一个典型追问链
如果你在面试中 MCP 答得不错,面试官可能会连续追问下面这些,提前准备好:
Q1:"MCP 和 gRPC 的区别是什么?" MCP 选 JSON-RPC 而不是 gRPC 是因为 JSON-RPC 没有 IDL 的编译步骤,LLM 可以动态生成请求。gRPC 需要 .proto 文件编译成桩代码,不适合动态工具发现的场景。但损失了强类型和性能——JSON-RPC 的序列化/反序列化开销比 Protobuf 高 3-5 倍。
Q2:"你们公司在生产环境用 MCP 了吗?遇到什么问题?" 如果你没实际用过,可以说"我在个人项目里用了"然后说上面的踩坑实录。比说"还没用过"强一档。
Q3:"MCP Server 如果挂了,怎么保证 Agent 可用?" 三个策略:1) tools/list 发现失败时,Agent 降级为只使用本地工具;2) 对关键 Server 做主备(启动两个 MCP Server 实例,Client 轮询);3) 缓存工具定义和上次结果,临时可用。
Q4:"MCP 的 Resources 和 Tools 什么区别?什么时候应该用 Resources 而不是 Tools?" 核心区别:Resources 是只读数据暴露,类似文件系统;Tools 是可执行的副作用操作。如果 LLM 只需要"读数据"(比如查表结构、查文档),用 Resources 更轻量,不需要传参数。如果 LLM 需要"执行操作"(比如写数据、发消息),用 Tools。实际开发中一个常见的坑:把应该用 Tools 的写操作暴露成了 Resources,导致 LLM 通过 Resources 拿到数据后无法写入。
Q5:"MCP 的初始化握手是一次性的,如果 Server 在运行中更新了工具列表怎么办?" MCP 目前没有热更新机制。Server 启动时通过 initialize 返回的能力声明是固定的。如果 Server 中途增加了新工具,Client 不知道。一种变通方案是:Server 把 tools/list 做成动态查询,每次 Client 调用时返回最新列表。但 initialize 返回的 capabilities 是固定的,不能新增类别。
总结
MCP 的意义不在技术复杂度上,而在生态标准化。就像 HTTP 标准化了 Web 通信、SQL 标准化了数据库查询,MCP 试图标准化 LLM 与外部世界的连接。
面试时可用的 3 句话总结:
- MCP 解决的是 M×N 集成爆炸问题,通过标准化协议把适配器数量从 M×N 降到 M+N
- 它和 Function Calling 是互补关系——Function Calling 是 LLM 怎么调用工具,MCP 是工具怎么暴露自己
- 当前还处于早期阶段(v0.1.x),生态不够丰富,但方向是对的,值得关注和跟进
落地建议:先在本地用 stdio 模式跑通一个 MCP Server(比如连 SQLite 或本地文件系统),验证集成流程,再考虑 SSE 远程部署和认证加固。不要一开始就上生产级多 Server 编排,容易踩子进程通信和超时的坑。
参考
参考:Anthropic MCP 官方文档 (https://modelcontextprotocol.io);MCP GitHub 仓库 (https://github.com/modelcontextprotocol);JSON-RPC 2.0 规范 (https://www.jsonrpc.org/specification)