目 录CONTENT

文章目录

MCP 协议实战:AI工具集成的"USB-C"从入门到生产

PySuper
2026-02-07 / 0 评论 / 0 点赞 / 2 阅读 / 0 字
温馨提示:
本文最后更新于2026-05-28,若内容或图片失效,请留言反馈。 所有牛逼的人都有一段苦逼的岁月。 但是你只要像SB一样去坚持,终将牛逼!!! ✊✊✊

1. MCP 是什么:从 N×M 到 N+M

1.1 痛点:N×M 集成噩梦

在 MCP 出现之前,如果你有 10 个 AI 应用和 20 个工具,你需要写多少个集成?

答案:200 个。每个 AI 应用对每个工具都需要一个定制化的连接器。

┌─────────────────────────────────────────────────────┐
│              N × M 集成噩梦(Before MCP)              │
│                                                     │
│  AI App 1 ──→ Tool A, Tool B, Tool C ... Tool T     │
│  AI App 2 ──→ Tool A, Tool B, Tool C ... Tool T     │
│  AI App 3 ──→ Tool A, Tool B, Tool C ... Tool T     │
│  ...                                                │
│  AI App N ──→ Tool A, Tool B, Tool C ... Tool T     │
│                                                     │
│  总连接数 = N × M = 10 × 20 = 200                  │
└─────────────────────────────────────────────────────┘

每条连接线都包含:认证逻辑、错误处理、数据格式转换、版本兼容维护。新增一个模型?重写所有连接。新增一个工具?适配所有模型。这就是所谓的 N×M 问题。

1.2 MCP 的解法:USB-C for AI

MCP(Model Context Protocol,模型上下文协议)是 Anthropic 于 2024 年 11 月 25 日发布的开放标准。它的核心思路极其简单:

USB-C 统一了设备接口,MCP 统一了 AI 接口。

┌─────────────────────────────────────────────────────┐
│            N + M 模式(After MCP)                    │
│                                                     │
│  AI App 1 ──┐                                       │
│  AI App 2 ──┤     ┌───────────┐     ┌──────────┐   │
│  AI App 3 ──┼────▶│  MCP 协议  │◀────│ Tool A   │   │
│  ...        │     │  (标准接口) │◀────│ Tool B   │   │
│  AI App N ──┘     └───────────┘◀────│ Tool C   │   │
│                                    │ ...      │   │
│                                    │ Tool M   │   │
│                                    └──────────┘   │
│  总连接数 = N + M = 10 + 20 = 30                  │
└─────────────────────────────────────────────────────┘

每个 AI 应用实现一次 MCP Client,每个工具实现一次 MCP Server,任何 Client 可以连接任何 Server。

1.3 关键数字

指标

数值

来源

月 SDK 下载量

9700 万

Anthropic 生态报告,2026年3月

公开 MCP Server

10,000+

Agentic AI Foundation,2026年3月

GitHub Stars(主仓库)

82,300

GitHub,2026年3月

官方 SDK 语言

10+

Python, TypeScript, Java, Go, C#, Rust 等

从发布到 9700 万下载

16 个月

2024.11 → 2026.3

据《MCP hits 97M downloads — Anthropic just gave it away》(https://aiforautomation.io/news/2026-03-27-mcp-linux-foundation-97m-downloads-anthropic-donates)报道,MCP 的增长速度是 React npm 的 2 倍——React 用了约 3 年才达到类似的月下载规模。

1.4 治理演进

2024.11 ── Anthropic 发布 MCP(MIT License)
    │
2025.03 ── OpenAI 采纳 MCP
    │
2025.04 ── Google DeepMind 采纳 MCP
    │
2025.06 ── VS Code、Cursor、JetBrains 原生支持
    │
2025.12 ── Anthropic 将 MCP 捐赠给 Linux 基金会
    │         → 成立 Agentic AI Foundation (AAIF)
    │         → 联合创始方:Anthropic, OpenAI, Block
    │         → 支持方:Google, Microsoft, AWS, Cloudflare
    │
2026.03 ── 9700 万月下载,行业事实标准

据《MCP Hits 97 Million Installs: The Protocol Powering Agentic AI》(https://ai2.work/blog/mcp-hits-97-million-installs-the-protocol-powering-agentic-ai)报道,OpenAI 甚至宣布将在 2026 年中废弃其 Assistants API,全面转向 MCP 架构——这相当于用自家标准给 MCP 投票。

2. 核心架构:三层模型与 JSON-RPC 2.0

2.1 Host / Client / Server 三层

MCP 的架构由三个角色组成:

┌──────────────────────────────────────────────────────────────┐
│                        MCP Host                              │
│  (AI 应用,如 Claude Desktop、VS Code、Cursor)              │
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │
│  │ MCP Client  │  │ MCP Client  │  │ MCP Client  │  ...     │
│  │  (连接 A)   │  │  (连接 B)   │  │  (连接 C)   │          │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘          │
└─────────┼────────────────┼────────────────┼─────────────────┘
          │                │                │
          ▼                ▼                ▼
   ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
   │  MCP Server  │ │  MCP Server  │ │  MCP Server  │
   │  (GitHub)    │ │ (PostgreSQL) │ │ (文件系统)    │
   │              │ │              │ │              │
   │ Tools:       │ │ Tools:       │ │ Tools:       │
   │  - create_   │ │  - query     │ │  - read_file │
   │    issue     │ │  - insert    │ │  - write_file│
   │  - list_prs  │ │  - update    │ │  - list_dir  │
   │              │ │              │ │              │
   │ Resources:   │ │ Resources:   │ │ Resources:   │
   │  - repo_info │ │  - schema    │ │  - file_tree │
   └──────────────┘ └──────────────┘ └──────────────┘

角色说明

角色

职责

示例

Host

AI 应用本体,管理所有 Client 连接

Claude Desktop, VS Code, Cursor

Client

维护与单个 Server 的连接,处理能力协商

Host 内部组件,每个 Server 一个

Server

暴露工具/资源/提示,执行实际操作

GitHub Server, PostgreSQL Server

关键设计:一个 Host 可以同时连接多个 Server,每个 Client 维护独立的连接和会话状态。Server 之间互不感知,Host 负责协调。

2.2 JSON-RPC 2.0 通信

MCP 使用 JSON-RPC 2.0 作为消息格式,定义了三种消息类型:

# 1. Request(请求)—— 期望响应
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "read_file",
        "arguments": {"path": "/data/report.csv"}
    }
}

