<!-- title: MCP三原语取舍：工具/资源/提示词 -->

> 本教程是 Claude Code 系列的第三轮，聚焦 **MCP 服务器开发中一个最容易被忽视却最关键的决策点**：什么时候该用 Tool，什么时候该用 Resource，什么时候该用 Prompt。绝大多数入门教程只教你"写一个 Tool"，而现实里 90% 的 MCP 服务器都把本该是 Resource 的数据、本该是 Prompt 的工作流，错误地塞进了 Tool 里，导致上下文爆炸、模型乱调用、用户体验割裂。本篇用一个完整的"代码仓库知识库"服务器，把三个原语同时暴露，讲透取舍逻辑。

## 一、概述与适用人群

**学习目标**：读完本教程，你将能够——

1. 准确区分 MCP 的三原语（Tools / Resources / Prompts）在控制权、触发方式、典型语义上的边界；
2. 用 FastMCP 在一个服务器里同时注册三个原语，并用 MCP Inspector 分通道验证；
3. 掌握"同一业务能力，三种实现方式"的取舍框架，避免把所有东西都写成 Tool。

**适用人群**：已经能跑通至少一个最小 MCP 服务器（会写 `@mcp.tool`）、想从"能跑"升级到"设计正确"的开发者。如果你还没写过任何 MCP 服务器，建议先完成第一轮的入门教程。

## 二、前置条件

- Python 3.11+，Node.js 20+（用于 MCP Inspector）
- 安装任一 MCP 高层 SDK：官方包 `pip install "mcp[cli]"`，或社区版 `pip install fastmcp`
- Claude Code CLI 已登录（用于最后接入验证）
- 一个本地 Git 仓库目录作为被服务的"知识库"样本

```bash
# 官方 Python SDK（推荐新手，文档最全）
pip install "mcp[cli]"

# 或社区版 FastMCP 2.x（API 兼容，工具链更丰富）
pip install fastmcp
```

> 两个包的装饰器写法几乎一致。下文代码以官方 SDK 的 `from mcp.server.fastmcp import FastMCP` 为主；若你用社区版，只需把导入改成 `from fastmcp import FastMCP`，其余不动。这是本教程刻意选的兼容点，避免你被"到底是哪个包"卡住。

## 三、核心概念：三原语决策框架

MCP 规范（2025-11-25 版）明确把服务器能力划分为三类，最关键的区分维度不是"功能"，而是**控制权归属（control）**：

| 原语 | 控制权 | 触发者 | 典型语义 | 代价 |
|------|--------|--------|----------|------|
| **Tools** | 模型控制（model-controlled） | LLM 自主决定何时调用 | 动作、副作用、查询计算 | 每次调用消耗 token，模型可能误触发 |
| **Resources** | 应用控制（application-driven） | 宿主/用户在 UI 里显式选取 | 只读上下文数据：文件、schema、文档 | 占用上下文窗口，但调用语义明确 |
| **Prompts** | 用户控制（user-controlled） | 用户点斜杠命令显式触发 | 工作流模板、角色扮演、结构化指令 | 强制结构化，灵活性低 |

规范原文给的三句话定义非常清楚：

- Tools 是 **model-controlled**，"the language model can discover and invoke tools automatically"；
- Resources 是 **application-driven**，宿主决定如何把上下文喂进去，例如树状/列表选择器；
- Prompts 是 **user-controlled**，典型形态就是 slash command。

**最常见的错误**：把一份 API 文档当成 Tool 返回。后果是模型为了"读文档"白白烧掉一轮 tool call，而且文档内容会进入 tool result，和真正的调用结果混在一起，难以管理。正确做法是把它做成 Resource，让宿主按需注入。

### 三条经验法则

1. **有副作用、需要决策的 → Tool**。例如"创建一个 issue"、"执行 grep 搜索"。
2. **纯只读、可寻址、体积可控的上下文 → Resource**。例如 `repo://readme`、`repo://docs/{name}`。用 URI 模板实现动态资源。
3. **可复用的、带参数的工作流指令 → Prompt**。例如"用资深 reviewer 视角审查这个 PR"。

下面这张架构图把三原语在客户端—服务器之间的流向画清楚：

