乐于分享
好东西不私藏

MCP 协议:让 AI 真正"会用工具"的那把钥匙

MCP 协议:让 AI 真正"会用工具"的那把钥匙
技术分享

MCP 协议:让 AI 真正"会用工具"的那把钥匙

约 5000 字 · 面向开发者,含完整 Python 代码 · 读完约需 20 分钟
1 一个让我心态崩了的下午

去年某个下午,我在接入一个 AI 助手,想让它能查我们公司内部的工单系统。

理论上很简单——写个函数,传参数,返回结果,告诉 AI"你有这个工具可以用",完事。

但实际上,我在以下问题上来回折腾了整整一个下午:API 参数格式不同模型的 function calling 定义方式不一样;上下文传递,工具返回的结果怎么塞回给 AI,每家有每家的规范;还有鉴权,我的 API key 放哪?怎么安全地让 AI 用?

最后我拼凑出了一个能跑的东西,但那个代码……我只能说,希望以后没人看到它。

这个问题不是我一个人遇到。2024 年之前,所有想给 AI 接外部工具的开发者,都在用各自的方式解决同一批问题。这意味着:每换一个 AI 模型,就得重新写一遍工具集成代码

直到 Anthropic 在 2024 年 11 月发布了 MCP。

2 MCP 到底是什么?

MCP,全称 Model Context Protocol(模型上下文协议)。名字听起来抽象,我用一个类比来说清楚:

类比: 你有没有想过,为什么 U 盘插哪台电脑都能用?因为有 USB 协议。所有设备遵循同一套标准,所以"插就能用",不需要为每台电脑单独适配驱动。

MCP 想做的事是一样的——让 AI 接任何工具,都只需要一套统一的方式,不需要为每个 AI 模型单独适配。

更正式地说:MCP 是一个开放协议,定义了 AI 应用(Host)、AI 模型客户端(Client)和外部工具服务(Server)之间怎么通信。

架构图:三个角色,两条通道

🏠 你的应用(Host)

Claude Desktop · Cursor · 你的 AI 产品

🔌 MCP Client

内嵌在 Host 里 · 建连接 · 发请求 · 能力协商

JSON-RPC 2.0

MCP Server(各管一摊工具)

🎫

Server A

工单系统
(你自己写的)

🗄️

Server B

数据库
(社区开源的)

🐙

Server C

GitHub
(官方提供的)

你的应用(Host)通过内置的 MCP Client,用标准协议连接各种 MCP Server,每个 Server 封装了一类工具能力。

3 三个核心概念深讲
角色 说明
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 模板,用户可以直接使用 代码审查模板、报告生成模板
4 通信协议:深入 JSON-RPC 2.0

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" }
  }
}
5 动手写一个 MCP Server(完整示例)

理论讲完,上代码。我们来写一个工单查询 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))
6 接入 Claude Desktop:完整配置步骤

代码写好了,怎么让 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。

7 常见坑与解决方案

坑 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 可执行文件,不是系统全局的。

8 生态现状:MCP 正在成为标准

截至 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 一样成为实际标准。
9 总结:三行字带走

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 验证 · 如有问题欢迎留言讨论