MCP 协议:Tool Calling 标准化背后的故事
你写了 10 个工具——天气查询、股票数据、文件操作、数据库访问……想给 Claude 用,得按 Anthropic 格式定义一遍。想换成 OpenAI?再按 OpenAI 格式定义一遍。想换成 Gemini?再来一遍。你开始怀疑:工具调用为什么不能像 USB-C 一样,一个接口通吃?MCP 就是答案。
Tool Calling 的"巴别塔"问题
你一定已经会 Tool Calling 了。用 LangChain 的@tool装饰器,让 LLM 自动调用外部函数:
fromlangchain_core.toolsimporttool@tooldefget_weather(city:str)->str:"""查询城市天气"""returnweather_api.query(city)llm_with_tools=llm.bind_tools([get_weather])# LLM 自动判断何时调用、传什么参数这很好。但有一个根本问题:工具定义和 LLM 平台是绑定的。
不同平台有不同的 Tool Calling 格式:
OpenAI: tools=[{"type": "function", "function": {"name": "...", "parameters": {...}}}] Anthropic: tools=[{"name": "...", "description": "...", "input_schema": {...}}] Google Gemini: tools=[{"functionDeclarations": [{"name": "...", "parameters": {...}}]}]如果你有 10 个工具,想给 3 个平台用——维护成本 = 10 × 3 = 30 套工具定义。
而且工具还嵌在你的应用代码里。换了项目就得重新写一遍。没有隔离——工具崩溃可能影响整个应用。没有发现机制——LLM 不知道你新加了什么工具,除非你手动更新配置。
这就是 MCP 要解决的问题。
MCP 是什么:LLM 的"USB-C 接口"
MCP(Model Context Protocol)= LLM 和外部工具的"USB-C 标准"。
USB-C 出现之前,每种设备有自己的接口。USB-C 之后,一个接口通吃所有设备。MCP 对 LLM 工具调用做了一模一样的事——定义了一个统一协议,任何 LLM 平台都能通过这个协议发现和调用任何工具。
三角色架构
┌──────────────────────────────────────┐ │ MCP Host │ ← AI 应用(Claude Desktop、Cursor 等) │ ┌────────────────────────────────┐ │ │ │ MCP Client │ │ ← 协议客户端,管理连接和 Tool 列表 │ │ • 连接 Server │ │ │ │ • 发现 Tool/Resource/Prompt │ │ │ │ • 调用 Tool │ │ │ └────────────┬───────────────────┘ │ └───────────────┼───────────────────────┘ │ JSON-RPC over stdio/SSE ┌───────────────┼───────────────────────┐ │ ┌────────────┴───────────────────┐ │ │ │ MCP Server │ │ ← 工具提供方(独立进程) │ │ • 声明 Tool/Resource/Prompt │ │ │ │ • 执行 Tool 调用 │ │ │ │ • 返回结果 │ │ │ └────────────────────────────────┘ │ └───────────────────────────────────────┘| 角色 | 做什么 | 举例 |
|---|---|---|
| MCP Host | 运行 LLM 的应用 | Claude Desktop、Cursor、Claude Code |
| MCP Client | Host 内部的协议客户端 | 管理连接、转发调用 |
| MCP Server | 提供工具的服务进程 | 天气服务、文件系统、数据库 |
通信方式极其简单——JSON-RPC 2.0 over stdio(本地)或 SSE(远程)。Host 启动 Server 作为子进程,通过 stdin/stdout 收发 JSON。
没有 MCP vs 有了 MCP
| 问题 | MCP 之前 | MCP 之后 |
|---|---|---|
| 工具定义 | 每个平台一种格式 | 一次定义,所有平台通用 |
| 工具部署 | 嵌入在应用代码里 | 独立进程,即插即用 |
| 工具发现 | 硬编码 | Client 自动从 Server 发现 |
| 工具复用 | 每个项目重写 | 一个 Server 多个项目共用 |
| 安全隔离 | 同进程,无隔离 | 独立进程,权限隔离 |
一句话:传统 Tool Calling 是"把工具焊死在 LLM 上",MCP 是"把工具变成可插拔的服务"。
MCP Server 提供三种能力
一个 MCP Server 不只是提供工具,它提供三种元能力:
┌─────────────────────────────────────────┐ │ MCP Server │ │ │ │ Tools Resources Prompts │ │ (执行) (读取) (模板) │ │ ─────── ──────── ─────── │ │ 查天气 读配置文件 周报模板 │ │ 写文件 数据库Schema 代码审查模板 │ │ 调API 日志内容 提交信息模板 │ │ │ │ LLM 主动调用 LLM 被动读取 LLM 请求模板 │ └─────────────────────────────────────────┘Tool(工具)——LLM 可以调用的"函数"。这是 MCP 最核心的概念。
Tool(name="get_weather",description="获取指定城市的天气信息。当用户询问天气时使用。",inputSchema={"type":"object","properties":{"city":{"type":"string","description":"城市名称"},},"required":["city"],},)三个要素:name(LLM 用来标识)、description(LLM 用来判断"什么时候用")、inputSchema(LLM 用来生成正确的参数)。description 是最关键的——LLM 完全靠它来决定要不要调你的工具。
Resource(资源)——LLM 可以"读取"的数据源。和 Tool 的区别:
| Tool | Resource | |
|---|---|---|
| 语义 | 做事情(执行操作) | 读数据(获取信息) |
| 副作用 | 有(写文件、发邮件) | 无(只读) |
| 标识 | 函数名 | URI(如file:///project/readme.md) |
| 简单判断 | 功能是"做 X" | 功能是"获取 X" |
Prompt(提示模板)——预定义的提示词模板,让 LLM 快速获取特定场景的提示词。比如 “write_weekly_report” 模板带参数 project 和 highlights,LLM 可以直接获取结构化的周报 Prompt。
实战:构建一个 MCP Server
我们用 Python 构建一个 MCP Server,提供两组工具:文件系统操作(list_directory、read_file、search_files、write_file)和SQLite 数据库查询(db_list_tables、db_describe_table、db_query、db_export_table)。
最小 Server 结构
frommcp.serverimportServerfrommcp.typesimportTool,TextContentfrommcp.server.stdioimportstdio_server server=Server("my-tools-server")# 1. 声明 Tool 菜单(LLM 先调用这个)@server.list_tools()asyncdeflist_tools()->list[Tool]:return[Tool(name="list_directory",description="列出目录内容",inputSchema={...}),Tool(name="read_file",description="读取文件内容",inputSchema={...}),Tool(name="db_query",description="执行 SELECT 查询",inputSchema={...}),# ...]# 2. 实现 Tool 调用(LLM 决定调哪个)@server.call_tool()asyncdefcall_tool(name:str,arguments:dict)->list[TextContent]:ifname=="list_directory":return[TextContent(type="text",text=format_dir(arguments["path"]))]elifname=="read_file":return[TextContent(type="text",text=read_file(arguments["path"]))]elifname=="db_query":return[TextContent(type="text",text=query_db(arguments["sql"]))]# ...# 3. 启动(监听 stdin/stdout)asyncwithstdio_server()as(read_stream,write_stream):awaitserver.run(read_stream,write_stream,...)就这么简单。三个核心步骤:声明菜单 → 实现调用 → 启动服务。
安全是 MCP 设计的核心
因为 MCP Server 是独立进程,你需要明确控制 LLM 的权限边界。我们的 Server 做了三层安全:
1. 路径穿越防护:
defsafe_path(user_path:str,allowed_root:str)->Path:"""防止 LLM 读取允许目录之外的文件"""resolved=(Path(allowed_root)/user_path).resolve()allowed=Path(allowed_root).resolve()ifnotstr(resolved).startswith(str(allowed)):raisePermissionError(f"拒绝访问:'{user_path}' 超出允许范围")returnresolved# safe_path("../../../etc/passwd") → PermissionError!# safe_path("demo/hello.py") → /home/user/mcp-workspace/demo/hello.py ✅2. SQLite 只读连接:
# 以只读模式连接,LLM 即使想删表也删不了conn=sqlite3.connect(f"file:{DB_PATH}?mode=ro",uri=True)3. SQL 注入防护:
defhandle_db_query(args):sql=args["sql"].strip()ifnotsql.upper().startswith("SELECT"):return"只允许 SELECT 查询"forkeywordin["DROP","DELETE","INSERT","UPDATE","ALTER"]:ifkeywordinsql.upper():returnf"查询包含禁止的关键词 '{keyword}'"这就是 MCP 的权限模型:最小权限原则。只给 LLM 完成任务所需的最小权限。
连接 Claude Code
配置极其简单。在项目根目录创建.mcp.json:
{"mcpServers":{"local-tools":{"command":"python3","args":["mcp_server.py"],"env":{"WORKSPACE_DIR":"/path/to/your/workspace","DB_PATH":"/path/to/your/database.db"}}}}然后直接在 Claude Code 对话中用:
"帮我看看 workspace 下有什么文件" "读取 config.json 的内容" "查询 sales.db 的 orders 表中 2026 年的订单总数"Claude Code 会自动:发现你的 Server → 获取工具菜单 → 根据用户意图选择工具 → 调用工具 → 把结果融入回答。你什么都不用做。
MCP vs Function Calling vs LangChain Tool
这三个概念容易混淆,一张表说清楚:
| OpenAI Function Calling | LangChain @tool | MCP | |
|---|---|---|---|
| 范围 | 单次对话内的工具 | 单个应用内的工具 | 跨应用、跨平台的标准 |
| 定义方式 | JSON Schema | Python 装饰器 | Python 装饰器 + JSON Schema |
| 部署 | 嵌入代码 | 嵌入代码 | 独立进程 |
| 传输 | HTTP API | 内存调用 | stdio / SSE |
| 平台绑定 | 仅 OpenAI | LangChain 生态 | 所有支持 MCP 的平台 |
| 工具复用 | 每个项目重新定义 | 可复用但不跨平台 | 一次定义到处使用 |
| 安全隔离 | 无 | 无 | 进程级隔离 |
它们不是互斥的,而是互补的:
MCP Server → 负责"提供工具"(独立进程,标准化发布) LangChain → 负责"使用工具"(在 Node 里调用 MCP 工具) OpenAI FC → 负责"工具调用格式"(LLM 层面的实现细节) 三者配合:用 @tool 开发 → 用 MCP 发布 → 任何 LLM 都能用什么时候用 MCP
单个 OpenAI 项目,1-2 个简单工具 → OpenAI Function Calling 够用 LangChain 项目,需要 LLM 组件配合 → LangChain @tool 多平台、多项目、需要复用的工具 → MCP Server 需要进程级安全隔离 → MCP Server 想用社区现成的工具 → MCP(数百个现成的 Server)MCP 的生态现状
MCP 由 Anthropic 于 2024 年 11 月发布,作为一个开放协议。短短半年多,生态已经相当丰富:
- Claude Desktop原生支持 MCP
- Claude Code(你正在用的)内置 MCP Client
- Cursor / VS Code Copilot支持 MCP
- 社区已有数百个 MCP Server:文件系统、GitHub、Slack、Postgres、Puppeteer……
这意味着什么?你不需要自己写所有的工具。有人已经写好了 GitHub MCP Server,你直接在.mcp.json里配置一下,Claude Code 就能自动获取 GitHub Issues、创建 PR、查看代码。这是一个"工具市场"的雏形。
总结
MCP 做的事情极其简单,但影响深远:
- 标准化—— 一次定义,所有 LLM 平台通用。不再重复造轮子
- 解耦—— 工具和 LLM 分离成独立进程,各自独立开发部署
- 复用—— 一个 Server 多个项目共用,生态里数百个现成的 Server
- 安全—— 进程级隔离,最小权限原则,路径校验、只读连接
OpenAI 发明了 Function Calling → "LLM 能调工具了" LangChain 提供了 @tool 装饰器 → "写工具变简单了" Anthropic 发布了 MCP → "工具可以跨平台复用了" 三者配合:用 @tool 开发 → 用 MCP 发布 → 任何 LLM 平台都能用去 github.com/barryness/cc-ai-learning 的008-mcp-learning/跑python mcp_server.py --demo,自测模式会模拟 LLM 调用所有工具,让你直观感受 MCP Server 是怎么工作的。