# 2. Response(响应)—— 匹配请求 ID
{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "content": [
            {"type": "text", "text": "date,revenue\n2026-01,150000"}
        ]
    }
}

# 3. Notification(通知)—— 不期望响应
{
    "jsonrpc": "2.0",
    "method": "notifications/tools/list_changed"
}

2.3 传输层:Stdio vs Streamable HTTP

MCP 定义了两种传输方式:

┌──────────────────────────────────────────────────────────────┐
│                     传输方式对比                              │
├──────────────┬──────────────────┬───────────────────────────┤
│     维度      │     Stdio        │    Streamable HTTP        │
├──────────────┼──────────────────┼───────────────────────────┤
│ 通信方式      │ stdin/stdout 管道 │ HTTP POST + SSE           │
├──────────────┼──────────────────┼───────────────────────────┤
│ 部署位置      │ 本地同一台机器    │ 本地或远程                 │
├──────────────┼──────────────────┼───────────────────────────┤
│ 认证          │ 无需(本地信任)  │ OAuth 2.0 / API Key       │
├──────────────┼──────────────────┼───────────────────────────┤
│ 多客户端      │ 不支持            │ 支持                       │
├──────────────┼──────────────────┼───────────────────────────┤
│ 网络延迟      │ 零(进程间通信)  │ 有(HTTP 往返)            │
├──────────────┼──────────────────┼───────────────────────────┤
│ 典型场景      │ IDE 插件、本地开发 │ 云部署、生产环境           │
├──────────────┼──────────────────┼───────────────────────────┤
│ 会话管理      │ 隐式(进程生命周期)│ Mcp-Session-Id Header    │
└──────────────┴──────────────────┴───────────────────────────┘

2.4 能力协商

Client 和 Server 在初始化时交换能力声明:

Client                                    Server
  │                                         │
  │  ── initialize ──────────────────────▶  │
  │     {protocolVersion: "2025-03-26",     │
  │      capabilities: {roots: ...},        │
  │      clientInfo: {name: "my-app"}}      │
  │                                         │
  │  ◀── initialize response ─────────────  │
  │     {protocolVersion: "2025-03-26",     │
  │      capabilities: {                    │
  │        tools: {listChanged: true},      │
  │        resources: {subscribe: true},    │
  │        prompts: {listChanged: true}     │
  │      },                                 │
  │      serverInfo: {name: "postgres-mcp"}}│
  │                                         │
  │  ── initialized (notification) ──────▶  │
  │                                         │
  │       === 会话建立,开始通信 ===          │

注意 listChanged 字段——这意味着 Server 可以在运行时通知 Client 工具列表发生了变化。这在动态环境中非常关键。

3. 三大原语:Tools / Resources / Prompts

MCP 定义了三个核心原语,分别对应 AI Agent 的三种基本交互模式:

┌──────────────────────────────────────────────────────────────┐
│                    MCP 三大原语                               │
├──────────────┬──────────────────┬───────────────────────────┤
│     原语      │     类比          │    核心特征               │
├──────────────┼──────────────────┼───────────────────────────┤
│   Tools      │   AI 的"手"      │  可执行操作,有副作用       │
│              │   (POST 请求)    │  写数据库、发邮件、调 API   │
├──────────────┼──────────────────┼───────────────────────────┤
│   Resources  │   AI 的"眼"      │  只读数据,无副作用         │
│              │   (GET 请求)     │  读文件、查数据库 schema    │
├──────────────┼──────────────────┼───────────────────────────┤
│   Prompts    │   AI 的"模板"    │  可复用的提示词模板          │
│              │   (模板引擎)     │  系统提示、少样本示例        │
└──────────────┴──────────────────┴───────────────────────────┘

3.1 Tools(工具调用)

Tools 是最常用的原语。它让 AI 能够执行操作。

# 工具定义示例
{
    "name": "query_database",
    "description": "Execute a SQL query on the PostgreSQL database",
    "inputSchema": {
        "type": "object",
        "properties": {
            "sql": {
                "type": "string",
                "description": "The SQL query to execute"
            },
            "database": {
                "type": "string",
                "description": "Database name",
                "default": "production"
            }
        },
        "required": ["sql"]
    }
}

3.2 Resources(数据读取)

Resources 提供只读的上下文数据,不需要消耗一个 Tool Call 的轮次:

# 资源定义示例
{
    "uri": "postgres://production/schema",
    "name": "Database Schema",
    "description": "Current schema of the production database",
    "mimeType": "application/json"
}

为什么 Resources 比 Tools 更重要? 很多开发者只关注 Tools,但 Resources 能让你在不消耗 Tool Call 的情况下注入上下文。这意味着 LLM 可以更快获得关键信息,减少不必要的往返。

3.3 Prompts(提示模板)

Prompts 是最被低估的原语,但在生产中极其有用:

# 提示模板示例
{
    "name": "code_review",
    "description": "Review code changes with specific criteria",
    "arguments": [
        {
            "name": "language",
            "description": "Programming language",
            "required": true
        },
        {
            "name": "severity",
            "description": "Review strictness level",
            "required": false
        }
    ]
}

4. 实战1:用 Python 写一个 MCP Server(文件管理工具)

下面我们用 Python SDK 写一个完整的文件管理 MCP Server。它支持列出目录、读取文件、写入文件、搜索文件四个工具。

4.1 项目结构

plaintext

file-manager-mcp/
├── server.py          # MCP Server 主文件
├── pyproject.toml     # 项目配置
└── README.md          # 说明文档

4.2 安装依赖

bash

pip install mcp

4.3 完整代码

python

