MCP 学习笔记

从 Tool Calling 到标准化协议 —— 构建 LLM 可调用的工具服务,一次定义、所有平台通用

你已经会了 Tool Calling

在 LangChain 中,用 @tool 装饰器就能让 LLM 调用函数:

@tool
def get_weather(city: str) -> str:
    """查询城市天气"""
    return f"{city}: 晴, 25°C"

llm_with_tools = llm.bind_tools([get_weather])
# LLM 自动判断:北京天气 → 调 get_weather("北京")

但有一个根本问题:平台绑定

每个 LLM 平台的 Tool Calling 格式都不同。如果你有 10 个工具,想在 3 个平台用——

平台格式维护成本
OpenAI tools=[{"type":"function","function":{...}}] 写 1 遍
Anthropic tools=[{"name":"...","input_schema":{...}}] 再写 1 遍
Google Gemini tools=[{"functionDeclarations":[{...}]}] 再写 1 遍
维护成本 = 工具数 × 平台数。10 个工具 × 3 个平台 = 30 份定义。而且格式细节各不相同,容易出错。

MCP 的答案:把工具变成"服务"

传统 Tool Calling
App ─── LLM ─── Tool₁, Tool₂, Tool₃
工具和 LLM 绑定在一起
换平台 = 重新定义
MCP
LLM
← 标准协议 →
MCP Server
Tool₁,₂,₃
工具变成独立"服务"
一次定义,所有平台通用
类比:MCP = LLM 世界的 "USB-C 接口"。USB-C 之前每种设备有自己的接口,USB-C 之后一个接口通吃。 MCP 对工具调用做了一样的事——统一的协议,任何 LLM 都能发现和调用任何工具。

MCP 解决的核心问题

问题MCP 之前MCP 之后
工具定义每个平台一种格式一次定义,所有平台通用
工具部署嵌入在应用代码里独立进程,即插即用
工具发现硬编码在代码里Client 自动发现 Server 提供的工具
工具复用每个项目重新写一个 Server 多个项目共用
安全隔离和 LLM 同进程独立进程,权限隔离

MCP 三角色架构

MCP Host
AI 应用(Claude Desktop、Cursor 等)
│ 包含
MCP Client
协议客户端:连接 Server · 管理 Tool 列表 · 转发调用
│ JSON-RPC over stdio / SSE
MCP Server
工具提供方:声明 Tool/Resource/Prompt · 执行调用 · 返回结果
角色做什么举例
MCP Host 运行 LLM 的应用 Claude Desktop、Cursor、VS Code Copilot
MCP Client Host 内部的协议客户端 连接 Server、管理 Tool 列表、转发调用
MCP Server 提供具体功能的服务 天气查询、文件操作、数据库访问、API 网关
关键理解:MCP Host 启动 MCP Server 作为子进程,通过 stdin/stdout 管道用 JSON-RPC 2.0 协议通信。 这带来了天然的进程级隔离——Server 崩溃不影响 Host。

一次完整的 Tool Calling 流程

1
初始化:Client 连接 Server,交换协议版本和能力
2
发现工具:Server 返回 Tool 列表(name + description + inputSchema)
3
LLM 决策:LLM 根据用户意图选择 Tool,按 Schema 生成参数
4
调用执行:Client 发送 (tool_name, arguments),Server 执行返回结果
5
结果整合:LLM 把工具返回结果融入最终回复

两种传输模式

stdio(本地)

通过 stdin/stdout 管道通信
Server 作为子进程运行
零网络配置,进程级隔离
适合:本地工具(文件系统、数据库、CLI)

SSE(远程)

通过 HTTP Server-Sent Events 通信
Server 作为独立服务运行
支持跨机器访问
适合:远程工具(API 网关、云服务)

Server 提供三种能力

🔧

Tool

执行操作

让 LLM "做"事情
查天气、写文件、发邮件、调 API

有副作用
📖

Resource

读取数据

让 LLM "看"数据
读配置、数据库 Schema、日志文件

只读
💬

Prompt

提示词模板

让 LLM 快速获取
周报模板、代码审查模板等

模板

Tool:最核心的能力

每个 Tool 有三个要素,description 是最关键的——LLM 靠它决定"什么时候用":

Tool(
    name="get_weather",              # 1. 名称:LLM 用来标识
    description="获取指定城市的天气信息",  # 2. 描述:LLM 用来理解"什么时候用"
    inputSchema={                       # 3. 参数 Schema:LLM 用来生成"正确的参数"
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "城市名称"}
        },
        "required": ["city"],
    },
)
三要素缺一不可:name 不对 → LLM 找不到工具。description 不好 → LLM 不知道该不该用。inputSchema 不准 → LLM 生成错误参数 → 调用失败。

