本文档记录了从零理解 Function Calling,再到理解 MCP 协议的完整思路。
读完你将能回答:MCP 是什么、为什么需要它、和 Function Calling 有什么区别、Client 和 Server 怎么通信。


一、起点:LLM 怎么查数据库?

LLM 本身只是一个文字接龙引擎,不能直接访问数据库。
要让它能查数据库,需要 Function Calling(工具调用) 机制。

Function Calling 的流程

用户:张伟老师的工号是多少?

1. 把用户问题 + 工具列表 发给 LLM
2. LLM 分析:需要查数据库,决定调用 get_teacher_info 工具
3. LLM 返回:{"tool": "get_teacher_info", "args": {"name": "张伟"}}
4. 你的代码执行工具,查 MySQL,得到结果
5. 把结果发回给 LLM
6. LLM 生成自然语言回答:"张伟老师的工号是 T20240001..."

关键点:LLM 自己不执行代码,它只负责"决策"——决定要不要调用工具、调用哪个、传什么参数。真正执行 SQL 的是你写的代码。


二、Function Calling 的实现:chat.py

chat.py 是最直接的实现方式,所有逻辑都在一个文件里:

第一步:手动定义工具列表

告诉 LLM “你有哪些工具可以用”:

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_teacher_info",
            "description": "根据教师姓名查询其工号、所在院系、职称、邮箱",
            "parameters": {
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "教师姓名,如:张伟"}
                },
                "required": ["name"],
            },
        },
    },
    # ... 其他工具
]

第二步:本地执行工具

LLM 决定调用工具后,代码本地直接执行:

def execute_tool(name: str, args: dict) -> str:
    if name == "get_teacher_info":
        rows = query("SELECT ... FROM teachers WHERE name = %s", (args["name"],))
        result = rows[0] if rows else {"error": "未找到"}
    # ...
    return json.dumps(result, ensure_ascii=False)

第三步:对话循环

def chat_once(messages):
    while True:
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=TOOLS, tool_choice="auto"
        )
        msg = response.choices[0].message

        if not msg.tool_calls:
            return msg.content       # 没有工具调用,直接返回答案

        messages.append(msg)
        for tc in msg.tool_calls:
            args = json.loads(tc.function.arguments)
            result = execute_tool(tc.function.name, args)   # ← 本地直接调函数
            messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
        # 继续循环,带着结果再问 LLM

chat.py 的特点:LLM 调用、工具执行、对话管理全在一个进程里,没有任何"协议",就是普通的函数调用。


三、问题来了:Function Calling 不能复用

假设你用 chat.py 写好了查数据库的工具。

现在你想在 Claude Desktop 里用、在 Cursor 里用、在另一个项目里用——每次都要把工具代码复制过去,还要适配不同的调用方式,没有统一标准。

这就是 MCP 要解决的问题。


四、MCP 是什么?

MCP(Model Context Protocol,模型上下文协议) 是 Anthropic 制定的开放标准:

只要你的工具按照 MCP 协议封装,任何支持 MCP 的 AI 客户端都能直接调用它,不需要修改任何代码。

一句话理解:

MCP 本质也是 Function Calling,只不过通过一个统一的协议,让不同的 AI 只要符合 MCP 协议,就能访问 MCP Server 获得服务。

类比:

  • Function Calling = 每家电器厂商自己造插头,只能插自己家的插座
  • MCP = 统一的 USB 标准,任何设备都能用

MCP 协议本身和 LLM 无关

这是一个重要的细节:MCP 协议本身完全不涉及 LLM,它只定义了 Client 和 Server 之间怎么传消息:

Client → Server:你有哪些工具?
Server → Client:有这些工具 [get_teacher_info, get_student_info, ...]
Client → Server:帮我调用 get_teacher_info,参数是 {"name": "张伟"}
Server → Client:结果是 {"name": "张伟", "employee_no": "T20240001", ...}

“把工具列表发给 LLM、LLM 决定调用哪个”——这是 Client 应用自己的逻辑,不是 MCP 协议规定的。
MCP Server 不知道、也不关心背后用的是哪个 LLM,甚至可以完全不用 LLM,手动调用 MCP 也完全可以。

