首页 › tutorials › 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 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) 用户点斜杠命令显式触发 工作流模板、角色扮演、结构化指令 强制结构化,灵活性低

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

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

三条经验法则

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

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

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 理解一个陌生仓库。同一个目标,三种原语各管一段——

Step 1:初始化项目骨架

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

新建 server.py:

# 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 启动后不报错(因为还没有入口,后面补)。先准备一个样本仓库:

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

Step 2:定义 Resource —— 只读上下文

静态资源(固定 URI)和资源模板(带参数的 URI)分开写:

# 静态资源:固定 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 —— 有副作用的动作

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 —— 工作流模板

@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:补入口并启动

文件末尾加上:

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 三个面板分开,你能直观看到三原语是否各自就位:

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:

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

.mcp.json 写法:

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

接入后,在 Claude Code 里:

五、决策流程图:遇到一个新能力,该归到哪个原语?

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 服务器会让三个原语互相协作:

@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 服务器开发中级营、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 — 官方构建指南,三原语能力速览与最小示例
  2. Resources / Prompts / Tools 规范(2025-11-25) — 三原语的控制权定义、capability 声明与 JSON-RPC 消息格式(含 Prompts、Tools 同系列规范页)
  3. MCP Server Patterns: Tools vs Resources vs Prompts (When to Use Each) — 三原语取舍的工程经验对照
  4. Resources & Prompts — python-sdk DeepWiki — FastMCP 装饰器反射成 schema、资源模板与上下文注入的源码级解读
  5. Resources & Templates — FastMCP — 资源模板 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学通信知识星球海报-扫码加入