<!-- title: MCP三原语：资源与提示词实战 -->

## 概述

本教程面向已用 MCP 写过一两个工具、却始终只用到 `@mcp.tool` 的开发者。学完你将掌握 MCP 的三大核心原语——**Tools（工具）、Resources（资源）、Prompts（提示词）**——理解它们各自"由谁控制、是否有副作用"的本质区别，并用 Python FastMCP 构建一个同时暴露三原语的项目文档知识库服务器，接入 Claude Code 后让资源被引用、提示词变成斜杠命令、工具被模型自主调用。

## 前置条件

- 已安装 Claude Code（v2.x），可用 `claude --version` 验证
- 已安装 Python 3.10+ 与 `uv`（`pip install uv` 或 `curl -LsSf https://astral.sh/uv/install.sh | sh`）
- 看得懂 Python 基本语法与 JSON 配置
- 建议先跑通过一个最简 MCP 工具，对 stdio 传输有体感

## 一、为什么你只用了 Tools：三大原语的"控制权"之分

大多数 MCP 入门教程止步于 `@mcp.tool`，于是很多人误以为"MCP 就是给大模型挂函数"。这是对协议的严重低估。MCP 的精妙之处在于用**三个原语 + 三种控制权**把"上下文"切成三种不同形态：

| 原语 | 控制方 | 是否有副作用 | 类比 | 典型用途 |
|------|--------|------------|------|---------|
| **Resources 资源** | 应用 / 用户控制 | 无（只读） | HTTP GET | 暴露文件、数据库 schema、配置、日志 |
| **Prompts 提示词** | 用户控制 | 无（生成文本） | 斜杠命令模板 | 标准化工作流：代码评审、日报、故障排查 |
| **Tools 工具** | 模型控制 | 可有（动作） | HTTP POST | 真正执行：查询、写入、调用 API |

关键洞察是"**控制方不同**"：

- **Resources 由宿主应用控制**——Claude Code 把它当"文档附件"，由用户/应用决定何时注入，模型不会自作主张去读。这非常适合静态、只读、不该被误触发的数据。
- **Prompts 由用户控制**——用户主动从命令菜单里挑一个带参数的模板，套用后变成给模型的指令。这是把团队最佳实践固化为可复用入口的方式。
- **Tools 由模型控制**——模型根据任务自主决定何时调用，可以执行真实动作、产生副作用。

把这三者混为一谈（比如把只读数据也做成 Tool）会让你失去协议给你的安全分层：只读数据本不该让模型"想读就读"，而应让用户显式引用。下面我们用一个完整项目把三者摆到正确的位置上。

## 二、项目设计：docs-kb 文档知识库服务器

我们构建一个**项目文档知识库 MCP 服务器** `docs-kb`，目录结构如下：

```
docs-kb/
├── server.py          # FastMCP 服务器，三原语齐发
├── pyproject.toml     # 依赖声明（依赖 mcp）
└── docs/              # 被 Resources 暴露的只读数据源
    ├── getting-started.md
    ├── api-design.md
    └── troubleshooting.md
```

三原语的落点设计得很刻意，帮你记住分工：

- **Resources**：`docs://index`（笔记目录）、`docs://note/{slug}`（单篇内容）、`docs://config`（配置）——全是只读、由用户显式引用。
- **Prompts**：`summarize-note`（总结一篇笔记）、`review-checklist`（代码评审清单）、`standup`（生成站会汇报）——用户主动触发的模板。
- **Tools**：`search_docs`（全文检索）、`create_note`（写文件，有副作用）、`delete_note`（删文件，带 Elicitation 确认）——模型按需调用的动作。

### 步骤 1：初始化项目与依赖

```bash
mkdir docs-kb && cd docs-kb
uv init
uv add "mcp[cli]"
```

在 `pyproject.toml` 里确认依赖包含 `mcp`。准备两三个示例笔记，例如 `docs/getting-started.md` 写几行项目说明。

### 步骤 2：编写 server.py——三原语一次性铺开

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

DOCS_DIR = Path(__file__).parent / "docs"
mcp = FastMCP("docs-kb")

# ───────────────────────── 1. RESOURCES（应用控制·只读）─────────────────────────

@mcp.resource("docs://index")
def list_notes() -> str:
    """返回所有笔记的标题与 slug 清单（只读目录）。"""
    items = []
    for p in sorted(DOCS_DIR.glob("*.md")):
        title = p.read_text(encoding="utf-8").splitlines()[0].lstrip("# ").strip()
        items.append(f"- {p.stem}: {title}")
    return "文档清单：\n" + "\n".join(items)