#!/usr/bin/env python3
"""
文件管理 MCP Server
提供目录列表、文件读写、文件搜索四个工具

作者:PySuper | 来源:zhengxingtao.com
"""

import os
import json
import fnmatch
from pathlib import Path
from typing import Optional

from mcp.server.fastmcp import FastMCP

# 创建 MCP Server 实例
mcp = FastMCP(
    name="file-manager",
    version="1.0.0",
    description="File management MCP server for reading, writing, and searching files"
)

# 安全限制:只允许访问指定目录
ALLOWED_BASE_DIR = os.environ.get("MCP_FILE_BASE_DIR", os.path.expanduser("~/mcp-workspace"))

# 最大文件大小限制(10MB)
MAX_FILE_SIZE = 10 * 1024 * 1024


def _validate_path(path: str) -> Path:
    """验证路径是否在允许的目录内,防止路径遍历攻击"""
    resolved = Path(path).resolve()
    base = Path(ALLOWED_BASE_DIR).resolve()
    
    if not str(resolved).startswith(str(base)):
        raise ValueError(
            f"路径 '{path}' 超出允许的范围。"
            f"只允许访问 '{base}' 及其子目录。"
        )
    return resolved


def _safe_read(path: Path) -> str:
    """安全读取文件内容,带大小检查"""
    if not path.exists():
        raise FileNotFoundError(f"文件不存在:{path}")
    if not path.is_file():
        raise ValueError(f"路径不是文件:{path}")
    if path.stat().st_size > MAX_FILE_SIZE:
        raise ValueError(
            f"文件过大({path.stat().st_size} 字节),"
            f"最大允许 {MAX_FILE_SIZE} 字节"
        )
    return path.read_text(encoding="utf-8", errors="replace")


# ============================================================
# Tool 1: 列出目录内容
# ============================================================
@mcp.tool()
def list_directory(
    path: str = ".",
    pattern: str = "*",
    show_hidden: bool = False
) -> str:
    """
    列出指定目录下的文件和子目录。
    
    Args:
        path: 目录路径(相对于工作空间根目录)
        pattern: 文件名匹配模式,支持通配符,如 *.py, *.md
        show_hidden: 是否显示隐藏文件(以 . 开头的文件)
    
    Returns:
        JSON 格式的目录列表
    """
    target = _validate_path(os.path.join(ALLOWED_BASE_DIR, path))
    
    if not target.exists():
        raise FileNotFoundError(f"目录不存在:{target}")
    if not target.is_dir():
        raise ValueError(f"路径不是目录:{target}")
    
    entries = []
    for entry in sorted(target.iterdir()):
        # 过滤隐藏文件
        if not show_hidden and entry.name.startswith("."):
            continue
        # 过滤匹配模式
        if not fnmatch.fnmatch(entry.name, pattern):
            continue
        
        entry_info = {
            "name": entry.name,
            "type": "directory" if entry.is_dir() else "file",
            "size": entry.stat().st_size if entry.is_file() else None,
            "modified": entry.stat().st_mtime,
        }
        entries.append(entry_info)
    
    result = {
        "path": str(target),
        "total": len(entries),
        "entries": entries
    }
    return json.dumps(result, indent=2, ensure_ascii=False)


# ============================================================
# Tool 2: 读取文件内容
# ============================================================
@mcp.tool()
def read_file(
    path: str,
    encoding: str = "utf-8",
    start_line: Optional[int] = None,
    end_line: Optional[int] = None
) -> str:
    """
    读取指定文件的内容。
    
    Args:
        path: 文件路径(相对于工作空间根目录)
        encoding: 文件编码,默认 utf-8
        start_line: 起始行号(从1开始),不指定则从第1行开始
        end_line: 结束行号(包含),不指定则到文件末尾
    
    Returns:
        文件内容(可能被截断)
    """
    target = _validate_path(os.path.join(ALLOWED_BASE_DIR, path))
    content = _safe_read(target)
    
    # 按行范围截取
    if start_line is not None or end_line is not None:
        lines = content.splitlines(keepends=True)
        start = (start_line or 1) - 1  # 转为0-based索引
        end = end_line or len(lines)
        lines = lines[start:end]
        content = "".join(lines)
    
    # 截断超长内容
    if len(content) > 50000:
        content = content[:50000] + "\n\n... [文件内容已截断,共 {} 字符]".format(len(content))
    
    return content


# ============================================================
# Tool 3: 写入文件
# ============================================================
@mcp.tool()
def write_file(
    path: str,
    content: str,
    mode: str = "overwrite"
) -> str:
    """
    写入文件内容。
    
    Args:
        path: 文件路径(相对于工作空间根目录)
        content: 要写入的内容
        mode: 写入模式,overwrite(覆盖)或 append(追加)
    
    Returns:
        操作结果信息
    """
    target = _validate_path(os.path.join(ALLOWED_BASE_DIR, path))
    
    # 自动创建父目录
    target.parent.mkdir(parents=True, exist_ok=True)
    
    if mode == "append":
        if target.exists():
            existing = _safe_read(target)
            content = existing + "\n" + content
        with open(target, "w", encoding="utf-8") as f:
            f.write(content)
    elif mode == "overwrite":
        with open(target, "w", encoding="utf-8") as f:
            f.write(content)
    else:
        raise ValueError(f"不支持的写入模式:{mode},请使用 overwrite 或 append")
    
    result = {
        "status": "success",
        "path": str(target),
        "mode": mode,
        "bytes_written": len(content.encode("utf-8")),
        "lines": content.count("\n") + 1
    }
    return json.dumps(result, indent=2, ensure_ascii=False)


