3995 字
约 13 分钟
15
8.3 核心原语:tools、resources、prompts
2026-10-25
无标签

8.3 核心原语:tools、resources、prompts

一句话结论:三个原语的分界线是**"谁发起、改不改变状态"**。混用它们会导致权限模型失效——把"读数据"做成"工具",就等于让模型自由决定要读什么。

读这篇之前

需要先读 8.2(两层结构)、4.1(工具调用)。这一篇讲 MCP 的语义核心。


一、三个原语一览

MCP 数据层定义了服务端能提供的三类东西:[核验:MCP 官方架构]

原语 语义 谁发起 改变状态
tools 让 AI 执行动作 模型(通过调用) 可能
resources 提供上下文数据 客户端(可主动读) 不改变
prompts 提供交互模板 用户/客户端(选用) 不改变

记住这张表的三列,后面所有区别都从这里推出来。


二、tools:模型能做的事

和 Function Calling 的关系

形态上几乎一致(name + description + schema + 返回结果)。区别在"谁提供":

Function Calling MCP tools
工具由谁定义 你的应用代码 独立运行的服务端
复用范围 单个应用 所有 MCP 客户端
生命周期 跟着应用 独立进程/服务

设计要点

MCP 的 tools 设计原则,和 4.2–4.4 讲的完全一致:

要点 出处
description 要写"什么时候用/不用" 4.2
参数用枚举而非自由文本 4.3
副作用分级(只读/写入/不可逆) 4.5
防呆设计优于报错 4.4

MCP 不改变这些原则。 它只改变"工具从哪来"。


三、resources:只读数据的正确位置

这是最容易被误用的原语。

它解决什么问题

假设你有一个知识库服务端。有两种暴露方式:

❌ 做成工具:read_document(path)
   → 模型自由决定读哪个文件
   → 等于把整个知识库的访问权交给模型

✅ 做成 resource:documents://handbook
   → 客户端决定提供哪些资源
   → 模型只能看被给予的内容

这个区别就是权限模型的区别。

resources 的典型形态

类型 例子
文件 file:///project/README.md
数据库记录 db://users/123
日志 logs://app/2026-10-25
配置 config://app/settings

为什么"客户端发起"很重要

因为是否把某个 resource 放进上下文,是客户端的决定,不是模型的。

这意味着:

  • 用户/应用可以控制"给模型看什么"
  • 不会出现"模型自己决定去读生产数据库"的情况

这条是资源与工具最本质的差别。


四、prompts:把"最佳提问方式"封装起来

它解决什么问题

一个数据源往往有"最佳提问姿势"。比如一个 GitHub 服务端知道:

  • 问"最近改了什么"应该用 compare(from, to)
  • 问"谁写的这行"应该用 blame(path, line)

prompts 允许把这类模板预置在服务端,客户端可以直接选用,而不用让用户自己摸索。

它和 Skill 的区别

MCP prompts Skill
位置 服务端 客户端/用户配置
内容 针对特定数据源的交互模板 针对特定任务的能力包
加载 用户选用 按需加载(9.1)

两者都是"提示模板",但归属不同:prompts 属于数据源,Skill 属于使用方。


五、怎么选:一个决策表

你想做的事 用哪个 理由
执行动作(改文件、发消息) tools 需要模型决定
提供可读数据(文件、记录) resources 让客户端控制暴露范围
预置提问模板 prompts 封装最佳实践
既读又改 拆成两个:read_x + update_x 副作用分级(4.5)

最后一行很重要:不要把"读"和"写"合成一个工具,否则无法单独对写操作加审批。


六、一个完整例子:GitHub 服务端

假设你要做一个 GitHub MCP 服务端,三个原语分别怎么用:

tools(模型决定调用):

create_issue(repo, title, body)     ← 写入
add_comment(issue_number, body)     ← 写入
merge_pr(pr_number)                 ← 不可逆,需审批

resources(客户端提供):

repo://owner/name/README.md
repo://owner/name/issues/123
repo://owner/name/pr/456/diff

prompts(用户选用):

"审查这个 PR"       → 模板:先读 diff,再按清单检查
"总结本周提交"      → 模板:读 commit 列表,按主题归类

这样划分的好处:

关注点 落在哪
模型能做什么(且可审计) tools
用户愿意暴露哪些数据 resources
常见任务怎么做最好 prompts


七、动手:把 GitHub 服务端画完整

上一节给了分类,这一节给出每个原语的完整定义形态,便于照抄。

tools 的三个定义

// ① 只读查询
{"name":"list_issues",
 "description":"列出仓库的 issue。只读。适用于:查看待办、统计数量。
                不适用于:搜索代码(用 search_code)。",
 "inputSchema":{"type":"object",
   "properties":{
     "repo":{"type":"string","description":"owner/name 格式"},
     "state":{"type":"string","enum":["open","closed","all"],"description":"默认 open"}},
   "required":["repo"]}}

// ② 写入(需要审批)
{"name":"create_issue",
 "description":"创建 issue。写操作,会通知仓库成员。",
 "inputSchema":{"type":"object",
   "properties":{"repo":{"type":"string"},"title":{"type":"string"},"body":{"type":"string"}},
   "required":["repo","title"]}}

