1. 这不是一场“取代”,而是一次协议层与交互层的错位对话
最近在好几个技术群和开发者论坛里,反复看到这个问题:“CLI 能取代 MCP 吗?”——尤其在看到zcode cli、codex cli、playwright mcp、trae cli这些词高频混搭出现后,很多人下意识觉得:既然 CLI 工具越来越智能,MCP(Model Control Protocol)又常被包装成一个“服务端”或“代理层”,那是不是只要把命令行工具做得足够强,就能绕开 MCP?甚至直接干掉它?
答案很明确:不能,也不该试图取代。
这不是工具强弱之争,而是协议定位与交互范式之间的根本性错位。
CLI(Command-Line Interface)本质是人机指令输入通道——它定义“用户想让系统做什么”,比如git commit -m "feat: add mcp handler"或zcode upload --target prod --token xxx。它的核心价值在于确定性、可脚本化、低延迟、高复用。你敲下回车那一刻,意图是明确的、动作是原子的、结果是可预期的。
而 MCP 是一套模型能力调度与上下文协商协议——它不关心你用终端还是 IDE 点击触发,它只负责回答三个关键问题:
- 当前请求来自哪个客户端(IDE 插件 / 浏览器扩展 / CLI wrapper)?
- 请求背后携带了哪些上下文(当前文件路径、选中文本、调试堆栈、HTTP 请求体、Burp Suite 的拦截流量)?
- 模型服务该如何响应(返回补全建议?执行代码分析?调用外部 API?生成测试用例?是否需要流式 chunk?是否需回调确认?)
提示:把 MCP 想象成“AI 时代的 USB-C 协议”——它不生产电,也不决定你插的是手机还是显示器,但它统一了“插上去之后怎么通信、传什么、怎么握手、如何断连”。CLI 是你的手指,MCP 是接口标准,二者不在同一抽象层级。
这也是为什么你会看到playwright mcp和browser use mcp被并列讨论:前者是用 Playwright 自动化驱动浏览器行为,并通过 MCP 协议向后端模型服务传递页面 DOM + 用户操作上下文;后者是浏览器扩展直接监听 DevTools 事件,再按 MCP 格式封装请求。它们都依赖 CLI 吗?不一定——playwright mcp可能由 CI 脚本触发,browser mcp完全无需终端介入。但它们若想互操作、被统一管理、支持跨 IDE 调用,就必须遵守 MCP。
再看热搜里的wss://api.xiaozhi.me/mcp/?token=...——这个地址本身就是一个典型 MCP Server 的 WebSocket 入口。它不提供 CLI 二进制,也不要求你装mcp-cli;它只等待符合 MCP v0.3 规范的 JSON-RPC over WebSocket 请求。你可以用curl手动构造,可以用 Python 的websockets库写个脚本,也可以用zcode cli封装一层——但底层协议不变,CLI 只是其中一种接入方式。
所以真正值得深挖的问题不是“CLI 能否取代 MCP”,而是:当 CLI 成为最主流的开发者入口时,如何让 MCP 协议在 CLI 场景中真正落地、不被架空、不沦为黑盒胶水层?这正是本文要拆解的核心——不是站队,而是厘清边界;不是鼓吹工具,而是还原协议价值。
2. MCP 的真实定位:它既不是软件协议,也不是硬件协议,而是“语义中间件协议”
很多开发者第一次接触 MCP 时,会在文档里看到类似描述:“MCP 是一种软件协议”或“MCP 类似于硬件协议中的 PCIe”,然后陷入概念混淆。这其实源于对协议分层认知的偏差。我们来一层层剥开:
2.1 协议栈视角:MCP 处于“能力抽象层”,而非传输层或应用层
传统网络协议栈(OSI 七层)中,TCP 是传输层协议,HTTP 是应用层协议,它们解决的是“数据怎么可靠送达”和“请求怎么表达”。而 MCP 完全不处理这些——它假设传输已就绪(WebSocket / HTTP/2 / Unix Socket 都可承载),也不规定业务逻辑(补全、解释、生成、调试),它只做一件事:标准化模型能力的“暴露方式”与“调用契约”。
举个具体例子:你在 VS Code 里选中一段 Python 代码,右键选择 “Explain with AI”。背后发生了什么?
- IDE 插件捕获选中文本、当前文件路径、语言模式、光标位置;
- 插件构造一个 MCP 请求对象:
{ "jsonrpc": "2.0", "id": "req-12345", "method": "textDocument/explain", "params": { "text": "def calculate_tax(amount, rate): return amount * rate / 100", "uri": "file:///project/src/tax.py", "language": "python", "position": { "line": 10, "character": 4 } } } - 这个 JSON 对象通过 WebSocket 发送给
wss://api.xiaozhi.me/mcp/...; - MCP Server 解析 method 字段,匹配到注册的
textDocument/explain处理器; - 处理器将上下文喂给 LLM,拿到结构化响应(含 plain text explanation + optional AST nodes + suggested fixes);
- 响应按 MCP 标准格式返回,IDE 插件据此渲染气泡提示。
注意:整个过程里,CLI 从未出现。但如果你用zcode explain --file tax.py --line 10 --char 4,背后很可能就是 CLI 工具读取同样字段,构造出一模一样的 MCP 请求体,再发给同一个 Server。CLI 是“前端”,MCP 是“API 规范”,Server 是“后端实现”。
注意:MCP 不强制要求使用 WebSocket。
trae cli支持--transport http参数,即走 HTTP POST;dify 浏览器mcp实际用 fetch 调用 RESTful endpoint;ruoyi-vue-pro合并mcp功能则可能通过 Spring Boot 的@PostMapping("/mcp")接收。只要 payload 符合 MCP Schema,传输方式完全自由。
2.2 为什么它不能叫“软件协议”或“硬件协议”?
不是软件协议:因为“软件协议”通常指代具体实现(如 HTTP/2 协议栈、gRPC 编码规范),而 MCP 是纯语义层定义——它不规定序列化格式(JSON-RPC 是推荐,非强制),不绑定传输(WS/HTTP/TCP 都可),不约束认证方式(Bearer Token / API Key / OAuth2 都可插拔)。它更像 OpenAPI Spec:一份人类可读、机器可校验的接口契约文档。
不是硬件协议:硬件协议(如 PCIe、USB)定义物理引脚、电气信号、时序握手。MCP 没有物理载体,不涉及电压、带宽、中断。它只定义“能力描述字段怎么写”、“错误码怎么归类”、“流式响应怎么分块”。你可以把它部署在树莓派上,也可以跑在 GPU 服务器集群里,协议本身无状态、无依赖。
所以准确说:MCP 是一种“模型能力语义中间件协议”(Model Capability Semantic Middleware Protocol)。关键词是“语义”——它让不同厂商的模型服务(Claude、Qwen、Llama)、不同形态的客户端(IDE、CLI、Browser Extension、CI Runner)、不同场景的上下文(代码、日志、网络包、数据库 schema)之间,能基于统一词汇表对话。
这也解释了为什么chrome devtools mcp playwright mcp会被同时提及:Playwright 自动化脚本可以注入 DevTools Hook,捕获 Network 面板里的请求,再按 MCP 的network/requestIntercepted方法格式打包发送;浏览器扩展则直接监听chrome.devtools.network.onRequestFinished事件,走同样协议。它们共享的不是代码,而是语义——requestIntercepted这个 method 名,意味着接收方必须理解这是“一条被拦截的 HTTP 请求”,并知道如何提取 headers、body、response status。
2.3 CLI 在 MCP 生态中的真实角色:轻量级客户端 + 协议翻译器
回到标题问题:CLI 能取代 MCP 吗?
不能。但 CLI 正在成为 MCP 最高效的“翻译器”和“启动器”。
翻译器角色:
gitlab cli本身不理解 MCP,但gitlab mcp sync命令会读取.gitlab-ci.yml中的MCP_SERVER_URL环境变量,将 pipeline 日志、失败堆栈、变更文件列表,转换为ci/pipelineFailed格式的 MCP 请求。它把 GitLab 的领域语义,翻译成 MCP 的通用语义。启动器角色:
trae ide启动时会自动检测本地是否存在mcp-server进程;若无,则运行mcp-server --port 3000 --model qwen2.5:7b;随后所有 IDE 操作都通过http://localhost:3000/mcp发送。CLI 在这里不是替代协议,而是协议的“守门人”和“协调员”。
实测发现:一个设计良好的 MCP CLI 工具,其核心逻辑往往只有 200 行左右——它不做模型推理,不存上下文,不管理会话,只做三件事:
- 解析命令行参数(
--file,--context,--method); - 按 MCP Schema 组装 JSON-RPC 请求体;
- 选择 transport(WS/HTTP)并发送,解析响应后格式化输出。
这才是 CLI 与 MCP 的健康关系:CLI 是瘦客户端,MCP 是厚协议。越轻量的 CLI,越能凸显 MCP 的价值。
3. CLI 与 MCP 的协同实操:从零搭建一个可验证的本地 MCP 开发环
光讲理论不够。下面我带你用不到 50 行代码,亲手搭建一个最小可行的 MCP 服务,并用curl和自定义 CLI 脚本验证它——全程不依赖任何商业平台、不触碰敏感服务、完全离线可运行。这个过程会彻底暴露 CLI 和 MCP 的协作本质。
3.1 环境准备:只需 Python 3.9+ 和一个终端
我们不用 Docker、不装 Node.js、不配 Kubernetes。目标是证明:MCP 的核心复杂度不在部署,而在协议理解。
首先创建项目目录:
mkdir mcp-demo && cd mcp-demo python -m venv .venv source .venv/bin/activate # macOS/Linux # 或 .venv\Scripts\activate.bat # Windows pip install fastapi uvicorn websockets pydantic提示:这里选 FastAPI 而非 Flask,是因为它原生支持 WebSocket 和 OpenAPI 文档,且类型注解能直接生成 MCP Schema 校验逻辑——这对理解协议字段约束至关重要。
3.2 编写 MCP Server:专注协议层,剥离业务逻辑
创建mcp_server.py:
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException from pydantic import BaseModel, Field from typing import Optional, Dict, Any import json import asyncio app = FastAPI(title="Minimal MCP Server", docs_url="/docs") # MCP 核心请求模型(精简版,仅含 textDocument/completion) class CompletionParams(BaseModel): text: str = Field(..., description="待补全的代码片段") uri: str = Field(..., description="文件 URI,如 file:///path/to/file.py") language: str = Field(..., description="编程语言标识符") position: Dict[str, int] = Field(..., description="光标位置 {line, character}") class MCPRequest(BaseModel): jsonrpc: str = "2.0" id: str method: str params: CompletionParams class MCPResponse(BaseModel): jsonrpc: str = "2.0" id: str result: Dict[str, Any] @app.post("/mcp") async def handle_mcp_http(request: MCPRequest): # 实际项目中,这里会调用 LLM API 或本地模型 # 我们模拟一个确定性响应:返回固定补全建议 if request.method == "textDocument/completion": return MCPResponse( id=request.id, result={ "suggestions": [ {"label": "return amount * rate / 100", "kind": "snippet"}, {"label": "if amount < 0: raise ValueError('Invalid amount')", "kind": "snippet"} ] } ) else: raise HTTPException(status_code=400, detail=f"Unsupported method: {request.method}") @app.websocket("/mcp/ws") async def websocket_endpoint(websocket: WebSocket): await websocket.accept() try: while True: data = await websocket.receive_text() try: req = json.loads(data) # 简单校验 JSON-RPC 结构 if not all(k in req for k in ["jsonrpc", "id", "method"]): await websocket.send(json.dumps({"jsonrpc": "2.0", "id": None, "error": {"code": -32600, "message": "Invalid Request"}})) continue # 模拟 completion 响应 if req["method"] == "textDocument/completion": resp = { "jsonrpc": "2.0", "id": req["id"], "result": { "suggestions": [ {"label": "return amount * rate / 100", "kind": "snippet"}, {"label": "if amount < 0: raise ValueError('Invalid amount')", "kind": "snippet"} ] } } await websocket.send(json.dumps(resp)) except json.JSONDecodeError: await websocket.send(json.dumps({"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error"}})) except WebSocketDisconnect: pass这段代码做了三件关键事:
- 定义了
CompletionParamsPydantic 模型,它直接映射 MCP 规范中textDocument/completion的参数结构; /mcpHTTP 端点接收 JSON-RPC POST 请求,自动校验字段类型(如uri必须是字符串,position必须是 dict);/mcp/wsWebSocket 端点处理长连接,模拟流式响应能力(虽然当前是单次返回,但架构已支持)。
注意:这里没有写一行 LLM 调用代码。因为 MCP 的价值恰恰在于——协议层与模型层解耦。你可以明天把
result替换为ollama.chat(model="qwen2.5:7b", messages=[...]),后天换成requests.post("https://api.anthropic.com/v1/messages", ...),只要响应格式符合 MCP Schema,客户端完全无感。
3.3 构建 CLI 客户端:用 Bash 实现最简 MCP 调用器
创建mcp-cli.sh(真正的 CLI,不是 Python 包):
#!/bin/bash # mcp-cli.sh - Minimal MCP Command Line Interface set -e SERVER_URL="${MCP_SERVER_URL:-http://localhost:8000/mcp}" METHOD="textDocument/completion" usage() { echo "Usage: $0 [OPTIONS]" echo "Options:" echo " -f, --file FILE Source file path (required)" echo " -l, --lang LANG Programming language (required, e.g., python, js)" echo " -t, --text TEXT Text to complete (required)" echo " -L, --line NUM Line number (default: 0)" echo " -C, --char NUM Character position (default: 0)" echo " -u, --url URL MCP server URL (default: $SERVER_URL)" exit 1 } while [[ $# -gt 0 ]]; do case $1 in -f|--file) FILE="$2" shift 2 ;; -l|--lang) LANG="$2" shift 2 ;; -t|--text) TEXT="$2" shift 2 ;; -L|--line) LINE="${2:-0}" shift 2 ;; -C|--char) CHAR="${2:-0}" shift 2 ;; -u|--url) SERVER_URL="$2" shift 2 ;; *) usage ;; esac done # 校验必填参数 if [[ -z "$FILE" || -z "$LANG" || -z "$TEXT" ]]; then echo "Error: --file, --lang, and --text are required." usage fi # 构造 MCP 请求体 PAYLOAD=$(cat <<EOF { "jsonrpc": "2.0", "id": "$(date +%s%N)", "method": "$METHOD", "params": { "text": "$TEXT", "uri": "file://$FILE", "language": "$LANG", "position": { "line": $LINE, "character": $CHAR } } } EOF ) # 发送请求并解析 if [[ "$SERVER_URL" == http* ]]; then RESPONSE=$(curl -s -X POST -H "Content-Type: application/json" \ --data "$PAYLOAD" "$SERVER_URL") else echo "Only HTTP transport supported in this minimal CLI." exit 1 fi # 提取 suggestions 并格式化输出 echo "$RESPONSE" | jq -r '.result.suggestions[] | "\(.label) [\(.kind)]"' 2>/dev/null || \ echo "$RESPONSE" | jq -r '.error.message // "Unknown error"' 2>/dev/null赋予执行权限:
chmod +x mcp-cli.sh3.4 一键启动与验证:三步完成端到端闭环
现在,我们用最原始的方式验证 CLI 与 MCP 的协作:
第一步:启动 MCP Server
uvicorn mcp_server:app --host 0.0.0.0 --port 8000 --reload访问http://localhost:8000/docs,你会看到 FastAPI 自动生成的 OpenAPI 文档,其中/mcp接口明确标注了MCPRequest输入模型——这就是协议契约的可视化体现。
第二步:用 curl 直接调用(绕过 CLI,证明协议独立性)
curl -X POST http://localhost:8000/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": "test-1", "method": "textDocument/completion", "params": { "text": "def calculate_tax(amount, rate): ", "uri": "file:///tmp/test.py", "language": "python", "position": { "line": 0, "character": 30 } } }'响应:
{ "jsonrpc": "2.0", "id": "test-1", "result": { "suggestions": [ {"label": "return amount * rate / 100", "kind": "snippet"}, {"label": "if amount < 0: raise ValueError('Invalid amount')", "kind": "snippet"} ] } }第三步:用 CLI 调用(证明 CLI 是协议友好封装)
./mcp-cli.sh \ --file "/tmp/test.py" \ --lang "python" \ --text "def calculate_tax(amount, rate): " \ --line 0 \ --char 30输出:
return amount * rate / 100 [snippet] if amount < 0: raise ValueError('Invalid amount') [snippet]实操心得:这个 demo 的价值不在于功能多强大,而在于它剥离了所有干扰项。你会发现:
- MCP Server 的核心逻辑只有 30 行有效代码;
- CLI 脚本的全部工作就是组装 JSON 并调用 curl;
- 两者之间没有 SDK、没有中间件、没有 vendor lock-in;
- 你随时可以把
curl换成fetch,把 Bash 换成 Pythonargparse,甚至用 Excel VBA 调用——只要 payload 符合协议,就一定能通。
这才是协议的价值:它让集成成本趋近于零,让创新发生在协议之上,而非协议之内。
4. 真实世界中的 CLI × MCP 组合:六个典型场景与避坑指南
理论和 demo 只是起点。在实际工程中,CLI 与 MCP 的组合远比“发个 completion 请求”复杂得多。下面我结合近期踩过的坑、客户现场的真实需求、以及开源项目的实践,梳理六个高频场景,并给出可直接抄作业的解决方案。
4.1 场景一:CI/CD 中自动化代码审查(gitlab cli+mcp-server)
需求背景:某金融客户要求每次 MR(Merge Request)提交时,自动对新增代码进行安全合规检查——比如禁止硬编码密码、检测 SQL 注入风险、验证日志脱敏。他们已有 GitLab CI,但不想在每个项目里重复写 Python 脚本。
MCP 方案:
- 在 CI Runner 上部署轻量
mcp-server,加载本地规则引擎(如 Semgrep + 自定义 YAML 规则); - GitLab CI job 中用
gitlab cli获取 diff 文件列表,再用mcp-cli批量调用codeReview/scan方法。
关键 CLI 脚本(.gitlab-ci.yml片段):
review-code: stage: test image: python:3.11 before_script: - pip install gitlab python-mcp-cli - export GITLAB_TOKEN=$CI_JOB_TOKEN script: - | # 获取本次 MR 修改的 Python 文件 CHANGED_FILES=$(gitlab api "/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/changes" \ | jq -r '.changes[] | select(.new_file == true or .changed == true) | .new_path' \ | grep '\.py$' | head -20) # 逐个文件调用 MCP 扫描 for file in $CHANGED_FILES; do echo "Scanning $file..." # 读取文件内容,构造 MCP 请求 CONTENT=$(cat "$file" | sed ':a;N;$!ba;s/\n/\\n/g') ./mcp-cli.sh \ --method "codeReview/scan" \ --param "file" "$file" \ --param "content" "$CONTENT" \ --param "rules" "security,pii" \ --url "http://mcp-server:8000/mcp" done避坑指南:
- ❌ 错误做法:在 CI 中直接调用
semgrep --config p/ci。问题在于规则更新需同步所有 Runner,且无法与 IDE 实时联动。 - ✅ 正确做法:MCP Server 统一管理规则库,CLI 只负责传递上下文。当安全团队更新一条正则规则时,所有 CI/IDE/Browser 扩展立即生效。
- ⚠️ 注意:GitLab API 返回的
diff是 patch 格式,需用git apply --reverse提取原始内容,否则 MCP Server 收到的是增量而非全量——这是我在某银行项目里 debug 了 3 小时才发现的坑。
4.2 场景二:浏览器自动化中的上下文桥接(playwright mcp)
需求背景:某电商公司要做竞品价格监控,需在 Chrome 中打开 100 个商品页,提取价格、库存、促销文案。但他们发现 Playwright 提取的 DOM 数据质量不稳定——有些页面用 React 动态渲染,page.content()拿不到最终 HTML。
MCP 方案:
- Playwright 脚本不再直接解析 DOM,而是注入 DevTools Hook,捕获 Network 请求和 Console 日志;
- 将这些原始上下文(XHR response body、console.error stack)按 MCP 的
browser/pageContext方法发送给本地mcp-server; - Server 端用 LLM 理解非结构化数据,返回结构化 JSON。
关键 Playwright 代码:
import { chromium } from 'playwright'; const browser = await chromium.launch(); const page = await browser.newPage(); // 注入 MCP 上下文捕获器 await page.addInitScript(` window.mcpContext = { networkRequests: [], consoleLogs: [] }; // 监听 XHR const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function() { window.mcpContext.networkRequests.push({ url: arguments[1], method: arguments[0] }); return originalOpen.apply(this, arguments); }; // 监听 console const originalLog = console.log; console.log = function(...args) { window.mcpContext.consoleLogs.push({ type: 'log', args: args.map(String) }); return originalLog.apply(console, args); }; `); await page.goto('https://example.com/product/123'); const context = await page.evaluate(() => window.mcpContext); // 用 curl 调用 MCP Server(Playwright 内置 fetch 不支持 WebSocket) const response = await fetch('http://localhost:8000/mcp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'playwright-' + Date.now(), method: 'browser/pageContext', params: context }) }); const result = await response.json(); console.log('Extracted price:', result.result.price); // {price: "¥299.00", stock: 12}避坑指南:
- ❌ 错误做法:用 Playwright 的
page.evaluate()直接调用 LLM API。这会导致 CORS、Token 泄露、超时等问题。 - ✅ 正确做法:Playwright 只做“数据采集器”,MCP Server 做“智能解析器”。两者物理隔离,安全边界清晰。
- ⚠️ 注意:
page.evaluate()中的 JS 无法访问 Node.js 环境,所以fetch必须指向公网可访问的 MCP Server(如http://host.docker.internal:8000/mcp),不能用localhost——这是 Docker 环境下最常被忽略的网络陷阱。
4.3 场景三:IDE 插件与 CLI 的双向同步(trae ide+zcode cli)
需求背景:前端团队用 Trae IDE 开发,但运维同学习惯用 CLI 部署。他们希望 IDE 中点击“Deploy to Staging”时,不仅触发 IDE 内置流程,还能同步更新 CLI 的~/.zcode/config.yaml,确保本地配置与 IDE 状态一致。
MCP 方案:
- Trae IDE 插件注册
deployment/trigger方法,当用户点击部署按钮时,发送包含环境、分支、镜像 tag 的 MCP 请求; zcode cli启动时注册为 MCP Client,监听deployment/configUpdate事件;- MCP Server 收到部署请求后,广播配置更新事件,CLI 自动重写本地 config。
关键 CLI 监听逻辑(Python 示例):
import asyncio import websockets import json async def listen_mcp_events(): async with websockets.connect("ws://localhost:8000/mcp/ws") as ws: # 发送订阅请求 await ws.send(json.dumps({ "jsonrpc": "2.0", "id": "sub-1", "method": "client/registerCapability", "params": { "registrations": [{ "id": "config-listener", "method": "deployment/configUpdate" }] } })) while True: try: msg = await ws.recv() data = json.loads(msg) if data.get("method") == "deployment/configUpdate": config = data["params"] with open("~/.zcode/config.yaml", "w") as f: yaml.dump(config, f) print(f"Config updated: {config['env']} -> {config['tag']}") except websockets.exceptions.ConnectionClosed: break asyncio.run(listen_mcp_events())避坑指南:
- ❌ 错误做法:IDE 插件直接写 CLI 配置文件。这会造成权限冲突(IDE 以用户身份运行,CLI 可能以 root 运行)。
- ✅ 正确做法:MCP Server 作为唯一可信源,CLI 和 IDE 都只读取 Server 广播的状态。
- ⚠️ 注意:WebSocket 连接必须心跳保活。我在某 SaaS 项目中发现,云服务商的 LB 默认 60 秒断连,导致 CLI 长时间失联。解决方案是在
listen_mcp_events中每 30 秒发一次{"jsonrpc":"2.0","method":"ping"}。
4.4 场景四:本地开发环境的协议调试(curl+jq+mcp-server)
需求背景:新加入团队的工程师总问:“我的 CLI 命令为什么没触发 MCP Server?”——但 Server 日志显示“收到请求,返回 200”,而 CLI 却报错“Connection refused”。
MCP 调试方案:
- 不依赖任何 CLI 工具,用
curl+jq构建最小验证链; - 用
tcpdump抓包确认请求是否发出; - 用
nc -l 8000模拟 Server,验证 CLI 是否真在发请求。
调试速查表:
| 现象 | 检查点 | 命令 |
|---|---|---|
CLI 报错Connection refused | CLI 是否连错端口?Server 是否监听 0.0.0.0? | netstat -tuln | grep :8000 |
| Server 收到请求但返回空响应 | CLI 发送的 JSON 是否合法?jsonrpc字段是否拼错? | echo '{"jsonrpc":"2.0","id":"1","method":"test"}' | curl -d @- http://localhost:8000/mcp |
响应中有error字段但 CLI 不显示 | CLI 是否忽略 stderr?是否用jq提取了错误信息? | curl -s http://localhost:8000/mcp -d '{"bad":"json"}' | jq '.error.message' |
| WebSocket 连接失败 | Server 是否启用 WS?URL 是否用ws://而非http://? | wscat -c ws://localhost:8000/mcp/ws |
避坑指南:
- ❌ 错误做法:在 Server 日志里加
print("Received:", request)调试。这会污染生产日志,且无法看到原始字节流。 - ✅ 正确做法:用
mitmproxy作为中间人,拦截 CLI 到 Server 的所有流量,查看原始请求/响应。 - ⚠️ 注意:
jq默认不处理空格和换行。mcp-cli.sh中的echo "$RESPONSE" | jq -r '.result.suggestions[]'若遇到未转义的双引号,会解析失败。解决方案是先用jq -r '.'输出原始 JSON,再二次处理。
4.5 场景五:多模型路由与负载均衡(codex cli+mcp-router)
需求背景:某 AI 平台同时接入 Claude、Qwen、Llama,但不同任务需不同模型——代码补全用 Qwen,SQL 生成用 Claude,日志分析用 Llama。用户不想记一堆 CLI 参数。
MCP 方案:
- 部署
mcp-router作为网关,根据method和params.language路由到不同后端; codex cli保持单一命令codex complete --file main.py,路由逻辑对用户透明。
Router 配置示例(router.yaml):
routes: - match: method: textDocument/completion params: language: python backend: http://qwen-server:8000/mcp - match: method: textDocument/completion params: language: sql backend: http://claude-server:8000/mcp - match: method: log/analyze backend: http://llama-server:8000/mcp避坑指南:
- ❌ 错误做法:在 CLI 里硬编码模型选择逻辑。这会导致每次新增模型都要发 CLI 版本。
- ✅ 正确做法:路由规则由
mcp-router统一管理,CLI 只需发送标准 MCP 请求。 - ⚠️ 注意:MCP Router 必须透传
id字段,否则客户端无法匹配响应。我在某项目中因 Router 重写了id,导致 CLI 等待超时——这是协议层最隐蔽的坑。
4.6 场景六:离线环境下的协议降级(unity mcp+ 本地 fallback)
需求背景:工业控制系统的 Unity 编辑器需在无网络环境下运行,但依然要提供基础代码补全。客户接受离线时精度下降,但不能完全失效。
MCP 方案:
- Unity 插件优先尝试连接
wss://api.xiaozhi.me/mcp/...; - 连接失败时,自动切换到本地
mcp-server,加载预训练的小模型(如 TinyLlama); - MCP 协议保持一致,只是
result字段的suggestions数量从 5 条降为 2 条。
关键 Unity C# 逻辑:
public async Task<McpResponse> SendMcpRequest(McpRequest request) { try { // 尝试云端 MCP Server