首页 › tutorials › MCP三原语:资源与提示词实战

概述

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

前置条件

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

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

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

关键洞察是"控制方不同":

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

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

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

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

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

步骤 1:初始化项目与依赖

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

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

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

# 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 传输最合适:

# 在项目根目录执行;-- 之后的参数原样传给服务器进程
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:

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

步骤 4:把配置沉淀到项目(.mcp.json)

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

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

生成的 .mcp.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 显示工具数量、已连接。

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

三、常见问题与解决方案

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 确认"的端到端控制流:

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 与环境变量配置。

延伸阅读

训练营适用性评估

参考来源

  1. Claude Code 官方 MCP 集成文档 — 传输方式、三种作用域、资源引用与提示词命令、动态能力刷新的权威说明
  2. Understanding MCP features: Tools, Resources, Prompts, Sampling, Roots, Elicitation — WorkOS — 六大特性的控制权与用途对比,厘清三原语定位
  3. Getting Started with MCP Part 2: Prompts and Resources — Dan Liden — FastMCP @mcp.tool/resource/prompt 装饰器与 ctx.read_resource 协作的实战代码
  4. MCP Elicitation: Human-in-the-Loop for MCP Servers — dev.to — 服务器执行中暂停、向用户请求结构化输入的人机在环机制
  5. MCP Developer Guide 2026 — lushbinary — 架构、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学通信知识星球海报-扫码加入