# ============================================================
# Tool 4: 搜索文件
# ============================================================
@mcp.tool()
def search_files(
    pattern: str,
    path: str = ".",
    max_depth: int = 5,
    file_type: Optional[str] = None
) -> str:
    """
    递归搜索匹配的文件。
    
    Args:
        pattern: 搜索模式,支持通配符,如 *.py, report*.csv
        path: 起始目录路径(相对于工作空间根目录)
        max_depth: 最大搜索深度
        file_type: 文件类型过滤,file 或 directory
    
    Returns:
        JSON 格式的搜索结果
    """
    target = _validate_path(os.path.join(ALLOWED_BASE_DIR, path))
    
    if not target.is_dir():
        raise ValueError(f"路径不是目录:{target}")
    
    matches = []
    base_depth = len(target.parts)
    
    for root, dirs, files in os.walk(target):
        current_depth = len(Path(root).parts) - base_depth
        if current_depth > max_depth:
            dirs.clear()  # 不再深入
            continue
        
        # 搜索文件
        for name in files:
            if fnmatch.fnmatch(name, pattern):
                full_path = Path(root) / name
                rel_path = full_path.relative_to(target)
                if file_type and file_type != "file":
                    continue
                matches.append({
                    "name": name,
                    "path": str(rel_path),
                    "size": full_path.stat().st_size,
                    "type": "file"
                })
        
        # 搜索目录
        for name in dirs:
            if fnmatch.fnmatch(name, pattern):
                full_path = Path(root) / name
                rel_path = full_path.relative_to(target)
                if file_type and file_type != "directory":
                    continue
                matches.append({
                    "name": name,
                    "path": str(rel_path),
                    "type": "directory"
                })
    
    result = {
        "pattern": pattern,
        "base_path": str(target),
        "total_matches": len(matches),
        "matches": matches[:100]  # 最多返回100条
    }
    return json.dumps(result, indent=2, ensure_ascii=False)


# ============================================================
# Resource: 工作空间概览
# ============================================================
@mcp.resource("workspace://overview")
def workspace_overview() -> str:
    """获取工作空间概览信息"""
    base = Path(ALLOWED_BASE_DIR)
    if not base.exists():
        return json.dumps({"error": f"工作空间目录不存在:{base}"})
    
    file_count = 0
    dir_count = 0
    total_size = 0
    
    for root, dirs, files in os.walk(base):
        dir_count += len(dirs)
        for f in files:
            file_count += 1
            total_size += (Path(root) / f).stat().st_size
    
    return json.dumps({
        "base_dir": str(base),
        "total_files": file_count,
        "total_directories": dir_count,
        "total_size_bytes": total_size,
        "total_size_human": f"{total_size / (1024*1024):.2f} MB"
    }, indent=2, ensure_ascii=False)


# ============================================================
# Prompt: 代码审查模板
# ============================================================
@mcp.prompt()
def code_review(language: str, focus: str = "all") -> str:
    """生成代码审查提示词模板"""
    return f"""请对以下 {language} 代码进行审查。

审查重点:{focus}

请从以下维度评估:
1. **代码质量**:命名规范、代码风格、可读性
2. **潜在 Bug**:边界条件、空指针、类型错误
3. **性能问题**:不必要的计算、内存泄漏、N+1 查询
4. **安全风险**:SQL 注入、XSS、路径遍历
5. **可维护性**:耦合度、测试覆盖、文档完整性

请按严重程度排序(Critical > Warning > Info),给出具体修改建议。"""


# ============================================================
# 启动 Server
# ============================================================
if __name__ == "__main__":
    # 确保工作空间目录存在
    Path(ALLOWED_BASE_DIR).mkdir(parents=True, exist_ok=True)
    print(f"[file-manager-mcp] 工作空间:{ALLOWED_BASE_DIR}")
    print(f"[file-manager-mcp] 启动 MCP Server...")
    
    # 以 stdio 模式启动
    mcp.run(transport="stdio")

4.4 测试运行

bash

# 设置工作空间目录
export MCP_FILE_BASE_DIR=/tmp/mcp-workspace

# 启动 Server
python server.py

4.5 用 MCP Inspector 调试

MCP 提供了一个可视化调试工具——Inspector:

bash

npx @modelcontextprotocol/inspector python server.py

打开浏览器访问 Inspector,你可以:

  • 查看所有注册的 Tools / Resources / Prompts

  • 手动调用工具并查看返回结果

  • 检查 JSON-RPC 消息流

  • 验证能力协商过程

5. 实战2:用 Python 写一个 MCP Client 对接 Claude/GPT

5.1 Client 代码

python

#!/usr/bin/env python3
"""
MCP Client:连接 MCP Server 并与 LLM 交互

作者:PySuper | 来源:zhengxingtao.com
"""

import asyncio
import json
from typing import Any, Optional

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


