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 / HTTPstdio(管道)

本机 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 消息,生命周期大致是:

  1. initialize:Client 发握手请求,Server 返回协议版本和能力(capabilities)
  2. initialized:Client 确认握手完成
  3. tools/list:Client 询问 Server 提供哪些工具
  4. 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 写日志

三条通道的职责

通道方向用途规则
stdinClient → Server传输 JSON-RPC 请求(initialize、tools/call 等)Server 只读
stdoutServer → Client传输 JSON-RPC 响应和通知协议通道,绝不能打日志
stderrServer → 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 之所以能稳定启动,靠的就是"自包含"设计:

  1. command 用的是项目自带 venv 的 python.exe 绝对路径,不经过 PATH 解析
  2. 所有依赖(fastmcp、mcp、requests、trafilatura 等)都装在 .venv 里,启动时不需要现场装包
  3. args 也是入口脚本的绝对路径

与 Docker 的差别

理念上都是"自包含、不依赖宿主全局环境",但机制粒度完全不同:

Dockervenv + 绝对路径
隔离级别操作系统级(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 CLIGitHub 官方 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-search

CC Switch 会重写 config.toml

本机用 CC Switch 管理模型 provider,它重写 config.toml 时可能丢掉手工追加的 MCP / skill 配置。对策:

  1. 用 codex mcp add 而不是手工追加,至少保证写入格式正确
  2. 把 MCP 配置同时维护进 CC Switch 自己的模板,这样重写后还能带回来
  3. 切换模型后顺手跑 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_session cookie(HMAC 签名,payload 含用户名);nginx location = /_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 请求给 Web2APIGo 内存中直接调用内部 streamGenerate() 函数
网络开销存在本地 127.0.0.1 HTTP 回环序列化与解析开销零本地网络开销,内存通道直连
资源利用两套 HTTP Client,独立的连接管理共享内部 Cookie 池、代理调度、Token 桶与限流器
维护成本架构解耦,Web2API 可无脑跟进官方上游二进制代码耦合,上游有大版本更新时需合并代码与编译
适用场景上游频繁更新、需热插拔不同模型后端资源敏感型服务器、追求极致极简与高吞吐

3. 实战关键避坑点

  1. 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",...}
      
  2. 多层限流保护 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