MCP (Model Context Protocol) 深度指南,协议原理、Python 实现与生产级安全实践

MCP 是由 Anthropic 于 2024 年底开源的标准协议,定义了 AI 客户端发现和调用外部工具的统一方式。本文从协议机制讲起,用可运行的 Python 代码拆解 MCP Server 的通信模式,再以 ServBay MCP Server 为实例,展示一个 39 个工具的生产级 MCP Server 在架构设计、安全模型和客户端集成方面的工程实践。

TL;DR

  • Model Context Protocol (MCP) 标准化了 AI 客户端与外部工具的通信方式,工具实现一次即可被多个客户端调用

  • 本文包含一个可运行的 Python MCP Server 示例,覆盖 initialize、tools/list、tools/call 三个关键方法

  • ServBay MCP Server 采用 Rust + Bridge 架构,提供 39 个 DevOps 工具,MCP 进程本身不接触数据库和文件系统,从架构层面实现安全隔离

  • 文章最后给出完整的 MCP 安全实践清单,涵盖认证、脱敏、二次确认、审计和供应链安全

为什么 AI 工具集成需要一套标准协议

大语言模型的推理能力持续提升,但真正制约 AI 应用落地效率的,是工具集成的碎片化问题。

Claude Code、Cursor、Codex 各有自己的插件体系。开发者如果要让 AI 助手操作数据库、管理服务或读取日志,需要为每个客户端分别编写适配层。功能相同,代码重复,维护成本随客户端数量线性增长。

MCP 的出发点就是消除这种重复。它把 AI 客户端与工具之间的交互抽象成一层标准协议。工具提供方只需按 MCP 的消息格式暴露能力,所有兼容 MCP 的客户端都能自动发现并调用这些工具。

三个直接的好处:

  • 写一次,接多端 。同一个 MCP Server 可以同时服务 Claude Code、Cursor、Codex 以及其他支持 MCP 的客户端

  • 工具可组合。服务管理、数据库操作、证书签发、日志读取等不同能力可以放在同一个 MCP Server 中,由 AI 客户端按需组合调用

  • 策略可收敛。认证、授权、审计、限流等安全策略在 MCP Server 侧统一实施,不再分散到各个客户端的插件中

MCP 协议的工作机制

两个角色

MCP 的通信只涉及两方。

MCP Client 是 AI 应用端,负责向用户提供交互界面,决定何时调用哪个工具。Claude Code、Cursor、GitHub Copilot 等都在逐步内置 MCP Client 能力。

MCP Server 是工具提供端,负责声明有哪些工具可用、每个工具需要什么参数、返回什么结果。它不关心调用方是哪个客户端,只处理符合协议格式的请求。

plain 复制代码
┌─────────────┐    MCP (JSON-RPC 2.0)    ┌─────────────┐
│  MCP Client │ ◄──────────────────────► │  MCP Server │
│  (AI 应用)   │    stdio / HTTP / SSE    │  (工具提供方) │
└─────────────┘                          └──────┬──────┘
                                                │
                                         ┌──────┴──────┐
                                         │  数据库、API  │
                                         │  文件、服务    │
                                         └─────────────┘

三个关键方法

MCP 基于 JSON-RPC 2.0 构建,一次完整的工具调用会经过三个阶段。

initialize ------ 握手

客户端连接后首先发送 initialize 请求,双方交换协议版本和能力声明。

json 复制代码
// 客户端 → 服务端
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "clientInfo": {"name": "claude-code", "version": "1.0.0"}
  }
}

// 服务端 → 客户端
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "serverInfo": {"name": "my-mcp-server", "version": "0.1.0"},
    "capabilities": {"tools": {}}
  }
}

2. tools/list ------ 发现工具

客户端请求工具列表。服务端返回所有可用工具的名称、描述和输入参数 the JSON Schema。

json 复制代码
// 客户端 → 服务端
{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}

// 服务端 → 客户端
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "search",
        "description": "Keyword search over local documents.",
        "inputSchema": {
          "type": "object",
          "properties": {"query": {"type": "string"}},
          "required": ["query"]
        }
      }
    ]
  }
}

AI 模型根据工具描述和用户意图判断是否需要调用某个工具。这里的 description 直接影响模型的调用决策,写法需要准确且具体。

3. tools/call ------ 调用工具

模型决定调用后,客户端发送 tools/call 请求。

json 复制代码
// 客户端 → 服务端
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": {"query": "MCP 安全"}
  }
}

// 服务端 → 客户端
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{"type": "text", "text": "{"results": [...]}"}],
    "isError": false
  }
}