@mcp.resource("docs://note://{slug}")   # ← URI 模板，可参数化
def read_note(slug: str) -> str:
    """按 slug 读取单篇笔记全文。"""
    f = DOCS_DIR / f"{slug}.md"
    if not f.exists():
        return f"找不到笔记：{slug}"
    return f.read_text(encoding="utf-8")

@mcp.resource("docs://config")
def kb_config() -> str:
    """知识库配置（只读）。"""
    return '{"version":"1.0","language":"zh","owner":"team-platform"}'

# ───────────────────────── 2. PROMPTS（用户控制·可复用模板）─────────────────────────

@mcp.prompt()
def summarize_note(slug: str) -> str:
    """生成『总结指定笔记』的提示词模板。"""
    return (
        f"请阅读 docs://note/{slug} 的内容，"
        "用三句话总结要点，并列出 3 个可执行的行动项。"
    )

@mcp.prompt()
def review_checklist() -> str:
    """返回团队统一的代码评审清单。"""
    return (
        "请按以下清单评审即将提交的 diff：\n"
        "1. 是否有硬编码密钥或凭据；\n"
        "2. 错误处理是否覆盖边界；\n"
        "3. 是否新增了未测试的分支；\n"
        "4. 命名与现有风格是否一致。"
    )

# ───────────────────────── 3. TOOLS（模型控制·可有副作用）─────────────────────────

@mcp.tool()
def search_docs(query: str) -> str:
    """全文检索文档，返回命中的笔记标题与片段。"""
    hits = []
    for p in DOCS_DIR.glob("*.md"):
        text = p.read_text(encoding="utf-8")
        if query.lower() in text.lower():
            i = text.lower().index(query.lower())
            hits.append(f"[{p.stem}] ...{text[max(0,i-30):i+50]}...")
    return "\n".join(hits) if hits else "无匹配结果"

@mcp.tool()
def create_note(title: str, body: str) -> str:
    """创建一篇新笔记（有副作用：写文件）。"""
    slug = title.strip().lower().replace(" ", "-")
    (DOCS_DIR / f"{slug}.md").write_text(f"# {title}\n\n{body}\n", encoding="utf-8")
    return f"已创建 docs://note/{slug}"

class Confirm(BaseModel):
    confirm: bool

@mcp.tool()
async def delete_note(slug: str, ctx: Context) -> str:
    """删除笔记——通过 Elicitation 在执行前向用户确认。"""
    result = await ctx.elicit(
        message=f"确定要删除笔记 {slug} 吗？此操作不可撤销。",
        schema=Confirm,
    )
    if result.action != "accept" or not result.data.confirm:
        return "已取消删除。"
    f = DOCS_DIR / f"{slug}.md"
    f.unlink(missing_ok=True)
    return f"已删除 {slug}"

if __name__ == "__main__":
    mcp.run()   # 默认 stdio 传输
```

**设计要点解读：**

1. `list_notes` 返回的是"目录"，**不返回每篇全文**——避免把大量只读数据一股脑塞进上下文，这正是 Resource 相比"一个返回所有内容的 Tool"的优势：可被引用、按需加载。
2. URI 模板 `docs://note://{slug}` 让单篇笔记可被精确引用，客户端按 slug 取数。
3. `summarize_note` 这个 Prompt 内部**引用了 Resource 的 URI**（`docs://note/{slug}`），体现了三原语的协作：用户选模板 → Prompt 生成指令 → 指令里引用 Resource → 模型据此读取数据 → 如需改动再调 Tool。
4. `delete_note` 用 **Elicitation**（MCP 较新的人类在环原语）在执行副作用前暂停，向用户要一个确认。这把"模型可能误删"的风险交还给了人。

### 步骤 3：在 Claude Code 中接入（stdio 传输）

本地进程型服务器用 stdio 传输最合适：

```bash
# 在项目根目录执行；-- 之后的参数原样传给服务器进程
claude mcp add --transport stdio docs-kb -- uv run server.py
```

`--` 是关键：它把 `--` 之后的 `uv run server.py` 与 `claude mcp add` 自身的参数隔开，否则 Claude Code 会尝试解析 `uv` 的 flag。若想配置环境变量，加 `--env KEY=value`：