# ============================================================
# MCP Client 封装
# ============================================================
class MCPClient:
    """MCP Client 封装,支持工具发现和调用"""
    
    def __init__(self, server_command: list[str], server_env: Optional[dict] = None):
        """
        Args:
            server_command: 启动 MCP Server 的命令
            server_env: 传递给 Server 的环境变量
        """
        self.server_params = StdioServerParameters(
            command=server_command[0],
            args=server_command[1:],
            env=server_env
        )
        self.session: Optional[ClientSession] = None
        self._available_tools: list[dict] = []
        self._available_resources: list[dict] = []
        self._available_prompts: list[dict] = []
    
    async def connect(self):
        """连接到 MCP Server"""
        self._stdio_context = stdio_client(self.server_params)
        streams = await self._stdio_context.__aenter__()
        
        self._session_context = ClientSession(*streams)
        self.session = await self._session_context.__aenter__()
        
        # 初始化会话
        result = await self.session.initialize()
        print(f"[MCP Client] 连接成功")
        print(f"  Server: {result.serverInfo.name} v{result.serverInfo.version}")
        print(f"  协议版本: {result.protocolVersion}")
        print(f"  能力: {result.capabilities}")
        
        # 获取可用工具、资源、提示
        await self._discover_capabilities()
    
    async def _discover_capabilities(self):
        """发现 Server 提供的所有能力"""
        # 发现工具
        try:
            tools_result = await self.session.list_tools()
            self._available_tools = [
                {
                    "name": t.name,
                    "description": t.description,
                    "inputSchema": t.inputSchema
                }
                for t in tools_result.tools
            ]
            print(f"\n[MCP Client] 发现 {len(self._available_tools)} 个工具:")
            for t in self._available_tools:
                print(f"  - {t['name']}: {t['description'][:60]}...")
        except Exception as e:
            print(f"[MCP Client] 工具发现失败: {e}")
        
        # 发现资源
        try:
            resources_result = await self.session.list_resources()
            self._available_resources = [
                {
                    "uri": r.uri,
                    "name": r.name,
                    "description": r.description,
                    "mimeType": r.mimeType
                }
                for r in resources_result.resources
            ]
            print(f"\n[MCP Client] 发现 {len(self._available_resources)} 个资源:")
            for r in self._available_resources:
                print(f"  - {r['name']} ({r['uri']})")
        except Exception as e:
            print(f"[MCP Client] 资源发现失败: {e}")
        
        # 发现提示模板
        try:
            prompts_result = await self.session.list_prompts()
            self._available_prompts = [
                {
                    "name": p.name,
                    "description": p.description,
                    "arguments": p.arguments
                }
                for p in prompts_result.prompts
            ]
            print(f"\n[MCP Client] 发现 {len(self._available_prompts)} 个提示模板:")
            for p in self._available_prompts:
                print(f"  - {p['name']}: {p['description'][:60]}...")
        except Exception as e:
            print(f"[MCP Client] 提示发现失败: {e}")
    
    async def call_tool(self, name: str, arguments: dict[str, Any]) -> str:
        """调用 MCP 工具"""
        if not self.session:
            raise RuntimeError("未连接到 MCP Server")
        
        result = await self.session.call_tool(name, arguments)
        
        # 提取文本内容
        text_parts = []
        for content in result.content:
            if content.type == "text":
                text_parts.append(content.text)
            elif content.type == "image":
                text_parts.append(f"[图片数据: {content.mimeType}]")
            elif content.type == "resource":
                text_parts.append(f"[资源: {content.resource.uri}]")
        
        return "\n".join(text_parts)
    
    async def read_resource(self, uri: str) -> str:
        """读取 MCP 资源"""
        if not self.session:
            raise RuntimeError("未连接到 MCP Server")
        
        result = await self.session.read_resource(uri)
        text_parts = []
        for content in result.contents:
            if hasattr(content, "text"):
                text_parts.append(content.text)
            elif hasattr(content, "blob"):
                text_parts.append(f"[二进制数据: {content.mimeType}]")
        return "\n".join(text_parts)
    
    async def get_prompt(self, name: str, arguments: dict[str, str] = None) -> str:
        """获取 MCP 提示模板"""
        if not self.session:
            raise RuntimeError("未连接到 MCP Server")
        
        result = await self.session.get_prompt(name, arguments)
        text_parts = []
        for msg in result.messages:
            if hasattr(msg.content, "text"):
                text_parts.append(msg.content.text)
        return "\n".join(text_parts)
    
    @property
    def tools_for_llm(self) -> list[dict]:
        """返回 LLM 可用的工具定义(兼容 OpenAI Function Calling 格式)"""
        return [
            {
                "type": "function",
                "function": {
                    "name": t["name"],
                    "description": t["description"],
                    "parameters": t["inputSchema"]
                }
            }
            for t in self._available_tools
        ]
    
    async def disconnect(self):
        """断开连接"""
        if self._session_context:
            await self._session_context.__aexit__(None, None, None)
        if self._stdio_context:
            await self._stdio_context.__aexit__(None, None, None)


# ============================================================
# 演示:连接文件管理 MCP Server
# ============================================================
async def demo():
    """演示 MCP Client 的完整使用流程"""
    
    client = MCPClient(
        server_command=["python", "server.py"],
        server_env={"MCP_FILE_BASE_DIR": "/tmp/mcp-demo"}
    )
    
    try:
        # 1. 连接
        await client.connect()
        
        # 2. 调用工具 - 写入文件
        print("\n" + "="*60)
        print("调用工具:write_file")
        result = await client.call_tool("write_file", {
            "path": "hello.txt",
            "content": "Hello, MCP! 这是通过 MCP 协议写入的文件。\n第二行内容。"
        })
        print(f"结果: {result}")
        
        # 3. 调用工具 - 列出目录
        print("\n" + "="*60)
        print("调用工具:list_directory")
        result = await client.call_tool("list_directory", {"path": "."})
        print(f"结果: {result}")
        
        # 4. 调用工具 - 读取文件
        print("\n" + "="*60)
        print("调用工具:read_file")
        result = await client.call_tool("read_file", {"path": "hello.txt"})
        print(f"结果: {result}")
        
        # 5. 调用工具 - 搜索文件
        print("\n" + "="*60)
        print("调用工具:search_files")
        result = await client.call_tool("search_files", {"pattern": "*.txt"})
        print(f"结果: {result}")
        
        # 6. 读取资源
        print("\n" + "="*60)
        print("读取资源:workspace://overview")
        result = await client.read_resource("workspace://overview")
        print(f"结果: {result}")
        
        # 7. 获取提示模板
        print("\n" + "="*60)
        print("获取提示模板:code_review")
        result = await client.get_prompt("code_review", {"language": "Python", "focus": "security"})
        print(f"结果: {result}")
        
        # 8. 打印 LLM 工具定义
        print("\n" + "="*60)
        print("LLM 工具定义(OpenAI 格式):")
        for tool in client.tools_for_llm:
            print(f"  - {tool['function']['name']}")
        
    finally:
        await client.disconnect()


if __name__ == "__main__":
    asyncio.run(demo())

5.2 对接 OpenAI API 的完整流程

python

#!/usr/bin/env python3
"""
MCP + OpenAI 完整集成示例

作者:PySuper | 来源:zhengxingtao.com
"""

import asyncio
import os
import json
from openai import OpenAI

from mcp_client import MCPClient