传输层

MCP 本身不绑定特定传输方式。目前最常用的是 stdio,即客户端以子进程方式启动 MCP Server,通过标准输入输出交换 JSON 消息。这种方式部署简单、无需网络端口,适合本地开发场景。HTTP+SSE 和 WebSocket 适用于远程部署的情况。

MCP 不是什么?

有几个常见的误解需要澄清。

MCP 不是安全框架。它定义了消息格式,但不包含认证、授权、加密等安全机制。这些需要在 MCP Server 实现中自行处理。

MCP 不是应用框架。它不管理状态、不处理 UI、不提供数据库连接池。它只负责 AI 客户端和工具之间的消息传递。

MCP 也不限定编程语言。官方提供了 TypeScript 和 Python SDK,社区有 Rust、Go、Java 等实现。服务端用什么语言写,客户端不需要知道。

用 Python 实现一个 MCP Server

理解协议最有效的方式是写一个能跑的实现。下面是一个精简的 Python MCP Server,通过 stdio 传输,暴露两个工具。

完整代码

python 复制代码
#!/usr/bin/env python3
"""
最小化 MCP Server 示例 (stdio JSON-RPC 2.0)
工具: search(本地关键词搜索)、file_read(受限文件读取)
"""
import sys, json, threading, queue
from pathlib import Path

# ---------- 配置 ----------

ALLOWED_DIRS = [Path(".").resolve()]  # 文件读取白名单

DOCS = [
    {"id": "1", "title": "MCP 协议入门",
     "body": "Model Context Protocol 让 AI 客户端以标准方式调用外部工具"},
    {"id": "2", "title": "MCP 安全实践",
     "body": "白名单、审计日志和执行超时是 MCP 安全的基本要求"},
]

# ---------- 工具实现 ----------

def tool_search(args):
    q = (args.get("query") or "").lower()
    if not q:
        return {"results": []}
    hits = [d for d in DOCS if q in f"{d['title']} {d['body']}".lower()]
    return {"results": hits}

def tool_file_read(args):
    p = Path(args.get("path", "")).resolve()
    # 路径白名单检查
    if not any(str(p).startswith(str(d)) for d in ALLOWED_DIRS):
        raise PermissionError("Path outside allowed directories")
    if not p.is_file():
        raise FileNotFoundError("File not found")
    if p.stat().st_size > 1_048_576:
        raise ValueError("File exceeds 1 MB limit")
    return {"path": str(p), "content": p.read_text(encoding="utf-8", errors="ignore")}

# ---------- 工具注册表 ----------

TOOLS = {
    "search": {
        "description": "Keyword search over a local document index.",
        "inputSchema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Search keywords"}
            },
            "required": ["query"],
        },
        "fn": tool_search,
    },
    "file_read": {
        "description": "Read a text file from an approved directory (max 1 MB).",
        "inputSchema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "File path to read"}
            },
            "required": ["path"],
        },
        "fn": tool_file_read,
    },
}

# ---------- stdio 传输层 ----------

_msg_queue: queue.Queue = queue.Queue()

def _stdin_reader():
    for line in sys.stdin:
        stripped = line.strip()
        if stripped:
            _msg_queue.put(stripped)

threading.Thread(target=_stdin_reader, daemon=True).start()

def _send(msg: dict):
    sys.stdout.write(json.dumps(msg, ensure_ascii=False) + "\n")
    sys.stdout.flush()

def _respond(req_id, result=None, error=None):
    resp = {"jsonrpc": "2.0", "id": req_id}
    if error is not None:
        resp["error"] = {"code": -32000, "message": str(error)}
    else:
        resp["result"] = result
    _send(resp)

# ---------- 请求处理 ----------

def handle(req: dict):
    rid = req.get("id")
    method = req.get("method", "")
    params = req.get("params", {})

    # 握手
    if method == "initialize":
        return _respond(rid, {
            "protocolVersion": "2024-11-05",
            "serverInfo": {"name": "demo-mcp-server", "version": "0.1.0"},
            "capabilities": {"tools": {}},
        })

    if method == "notifications/initialized":
        return  # 客户端确认初始化完成,无需响应

    # 列出工具
    if method == "tools/list":
        tool_list = [
            {"name": n, "description": t["description"], "inputSchema": t["inputSchema"]}
            for n, t in TOOLS.items()
        ]
        return _respond(rid, {"tools": tool_list})

    # 调用工具
    if method == "tools/call":
        name = params.get("name")
        arguments = params.get("arguments", {})
        if name not in TOOLS:
            return _respond(rid, error=f"Unknown tool: {name}")
        try:
            data = TOOLS[name]["fn"](arguments)
            text = json.dumps(data, ensure_ascii=False, indent=2)
            return _respond(rid, {
                "content": [{"type": "text", "text": text}],
                "isError": False,
            })
        except Exception as e:
            return _respond(rid, {
                "content": [{"type": "text", "text": str(e)}],
                "isError": True,
            })

    _respond(rid, error=f"Method not found: {method}")