Tool vs Resource:什么时候用哪个

ToolResource
语义事情(执行操作)数据(获取信息)
副作用有(写文件、发邮件)无(只读)
标识通过 name 标识通过 URI 标识
参数每次调用时传入通过 URI 标识资源
示例write_filesend_emailfile:///readme.mddb:///schema
简单判断:如果功能是"获取 X"且无副作用 → Resource。如果功能是"做 X"且有副作用 → Tool。

Prompt:预定义的提示词模板

Prompt(
    name="write_weekly_report",
    description="生成周报的提示词模板",
    arguments=[
        {"name": "project", "description": "项目名称", "required": True},
    ],
)

# LLM 收到 Prompt 模板后,按照专业结构生成内容
# 比用户手写"帮我写周报"质量更高
三种能力的使用频率:Tool(90%) > Resource(8%) > Prompt(2%)。绝大多数 MCP Server 只提供 Tool。Resource 和 Prompt 是锦上添花。

我们的 MCP Server:文件系统 + SQLite

一个 Server 提供两组共 8 个工具,LLM 根据用户意图自动选择:

list_directory
📂 文件系统
列出目录内容
read_file
📂 文件系统
读取文件(多编码)
search_files
📂 文件系统
通配符搜索
write_file
📂 文件系统
写入文件
db_list_tables
🗄️ SQLite
列出所有表
db_describe_table
🗄️ SQLite
查看表结构
db_query
🗄️ SQLite
SELECT 查询(只读)
db_export_table
🗄️ SQLite
导出表数据
设计理念:一个 Server 可以提供多组不相关的工具。"服务聚合"是 MCP 的常见模式——文件系统和数据库放在同一个 Server,LLM 自动判断用户想操作文件还是查询数据库。

Server 的最小结构

MCP Server 的核心骨架(4 步)
from mcp.server import Server
from mcp.types import Tool, TextContent
from mcp.server.stdio import stdio_server

# 1. 创建 Server
server = Server("my-server")

# 2. 声明 Tool 列表(LLM 的"工具菜单")
@server.list_tools()
async def list_tools() -> list[Tool]:
    return [Tool(name=..., description=..., inputSchema=...)]

# 3. 实现 Tool 调用(执行逻辑)
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "my_tool":
        return [TextContent(type="text", text=result)]

# 4. 启动 Server(监听 stdin/stdout)
async with stdio_server() as (read_stream, write_stream):
    await server.run(read_stream, write_stream, ...)

两个层次的工具设计

层次 1:文件系统(基础)

LLM 通过文件系统工具操作本地文件
→ 探索目录结构 list_directory
→ 读取文件内容 read_file
→ 搜索特定文件 search_files
→ 创建/修改文件 write_file

层次 2:SQLite 数据库(扩展)

LLM 通过数据库工具查询结构化数据
→ 了解有哪些表 db_list_tables
→ 理解表结构 db_describe_table
→ 执行分析查询 db_query
→ 导出完整数据 db_export_table

安全是 MCP Server 的第一要务

LLM 是不可信的调用方。它可能被用户诱导,尝试读取系统文件、删除数据、执行危险 SQL。
MCP Server 必须自己做好防护——永远不要信任 LLM 传来的参数。

防护 1:路径穿越攻击防御

用户可能对 LLM 说"读取 /etc/passwd",LLM 可能传入 ../../../etc/passwd

不安全的实现
def read_file(user_path: str):
    # 危险!直接拼接用户路径
    full = Path("/workspace") / user_path
    return full.read_text()

# user_path = "../../../etc/passwd"
# → /etc/passwd ← 系统文件泄露!
安全的实现:safe_path()
def safe_path(user_path: str) -> Path:
    full = Path(allowed_root) / user_path
    resolved = full.resolve()  # 展开 ../ 和符号链接

    if not str(resolved).startswith(str(allowed_root)):
        raise PermissionError("路径越界!")

    return resolved

# user_path = "../../../etc/passwd"
# → PermissionError: 路径穿越检测!

防护 2:SQL 注入和危险操作拦截

def _handle_db_query(args: dict):
    sql = args["sql"].strip()

    # 检查 1:只允许 SELECT
    if not sql.upper().startswith("SELECT"):
        return [TextContent(text="⛔ 只允许 SELECT 查询")]

    # 检查 2:禁止危险关键词
    dangerous = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER"]
    for kw in dangerous:
        if kw in sql.upper():
            return [TextContent(text=f"⛔ 禁止 {kw}")]

    # 检查 3:只读连接(即使绕过前两步,数据库也不允许写)
    conn = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True)
