绝大多数「第一个 MCP 服务器」教程止步于 Tools:写几个 @mcp.tool() 函数,跑通 Inspector,挂到 Claude Code,收工。但 MCP 协议一共定义了三种原语——Tools、Resources、Prompts,它们各自回答不同的问题,在 Claude Code 里的暴露方式也完全不同。只造工具,等于只用了协议的三分之一。
本篇聚焦那两块最常被忽略的拼图:用 Resources 把数据像文件一样暴露出去(可被 @ 提及),用 Prompts 把可复用工作流变成 / 斜杠命令。我们用一个可运行的「个人知识库 MCP」把三原语一次性串起来。
学习目标:读完本篇,你将能(1)在 30 秒内判断某个能力该做成 Tool、Resource 还是 Prompt;(2)用官方 Python SDK 的 FastMCP 在一个文件里同时实现三原语;(3)用 MCP Inspector 调试,并挂载到 Claude Code,分别用「工具调用 / @ 提及 / 斜杠命令」三种方式触发。
适用人群:已经写过一个 Hello-World 级别的 MCP 工具、想理解协议全貌的开发者;给团队设计内部 MCP 服务、需要把文档和标准操作流程(SOP)一并交付的架构师。
venv 与 pipclaude --version 可用)python -m venv .venv && source .venv/bin/activate
pip install "mcp[cli]"
mcp --version # 确认 MCP CLI 可用
这是本篇最重要的一节。三个原语的根本区别在于「谁来决定使用它」(控制方向)和「它有没有副作用」:
落到 Claude Code 里,三种原语长得完全不一样:
| 能力类型 | 该做成 | 在 Claude Code 中的形态 | 典型触发者 |
|---|---|---|---|
| 只读数据 / 文档 / 配置 | Resource | @server:scheme://path 的 @ 提及,自动注入为附件 |
宿主/你 |
| 执行动作、有副作用 | Tool | 模型自主调用,命中权限规则 | 模型 |
| 可复用的提示词工作流 | Prompt | /mcp__server__prompt 斜杠命令 |
你 |
一句话判断法:「提供上下文」用 Resource,「改变世界」用 Tool,「复用打法」用 Prompt。 把一条 SOP 文档做成 Tool 是错配——它是数据不是动作;把「代码评审流程」做成 Resource 也是错配——它是流程不是数据。
flowchart LR
Q["新能力需求"] --> R1{"只读数据/文档?"}
R1 -->|是| RES["Resource\n@server:..."]
R1 -->|否| R2{"有副作用/执行动作?"}
R2 -->|是| TOOL["Tool\n模型自主调用"]
R2 -->|否| PROMPT["Prompt\n/mcp__server__cmd"]
RES --> CC1["自动注入上下文"]
TOOL --> CC2["需权限确认"]
PROMPT --> CC3["用户显式触发"]
style RES fill:#e8f5e9,stroke:#2e7d32
style TOOL fill:#fff3e0,stroke:#ef6c00
style PROMPT fill:#e3f2fd,stroke:#1565c0
我们要造一个 markdown 笔记知识库服务器 kb-mcp,把三原语用足:
notes://index)和单条笔记(notes://{slug} 资源模板)作为只读数据暴露,方便 @ 提及。create_note / search_notes / delete_note——会改数据、有副作用,交给模型调用。summarize_note / weekly_digest——固定打法,做成斜杠命令。笔记存成一个本地 JSON 索引文件(index.json),路径由 KB_DIR 环境变量控制,便于隔离测试。新建 kb_mcp.py:
import json, os, re
from pathlib import Path
from mcp.server.fastmcp import FastMCP
KB_DIR = Path(os.environ.get("KB_DIR", Path.home() / ".kb-mcp"))
KB_DIR.mkdir(parents=True, exist_ok=True)
INDEX_FILE = KB_DIR / "index.json"
mcp = FastMCP("kb-mcp")
def slugify(title: str) -> str:
s = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")[:60]
return s or "note"
def _load() -> dict:
return json.loads(INDEX_FILE.read_text("utf-8")) if INDEX_FILE.exists() else {}
def _save(idx: dict) -> None:
INDEX_FILE.write_text(json.dumps(idx, ensure_ascii=False, indent=2), "utf-8")
预期结果:
KB_DIR不存在会自动创建;index.json首次写入即生成。
@mcp.tool()
def create_note(title: str, body: str, tags: str = "") -> str:
"""创建一条笔记。title 标题;body 正文(Markdown);tags 逗号分隔标签。返回新笔记 URI。"""
idx = _load()
slug = slugify(title)
idx[slug] = {"title": title, "body": body,
"tags": [t.strip() for t in tags.split(",") if t.strip()]}
_save(idx)
return f"created notes://{slug}"
@mcp.tool()
def search_notes(query: str) -> str:
"""按关键词检索标题、正文、标签,返回匹配的 URI 列表。"""
idx, q = _load(), query.lower()
hits = [s for s, n in idx.items()
if q in s or q in n["title"].lower() or q in n["body"].lower()
or any(q in t.lower() for t in n["tags"])]
return "\n".join(f"- notes://{s} — {idx[s]['title']}" for s in hits) or "无匹配笔记。"
@mcp.tool()
def delete_note(slug: str) -> str:
"""按 slug 删除笔记。"""
idx = _load()
if slug not in idx:
return f"未找到 notes://{slug}"
del idx[slug]; _save(idx)
return f"deleted notes://{slug}"
每个工具的文档字符串会变成给模型的工具说明——这是触发准确性的关键,务必写清「做什么」和「参数含义」。
这是与 Tools 教程的分水岭。Resources 用 @mcp.resource("URI"),URI 是数据标识而非函数名。静态资源用一个常量 URI;资源模板用 RFC 6570 风格的 {param} 占位,函数参数名与之对应:
@mcp.resource("notes://index")
def note_index() -> str:
"""知识库目录清单(所有笔记的 URI 与标题)。"""
idx = _load()
if not idx:
return "(知识库为空)"
return "\n".join(f"- notes://{s} — {n['title']} [{', '.join(n['tags'])}]"
for s, n in idx.items())
@mcp.resource("notes://{slug}")
def read_note(slug: str) -> str:
"""读取单条笔记全文。"""
idx = _load()
if slug not in idx:
raise ValueError(f"未知笔记: {slug}")
n = idx[slug]
return f"# {n['title']}\n\n{n['body']}\n\n_标签: {', '.join(n['tags'])}_"
if __name__ == "__main__":
mcp.run()
两个关键点:
notes://{slug} 是资源模板。Claude Code 在 @ 自动补全里会模糊匹配 slug,命中后自动 resources/read 拉取全文注入上下文。resources/read notes://index 立刻反映最新状态,天然「动态」。关于
list_changed:当你的资源列表是静态注册(启动时固定)时,若运行期增删,应发送notifications/resources/list_changed通知让宿主刷新列表。FastMCP 这种「函数即资源」写法每次读取都重新求值,已隐含动态性;若改用预注册列表,记得补这条通知。
Prompts 是「带参数的预制提示词」,在 Claude Code 里变成 /mcp__server__name 斜杠命令:
@mcp.prompt()
def summarize_note(slug: str) -> str:
"""对指定笔记生成结构化摘要:三句话概述 + 关键点 + 一条建议。"""
return (f"请引用 @kb-mcp:notes://{slug},"
"输出:1) 三句话概述;2) 3-5 个关键点;3) 一条可执行建议。")
@mcp.prompt()
def weekly_digest() -> str:
"""生成知识库简报:读取目录并把笔记按标签归类。"""
return "请读取 @kb-mcp:notes://index,把所有笔记按标签分组,挑出 3 条最重要的。"
注意 Prompt 的返回值本身就是给模型的指令——它可以把 Resource(@kb-mcp:notes://...)、Tool 调用编排进一条工作流里,这正是 Prompt 作为「打法封装」的价值。
Inspector 是官方 GUI 调试器,能分别对三种原语做单点测试,强烈建议先在这里验证:
mcp dev kb_mcp.py
浏览器打开 Inspector 后:
title/body/tags 调用 create_note,看返回 created notes://xxx。notes://index 和模板 notes://{slug};点开某条资源读全文。summarize_note / weekly_digest;给 summarize_note 传 slug 参数,预览渲染后的消息。常见问题:Resources 面板空白 → 多半是没
import或函数没加@mcp.resource;Prompts 参数不生效 → 检查函数形参名与 Inspector 字段是否一致。
注意 stdio 用法中 -- 把 Claude 自己的选项和启动命令隔开:
# 用绝对路径,避免依赖当前工作目录
KB_DIR="$HOME/.kb-mcp" \
claude mcp add --transport stdio --env KB_DIR="$HOME/.kb-mcp" \
kb-mcp -- python "$PWD/kb_mcp.py"
claude mcp list # 应看到 kb-mcp
进入会话后用 /mcp 确认连接,面板会显示该服务器暴露的工具数量。Claude Code 支持 list_changed 通知,资源/提示词变动会自动刷新,无需重连。
帮我用 create_note 建一条笔记:标题 "MCP 三原语",正文讲 Tools/Resources/Prompts 区别,标签 mcp,arch
模型自主决定调用 create_note,命中权限规则后执行;返回 created notes://mcp-san-yuan-yu。
在输入框敲 @,自动补全里会列出 @kb-mcp:notes://index 等:
把 @kb-mcp:notes://index 读出来,告诉我知识库里都有什么
引用的资源会被自动 resources/read 并作为附件注入。这是 Resource 区别于 Tool 的核心体验——你主动选定上下文,而不是让模型去猜。
敲 / 能看到 /mcp__kb-mcp__summarize_note 与 /mcp__kb-mcp__weekly_digest。带参数的 prompt 用空格传参:
/mcp__kb-mcp__summarize_note mcp-san-yuan-yu
渲染后的提示词直接进入对话,模型据此执行摘要工作流。
sequenceDiagram
participant U as 你
participant CC as Claude Code
participant S as kb-mcp 服务器
Note over U,S: Tool 路径 —— 模型自主调用
U->>CC: "建一条关于 MCP 的笔记"
CC->>S: tools/call create_note(...)
S-->>CC: created notes://mcp-san-yuan-yu
Note over U,S: Resource 路径 —— @ 提及注入
U->>CC: "分析 @kb-mcp:notes://index"
CC->>S: resources/read notes://index
S-->>CC: 目录全文(作为附件)
Note over U,S: Prompt 路径 —— 斜杠命令
U->>CC: /mcp__kb-mcp__summarize_note mcp-san-yuan-yu
CC->>S: prompts/get summarize_note{slug}
S-->>CC: 渲染后的提示词
CC->>CC: 据此执行摘要工作流
把上面四段代码拼进同一个 kb_mcp.py(Step 1 的导入/存储 + Step 2~4 的三原语),python kb_mcp.py 即为标准 stdio 服务器。配套验证脚本:
# 1. 清空环境变量起步
export KB_DIR="$(mktemp -d)"
# 2. Inspector 手测
mcp dev kb_mcp.py
# 3. 挂载(另开终端)
claude mcp add --transport stdio --env KB_DIR="$KB_DIR" kb-mcp -- python "$PWD/kb_mcp.py"
# 4. 进会话验证
claude
# /mcp → 确认 kb-mcp 已连接
# "用 create_note 建一条笔记..." → 验证 Tool
# "@kb-mcp:notes://index 读出来" → 验证 Resource
# /mcp__kb-mcp__weekly_digest → 验证 Prompt
@ 菜单里看不到我的资源:确认 @mcp.resource 装饰器已加、URI scheme 合法(自定义 scheme 须符合 RFC 3986);/mcp 里服务器要显示已连接。/mcp__服务器名__prompt名,注意是双下划线分隔;服务器名里的空格会被规范化成下划线。_meta 里声明 anthropic/maxResultSizeChars(上限 50 万字符)放宽阈值,或调 MAX_MCP_OUTPUT_TOKENS。.mcp.json 给服务器加 "alwaysLoad": true,绕过 Tool Search 延迟加载(代价是常驻占用上下文)。--scope project 把配置写进 .mcp.json 入版本控制,配合 ${VAR:-default} 变量展开处理机器相关路径与密钥。/docs/concepts/resources)、Prompts(/docs/concepts/prompts)——协议层最权威的语义与 JSON-RPC 报文定义。code.claude.com/docs/en/mcp):claude mcp add 全部选项、scope 体系、Tool Search、maxResultSizeChars 等服务端作者必读项。modelcontextprotocol.io/docs/develop/build-server:三原语的服务端构建总览。mcp-server-dev 插件:在 Claude Code 里 /plugin install mcp-server-dev@claude-plugins-official 后 /mcp-server-dev:build-mcp-server,让 Claude 直接脚手架一个本地或远程服务器。audience/priority/lastModified 注解、把 Prompt 改成多轮嵌入式资源引用)。claude mcp add/scope/.mcp.json、@ 引用资源、/mcp__server__prompt 斜杠命令、Tool Search、maxResultSizeChars 注解。由「51学通信」原创出品 · 站长爱卫生(微信:gprshome201101)
[!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 等场景全覆盖。