MCP 不只有 Tools

本项目只用到了 MCP 的 Tools 能力,但 MCP 完整规范包含三种能力:

能力作用本项目
Tools让 AI 调用函数执行操作(查数据库、发邮件等)✅ 使用
Resources向 AI 暴露数据或文件(如数据库 schema、文档内容)❌ 未使用
Prompts预定义可复用的提示词模板❌ 未使用

理解本项目时聚焦 Tools 即可,但知道 MCP 不止于此。


五、MCP 协议长什么样?

MCP 底层基于 JSON-RPC 2.0——一种用 JSON 格式传消息的轻量协议。
每条消息就是一段 JSON,通过 stdin/stdout(本地)或 HTTP(远程)传输。

握手:initialize

Client 启动后第一件事是和 Server 握手,确认双方协议版本:

Client → Server:
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "clientInfo": {"name": "mcp_client", "version": "1.0"}
  }
}

Server → Client:
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "serverInfo": {"name": "SchoolDB", "version": "1.0"},
    "capabilities": {"tools": {}}
  }
}

获取工具列表:tools/list

Client → Server:
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list"
}

Server → Client:
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "get_teacher_info",
        "description": "根据教师姓名查询其工号、所在院系、职称、邮箱",
        "inputSchema": {
          "type": "object",
          "properties": {
            "name": {"type": "string", "description": "教师姓名,如:张伟"}
          },
          "required": ["name"]
        }
      }
    ]
  }
}

调用工具:tools/call

Client → Server:
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "get_teacher_info",
    "arguments": {"name": "张伟"}
  }
}

Server → Client:
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"name\": \"张伟\", \"employee_no\": \"T20240001\", \"department\": \"计算机学院\", \"title\": \"教授\"}"
      }
    ]
  }
}

小结

MCP 协议规定的东西其实很简单:

规定了什么内容
消息格式JSON-RPC 2.0,每条消息是一段 JSON
方法名initialize / tools/list / tools/call 等固定方法名
数据结构工具用 name + description + inputSchema 描述,结果用 content 包裹
没有规定用什么 LLM、怎么管理对话、业务逻辑是什么

你在 mcp_client.py 里写的 session.list_tools()session.call_tool(),本质上就是 SDK 帮你把上面这些 JSON 消息发出去、把响应解析回来,不需要手动拼 JSON。


六、MCP 的架构:把 LLM 和工具执行拆开

MCP 把原来 chat.py 里一体的逻辑拆成了两个独立的部分:

┌─────────────────────────────┐
│       MCP Client            │  ← Cursor、Claude Desktop,或你自己写的 mcp_client.py
│  - 管理对话                  │
│  - 调用 LLM                 │
│  - 通过 MCP 协议请求工具      │
└────────────┬────────────────┘
             │  MCP 协议(stdin/stdout 或 HTTP)
             ▼
┌─────────────────────────────┐
│       MCP Server            │  ← mcp_server.py
│  - 注册工具                  │
│  - 执行工具(查 MySQL)       │
│  - 返回结果                  │
└─────────────────────────────┘

一次完整的问答流程:

用户
 ↓ 提问
MCP Client
 ↓ "你有哪些工具?" (list_tools)
MCP Server → 返回工具列表
 ↓ 带工具列表发给 LLM
LLM → 决定调用 get_teacher_info
 ↓ "帮我调用这个工具" (call_tool)
MCP Server → 查 MySQL → 返回结果
 ↓ 把结果发给 LLM
LLM → 生成自然语言回答
 ↓
用户收到回答

六、MCP Server 的实现:mcp_server.py

@mcp.tool() 装饰器注册工具,比手写 TOOLS 字典简洁很多:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("SchoolDB")

@mcp.tool()
def get_teacher_info(name: str) -> dict:
    """
    根据教师姓名查询工号、院系、职称、邮箱。
    参数:
        name: 教师姓名,如 "张伟"
    """
    rows = query(
        "SELECT name, employee_no, department, title, email FROM teachers WHERE name = %s",
        (name,),
    )
    return rows[0] if rows else {"error": f"未找到教师:{name}"}