async def chat_with_mcp_tools(user_message: str):
    """使用 MCP 工具与 OpenAI 对话"""
    
    # 1. 连接 MCP Server
    client = MCPClient(
        server_command=["python", "server.py"],
        server_env={"MCP_FILE_BASE_DIR": "/tmp/mcp-demo"}
    )
    
    try:
        await client.connect()
        
        # 2. 初始化 OpenAI Client
        openai_client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
        
        # 3. 构造消息
        messages = [
            {"role": "system", "content": "你是一个文件管理助手,可以帮助用户管理文件。"},
            {"role": "user", "content": user_message}
        ]
        
        # 4. 第一次调用 LLM,带上 MCP 工具
        response = openai_client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=client.tools_for_llm,
            tool_choice="auto"
        )
        
        message = response.choices[0].message
        messages.append(message)
        
        # 5. 处理工具调用循环
        max_iterations = 10
        for _ in range(max_iterations):
            if not message.tool_calls:
                break
            
            for tool_call in message.tool_calls:
                function_name = tool_call.function.name
                function_args = json.loads(tool_call.function.arguments)
                
                print(f"\n[工具调用] {function_name}({function_args})")
                
                # 调用 MCP 工具
                result = await client.call_tool(function_name, function_args)
                
                print(f"[工具结果] {result[:200]}...")
                
                # 将结果回填给 LLM
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result
                })
            
            # 再次调用 LLM
            response = openai_client.chat.completions.create(
                model="gpt-4o",
                messages=messages,
                tools=client.tools_for_llm,
                tool_choice="auto"
            )
            message = response.choices[0].message
            messages.append(message)
        
        # 6. 输出最终回复
        print(f"\n[AI 回复] {message.content}")
        return message.content
        
    finally:
        await client.disconnect()


# ============================================================
# 运行示例
# ============================================================
async def main():
    # 示例1:简单文件操作
    await chat_with_mcp_tools("帮我在工作空间创建一个 config.json 文件,内容是默认配置")
    
    print("\n" + "="*60 + "\n")
    
    # 示例2:多步操作
    await chat_with_mcp_tools("列出工作空间的所有文件,然后告诉我有哪些是 JSON 文件")


if __name__ == "__main__":
    asyncio.run(main())

6. 安全模型:权限控制、沙箱执行、Prompt 注入防御

6.1 三层权限模型

plaintext

┌──────────────────────────────────────────────────────────────┐
│                    MCP 安全架构                               │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ 第一层:Host 控制(用户授权)                           │  │
│  │   - 用户决定是否允许连接某个 MCP Server                 │  │
│  │   - 用户审批工具调用请求                                │  │
│  │   - 类似:手机 App 的权限弹窗                          │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ 第二层:Client 控制(连接限制)                         │  │
│  │   - Client 决定发送什么请求                             │  │
│  │   - Client 过滤敏感参数                                │  │
│  │   - 类似:浏览器 CORS 策略                             │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ 第三层:Server 控制(能力暴露)                         │  │
│  │   - Server 决定暴露哪些工具和资源                       │  │
│  │   - Server 实现访问控制和数据过滤                       │  │
│  │   - 类似:API 的鉴权和脱敏                             │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

6.2 Prompt 注入防御

MCP 的 Server 返回数据会被注入到 LLM 的上下文中,这带来了 Prompt 注入风险。防御策略:

python

# 在 MCP Server 中实现输出过滤
import re

def sanitize_output(text: str) -> str:
    """过滤潜在的 Prompt 注入内容"""
    # 1. 移除可能的系统提示伪装
    patterns = [
        r"(?i)ignore\s+(previous|above|all)\s+instructions",
        r"(?i)you\s+are\s+now\s+",
        r"(?i)system\s*:\s*",
        r"(?i)<\s*/?\s*(system|instruction|prompt)\s*>",
    ]
    for pattern in patterns:
        text = re.sub(pattern, "[FILTERED]", text)
    
    # 2. 限制输出长度
    if len(text) > 100000:
        text = text[:100000] + "\n[输出已截断]"
    
    return text

6.3 沙箱执行

对于需要执行代码的 MCP Server,推荐使用沙箱:

python

import subprocess
import tempfile

def execute_in_sandbox(code: str, timeout: int = 30) -> dict:
    """在 Docker 沙箱中执行代码"""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
        f.write(code)
        f.flush()
        
        try:
            result = subprocess.run(
                [
                    "docker", "run", "--rm",
                    "--network", "none",         # 无网络访问
                    "--memory", "512m",           # 内存限制
                    "--cpus", "1.0",              # CPU 限制
                    "--read-only",                # 只读文件系统
                    "-v", f"{f.name}:/tmp/code.py:ro",
                    "python:3.12-slim",
                    "python", "/tmp/code.py"
                ],
                capture_output=True,
                text=True,
                timeout=timeout
            )
            
            return {
                "stdout": result.stdout[:5000],
                "stderr": result.stderr[:5000],
                "exit_code": result.returncode
            }
        except subprocess.TimeoutExpired:
            return {"error": f"执行超时({timeout}秒)"}
        finally:
            os.unlink(f.name)

7. 生产部署:SSE 传输、OAuth 认证、水平扩展、监控

7.1 Streamable HTTP 传输

生产环境必须使用 Streamable HTTP 传输:

python

#!/usr/bin/env python3
"""
生产级 MCP Server(Streamable HTTP 传输)

作者:PySuper | 来源:zhengxingtao.com
"""

import os
from mcp.server.fastmcp import FastMCP

mcp = FastMCP(
    name="file-manager-prod",
    version="1.0.0",
    description="Production file management MCP server"
)

# ... (工具定义与前面相同,省略)

if __name__ == "__main__":
    # 以 Streamable HTTP 模式启动
    mcp.run(
        transport="streamable-http",
        host="0.0.0.0",
        port=8080
    )

7.2 OAuth 2.0 认证

python

from authlib.integrations.starlette_client import OAuth
from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import JSONResponse

# OAuth 配置
oauth = OAuth()
oauth.register(
    name="github",
    client_id=os.environ.get("GITHUB_CLIENT_ID"),
    client_secret=os.environ.get("GITHUB_CLIENT_SECRET"),
    authorize_url="https://github.com/login/oauth/authorize",
    access_token_url="https://github.com/login/oauth/access_token",
    userinfo_endpoint="https://api.github.com/user",
)