# ---------- 主循环 ----------

if __name__ == "__main__":
    while True:
        try:
            raw = _msg_queue.get()
            handle(json.loads(raw))
        except KeyboardInterrupt:
            break
        except json.JSONDecodeError as e:
            sys.stderr.write(f"Invalid JSON: {e}\n")
        except Exception as e:
            sys.stderr.write(f"Unhandled error: {e}\n")

运行和测试

保存为 server.py,用 Python 3.10+ 运行。发送一行 JSON 到标准输入即可得到响应。

bash 复制代码
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | python3 server.py

返回结果包含 search 和 file_read 两个工具的定义。再试一次工具调用:

bash 复制代码
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search","arguments":{"query":"MCP"}}}' | python3 server.py

这个 Demo 足以说明 MCP 的基本工作方式,但距离生产级使用还有明显差距。它没有认证机制、没有操作审计、工具数量有限、安全策略只有最基础的路径白名单。接下来看一个真实的产品级实现如何解决这些问题。

生产级实现:ServBay MCP Server 的工程实践

ServBay 不仅是本地开发环境管理工具,还能够一句话就让 AI Agent控制你的本地环境的AI开发底座,集成了 Nginx、PHP、MySQL、PostgreSQL、Redis、Node.js 等常见开发组件的管理能力。它的 MCP Server 随主程序一起分发,让 AI 客户端可以直接管理本地开发环境中的服务、网站、数据库、证书和 DNS 等。

Bridge 架构:MCP 进程与业务逻辑的隔离

ServBay MCP Server 采用 Rust 编写,使用 MCP 官方 Rust SDK(rmcp),通过 stdio 与 AI 客户端通信。

与前面 Python 示例直接在 MCP Server 进程内执行业务逻辑不同,ServBay 的做法是把 MCP Server 定位为纯粹的协议翻译器。它本身不接触数据库、不读写文件、不执行提权操作,所有实际工作都通过 Bridge 层委托给 ServBay 主进程完成。

plain 复制代码
AI 客户端 (Claude Code / Cursor / Codex)
    │
    │  MCP 协议 (stdio)
    ▼
ServBay MCP Server (Rust 二进制)
    │
    │  Bridge JSON-RPC (unix socket / named pipe)
    ▼
ServBay 主进程
    │
    ├── 服务管理 (Nginx, PHP, MySQL ...)
    ├── 网站/虚拟主机
    ├── 数据库
    ├── SSL 证书
    ├── DNS / Hosts
    └── ...

这种分层带来几个工程上的好处。

MCP Server 的二进制体积小、启动快。AI 客户端每次会话都会 spawn 一个 MCP Server 子进程,启动速度直接影响用户体验。

安全边界清晰。MCP Server 不内嵌任何密钥,运行时从文件读取 Token 后与 ServBay 主进程的 Bridge 端口建立认证连接。即使 MCP Server 进程被恶意利用,攻击面仅限于 Bridge 协议允许的操作范围。

ServBay 未运行时,MCP Server 会返回明确的错误提示,不会挂起或产生未定义行为。

接入 AI 客户端

MCP 的客户端发现机制

MCP 客户端通过配置文件找到可用的 MCP Server。不同客户端的配置路径和格式有所差异,但结构相似,都需要指定 MCP Server 的启动命令和参数。

以 Claude Code 为例,在 ~/.claude.json 中添加:

json 复制代码
{
  "mcpServers": {
    "servbay": {
      "command": "/Applications/ServBay.app/Contents/Resources/bin/servbay-mcp",
      "args": []
    }
  }
}

Cursor 的配置写在 ~/.cursor/mcp.json,格式与上面相同。

Codex 使用 TOML 格式,配置文件是 ~/.codex/config.toml

toml 复制代码
[mcp_servers.servbay]
command = "/Applications/ServBay.app/Contents/Resources/bin/servbay-mcp"

客户端读取配置后,在会话开始时 spawn 对应的 MCP Server 子进程,经过 initialize 握手和 tools/list 查询,就能拿到完整的工具列表供 AI 模型使用。