纵深防御:三层防护——代码白名单 + 关键词拦截 + 数据库只读连接。即使前两层被绕过,数据库层也会阻止写操作。

安全设计清单

防护层措施防什么
路径校验safe_path() resolve + prefix 检查读取系统文件、覆盖关键配置
操作白名单只允许 SELECT,拒绝 DROP/DELETE/INSERT 等数据被删除、篡改
只读连接sqlite3.connect("file:...?mode=ro")即使绕过代码检查也无法写入
文件大小限制拒绝读取超过 1MB 的文件内存溢出、读取二进制大文件
结果行数限制SQL 查询最多返回 100 行全表扫描导致 Token 爆炸
核心原则:最小权限。LLM 只需要"读取"就够了?就不要给"写入"权限。永远从最严格开始,按需放开。

两种配置方式

方式 1:项目级配置(推荐)

在项目根目录创建 .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"
      }
    }
  }
}

方式 2:全局配置

编辑 ~/.claude/settings.json

{
  "mcpServers": {
    "local-tools": {
      "command": "python3",
      "args": ["/absolute/path/to/mcp_server.py"]
    }
  }
}

验证连接 + 使用示例

配置完成后,在 Claude Code 中验证:

# 验证连接成功
/list_tools

# 应该看到 list_directory, read_file, db_query 等工具

然后直接用自然语言操作:

文件操作示例

"帮我看看工作目录下有什么文件"
"读取 config.json 的内容"
"搜索所有 .py 文件"
"创建一个 hello.py 文件"

数据库查询示例

"数据库中有哪些表?"
"查看 products 表的结构"
"查询每个分类的产品数量和平均价格"
"导出 orders 表的数据"

Claude Code 会自动:发现你的 MCP Server 工具 → 根据问题选择合适的工具 → 调用工具获取结果 → 将结果融入回答。你只需要用自然语言描述需求。

MCP vs Function Calling vs LangChain Tool

OpenAI Function CallingLangChain @toolMCP
范围单次对话内的工具单个应用内的工具跨应用、跨平台标准
定义方式JSON SchemaPython 装饰器Python 装饰器 + JSON Schema
部署嵌入代码嵌入代码独立进程
传输HTTP API内存调用stdio / SSE
平台绑定仅 OpenAILangChain 生态所有支持 MCP 的平台
工具复用每个项目重新定义可复用但不跨平台一次定义到处使用
安全隔离进程级隔离

决策树:什么时候用什么

1️⃣
单个 OpenAI 项目
1-2 个简单工具
→ Function Calling
🔗
LangChain 项目
多个 LLM 组件
→ @tool 装饰器
🌐
多平台、多项目
需要复用的工具
→ MCP Server

它们可以共存

MCP Server
提供工具(独立进程)
│ 标准协议
LangChain @tool
在一个 Node 内调用 MCP 工具
│ bind_tools
OpenAI Function Calling
LLM 层面的调用格式
三层协作:MCP Server 提供工具 → LangChain @tool 在应用层使用 → OpenAI Function Calling 在 LLM 层执行调用。
每一层解决不同的问题,配合使用效果最好。

核心收获

MCP 做了什么?

  • 把工具定义 → 标准化(跨平台通用)
  • 把工具部署 → 独立进程化(即插即用)
  • 把工具发现 → 自动化(list_tools)
  • 把工具调用 → 协议化(JSON-RPC)
  • 把安全隔离 → 进程级(独立权限)

关键原则

  • MCP 不是替代 Function Calling——是标准化
  • 简单场景用原生 API,多平台复用用 MCP
  • Tool description 是最关键的——LLM 靠它决策
  • 永远不要信任 LLM 传入的参数——做好安全防护
  • 最小权限原则:只开放 LLM 真正需要的
一句话总结:OpenAI 发明了 Function Calling(LLM 调工具的能力),Anthropic 发明了 MCP(工具调用的标准化协议),LangChain 提供了 @tool(工具定义的最佳开发体验)。三者配合:用 @tool 开发 → 用 MCP 发布 → 任何 LLM 都能用。

配套资源

资源说明
mcp_guide.md完整教学指南(11 章:Tool Calling → MCP 概念 → Claude Code 集成)
mcp_server.pyMCP Server 实现(8 个工具 + 安全防护 + --demo 自测模式)
006-langchain-learning前置知识:LangChain Tool Calling、@tool 装饰器