```bash
claude mcp add --env LOG_LEVEL=debug --transport stdio docs-kb -- uv run server.py
```

### 步骤 4：把配置沉淀到项目（.mcp.json）

要让团队成员共享同一服务器配置，用项目作用域，它会写入仓库根目录的 `.mcp.json`：

```bash
claude mcp add --scope project --transport stdio docs-kb -- uv run server.py
```

生成的 `.mcp.json`：

```json
{
  "mcpServers": {
    "docs-kb": {
      "command": "uv",
      "args": ["run", "server.py"]
    }
  }
}
```

三种作用域的区别（影响团队协作）：

| 作用域 | 存储位置 | 是否共享 | 适用场景 |
|--------|---------|---------|---------|
| **local**（默认） | 本机私有 | 否 | 个人临时调试 |
| **project** | 仓库 `.mcp.json` | 是（随 git） | 团队统一工具链 |
| **user** | 用户全局 | 否（跨项目） | 个人常用服务器 |

> 提示：Claude Code 会把项目根目录注入到 stdio 服务器的 `CLAUDE_PROJECT_DIR` 环境变量，你的服务器可用它解析项目相对路径，不依赖当前工作目录。

### 步骤 5：验证三原语是否各就各位

进入 Claude Code 后输入 `/mcp` 查看连接状态，应看到 `docs-kb` 显示工具数量、已连接。

然后分别验证三种控制权：

- **验证 Prompts（用户控制）**：在输入框敲 `/`，应能从命令列表里看到 `summarize-note`、`review-checklist`。选 `summarize-note` 并填入 slug，Claude 会收到填好的提示词。
- **验证 Resources（应用控制）**：让 Claude 引用资源，例如"读一下 `docs://index`"，或用 Claude Code 的资源引用入口选取 `docs://note/getting-started`，模型会读到对应内容。
- **验证 Tools（模型控制）**：直接说"帮我搜一下文档里关于认证的内容"，模型会**自主**调用 `search_docs`；说"创建一篇标题为『部署手册』的笔记"会触发 `create_note`；要求删除时会先弹出 Elicitation 确认。

## 三、常见问题与解决方案

**Q1：提示词没出现在 `/` 命令列表里？**
确认服务器声明了 `prompts` 能力（FastMCP 自动声明），且 `/mcp` 里服务器为已连接状态。若服务器连接中，Claude 会等待它就绪再继续；用 `/mcp` 面板可看每个服务器旁的工具数量。

**Q2：Resource 该用 Tool 暴露吗？**
看是否有副作用与控制权需求。只读、希望由用户/应用决定何时注入的数据用 Resource；需要模型主动检索的"动作"用 Tool。本例中 `search_docs` 故意做成 Tool（模型按需检索），而单篇笔记做成 Resource（用户显式引用），分工即安全。

**Q3：stdio 服务器改了代码不生效？**
stdio 服务器是本地进程，改代码后需重启会话或重新连接。项目作用域下改 `.mcp.json` 后也要重连。可发送 `notifications/tools/list_changed` 让 Claude Code 动态刷新能力，无需断开重连。

**Q4：远程服务器该用哪种传输？**
官方推荐 **Streamable HTTP**（最广支持、支持 OAuth）；SSE 已废弃；只有需要服务器主动推送事件时才考虑 WebSocket。本地优先 stdio。

**Q5：Elicitation 弹不出来？**
Elicitation 是较新原语，需较新版的 `mcp` SDK 与支持该能力的客户端。若客户端不支持，服务器会拿到 `decline` 或报能力缺失——对核心逻辑做兼容降级（如默认拒绝危险操作）。

## 四、三原语协作的完整工作流

下图展示一次"用户触发 Prompt → Prompt 引用 Resource → 模型读取 → 模型按需调 Tool → 副作用前 Elicitation 确认"的端到端控制流：

```mermaid
flowchart LR
    U([用户]) -->|1 选 Prompt 模板| P[mcp.prompt<br/>summarize-note]
    P -->|2 填充参数生成指令| M((Claude 模型))
    U -.->|引用 Resource<br/>docs://note/slug| M
    R[(mcp.resource<br/>只读笔记数据)] -->|3 返回内容| M
    M -->|4 需要改动时| T[mcp.tool<br/>create_note/delete_note]
    T -->|5 副作用前| E{Elicitation<br/>向用户确认}
    E -->|confirm| FS[(文件系统<br/>docs/*.md)]
    E -->|decline| CANCEL[取消操作]

    classDef ctrl fill:#eef,stroke:#88f
    classDef data fill:#efe,stroke:#8c8
    classDef act fill:#fee,stroke:#e88
    class P ctrl
    class R data
    class T act
```

