MCP (Model Context Protocol)
MCP (Model Context Protocol, 模型上下文协议) 是 一种让 AI 应用(客户端)以统一方式调用外部工具和数据源的开放协议。它的核心价值是:把"工具调用"从"每个应用各自写死集成"变成"一套标准协议 + 可插拔的服务器"。
解决什么问题
没有 MCP 之前,每个 AI 应用想接入搜索、数据库、文件系统、浏览器,都要为每个工具写专属集成代码。有了 MCP 之后:
- 工具提供方(如搜索、数据库)实现一个 MCP Server
- AI 应用(如 Codex、Claude Desktop)作为 MCP Client
- 双方只通过协议通信,互相不知道对方的内部实现
核心架构
| 角色 | 说明 | 本机实例 |
|---|---|---|
| Client | 宿主 AI 应用,负责拉起服务器并调用工具 | Codex |
| Server | 提供一组工具的独立进程 | BingSearch-MCP(FastMCP) |
| Transport | 通信通道,常见 stdio / SSE / HTTP | stdio(管道) |
本机 C:\Users\18086\.codex\config.toml 中的真实配置(注意:2026-08 后 BingSearch-MCP 已拷贝到 C:\Users\18086\.agents\MCP\bing-search-mcp 使用):
[mcp_servers.bing-search]
type = "stdio"
command = 'C:\Users\18086\.agents\MCP\bing-search-mcp\.venv\Scripts\python.exe'
args = ['C:\Users\18086\.agents\MCP\bing-search-mcp\stdio_entry.py']
startup_timeout_sec = 120通信协议:JSON-RPC 2.0
Client 与 Server 之间走 JSON-RPC 2.0 消息,生命周期大致是:
- initialize:Client 发握手请求,Server 返回协议版本和能力(capabilities)
- initialized:Client 确认握手完成
- tools/list:Client 询问 Server 提供哪些工具
- tools/call:Client 调用具体工具,Server 执行并返回结果
每个工具调用在 Server 端就是一个普通函数,例如本机 src/index.py 里的:
@server.tool()
async def bing_search(query: str, count: int = 10, offset: int = 0) -> CallToolResult:
...stdin / stdout / stderr 的逻辑(重点)
stdio transport 的本质是:Client 以子进程方式 spawn Server,并给子进程接上三条管道。搞清楚这三条管道的职责,就理解了 MCP 的一半:
数据流
Codex (Client) ──spawn──> python.exe stdio_entry.py (Server 子进程)
│ stdin (写) ──────────────┐
│ │ Server 读取
│ stdout (读) <─────────────┤ Server 写入
│ stderr (读) <─────────────┘ Server 写日志三条通道的职责
| 通道 | 方向 | 用途 | 规则 |
|---|---|---|---|
| stdin | Client → Server | 传输 JSON-RPC 请求(initialize、tools/call 等) | Server 只读 |
| stdout | Server → Client | 传输 JSON-RPC 响应和通知 | 协议通道,绝不能打日志 |
| stderr | Server → Client | 日志、错误、调试输出 | 非协议通道,随便打日志 |
关键约束:stdout 是协议通道
- 消息格式是 newline-delimited JSON:每行一条 JSON-RPC 消息,UTF-8 编码,消息内不能含换行
- Server 永远不能往 stdout 打印普通日志,否则 Client 解析 JSON 会失败,连接直接坏掉
- 所有调试日志必须走 stderr,Client 会单独收集这些输出用于排查问题
这就是为什么本机这个项目的 stdio_entry.py 开头长这样:
import sys
# 强制 UTF-8,避免 Windows 管道/编码问题污染协议流或导致崩溃
for s in (sys.stdin, sys.stdout, sys.stderr):
try:
s.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass它同时做了两件事:把三个流都锁成 UTF-8(避免 Windows 默认代码页 GBK 把中文弄乱、把 JSON 搞坏),并且没往 stdout 写任何日志。项目源码里连 logger.info(...) 都被注释掉了,就是为了不污染协议流。
类比:和 LSP 一模一样
这套"子进程 + 三条管道 + JSON-RPC"的机制,和 LSP (Language Server Protocol) 完全相同——VS Code 也是用同样的方式 spawn 一个语言服务器,通过 stdin/stdout 通信。MCP 与 LSP 是同一类架构模式在不同领域的应用:LSP 让编辑器接入语言能力,MCP 让 AI 接入工具能力。
比喻:"绿色便携版",不是 Docker
MCP Server 的启动方式最像绿色便携软件:整个程序(解释器 + 依赖)打包在一个文件夹里,不装系统、不写注册表、不依赖全局环境,指哪跑哪。
本机这个 BingSearch-MCP 之所以能稳定启动,靠的就是"自包含"设计:
command用的是项目自带 venv 的 python.exe 绝对路径,不经过 PATH 解析- 所有依赖(fastmcp、mcp、requests、trafilatura 等)都装在
.venv里,启动时不需要现场装包 args也是入口脚本的绝对路径
与 Docker 的差别
理念上都是"自包含、不依赖宿主全局环境",但机制粒度完全不同:
| Docker | venv + 绝对路径 | |
|---|---|---|
| 隔离级别 | 操作系统级(namespace、文件系统、网络、进程树) | 只有 Python 包路径隔离(site-packages) |
| 能装什么 | 任意软件、系统库、服务 | 只有 Python 包 |
| 跨机器分发 | 镜像可 pull,到处跑 | 基本不能,绑死路径和平台 |
| 边界 | 容器内默认看不到宿主机文件 | python.exe 完全能读写宿主文件系统 |
| 资源隔离 | 有 cgroup 限 CPU/内存 | 无,就是个普通子进程 |
| 可移植性 | 任何机器都能跑 | 只在这台机器的这个位置能跑,换机器要改配置 |
一句话:Docker 是"整个小电脑",MCP Server 是"一个装了依赖的绿色版 Python"。
反面对比:为什么很多 MCP 会"环境爆炸"
很多第三方 MCP 配置写的是:
command = "npx"
args = ["-y", "@some/mcp-server"]或
command = "uvx"
args = ["some-mcp-server"]这种就要依赖 Node.js / uv 在 PATH 里、registry 可达、首次还要现场下载。这才是"和各种各样环境有关"的典型。用 venv + 绝对路径是最可控的一种。
托管 MCP:GitHub 官方 MCP 与 gh CLI 的区别
MCP Server 不一定要本地跑,也可以由服务方远程托管。本机 Codex 的 github MCP 就是一个实例:
- 地址:
https://api.githubcopilot.com/mcp/(GitHub 官方托管的 Streamable HTTP 服务) - 来源:Codex 插件
github@openai-curated自带.mcp.json声明,不需要本地安装 server - 认证:通过
bearer_token_env_var = GITHUB_PAT_TOKEN读取环境变量中的 PAT
GitHub 插件按 Codex 登录方式区分认证路径:
| Codex 登录方式 | GitHub MCP 认证 |
|---|---|
| ChatGPT 登录 | 走 GitHub App Connector(.app.json),不需要 PAT |
| API-key 登录(本机是这种方式) | 必须设置 GITHUB_PAT_TOKEN,否则启动报 Environment variable ... is not set |
GitHub MCP 与 gh CLI 的对比
| 维度 | gh CLI | GitHub 官方 MCP |
|---|---|---|
| 运行位置 | 本地命令行工具 | GitHub 官方托管,远程 API |
| 本地安装 | 需要装 gh + git | 不需要本地 server |
| 认证 | keyring 里保存的 gh token | 环境变量 GITHUB_PAT_TOKEN |
| 本地 git 操作 | 支持(clone/push 等) | 不支持,只有远程 API 工具 |
| 工具形态 | shell 命令,输出文本 | Codex 原生 MCP 工具,结构化 JSON |
| 网络依赖 | 本机网络,可走 7890/7897 代理 | Codex 进程直连 api.githubcopilot.com |
一句话:GitHub MCP 省的是本地 server 环境,不是省认证;gh CLI 省的是环境变量,因为认证已经在 keyring 里。
本机 Codex MCP 运维要点
配置位置与官方 CLI
- 全局配置:
C:\Users\18086\.codex\config.toml,段为[mcp_servers.<name>] - 优先使用官方命令管理,不要手工往 config.toml 追加:
codex mcp list
codex mcp get bing-search
codex mcp add bing-search -- "D:\git program\BingSearch-MCP\.venv\Scripts\python.exe" "D:\git program\BingSearch-MCP\stdio_entry.py"
codex mcp remove bing-searchCC Switch 会重写 config.toml
本机用 CC Switch 管理模型 provider,它重写 config.toml 时可能丢掉手工追加的 MCP / skill 配置。对策:
- 用
codex mcp add而不是手工追加,至少保证写入格式正确 - 把 MCP 配置同时维护进 CC Switch 自己的模板,这样重写后还能带回来
- 切换模型后顺手跑
codex mcp list确认配置还在
GitHub MCP 的 PAT 设置
API-key 登录的 Codex 会话需要 GITHUB_PAT_TOKEN。本机已用 gh 的 token 设置用户环境变量:
[Environment]::SetEnvironmentVariable('GITHUB_PAT_TOKEN', (gh auth token), 'User')gh 当前 token scopes:gist, read:org, repo, workflow,覆盖 GitHub 官方建议的最小权限集。注意该值是明文存在用户环境变量里的,轮换 token 后要重新设置。
Codex 更新后 node_repl 路径失效
Codex 升级后,bin 下的 hash 目录会变,旧配置里的绝对路径会失效,启动报 系统找不到指定的路径 (os error 3)。2026-08-06 本机修复后的路径:
command = 'C:\Users\18086\AppData\Local\OpenAI\Codex\runtimes\cua_node\fb8898c05a62885e\bin\node_repl.exe'
CODEX_CLI_PATH = 'C:\Users\18086\AppData\Local\OpenAI\Codex\bin\d7e8094cfb76a267\codex.exe'
NODE_REPL_NODE_PATH = 'C:\Users\18086\AppData\Local\OpenAI\Codex\runtimes\cua_node\fb8898c05a62885e\bin\node.exe'以后遇到同类问题,先 Get-ChildItem ... -Recurse -Include 'node_repl.exe','node.exe','codex.exe' 找当前实际路径,再更新配置。
Windows 下 FastMCP 的 stderr 编码坑
Codex 的 MCP 客户端(Rust rmcp)读取 server stderr 时要求合法 UTF-8。FastMCP 默认会在 stderr 打印带 emoji 的 banner,Windows 管道下可能产生非法 UTF-8,导致握手失败:
Failed to read MCP server stderr: stream did not contain valid UTF-8
handshaking with MCP server failed: connection closed: initialize response修复方式(本机 stdio_entry.py 已采用):
server.run(show_banner=False)并在入口把 stdin/stdout/stderr 全部 reconfigure(encoding="utf-8", errors="replace")。
MCP Python SDK 2.0 移除了 FastMCP(2026-08 验证)
pip install mcp 装到 2.x 后,from mcp.server.fastmcp import FastMCP 直接 ModuleNotFoundError。新 API 改为 MCPServer,位于 mcp.server.mcpserver.server:
from mcp.server.mcpserver.server import MCPServer
server = MCPServer(name="my-agent", title="My Agent", version="1.0.0")
@server.tool(name="my_tool", title="My Tool", description="...")
async def my_tool(query: str, model: str | None = None) -> str:
...
asyncio.run(server.run_stdio_async())要点:
add_tool(fn, ...)/@server.tool()从函数类型注解生成参数 JSON Schema,返回值直接 return 字符串即可。- 工具函数可 async;同步的重活(如 google-genai SDK)放
await asyncio.to_thread(...)避免阻塞事件循环。 - 日志一律走 stderr(print(..., file=sys.stderr)),stdout 只准跑 MCP 协议;否则客户端握手/解析失败。
- 客户端侧
CallToolResult字段是is_error(不是旧版的isError)。 - 一个坑:旧笔记里的
server.run(show_banner=False)是 FastMCP 参数,新 MCPServer 没有 banner,不需要也不能传。
本机实例:D:\MCP\google-search-agent\google_search_mcp.py(Google Search Agent,stdio,工具 google_search)。
本机 MCP 项目存放约定(2026-08 确立)
MCP Server 项目统一放在 C:\Users\18086\.agents\MCP(与 C:\Users\18086\.agents\skills 平级):
- 每个 MCP server 一个子目录,自包含 + 自带
.venv,绿色便携、指哪跑哪(同 BingSearch-MCP 形态)。 - 已有实例:
C:\Users\18086\.agents\MCP\google-search-agent(工具google_search)、C:\Users\18086\.agents\MCP\bing-search-mcp(工具bing_search/crawl_webpage)。 客户端注册用 venv 内的解释器,例如 Codex config.toml:
[mcp_servers.google-search] type = "stdio" command = 'C:\Users\18086\.agents\MCP\google-search-agent\.venv\Scripts\python.exe' args = ['C:\Users\18086\.agents\MCP\google-search-agent\google_search_mcp.py'] startup_timeout_sec = 120
远程异步任务型 MCP Server 架构(2026-09 实测 tx)
应用场景:MCP 客户端要触发"耗时长"的服务器端任务(如用 Claude CLI 生成讲义 PDF)。结论:MCP 请求只做入队,任务在服务器后台跑,客户端轮询。不要赌客户端支持实验性的 MCP Tasks(SEP-1686,Claude Code 未确认支持)。
落地实例:tx 服务器 /home/ubuntu/workspace/ops/handouts-agent-server(Node 22 + @modelcontextprotocol/sdk@1.30.0,systemd handouts-agent.service,端口 3180,公网 https://6767.chat/agent/handouts/mcp)。
关键设计(都可复用)
- stateless Streamable HTTP:
sessionIdGenerator: undefined,每个 POST 请求新建McpServer+StreamableHTTPServerTransport,用完即关,不跨请求共享实例(官方安全公告要求)。GET/DELETE 返回 405。 - 三个 MCP 工具:
check(服务能力+队列概览)、create(入队,秒回 taskId)、list(按 taskId 查进度,轮询用)。create不等待任务完成。 - 进度即状态:MCP progress 通知依赖客户端
_meta.progressToken+ 长连接,stateless 下不可靠;统一用list轮询(5–10 秒)作为唯一状态源。 - 持久化任务队列:每任务一个目录
tasks/<uuid>/,含input/ workspace/ output/ meta.json;worker 只并发 1,启动时扫描 meta.json 恢复状态(running→queued 重排)。 - 多入口共用核心:MCP 与普通 REST(
/api/tasks)共用同一个 JobStore,不复制业务逻辑;下载 URL 也要 Bearer key(不放公网可猜 URL)。 - 文件上传:MCP JSON 里 base64(单文件 8MiB、总 20MiB、最多 20 个);文件名必须 POSIX 相对路径,禁绝对路径/
../NUL/隐藏文件,只放行白名单扩展名。 - worker 用 CLI 而非 SDK query():
claude -p <prompt> --setting-sources user --permission-mode dontAsk --output-format stream-json --verbose;--output-format stream-json必须配--verbose,否则报错退出。 - 每任务独立 CLAUDE_CONFIG_DIR:为每个任务生成只含该任务白名单的 settings.json,不共享全局配置,防任务互读;上游 API key 由服务进程注入 env,不落盘、不进子进程继承的完整环境。
- Claude 上游走 DeepSeek:
ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic(官方 Anthropic 兼容端点),模型deepseek-v4-flash-vision-exp;必须走 mihomo 代理127.0.0.1:7890(tx 无 IPv6 出口,直连国外 API 会卡死)。 - nginx 路由:routes.json 注册 4 条 proxy → 3180,
deploy.sh生成后必须nginx -t+ reload + 逐层 curl。平台注册走platform/ops/<id>.json+docs/ops/<id>.md+node hooker.mjs doctor/test。
排过的坑
- tx 上 Bash 工具 heredoc 会吞反斜杠/引号 → 复杂脚本写成文件 scp 过去执行,别内联 heredoc。
- 远程
ops/根目录属 root,子目录才归 ubuntu;新服务目录要sudo mkdir+chown。 - 静态 key 只存
.env(600),claude mcp add --transport http <name> <url> --header "Authorization: Bearer <key>"。 - 中途实测进程占端口会导致 systemd
EADDRINUSE;先 kill 手动进程再systemctl restart。
Key 自助发放面板 / Console(复用站内鉴权,2026-09-01)
MCP 的客户端 key 不能写死在文档/聊天里,需要给每个用户按账号自助发放。如果宿主站已有鉴权(tx 的 auth-api + nginx auth_request),不要另起一套登录:
- 登录态:auth-api 发
admin_sessioncookie(HMAC 签名,payload 含用户名);nginxlocation = /_auth_verify是 internal 端点,转发 cookie 到 auth-api 的/internal/auth/verify校验。 - nginx 生成器(
generate-config.cjs)对带authRequest: true的路由自动加auth_request /_auth_verify;,authRedirect: "api"时未登录自动跳/api/login?next=...。这是现成设施,新服务只要在 routes.json 里声明字段即可,不用手写 nginx。 关键:auth-api 校验成功后响应头带
X-Auth-User: <用户名>。nginx 默认不透传这个头给后端。要在生成器加两行(仅对该路由生效,靠authUserHeader字段开关):auth_request_set $auth_user $upstream_http_x_auth_user; proxy_set_header X-Auth-User $auth_user;后端从
req.headers['x-auth-user']拿登录用户名,不信任客户端自报。- Key 存储:只存 SHA-256 哈希(
data/keys.json,权限 600),明文只在申请时返回一次。同一账号重复申请会覆盖旧 key(旧 key 立即失效)。 - 同源判断:只要都在
6767.chat主域下、cookie 是Path=/,/agent/handouts/console面板 fetch/agent/handouts/api/keys、/api/tasks就自动带 cookie,不跨域,无需 CORS。 - 页面与 API 分开两个路由:页面走
authRequest + authRedirect(未登录跳转登录页),API 只走authRequest(未登录返回 401,前端 JS 自行处理)。 - 复用前提是信任现有站内账号体系(本项目是 6767 admin htpasswd),适合自托管/内部服务;面向未知多租户要另行设计。
异步任务的进度呈现(2026-09-01)
长任务(>10 分钟)给前端看什么?结论:不要实时百分比(拿不到),用"状态 + 步骤日志"两级:
- 短状态
statusLabel:排队中 / 制作中 / 已完成 / 已失败。排队是真实状态(FIFO 队列 +queuePosition),要单独建模,不能只有 queued 英文。 - 长消息
message:当前步一句话总结(如"正在调用工具:Write")。 - 步骤日志
console:按时间戳的数组,agent 每个阶段/每次工具调用记一条。来源是claude -p --output-format stream-json的消息流——assistant块里的tool_use就是工具名,result是结束。只在阶段/工具变化时记一条,别每行 token 都记(否则日志被刷爆)。 - 存储:追加到任务 meta.json 的
console数组,限 200 条、保留最近 50 条给前端;MCP 和 REST 共用同一份。 - 磁盘压力清理:保留 24h 只是上限保护;每小时清理扫描时若磁盘剩余 < 阈值(如 1 GiB),终态任务提前清理释放空间(
fs.statfs读bavail*bsize),并打 warn 日志。
内嵌型 vs 外挂型 MCP Server 架构对比(Gemini Web2API 实战,2026-09)
在将具备特定能力的大模型(或反向代理/Web2API)封装为对外暴露工具的 MCP Server 时(例如将 Gemini Web2API 封装为 google_search 工具),存在两种经典架构模式:
1. 核心机制:调用的本质都是“提示词驱动”
很多人的疑问是:如果把 MCP 和 Web2API 分开复用,MCP 调用底层的机制是不是就是本地发一个带提示词的 HTTP 请求?
结论:完全正确。无论分开还是合体,底层机制本质上都是提示词注入(Prompt Injection)。
- 为什么必须是提示词?:因为 Gemini 网页端逆向(Web2API)并没有一个纯静态的
search_raw(keywords)纯搜索引擎接口;它的搜索能力是由大模型在对话理解中自主唤起 Google Search Grounding。 - 提示词的职责:通过在请求中注入专门的 Search Agent System Prompt(强制模型使用实时 Google 搜索,并严格使用
<conclusion>、<detail>、<sources>XML 标签输出),将一个通用的对话模型“降维特化”为一个高可信、带引用溯源的确定性搜索引擎。
2. 两种架构对比
【外挂独立模式】
Client ──(MCP 协议)──> 独立 MCP 进程 ──(本地 HTTP + Prompt)──> Web2API 进程 ──(逆向协议)──> Gemini 网页端
【一体化内嵌模式】
Client ──(MCP 协议)──> 单一进程 (/mcp 路由) ──(内存函数直调 + 提示词)──> 内部流生成 ──(逆向协议)──> Gemini 网页端| 维度 | 外挂独立型 MCP(分立双进程) | 一体化内嵌型 MCP(单二进制单进程) |
|---|---|---|
| 进程与端口 | 2 个进程(MCP 进程如 3182 + Web2API 进程 8083) | 1 个进程(仅 8083,路由挂载 /mcp 与 /v1/*) |
| 调用方式 | 独立 MCP 在本地发 HTTP POST 请求给 Web2API | Go 内存中直接调用内部 streamGenerate() 函数 |
| 网络开销 | 存在本地 127.0.0.1 HTTP 回环序列化与解析开销 | 零本地网络开销,内存通道直连 |
| 资源利用 | 两套 HTTP Client,独立的连接管理 | 共享内部 Cookie 池、代理调度、Token 桶与限流器 |
| 维护成本 | 架构解耦,Web2API 可无脑跟进官方上游二进制 | 代码耦合,上游有大版本更新时需合并代码与编译 |
| 适用场景 | 上游频繁更新、需热插拔不同模型后端 | 资源敏感型服务器、追求极致极简与高吞吐 |
3. 实战关键避坑点
MCP HTTP 传输规范(SSE 与 JSON 自动协商):
- 标准 MCP SDK Client 在发起 HTTP 请求时通常带
Accept: application/json, text/event-stream。 - 若服务器强行返回标准
application/json,部分严格遵从 Streamable HTTP 规范的客户端会直接报错断开或报 406。 正确做法:检测客户端
Accept头若含text/event-stream,则用 SSE 格式封装响应帧:event: message data: {"jsonrpc":"2.0",...}
- 标准 MCP SDK Client 在发起 HTTP 请求时通常带
多层限流保护 Cookie 池:
网页端逆向易受 Google 风控。必须在 MCP 入口层增加双重限流:
- 全局并发上限(如 Max Concurrency 30):超过直接拒绝排队。
- 单 IP 频次限制(如 3 RPM):防止单客户端连续并发爆破。
- 启动机制 = 父进程 spawn 子进程 + 三条管道,和 LSP 是同一架构模式
- 三条管道:stdin 收请求、stdout 出协议响应(不能打日志)、stderr 打日志
- 本机实例 = 绿色便携式 Python(venv 绝对路径),不是 Docker
- GitHub 官方也提供托管 MCP,API-key 会话需要 PAT,不能和本地 gh CLI 认证混为一谈
- Codex MCP 配置用
codex mcp命令管理,注意 CC Switch 重写和 Codex 升级导致的路径/配置丢失
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/AI apps