// ③ 不可逆(需人工确认)
{"name":"merge_pr",
 "description":"合并 PR 到目标分支。不可逆,会触发部署流水线。",
 "inputSchema":{"type":"object",
   "properties":{"repo":{"type":"string"},"pr":{"type":"integer"}},
   "required":["repo","pr"]}}

注意三者的 description 写法差异:

类型 description 里必须有什么
只读 "只读"+ 不适用场景
写入 "写操作"+ 副作用(通知谁)
不可逆 "不可逆"+ 后续影响(触发部署)

这就是 4.5 讲的"用描述暴露副作用等级"——模型看到"不可逆"三个字,会更谨慎。

resources 的 URI 设计

repo://owner/name/README.md
repo://owner/name/issues/123
repo://owner/name/pr/456/diff

URI 设计的三条原则:

原则 理由
层次化 便于客户端做前缀匹配(如只允许 repo://myorg/*)
可预测 客户端能拼出自己需要的 URI
不含敏感信息 防止日志泄露(14.5)

prompts 的定义

{"name":"review_pr",
 "description":"审查一个 PR 的完整流程",
 "arguments":[{"name":"pr","description":"PR 编号","required":true}]}

调用后服务端返回一段预置的提示模板。


八、一个必须回答的问题:三个原语能不能互通

技术上可以(用 tools 实现读文件),但会改变权限模型。

用 tools 实现读 用 resources 实现读
模型自己决定读哪个 客户端决定提供哪个
无需用户确认 可事先审批范围
攻击面大(8.6) 攻击面小

所以"能不能"和"该不该"是两件事。 只读数据用 resources,是安全设计而非风格偏好。



九、三个原语的返回形态与错误约定

定义清楚还不够,返回什么同样影响模型能否用好。

原语 返回形态 关键约定
tools 内容块数组 + isError 业务错误放 isError,不放协议 error
resources MIME 类型 + 内容 要声明类型(text/markdown、application/json)
prompts 消息数组 可带参数占位符

tools 返回的一个细节

好:content: [{"type":"text","text":"共 42 条 issue,已返回前 20 条(state=open)"}]
差:content: [{"type":"text","text":"{...完整 JSON 500KB...}"}]

理由(4.4 原则三):返回值只给模型需要的。大 JSON 既占上下文又稀释注意力(2.2)。

resources 的 MIME 类型为什么重要

类型 客户端怎么处理
text/markdown 直接放进上下文
application/json 可能要先摘要(12.3 的摘要传递)
image/png 走图像通道,不进文本上下文

不声明类型,客户端只能猜——猜错就会把二进制塞进文本上下文。

prompts 的参数

{"name":"review_pr","arguments":[{"name":"pr","required":true}]}

参数要声明必填,否则模型可能传空值进去,得到一段残缺的模板。


补充误区

误区七:resources 就是"换个名字的工具"。
发起方与权限模型完全不同(第三节)。

误区八:prompts 不重要,客户端自己写就行。
服务端最懂自己的数据结构与最佳提问方式。

误区九:URI 可以随便设计。
不层次化就无法做前缀白名单。

误区十:读取数据做成 tools 更灵活。
灵活等于把访问权交给模型。只读用 resources。


十、设计三个原语时的三条原则

原则一:按"谁发起"分配,不按"方便"分配

错:把读文件做成 tool,因为"模型可以自己决定读哪个"更方便
对:做成 resource,因为"客户端决定给哪个"才安全

"方便"往往等于"把权限交给模型"(14.4)。

原则二:一个原语对应一类能力,不要混杂

原语 该装什么 不该装什么
tools 会改变状态的动作 只读查询
resources 静态数据 需要参数的复杂查询
prompts 固定模板 需要动态数据的内容

"需要参数的复杂查询"放 tools 更合适——因为 resource 的 URI 表达能力有限。

原则三:给客户端留"选择权"

resources 的价值是客户端能选择提供哪些。所以:

好:resources 提供 20 个细粒度资源,客户端按需挑 3 个
差:resources 只提供 1 个"全部数据"的巨型资源

细粒度让客户端能控制上下文(2.2)。


十一、常见误区

误区一:所有能力都做成 tools 最方便。
方便,但等于把访问权交给模型。只读数据应该是 resources。

误区二:resources 就是"另一种工具"。
发起方不同:resources 由客户端提供,tools 由模型调用。这是权限分界。

误区三:prompts 是多余的,客户端自己写就行。
服务端更懂自己的数据源该怎么问。prompts 是把专家知识封装起来。

误区四:读和写可以合一个工具。
无法分级审批。必须拆开。

误区五:三个原语可以随便互换。
互换会改变权限模型与发起方,不是等价替换。

误区六:MCP 原语和 Skill 是竞争关系。
不是。prompts 属数据源,Skill 属使用方,可以并存。


十二、动手:三个原语的完整定义示例

以文件系统服务端为例,三个原语各自怎么写。

tools(会改变状态的动作)

{"name": "write_file",
 "description": "写入文件。写操作,会覆盖原有内容。不可逆。",
 "inputSchema": {
   "type": "object",
   "properties": {
     "path": {"type": "string", "description": "项目内相对路径,如 src/app.ts"},
     "content": {"type": "string", "description": "完整文件内容"}
   },
   "required": ["path", "content"]}}

注意 description 里的三个词:"写操作""会覆盖""不可逆" —— 这是给模型的风险信号(4.5)。

resources(只读数据)

{"uri": "file:///project/README.md",
 "name": "README",
 "mimeType": "text/markdown"}

关键差异:没有参数,客户端决定读哪个(8.3 第三节)。

prompts(预置模板)

{"name": "explain_file",
 "description": "解释一个文件的作用",
 "arguments": [{"name": "path", "required": true}]}

三者的选择速查

你想 用
让模型执行动作 tools
让客户端控制暴露哪些数据 resources
封装"最佳提问方式" prompts
读和写都要 拆成两个工具(副作用不同)

最后一行最容易做错:把读写合在一个工具里,就没法单独给写操作加审批(14.3)。


十三、动手:一个最简 MCP Server 的 60 行骨架

知道协议后,最快的学习方式是自己写一个。

import sys, json

TOOLS = {
    "read_file": {
        "description": "读取项目内文本文件。只读。",
        "inputSchema": {
            "type": "object",
            "properties": {"path": {"type": "string", "description": "相对路径"}},
            "required": ["path"],
        },
    }
}

def handle(req):
    m = req.get("method")
    if m == "server/discover":
        return {"protocolVersion": "2026-07-28",
                "capabilities": {"tools": {}},
                "serverInfo": {"name": "mini", "version": "0.1"}}
    if m == "tools/list":
        return {"tools": [{"name": k, **v} for k, v in TOOLS.items()]}
    if m == "tools/call":
        name = req["params"]["name"]
        args = req["params"]["arguments"]
        if name not in TOOLS:
            return {"error": {"code": -32601, "message": f"未知工具 {name}"}}
        try:
            text = open(args["path"], encoding="utf-8").read()[:20000]
            return {"content": [{"type": "text", "text": text}], "isError": False}
        except Exception as e:
            return {"content": [{"type": "text", "text": f"读取失败:{e}"}],
                    "isError": True}
    return {"error": {"code": -32601, "message": f"未知方法 {m}"}}

for line in sys.stdin:                      # 一行一个 JSON
    req = json.loads(line)
    result = handle(req)
    resp = {"jsonrpc": "2.0", "id": req.get("id"), **result}
    print(json.dumps(resp, ensure_ascii=False), flush=True)

这个 60 行里包含的四个协议要点

要点 代码
能力发现 server/discover 分支
工具列表 tools/list
业务错误 result.isError: true(不是 JSON-RPC error)
协议错误 error: {code, message}

第三、四行是新手最容易混的 —— 混用会导致模型看到一堆技术报错,或者客户端无法识别失败。

两个必须注意的实现细节

细节 原因
flush=True 不刷新缓冲,客户端会一直等
日志必须写到 stderr stdout 只能有协议消息

十四、三个原语的一个常见误解:resources 不能带参数

很多人想用 resources 做"带参数的查询":

错:resource: "db://users?name=张三"

问题是:resources 的语义是"可枚举的静态资源",带参数会让它变成"查询接口"——那就该是 tool。

判据表

你的需求 用
固定的一组资源(文件、记录、配置) resources
需要按条件查询 tools
查询结果集大小未知 tools(可加 limit 参数)
结果集固定且小 resources

关键区别:

resources:客户端知道有哪些,选择给哪些
tools:模型提出条件,由服务端执行查询

把带参数的查询塞进 resources,等于放弃了"客户端控制暴露范围"这个核心优势(8.3 第三节)。

一个折中

如果确实需要"按类别取资源",可以定义一组 resource 模板:

repo://{owner}/{name}/README.md
repo://{owner}/{name}/issues/{id}

但模板参数是"定位",不是"查询条件" —— 这个界限要守住。


十五、下一篇

数据层还有一个设计值得单独讲:能力协商与版本发现。

它看起来只是协议的"握手",但它是让生态能持续演进的前提——否则客户端和不同版本的服务端根本无法共存。

下一篇讲这个机制。

十二、一个补充:三个原语与权限模型的对应

原语 谁决定"用不用" 权限控制点
tools 模型 工具白名单 + 参数校验 + 审批
resources 客户端 资源 URI 前缀白名单
prompts 用户/客户端 一般无风险(只提供模板)

这张表的实际用途:设计权限时,按原语分别配置,不要一刀切。

一个常见的权限漏洞

错:只给 tools 配了白名单,resources 全放开
后果:服务端列出 file:///etc/,客户端全盘接受

resources 也必须做白名单 —— 按 URI 前缀限制(如只允许 repo://myorg/*)。

这也是 8.6 检查清单里"看 resources 列表"那一条的原因。

8.3 核心原语:tools、resources、prompts
https://clxhxhhr.top/posts/4904/
作者
clxstart
发布于
2026-10-25
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。