读图要点：**蓝色为用户/应用控制（Prompt、Resource），绿色为只读数据，红色为模型控制且有副作用（Tool）**——颜色即对应三原语的控制权分层，这正是把"只读"与"动作"用不同原语隔离的安全价值。

## 五、把服务器变成可分发产物（进阶）

当服务器稳定后，可标准化打包供他人一行接入：

1. **声明可执行入口**：在 `pyproject.toml` 配置 `[project.scripts]`，让 `docs-kb` 成为控制台命令，他人无需关心 `uv run server.py` 的细节。
2. **发布到 PyPI**：`uv publish` 后，他人用 `claude mcp add --transport stdio docs-kb -- uvx docs-kb` 一行接入（`uvx` 会临时拉取并运行）。
3. **或交给官方脚手架**：在 Claude Code 里 `/plugin install mcp-server-dev@claude-plugins-official`，再 `/mcp-server-dev:build-mcp-server`，让 Claude 根据用途为你脚手架一个远程 HTTP 或本地 stdio 服务器骨架。

注意发布前剥离任何硬编码密钥与本地绝对路径，改用 `CLAUDE_PROJECT_DIR` 与环境变量配置。

## 延伸阅读

- **MCP 官方规范与概念**：[modelcontextprotocol.io](https://modelcontextprotocol.io/docs/concepts/resources) — Resources / Prompts / Tools 的权威定义与协议细节
- **Claude Code MCP 集成文档**：[code.claude.com/docs/en/mcp](https://code.claude.com/docs/en/mcp) — 传输、作用域、资源引用、提示词命令、Tool Search 的官方说明
- **MCP 六大特性全景**：[WorkOS: MCP features guide](https://workos.com/blog/mcp-features-guide) — 在三原语之外补全 Sampling、Roots、Elicitation 的来龙去脉
- **FastMCP 三原语实战**：[Dan Liden: Prompts and Resources](https://www.danliden.com/posts/20250921-mcp-prompts-resources.html) — 同一套 `@mcp.tool/resource/prompt` 装饰器模式的逐步演示
- **Elicitation 人类在环**：[dev.to: MCP Elicitation](https://dev.to/kachurun/mcp-elicitation-human-in-the-loop-for-mcp-servers-m6a) — 服务器暂停执行、向客户端请求输入的机制详解

## 训练营适用性评估

- **适用类型**：面向"已入门 MCP、要写生产级服务器"的中级训练营；不适合零基础（建议先有 Tool 实操）。
- **预计教学时长**：3–4 学时（讲解 1 学时 + 跟做项目 2 学时 + 调试答疑 1 学时）。
- **学员产出**：每人一个三原语齐发的本地 MCP 服务器，并能在 Claude Code 中演示"提示词变命令、资源被引用、工具被自主调用、删除前人确认"四种交互。
- **考核点**：能否正确解释三者控制权差异、能否把只读数据放进 Resource 而非 Tool、能否实现一个带 Elicitation 确认的副作用工具。

## 参考来源

1. [Claude Code 官方 MCP 集成文档](https://code.claude.com/docs/en/mcp) — 传输方式、三种作用域、资源引用与提示词命令、动态能力刷新的权威说明
2. [Understanding MCP features: Tools, Resources, Prompts, Sampling, Roots, Elicitation — WorkOS](https://workos.com/blog/mcp-features-guide) — 六大特性的控制权与用途对比，厘清三原语定位
3. [Getting Started with MCP Part 2: Prompts and Resources — Dan Liden](https://www.danliden.com/posts/20250921-mcp-prompts-resources.html) — FastMCP `@mcp.tool/resource/prompt` 装饰器与 `ctx.read_resource` 协作的实战代码
4. [MCP Elicitation: Human-in-the-Loop for MCP Servers — dev.to](https://dev.to/kachurun/mcp-elicitation-human-in-the-loop-for-mcp-servers-m6a) — 服务器执行中暂停、向用户请求结构化输入的人机在环机制
5. [MCP Developer Guide 2026 — lushbinary](https://lushbinary.com/blog/mcp-model-context-protocol-developer-guide-2026/) — 架构、TypeScript/Python 实现、部署与安全实践的 2026 年度开发指南

> [!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)
