MCP 协议:让 AI 真正"会用工具"的那把钥匙
去年某个下午,我在接入一个 AI 助手,想让它能查我们公司内部的工单系统。
理论上很简单——写个函数,传参数,返回结果,告诉 AI"你有这个工具可以用",完事。
但实际上,我在以下问题上来回折腾了整整一个下午:API 参数格式不同模型的 function calling 定义方式不一样;上下文传递,工具返回的结果怎么塞回给 AI,每家有每家的规范;还有鉴权,我的 API key 放哪?怎么安全地让 AI 用?
最后我拼凑出了一个能跑的东西,但那个代码……我只能说,希望以后没人看到它。
这个问题不是我一个人遇到。2024 年之前,所有想给 AI 接外部工具的开发者,都在用各自的方式解决同一批问题。这意味着:每换一个 AI 模型,就得重新写一遍工具集成代码。
直到 Anthropic 在 2024 年 11 月发布了 MCP。
MCP,全称 Model Context Protocol(模型上下文协议)。名字听起来抽象,我用一个类比来说清楚:
类比: 你有没有想过,为什么 U 盘插哪台电脑都能用?因为有 USB 协议。所有设备遵循同一套标准,所以"插就能用",不需要为每台电脑单独适配驱动。
MCP 想做的事是一样的——让 AI 接任何工具,都只需要一套统一的方式,不需要为每个 AI 模型单独适配。
更正式地说:MCP 是一个开放协议,定义了 AI 应用(Host)、AI 模型客户端(Client)和外部工具服务(Server)之间怎么通信。
架构图:三个角色,两条通道
🏠 你的应用(Host)
Claude Desktop · Cursor · 你的 AI 产品
⬇
🔌 MCP Client
内嵌在 Host 里 · 建连接 · 发请求 · 能力协商
⬇
MCP Server(各管一摊工具)
🎫
Server A
工单系统
(你自己写的)
🗄️
Server B
数据库
(社区开源的)
🐙
Server C
GitHub
(官方提供的)
你的应用(Host)通过内置的 MCP Client,用标准协议连接各种 MCP Server,每个 Server 封装了一类工具能力。
| 角色 | 说明 |
|---|---|
| Host | 用户直接使用的 AI 应用。比如 Claude Desktop、Cursor、你自己开发的 AI 产品。它负责维护 MCP Client 实例,把用户请求路由到合适的 Server。 |
| MCP Client | 跑在 Host 里的协议层。维护和每个 Server 的 1:1 连接,处理协议握手、能力协商(告诉 AI "这个 Server 有哪些工具")、消息序列化。 |
| MCP Server | 暴露具体工具能力的服务。可以是一个本地进程,也可以是远程 HTTP 服务。你写的工具逻辑就在这里。 |
MCP Server 能暴露三类东西:
| 类型 | 说明 | 例子 |
|---|---|---|
| Tools(工具) | AI 可以主动调用的函数,执行有副作用的操作 | 查工单、发邮件、写文件 |
| Resources(资源) | AI 可以读取的数据,类似文件系统或数据库 | 读取配置文件、获取文档内容 |
| Prompts(提示模板) | 预定义的 prompt 模板,用户可以直接使用 | 代码审查模板、报告生成模板 |
MCP 底层用的是 JSON-RPC 2.0 协议,传输层支持 stdio(本地进程)和 HTTP with SSE(远程服务)两种。一次完整的工具调用,消息流大概长这样:
// 1. 初始化握手:Client 向 Server 发送
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"clientInfo": { "name": "MyApp", "version": "1.0.0" }
}
}
// 2. Client 查询可用工具列表
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
// 3. AI 决策后,Client 调用具体工具
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "get_ticket",
"arguments": { "ticket_id": "TK-2024-001" }
}
}
理论讲完,上代码。我们来写一个工单查询 MCP Server,包含两个工具:查询工单详情、列出未解决工单。
环境准备:Python 3.10+,安装依赖:
pip install mcp
完整的 server.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ticket_server.py — 工单查询 MCP Server
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent, CallToolResult, ListToolsResult
import json
# ─── 模拟数据库 ──────────────────────────────────────────
MOCK_TICKETS = {
"TK-001": {
"id": "TK-001",
"title": "登录页面报 500 错误",
"status": "open",
"priority": "high",
"assignee": "张伟",
"description": "用户反映使用微信登录时偶现 500,已复现两次,影响约 3% 用户。"
},
"TK-002": {
"id": "TK-002",
"title": "导出功能无法下载 Excel",
"status": "in_progress",
"priority": "medium",
"assignee": "李娜",
"description": "点击导出后浏览器无反应,控制台有 CORS 报错。"
},
"TK-003": {
"id": "TK-003",
"title": "首页加载时间超过 8 秒",
"status": "open",
"priority": "high",
"assignee": "未分配",
"description": "Performance 测试显示 FCP 达到 8.2s,需要排查图片和接口。"
},
}
# ─── 创建 Server 实例 ─────────────────────────────────────
app = Server("ticket-server")
# ─── 声明工具列表 ─────────────────────────────────────────
@app.list_tools()
async def list_tools() -> ListToolsResult:
return ListToolsResult(tools=[
Tool(
name="get_ticket",
description="根据工单 ID 查询工单详情,包括标题、状态、优先级、负责人和描述。",
inputSchema={
"type": "object",
"properties": {
"ticket_id": {
"type": "string",
"description": "工单 ID,格式如 TK-001"
}
},
"required": ["ticket_id"]
}
),
Tool(
name="list_open_tickets",
description="列出所有未关闭的工单(状态为 open 或 in_progress),支持按优先级筛选。",
inputSchema={
"type": "object",
"properties": {
"priority": {
"type": "string",
"enum": ["high", "medium", "low"],
"description": "按优先级筛选,不传则返回全部未关闭工单"
}
},
"required": []
}
)
])
# ─── 实现工具逻辑 ─────────────────────────────────────────
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> CallToolResult:
if name == "get_ticket":
ticket_id = arguments.get("ticket_id", "").upper()
ticket = MOCK_TICKETS.get(ticket_id)
if not ticket:
return CallToolResult(
content=[TextContent(type="text", text=f"工单 {ticket_id} 不存在")],
isError=True
)
status_map = {"open": "待处理", "in_progress": "处理中", "closed": "已关闭"}
priority_map = {"high": "高", "medium": "中", "low": "低"}
result = (
f"工单详情\n════════════\n"
f"ID:{ticket['id']}\n"
f"标题:{ticket['title']}\n"
f"状态:{status_map.get(ticket['status'])}\n"
f"优先级:{priority_map.get(ticket['priority'])}\n"
f"负责人:{ticket['assignee']}\n"
f"描述:{ticket['description']}"
)
return CallToolResult(content=[TextContent(type="text", text=result)])
elif name == "list_open_tickets":
priority_filter = arguments.get("priority")
open_tickets = [
t for t in MOCK_TICKETS.values()
if t["status"] in ("open", "in_progress")
and (priority_filter is None or t["priority"] == priority_filter)
]
lines = [f"共找到 {len(open_tickets)} 个未关闭工单:\n"]
for t in open_tickets:
badge = {"high": "[高]", "medium": "[中]", "low": "[低]"}
lines.append(f"{badge.get(t['priority'],'')} {t['id']} — {t['title']} ({t['assignee']})")
return CallToolResult(content=[TextContent(type="text", text="\n".join(lines))])
# ─── 启动入口 ─────────────────────────────────────────────
if __name__ == "__main__":
import asyncio
asyncio.run(stdio_server(app))
代码写好了,怎么让 Claude Desktop 用上它?分四步:
第一步:找到配置文件
macOS:~/Library/Application Support/Claude/claude_desktop_config.json
Windows:%APPDATA%\Claude\claude_desktop_config.json
第二步:编辑配置,添加你的 Server
{
"mcpServers": {
"ticket-server": {
"command": "python",
"args": ["/你的绝对路径/ticket_server.py"],
"env": {
"PYTHONPATH": "/你的绝对路径"
}
}
}
}
第三步:完全退出并重启 Claude Desktop
注意是完全退出,不是最小化。macOS 用 Cmd+Q,Windows 从托盘右键退出。
第四步:验证
打开 Claude Desktop,在输入框附近应该能看到工具图标。发送"帮我查一下工单 TK-001 的详情",AI 会自动调用你的 Server。
坑 1:Claude Desktop 启动后看不到工具图标
多半是 Server 启动失败了。最常见原因是 python 路径写的是 python 但系统默认的是 python3,改一下 command 字段。
坑 2:工具调用成功,但返回乱码
Python 文件头没加 # -*- coding: utf-8 -*-,或者 Windows 下终端编码不是 UTF-8。加上 sys.stdout.reconfigure(encoding='utf-8')。
坑 3:AI 有时候不调用工具,直接瞎编答案
Tool 的 description 写得不够清晰。AI 是靠 description 来判断"什么情况该用什么工具"的。要写得具体,说清楚"输入什么、输出什么、在什么场景下用"。
坑 4:本地跑没问题,配置进 Claude 就报错
大概率是路径问题。配置文件里必须用绝对路径,不能用 ~/ 或相对路径。另外,venv 虚拟环境里安装的包,command 要指向 venv 里的 python 可执行文件,不是系统全局的。
截至 2026 年上半年,MCP 的生态已经相当活跃:
| 官方 | Anthropic 官方维护了 filesystem、GitHub、Google Drive、Slack、PostgreSQL 等十几个常用 Server,代码都在 GitHub 上开源,直接拿来用就行。 |
| IDE | Cursor、VS Code(GitHub Copilot)、Zed 都已经支持 MCP,在 IDE 里接自定义工具变成了标配能力。 |
| 社区 | awesome-mcp-servers 仓库收录了数百个社区维护的 MCP Server,覆盖 Notion、Jira、Figma、各类数据库等等,基本上你想要的都有人写过了。 |
| 竞争 | Google、OpenAI 也在推自己的工具调用标准,但 MCP 先发优势明显,目前社区接受度最高。很可能像 USB 一样成为实际标准。 |
MCP 是什么: AI 调用外部工具的通用协议,类比 USB。
核心架构: Host(你的应用)→ Client(协议层)→ Server(工具实现),三者通过 JSON-RPC 2.0 通信。
你现在能做什么: 给你的内部系统写一个 MCP Server,接进 Claude Desktop,从今天开始把 AI 变成真正会用工具的助手。
推荐阅读: 官方文档 modelcontextprotocol.io | 官方 Server 示例仓库 github.com/modelcontextprotocol/servers
MCP 解决的不是"AI 能不能用工具"的问题,
而是"AI 用工具能不能标准化"的问题。
前者是能力,后者才是生产力。
技术分享系列 · 代码已在 Python 3.12 + mcp 1.x 验证 · 如有问题欢迎留言讨论
夜雨聆风