# 在 MCP Server 前加认证中间件
async def auth_middleware(request, call_next):
    """验证请求中的 Bearer Token"""
    auth_header = request.headers.get("Authorization", "")
    
    if not auth_header.startswith("Bearer "):
        return JSONResponse(
            {"error": "unauthorized", "message": "Missing or invalid token"},
            status_code=401
        )
    
    token = auth_header[7:]
    # 验证 token(这里简化,实际应查询 OAuth Provider)
    if not validate_token(token):
        return JSONResponse(
            {"error": "forbidden", "message": "Invalid or expired token"},
            status_code=403
        )
    
    response = await call_next(request)
    return response


def validate_token(token: str) -> bool:
    """验证 OAuth Token"""
    # 实际实现:查询 OAuth Provider 的 /introspect 端点
    # 或验证 JWT 签名
    return len(token) > 10  # 简化示例

7.3 水平扩展

plaintext

┌──────────────────────────────────────────────────────────────┐
│                   MCP Server 水平扩展架构                     │
│                                                              │
│                   ┌──────────────┐                           │
│                   │  负载均衡器   │                           │
│                   │ (Nginx/ALB)  │                           │
│                   └──────┬───────┘                           │
│                          │                                   │
│            ┌─────────────┼─────────────┐                     │
│            │             │             │                     │
│     ┌──────▼─────┐ ┌────▼──────┐ ┌────▼──────┐             │
│     │ MCP Server │ │ MCP Server│ │ MCP Server│             │
│     │  Instance 1│ │ Instance 2│ │ Instance 3│             │
│     └──────┬─────┘ └────┬──────┘ └────┬──────┘             │
│            │             │             │                     │
│            └─────────────┼─────────────┘                     │
│                          │                                   │
│                   ┌──────▼───────┐                           │
│                   │ 共享存储层    │                           │
│                   │ (Redis/DB)   │                           │
│                   └──────────────┘                           │
│                                                              │
│  注意:Streamable HTTP 的会话亲和性                          │
│  - 同一个 Mcp-Session-Id 必须路由到同一个实例                │
│  - 使用 Nginx 的 hash 负载均衡策略                          │
└──────────────────────────────────────────────────────────────┘

Nginx 配置示例:

nginx

upstream mcp_servers {
    # 基于 Session ID 的会话亲和
    hash $http_mcp_session_id consistent;
    
    server mcp-server-1:8080;
    server mcp-server-2:8080;
    server mcp-server-3:8080;
}

server {
    listen 443 ssl;
    server_name mcp.example.com;
    
    location /mcp {
        proxy_pass http://mcp_servers;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        
        # SSE 支持
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 86400s;
    }
}

7.4 监控

python

import time
import logging
from collections import defaultdict
from dataclasses import dataclass, field

logger = logging.getLogger("mcp-monitor")

@dataclass
class MCPMetrics:
    """MCP Server 监控指标"""
    tool_calls: dict = field(default_factory=lambda: defaultdict(int))
    tool_errors: dict = field(default_factory=lambda: defaultdict(int))
    tool_latency: dict = field(default_factory=lambda: defaultdict(list))
    active_sessions: int = 0
    total_requests: int = 0
    
    def record_call(self, tool_name: str, latency_ms: float, success: bool):
        self.tool_calls[tool_name] += 1
        self.total_requests += 1
        if not success:
            self.tool_errors[tool_name] += 1
        self.tool_latency[tool_name].append(latency_ms)
        # 只保留最近 1000 条延迟记录
        if len(self.tool_latency[tool_name]) > 1000:
            self.tool_latency[tool_name] = self.tool_latency[tool_name][-1000:]
    
    def get_summary(self) -> dict:
        summary = {}
        for tool, count in self.tool_calls.items():
            latencies = self.tool_latency[tool]
            summary[tool] = {
                "calls": count,
                "errors": self.tool_errors[tool],
                "error_rate": self.tool_errors[tool] / max(count, 1),
                "avg_latency_ms": sum(latencies) / max(len(latencies), 1),
                "p99_latency_ms": sorted(latencies)[int(len(latencies) * 0.99)] if latencies else 0,
            }
        return summary

metrics = MCPMetrics()

# 在工具调用前后记录
def track_tool(func):
    """装饰器:记录工具调用的延迟和成功率"""
    async def wrapper(*args, **kwargs):
        start = time.time()
        success = True
        try:
            result = await func(*args, **kwargs)
            return result
        except Exception as e:
            success = False
            raise
        finally:
            latency = (time.time() - start) * 1000
            metrics.record_call(func.__name__, latency, success)
            logger.info(f"Tool {func.__name__}: {latency:.1f}ms, success={success}")
    return wrapper

8. 与 Skill 的关系:MCP 是"连工具的手",Skill 是"存能力的脑"

这是很多人混淆的概念,让我用一张图说清楚:

plaintext

┌──────────────────────────────────────────────────────────────┐
│                 MCP 与 Skill 的定位对比                       │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐   │
│  │                   AI Agent                            │   │
│  │                                                      │   │
│  │  ┌─────────────┐              ┌─────────────────┐   │   │
│  │  │   Skill     │              │      MCP        │   │   │
│  │  │  "存能力的脑"│              │   "连工具的手"   │   │   │
│  │  │             │              │                 │   │   │
│  │  │ • 知道"做什么"│              │ • 知道"怎么连"  │   │   │
│  │  │ • 封装流程   │              │ • 标准化接口     │   │   │
│  │  │ • 行业知识   │              │ • 发现工具       │   │   │
│  │  │ • 最佳实践   │              │ • 调用工具       │   │   │
│  │  │             │              │                 │   │   │
│  │  │ 格式:       │              │ 格式:           │   │   │
│  │  │ SKILL.md    │              │ JSON-RPC 2.0    │   │   │
│  │  └──────┬──────┘              └────────┬────────┘   │   │
│  │         │                              │            │   │
│  │         │    Skill 指导 Agent           │            │   │
│  │         │    何时调用哪个工具            │            │   │
│  │         └──────────────────────────────┘            │   │
│  │                        │                            │   │
│  └────────────────────────┼────────────────────────────┘   │
│                           │                                │
│                           ▼                                │
│                  ┌─────────────────┐                       │
│                  │   MCP Server    │                       │
│                  │   (外部工具)     │                       │
│                  └─────────────────┘                       │
└──────────────────────────────────────────────────────────────┘