```mermaid
flowchart LR
    U([用户]) -->|选择 slash command| P[Prompt 用户控制]
    U -->|在列表里点选| R[Resource 应用控制]
    H[宿主 Host/Claude Code] -->|按需注入上下文| R
    H -->|渲染后下发给模型| P
    LLM[LLM 模型] -->|自主决策调用| T[Tool 模型控制]
    P --> LLM
    R --> LLM
    T -->|执行副作用/查询| S[(外部系统\nGit/DB/API)]
    T --> LLM
    subgraph server[你的 MCP 服务器]
      R
      P
      T
    end
```

## 四、实战项目：代码仓库知识库服务器

我们要构建的 `repo-kb` 服务器面向一个真实痛点：**让 Claude Code 理解一个陌生仓库**。同一个目标，三种原语各管一段——

- **Resource**：暴露仓库的 README、CONTRIBUTING、各模块文档（只读上下文）；
- **Tool**：搜索代码、查询 git blame（需要决策的动作）；
- **Prompt**：`review_pr`、`explain_module`（结构化的工作流模板）。

### Step 1：初始化项目骨架

```bash
mkdir repo-kb && cd repo-kb
python -m venv .venv && source .venv/bin/activate
pip install "mcp[cli]"
```

新建 `server.py`：

```python
# server.py
from pathlib import Path
from mcp.server.fastmcp import FastMCP, Context

REPO_ROOT = Path(__file__).resolve().parent / "sample-repo"

mcp = FastMCP("repo-kb")
```

预期结果：`python server.py` 启动后不报错（因为还没有入口，后面补）。先准备一个样本仓库：

```bash
mkdir -p sample-repo/docs
echo "# Sample Repo\n这是一个用于演示的仓库。" > sample-repo/README.md
echo "贡献指南……" > sample-repo/docs/architecture.md
```

### Step 2：定义 Resource —— 只读上下文

**静态资源**（固定 URI）和**资源模板**（带参数的 URI）分开写：

```python
# 静态资源：固定 URI，宿主可在列表里直接看到
@mcp.resource("repo://readme")
def readme() -> str:
    """仓库的 README 全文。"""
    return (REPO_ROOT / "README.md").read_text(encoding="utf-8")

# 资源模板：URI 里带 {name}，按需实例化
@mcp.resource("repo://docs/{name}")
def doc_by_name(name: str) -> str:
    """按文档名读取 docs/ 下的 Markdown。"""
    path = REPO_ROOT / "docs" / f"{name}.md"
    if not path.exists():
        raise FileNotFoundError(f"文档不存在: {name}")
    return path.read_text(encoding="utf-8")
```

**关键点**：函数签名的类型注解会被 SDK 反射成参数 schema；`@mcp.resource` 接收的字符串就是 URI 模板，`{name}` 既是路由参数也会成为函数入参。读取时客户端发 `resources/read`，规范要求服务器声明 `resources` capability（FastMCP 自动处理）。

### Step 3：定义 Tool —— 有副作用的动作

```python
import subprocess

@mcp.tool
def search_code(query: str, ctx: Context) -> str:
    """
    在仓库里用 ripgrep 搜索代码片段。

    Args:
        query: 搜索关键词或正则。
    Returns:
        命中的文件名与行号列表。
    """
    ctx.info(f"开始搜索: {query}")          # 向客户端推送进度日志
    try:
        out = subprocess.run(
            ["rg", "-n", query, str(REPO_ROOT)],
            capture_output=True, text=True, timeout=10,
        )
        hits = out.stdout.strip().splitlines()
        ctx.info(f"命中 {len(hits)} 处")
        return "\n".join(hits) or "无匹配"
    except FileNotFoundError:
        return "未安装 ripgrep，请先安装 rg"
```

**为什么这是 Tool 而不是 Resource**：搜索需要模型判断"要不要搜、搜什么"，结果还会随查询变化——这是 model-controlled 的典型场景。注意 `ctx: Context` 是可选注入参数，SDK 会自动传入，不会暴露给模型的工具 schema。

### Step 4：定义 Prompt —— 工作流模板