ServBay 的一键接入

手动编辑配置文件容易出错,特别是多客户端并存的情况。ServBay 在设置页面提供了一键接入功能。选择要接入的 AI 客户端,ServBay 会自动检测对应的配置文件路径,写入正确的 MCP Server 配置。写入前会备份原文件,同时提供还原按钮。

这个功能也处理了配置文件已有其他 MCP Server 的情况,会做 JSON 合并而不是覆盖,避免丢失已有的配置。

MCP 安全实践

MCP 协议本身不包含安全机制,安全责任完全在 MCP Server 的实现侧。以下是在生产环境中部署 MCP Server 应当考虑的安全实践,每一项都结合 ServBay 的实际做法进行说明。

1. 身份认证与传输安全

MCP Server 启动后应当验证调用方的身份。在 stdio 模式下,客户端直接启动 MCP Server 子进程,调用方即父进程,身份认证可以通过 Token 文件、环境变量或操作系统级别的进程权限来实现。

ServBay MCP Server 启动时从文件读取 Token,通过 unix socket(macOS)或 named pipe(Windows)与 ServBay 主进程的 Bridge 端口建立认证连接。二进制文件本身不内嵌任何密钥或凭据。

如果需要通过网络暴露 MCP Server(如远程开发场景),应当在前置代理层终止 TLS,并增加 OAuth2 或 mTLS 认证。

2. 数据边界与脱敏

MCP Server 返回的数据可能包含敏感信息。密码、API Key、Token 等字段不应原样出现在工具返回值中。

ServBay 的做法是对服务配置采用整体序列化加敏感字段黑名单脱敏。字段名匹配 password、passwd、secret、token、apiKey、accessKey、secretKey、privateKey、authToken、credential、masterKey、clientKey、defaultPw 时,字段值自动替换为 ***。匹配规则不区分大小写,按词边界划分以避免误伤(比如 keyLength、serverTokens 不会被当作敏感字段)。

这套脱敏规则全局生效,新增服务无需单独配置。

3. 破坏性操作的二次确认

删除站点、卸载软件包、修改数据库密码等操作应当有额外的确认机制,防止 AI 模型因为理解偏差导致不可逆的操作。

ServBay 对所有破坏性工具在 tool description 中标记 destructiveHint。支持该标记的 MCP 客户端(如 Claude Code)会在执行前弹出确认对话框,让用户明确批准后才继续。工具描述中也直接用 WARNING 标注了操作后果。

对于密码修改类工具(set_database_password),输入的新密码不会出现在返回结果和日志中。

4. 只读优先与幂等设计

39 个工具中,19 个是只读工具、7 个是可逆的控制类操作。只读和控制类工具合计占 67%,大部分使用场景不涉及状态变更。

写入类工具尽量设计为幂等操作。比如 enable_package 对已启用的包执行等于 no-op,upsert_host 对已存在的域名执行覆盖更新。这样即使 AI 模型重复调用也不会造成副作用累积。

5. 审计日志

每一次工具调用都应当记录。记录内容至少包含时间戳、工具名、调用参数和执行结果。敏感参数(如密码)在日志中同样需要脱敏。

在前面的 Python 示例中可以加入一个 JSON Lines 格式的审计日志。

python 复制代码
import time

def audit(event: dict):
    event["ts"] = int(time.time() * 1000)
    with open("audit.log", "a", encoding="utf-8") as f:
        f.write(json.dumps(event, ensure_ascii=False) + "\n")

在 tools/call 的处理逻辑中,工具执行前后各记录一条日志:

python 复制代码
audit({"event": "tool_call", "tool": name, "args": arguments})
# ... 执行工具 ...
audit({"event": "tool_result", "tool": name, "ok": True})

JSON Lines 格式方便后续导入 ELK、Splunk 等日志系统做审计分析。

6. 内容防火墙与输入校验

MCP Server 接收的参数来自 AI 模型的推理结果,不能假定所有输入都是安全的。SQL 注入、路径穿越、提示词注入等攻击方式都可能通过工具参数传入。

针对数据库查询工具,应当限制为只允许 SELECT 语句,或者更进一步,只通过预定义的查询模板暴露能力而不接受原始 SQL。

针对文件读取工具,路径必须做 resolve 后再与白名单比对,防止 ../ 穿越。

ServBay 的做法更彻底。MCP Server 本身不提供任何 shell 执行或任意文件读写类工具。所有操作都是预定义的、语义明确的业务接口(如 restart_service、create_website),不存在传入任意命令或路径的可能。