# 启动 Server,等待 Client 连接
mcp.run(transport="stdio")

@mcp.tool() 自动从函数签名和 docstring 生成工具描述,等价于 chat.py 里手写的整个 TOOLS 字典。


七、MCP Client 的实现:mcp_client.py

这里展示 MCP Client 内部是怎么工作的:

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

async def main():
    # 1. 告诉 SDK 怎么启动 Server 子进程
    server_params = StdioServerParameters(
        command=sys.executable,
        args=["mcp_server.py"],
        env={...},
    )

    # 2. 启动 Server 子进程,建立 stdin/stdout 管道连接
    async with stdio_client(server_params) as (read_stream, write_stream):
        async with ClientSession(read_stream, write_stream) as session:

            # 3. 握手初始化
            await session.initialize()

            # 4. 从 Server 获取工具列表(MCP 协议的 list_tools 请求)
            tools_response = await session.list_tools()
            openai_tools = mcp_tools_to_openai(tools_response.tools)

            # 5. 带工具列表发给 LLM,LLM 决定调用哪个工具
            response = llm.chat.completions.create(
                model=MODEL, messages=messages, tools=openai_tools, tool_choice="auto"
            )

            # 6. 通过 MCP 协议调用 Server 上的工具(核心!)
            mcp_result = await session.call_tool(tool_name, args)

            # 7. 把结果发给 LLM,生成最终回答

MCP Client 的核心只有两行:

# 从 Server 拿工具列表
tools_response = await session.list_tools()

# 通过 MCP 协议调用 Server 上的工具
mcp_result = await session.call_tool(tool_name, args)

其余对话逻辑和 chat.py 完全一样。


八、Client 和 Server 怎么通信?

stdio 模式(本地,本项目使用的方式)

Client 直接把 Server 作为子进程启动,通过操作系统的管道通信:

mcp_client.py(父进程)
    │
    │  启动子进程:python mcp_server.py
    │
    ├─ 向 Server 的 stdin 写入 → {"method": "tools/call", "params": {...}}
    └─ 从 Server 的 stdout 读取 ← {"result": {...}}
  • 不走网络,不需要端口
  • Client 启动时自动把 Server 跑起来,不需要提前手动启动
  • 这就是为什么 Cursor 配置里只填 commandargs,不填 URL

HTTP 模式(远程)

Server 部署在远程服务器上,Client 通过 HTTP 连接:

mcp_client.py  ──── HTTP 请求 ────→  mcp_server(云端)
               ←─── HTTP 响应 ────

两种模式对比

stdio(本地)HTTP(远程)
Server 在哪同一台机器任意服务器
需要端口不需要需要
谁启动 ServerClient 自动启动提前部署好
典型场景Cursor/Claude Desktop 接本地工具把工具部署成云服务

九、chat.py vs mcp_server.py vs mcp_client.py 对比

chat.pymcp_server.pymcp_client.py
角色一体化(Client + 工具执行)MCP ServerMCP Client
工具列表从哪来自己手写 TOOLS装饰器自动生成启动时问 Server
怎么执行工具本地调函数接收 MCP 请求后执行通过 MCP 发给 Server
使用 LLM豆包不涉及(只执行工具)豆包
能被复用吗不能能(任何 MCP Client 都能用)

chat.py 和 mcp_client.py 唯一的本质区别:

# chat.py:本地直接调函数
result = execute_tool(name, args)

# mcp_client.py:通过 MCP 协议跨进程调用
result = await session.call_tool(name, args)

十、为什么 MCP 是亮点?

  1. 标准化:工具只写一次,Cursor、Claude Desktop、任何支持 MCP 的客户端都能用
  2. 解耦:LLM 和工具执行完全分离,换 LLM 不影响工具,改工具不影响 LLM
  3. 可扩展:新增工具只需在 Server 加一个函数,Client 下次连接自动获取

本项目中,mcp_server.py 封装了查数据库的能力,在 Cursor 里直接提问就能查学校数据库,不需要写任何对话界面——MCP Server 就是你的工具插件,AI 客户端就是你的前端


项目地址:https://github.com/ErizJ/school-mcp-chat

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