```python
@mcp.prompt
def review_pr(pr_url: str) -> str:
    """
    以资深代码审查者的视角审查一个 Pull Request。
    会自动附带仓库 README 作为背景。
    """
    bg = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
    return (
        f"你是一位严格的资深代码审查者。\n"
        f"仓库背景:\n{bg}\n\n"
        f"请审查以下 PR，从正确性、可读性、安全性三个维度给出意见:\n{pr_url}"
    )

@mcp.prompt
def explain_module(module: str) -> str:
    """用费曼学习法向新人解释某个模块。"""
    return f"请像向完全的新人讲解那样，解释仓库中「{module}」模块的职责与核心数据流。"
```

**为什么这是 Prompt 而不是 Tool**：它的产出是"给模型的指令文本"而非副作用，且期望由用户主动以斜杠命令触发（如 `/review_pr https://github.com/.../pull/42`）。Prompt 在规范里返回的是一组 Message（用户/助手角色），FastMCP 允许直接返回字符串，SDK 会自动包成 `UserMessage`。

### Step 5：补入口并启动

文件末尾加上：

```python
if __name__ == "__main__":
    mcp.run(transport="stdio")
```

**常见问题**：

| 现象 | 原因 | 解决 |
|------|------|------|
| 启动报 `ImportError: mcp` | 没装 SDK 或装错虚拟环境 | 确认 `pip install "mcp[cli]"` 在当前 venv |
| 资源模板 `{name}` 不生效 | URI 没用花括号或参数名与函数形参不一致 | 二者必须同名 |
| `ctx.info` 抛错 | 没给函数加 `ctx: Context` 形参 | 加上即可，SDK 自动注入 |

### Step 6：用 MCP Inspector 分通道验证

MCP Inspector 是官方调试工具，最大的价值是**它把 Tools / Resources / Prompts 三个面板分开**，你能直观看到三原语是否各自就位：

```bash
npx @modelcontextprotocol/inspector python server.py
```

浏览器打开提示的本地地址后：

1. 左侧点 **Connect**；
2. **Tools** 标签应出现 `search_code`；
3. **Resources** 标签应出现 `repo://readme`，模板 `repo://docs/{name}` 也能填参读取；
4. **Prompts** 标签应出现 `review_pr`、`explain_module`，可填 `pr_url` 后预览生成的消息。

**预期结果**：三块面板各自有内容。如果某块是空的，说明对应原语没被注册（多半是装饰器漏写或函数没返回值）。

### Step 7：接入 Claude Code 验证

最贴近真实使用的验证是接进 Claude Code：

```bash
claude mcp add repo-kb -- python /绝对路径/server.py
# 或在项目根写 .mcp.json
```

`.mcp.json` 写法：

```json
{
  "mcpServers": {
    "repo-kb": {
      "command": "python",
      "args": ["/绝对路径/server.py"]
    }
  }
}
```

接入后，在 Claude Code 里：

- 输入 `/` 应能看到 `review_pr`、`explain_module` 两个斜杠命令（Prompt 通道）；
- 提问"搜一下哪里用了 FastMCP"，模型会自主调用 `search_code`（Tool 通道）；
- 在资源选择器里能看到 `repo://readme`（Resource 通道）。

## 五、决策流程图：遇到一个新能力，该归到哪个原语？

```mermaid
flowchart TD
    A[新能力] --> B{有副作用或外部查询?}
    B -- 是 --> C{需不需要模型自主决定<br/>何时执行?}
    C -- 是 --> T[Tool 工具]
    C -- 否 --> R2[考虑做成内部函数<br/>而非对外原语]
    B -- 否 --> D{是只读上下文数据?}
    D -- 是 --> R[Resource 资源]
    D -- 否 --> E{是带参数的<br/>结构化工作流指令?}
    E -- 是 --> P[Prompt 提示词]
    E -- 否 --> F[多半不该做成原语<br/>重新评估需求]
    style T fill:#ffe0e0
    style R fill:#e0f5ff
    style P fill:#fff4d0
```

把这张图打印出来贴在屏幕边，能挡掉 80% 的设计纠结。一个高频误判：**"返回一段文档"该是 Tool 还是 Resource**——按图走，没有副作用、是只读上下文 → Resource，哪怕你想"按查询动态生成"，也用资源模板 `docs://{name}` 而非工具。