场景举例:
  "审查这段 Python 代码"

  1. Agent 匹配到 code-review Skill → 知道要检查安全、性能、可维护性
  2. Skill 指导 Agent 调用 MCP 工具 → read_file 读取代码
  3. MCP Server 执行 read_file → 返回代码内容
  4. Agent 按照 Skill 的规则分析代码 → 生成审查报告

一句话总结:MCP 解决的是"能不能连上"的问题,Skill 解决的是"会不会用"的问题。两者互补,不是竞争关系。

9. 踩坑记录:5 个常见问题

坑1:Stdio 模式下 Server 的 stdout 污染

问题:在 Stdio 模式下,MCP 通过 stdin/stdout 传递 JSON-RPC 消息。如果你的 Server 代码里有 print() 调试输出,它会混入 JSON-RPC 消息流,导致 Client 解析失败。

python

# ❌ 错误:print 输出会污染 JSON-RPC 消息流
print("Server started!")  # Client 会收到:Server started!\n{"jsonrpc":"2.0"...}
                          # → JSON 解析失败!

# ✅ 正确:所有日志输出到 stderr
import sys
print("Server started!", file=sys.stderr)

# ✅ 更好的做法:使用 logging
import logging
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("Server started!")

排查方法:如果 Client 报 Unexpected token 或 JSON 解析错误,检查 Server 是否有 stdout 输出。

坑2:能力协商不匹配

问题:Client 请求了 Server 没有声明的能力,导致静默失败。

python

# ❌ 错误:没有在 capabilities 中声明 tools 支持
# Client 调用 list_tools() 时可能得到空结果

# ✅ 正确:在 Server 初始化时声明完整能力
# FastMCP 自动处理能力声明,但如果你手动实现,必须显式声明

坑3:Session-Id 丢失导致状态丢失

问题:在 Streamable HTTP 模式下,如果 Client 没有在后续请求中携带 Mcp-Session-Id Header,Server 会认为这是新会话,之前的所有状态(订阅、上下文)都丢了。

python

# ✅ 正确:每次请求都携带 Session ID
import httpx

async def call_with_session(url: str, session_id: str, payload: dict):
    headers = {
        "Content-Type": "application/json",
        "Mcp-Session-Id": session_id  # 必须携带!
    }
    async with httpx.AsyncClient() as client:
        response = await client.post(url, json=payload, headers=headers)
    return response.json()

坑4:工具描述不够具体导致 LLM 误调

问题:工具的 description 太模糊,LLM 在不该调用的时候调用了,或者该调用的时候没调用。

python

# ❌ 错误:描述太模糊
@mcp.tool()
def search(query: str) -> str:
    """Search for something"""
    ...

# ✅ 正确:描述包含触发条件和使用场景
@mcp.tool()
def search_files(pattern: str, path: str = ".", max_depth: int = 5) -> str:
    """
    在工作空间中递归搜索文件。
    当用户需要查找特定文件、按名称模式搜索文件时使用。
    触发词:'找文件', '搜索文件', 'where is', 'find file'。
    不适用于:搜索文件内容(请用 grep_tool)。
    """
    ...

坑5:资源泄漏——忘记关闭连接

问题:MCP Client 使用 async context manager,如果忘记关闭,会留下僵尸进程。

python

# ❌ 错误:忘记关闭连接
async def bad_example():
    client = MCPClient(...)
    await client.connect()
    result = await client.call_tool("read_file", {"path": "test.txt"})
    return result
    # Server 进程永远不会被终止!

# ✅ 正确:使用 try/finally 确保清理
async def good_example():
    client = MCPClient(...)
    try:
        await client.connect()
        result = await client.call_tool("read_file", {"path": "test.txt"})
        return result
    finally:
        await client.disconnect()

10. 总结与展望

MCP 的现状

MCP 已经从一个 Anthropic 的内部项目,成长为 AI 工具集成的事实标准。9700 万月 SDK 下载量、10,000+ 公开 Server、Linux 基金会治理——这些数字说明了一切。

选择 MCP 的时机

plaintext

┌──────────────────────────────────────────────────────────────┐
│                  是否需要 MCP?决策树                          │
│                                                              │
│  你需要连接外部工具吗?                                       │
│       │                                                      │
│       ├── 否 → 不需要 MCP                                    │
│       │                                                      │
│       └── 是 → 需要连接的工具数量 > 3?                       │
│                    │                                         │
│                    ├── 否 → 手动集成可能更简单                 │
│                    │                                         │
│                    └── 是 → 需要支持多个 AI 应用?             │
│                              │                               │
│                              ├── 否 → MCP 也值得考虑          │
│                              │                               │
│                              └── 是 → ✅ 必须用 MCP           │
└──────────────────────────────────────────────────────────────┘

MCP 的未来

据《MCP (Model Context Protocol): A Developer's Guide for 2026》(https://www.marsdevs.com/blog/model-context-protocol-mcp)分析,MCP 的演进方向包括:

  1. Tool Search:Server 可以声明工具的语义标签,Client 可以按功能搜索而非按名称

  2. Async Operations:长时间运行的工具调用支持异步结果通知

  3. Server Identity Verification:企业级的服务器身份验证

  4. 与 A2A 协议互补:MCP 负责 Agent→Tool(垂直),A2A 负责 Agent→Agent(水平)

MCP 不是万能药,但它是 2026 年 AI 工程化基础设施的基石。掌握它,你就掌握了 AI 应用与外部世界连接的标准语言。

本文作者:PySuper | 来源zhengxingtao.com

参考资源

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区