文章目录
-
- [一、起点:LLM 怎么查数据库?](#一、起点:LLM 怎么查数据库?)
-
- [Function Calling 的流程](#Function Calling 的流程)
- [二、Function Calling 的实现:chat.py](#二、Function Calling 的实现:chat.py)
- [三、问题来了:Function Calling 不能复用](#三、问题来了:Function Calling 不能复用)
- [四、MCP 是什么?](#四、MCP 是什么?)
-
- [MCP 协议本身和 LLM 无关](#MCP 协议本身和 LLM 无关)
- [MCP 不只有 Tools](#MCP 不只有 Tools)
- [五、MCP 协议长什么样?](#五、MCP 协议长什么样?)
- [六、MCP 的架构:把 LLM 和工具执行拆开](#六、MCP 的架构:把 LLM 和工具执行拆开)
- [六、MCP Server 的实现:mcp_server.py](#六、MCP Server 的实现:mcp_server.py)
- [七、MCP Client 的实现:mcp_client.py](#七、MCP Client 的实现:mcp_client.py)
- [八、Client 和 Server 怎么通信?](#八、Client 和 Server 怎么通信?)
-
- [stdio 模式(本地,本项目使用的方式)](#stdio 模式(本地,本项目使用的方式))
- [HTTP 模式(远程)](#HTTP 模式(远程))
- 两种模式对比
- [九、chat.py vs mcp_server.py vs mcp_client.py 对比](#九、chat.py vs mcp_server.py vs mcp_client.py 对比)
- [十、为什么 MCP 是亮点?](#十、为什么 MCP 是亮点?)
本文档记录了从零理解 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 "你有哪些工具可以用":
python
TOOLS = [
{
"type": "function",
"function": {
"name": "get_teacher_info",
"description": "根据教师姓名查询其工号、所在院系、职称、邮箱",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "教师姓名,如:张伟"}
},
"required": ["name"],
},
},
},
# ... 其他工具
]
第二步:本地执行工具
LLM 决定调用工具后,代码本地直接执行:
python
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)
第三步:对话循环
python
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 握手,确认双方协议版本:
json
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
json
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
json
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 字典简洁很多:
python
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 内部是怎么工作的:
python
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 的核心只有两行:
python
# 从 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 配置里只填
command和args,不填 URL
HTTP 模式(远程)
Server 部署在远程服务器上,Client 通过 HTTP 连接:
mcp_client.py ──── HTTP 请求 ────→ mcp_server(云端)
←─── HTTP 响应 ────
两种模式对比
| stdio(本地) | HTTP(远程) | |
|---|---|---|
| Server 在哪 | 同一台机器 | 任意服务器 |
| 需要端口 | 不需要 | 需要 |
| 谁启动 Server | Client 自动启动 | 提前部署好 |
| 典型场景 | Cursor/Claude Desktop 接本地工具 | 把工具部署成云服务 |
九、chat.py vs mcp_server.py vs mcp_client.py 对比
| chat.py | mcp_server.py | mcp_client.py | |
|---|---|---|---|
| 角色 | 一体化(Client + 工具执行) | MCP Server | MCP Client |
| 工具列表从哪来 | 自己手写 TOOLS | 装饰器自动生成 | 启动时问 Server |
| 怎么执行工具 | 本地调函数 | 接收 MCP 请求后执行 | 通过 MCP 发给 Server |
| 使用 LLM | 豆包 | 不涉及(只执行工具) | 豆包 |
| 能被复用吗 | 不能 | 能(任何 MCP Client 都能用) | --- |
chat.py 和 mcp_client.py 唯一的本质区别:
python
# chat.py:本地直接调函数
result = execute_tool(name, args)
# mcp_client.py:通过 MCP 协议跨进程调用
result = await session.call_tool(name, args)
十、为什么 MCP 是亮点?
- 标准化:工具只写一次,Cursor、Claude Desktop、任何支持 MCP 的客户端都能用
- 解耦:LLM 和工具执行完全分离,换 LLM 不影响工具,改工具不影响 LLM
- 可扩展:新增工具只需在 Server 加一个函数,Client 下次连接自动获取
本项目中,mcp_server.py 封装了查数据库的能力,在 Cursor 里直接提问就能查学校数据库,不需要写任何对话界面------MCP Server 就是你的工具插件,AI 客户端就是你的前端。