## 六、把三原语组合起来的高级用法

真正强大的 MCP 服务器会**让三个原语互相协作**：

- 一个 `explain_module` Prompt 在内部要求模型先调用 `search_code` Tool 收集证据；
- Resource 提供的 schema 帮助模型正确填 Tool 的参数；
- Tool 的执行日志通过 `ctx.info` 流回宿主，让用户看到"模型在干嘛"。

```python
@mcp.prompt
def audit_security() -> str:
    """安全审计工作流：先用 search_code 找危险调用，再逐条评估。"""
    return (
        "请按以下步骤执行安全审计：\n"
        "1. 调用 search_code 搜索 eval( 、exec( 、subprocess、os.system\n"
        "2. 对每个命中点评估是否存在用户可控输入\n"
        "3. 输出风险清单与修复建议"
    )
```

这种"Prompt 编排 Tool"的模式，正是 MCP 区别于普通函数调用库的核心价值：它把**人控制的流程骨架**（Prompt）、**模型控制的执行动作**（Tool）、**应用控制的静态知识**（Resource）分层解耦，每一层的控制权和可见性都清晰。

## 七、延伸阅读

- **MCP 官方文档 · 构建服务器**：[modelcontextprotocol.io/docs/develop/build-server](https://modelcontextprotocol.io/docs/develop/build-server) — 官方天气服务器示例，三原语速览
- **规范三件套（2025-11-25）**：Tools / Resources / Prompts 三份 server 规范页，适合查能力声明与消息格式
- **FastMCP 资源与模板**：[gofastmcp.com/servers/resources](https://gofastmcp.com/servers/resources) — 资源模板与 MIME 配置
- **python-sdk 源码导读**：[DeepWiki · Resources & Prompts](https://deepwiki.com/modelcontextprotocol/python-sdk/2.3-resources-and-prompts) — 看装饰器如何反射成 schema
- **原语取舍长文**：[MCP Server Patterns: Tools vs Resources vs Prompts](https://dev.to/webbywisp/mcp-server-patterns-tools-vs-resources-vs-prompts-when-to-use-each-5bgp)

## 八、训练营适用性评估

| 维度 | 评估 |
|------|------|
| **适用训练营** | MCP 服务器开发中级营、AI Agent 工具链营、企业 RAG/上下文工程营 |
| **不适合** | 零基础营（需先有 Tool 基础）、纯前端营（本篇偏 Python 后端） |
| **教学时长** | 讲解 45 分钟 + 动手 90 分钟 + 调试答疑 45 分钟，合计约 3 课时 |
| **可考核点** | ① 能否独立判定新能力归 Tool/Resource/Prompt；② Inspector 三面板是否齐全；③ 是否避免"把文档塞进 Tool" |
| **延伸作业** | 给学员一个真实 API，要求把列表查询做成 Resource、写入做成 Tool、常用请求模板做成 Prompt，并写出取舍理由 |

## 参考来源

1. [Build an MCP server — Model Context Protocol](https://modelcontextprotocol.io/docs/develop/build-server) — 官方构建指南，三原语能力速览与最小示例
2. [Resources / Prompts / Tools 规范（2025-11-25）](https://modelcontextprotocol.io/specification/2025-11-25/server/resources) — 三原语的控制权定义、capability 声明与 JSON-RPC 消息格式（含 Prompts、Tools 同系列规范页）
3. [MCP Server Patterns: Tools vs Resources vs Prompts (When to Use Each)](https://dev.to/webbywisp/mcp-server-patterns-tools-vs-resources-vs-prompts-when-to-use-each-5bgp) — 三原语取舍的工程经验对照
4. [Resources & Prompts — python-sdk DeepWiki](https://deepwiki.com/modelcontextprotocol/python-sdk/2.3-resources-and-prompts) — FastMCP 装饰器反射成 schema、资源模板与上下文注入的源码级解读
5. [Resources & Templates — FastMCP](https://gofastmcp.com/servers/resources) — 资源模板 URI 语法、MIME 类型与动态资源配置

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