7. 执行超时与限流

工具调用应当有执行时间上限,防止 Bridge 或后端服务无响应时 MCP Server 进程挂起。对于需要长时间执行的操作(如 install_package),可以通过 MCP 的 progress notification 机制向客户端推送进度更新,同时设置总超时。

在 Python 示例中实现超时的方式:

python 复制代码
import threading

def call_with_timeout(fn, args, timeout=8.0):
    result = [None]
    error = [None]
    def runner():
        try:
            result[0] = fn(args)
        except Exception as e:
            error[0] = e
    t = threading.Thread(target=runner, daemon=True)
    t.start()
    t.join(timeout=timeout)
    if t.is_alive():
        raise TimeoutError("Tool execution timed out")
    if error[0]:
        raise error[0]
    return result[0]

限流可以用令牌桶或滑动窗口算法实现,按工具粒度或全局设定调用频率上限。

8. 供应链与分发安全

通过 mcp.json 或类似配置文件声明的 MCP Server 会被 AI 客户端自动启动执行,分发渠道的安全直接关系到用户系统的安全。

ServBay MCP Server 随 ServBay 主程序一起打包发布。macOS 端经过 Developer ID 签名和 Apple notarization,Windows 端经过 Authenticode 签名并附带时间戳。构建流水线中签名或校验失败会阻断发布,不允许跳过。

如果是独立分发的 MCP Server,建议发布时附带 SHA-256 校验和,并在仓库中维护 SBOM(Software Bill of Materials)。

常见问题

Model Context Protocol (MCP) 和传统 REST API 有什么区别?

REST API 是点对点的接口设计,每个 API 有自己的 URL、鉴权方式和调用约定。MCP 是一层元协议,它不替代具体的 API,而是定义了 AI 客户端如何发现和调用这些 API。一个 MCP Server 可以在内部对接多个 REST API,对外暴露统一的工具列表。

MCP 是否只能用在 IDE 中?

不是。IDE 是目前最常见的 MCP 客户端类型,但 MCP 协议本身不限制客户端形态。聊天助手、CLI 工具、自动化 Agent、语音助手等任何能发送 JSON-RPC 消息的应用都可以作为 MCP 客户端。

ServBay MCP Server 的 39 个工具都需要付费使用吗?

ServBay MCP Server 作为免费功能随主程序分发,不限制在 VIP 服务范围内。

如何保证 AI 模型不会误调用破坏性工具?

三层防护。第一层是工具描述中标注了操作性质和 WARNING,AI 模型在推理阶段就能据此判断风险。第二层是 destructiveHint 标记触发客户端的确认弹窗,用户需要明确批准。第三层是操作本身的设计,比如 delete_website 默认保留文件目录,set_database_password 的密码不回显。

MCP Server 的 description 怎么写比较好?

description 直接影响 AI 模型的调用决策。建议写明工具的功能、输入输出、副作用和限制条件,用英文撰写(面向 AI 模型而非终端用户),保持准确和具体,避免模糊的表述。

总结

Model Context Protocol 解决的是 AI 工具集成的标准化问题。它的协议设计足够简单,一个百行级别的 Python 脚本就能实现基本的 MCP Server。但从 Demo 到生产之间还需要填补认证、脱敏、审计、二次确认、超时控制、分发签名等一系列工程问题。

对于正在考虑为自己的产品或服务添加 MCP 支持的开发者,建议从只读工具开始,验证协议流程和客户端兼容性,再逐步扩展到写入和控制类操作,每扩展一类就同步补齐对应的安全策略。

相关推荐
道友可好1 小时前
Claude Code 泄露源码里的 89 个秘密
前端·人工智能·后端
阿拉斯攀登2 小时前
Embedding 模型选择与领域适配
人工智能·chatgpt·embedding·agent·ai编程·loop·rag
饼干哥哥2 小时前
用爱马仕多Agent组建跨境电商公司,出海赚美金
openai·agent·ai编程
用户8356290780512 小时前
使用 Python 保护和取消保护 Excel 工作表和工作簿
后端·python
Java面试题总结2 小时前
CubeSandbox 实战:从零部署到快照/克隆/回滚全体验
后端
Csvn3 小时前
Python 开发技巧:函数式编程与 lambda 进阶
后端
考虑考虑3 小时前
nginx配置ssl
运维·后端·nginx
HjhIron3 小时前
用RAG + Node.js 手搓一个语义搜索引擎,真香!
aigc·ai编程
uhakadotcom3 小时前
Python 中,`None` 不等于 `False`导致的问题
后端·面试·github