深入浅出 MCP 系列教程(二):三大核心原语实战

本系列共 5 篇。上一篇讲解了协议基础与架构设计,本篇深入 MCP 的三大核心原语:Resources(资源)、Tools(工具)、Prompts(提示模板),覆盖完整规范、字段、调用链路、校验规则与典型踩坑点。

一、为什么拆成三类,而不是全部打包成”函数”

MCP 刻意把 Agent 和外部交互拆成三类,这是区别于普通 function calling 的核心:

  • Resources:只读上下文源——把外部世界的信息送入模型,不产生副作用;支持 URI 寻址、变更订阅;
  • Tools:可执行动作——调用后可能修改外部状态;带输入 JSON Schema,高危动作需要 Host 侧人工审批;
  • Prompts:预编排任务模板——封装 system prompt、参数占位、子任务编排,提供可复用的任务入口。

一个 MCP Server 可以只实现其中任意一个或多个,在 initialize 阶段通过 capabilities 声明:

1
2
3
4
5
"capabilities": {
"tools": {},
"resources": { "subscribe": true },
"prompts": {}
}

只要能力没有在双方协商交集里存在,Host 就绝对不能发起对应系列的请求。

二、Resources(资源)—— 只读上下文

2.1 核心概念:Resource URI

Resources 的设计思想:任何一条可被模型消费的信息,都拥有一个全局可定位的 URI,类似浏览器的 URL,但不局限于 HTTP。

官方推荐的常见 scheme:

  • file://:本地文件
  • postgres://:数据库表/记录
  • git://:仓库提交、分支、diff
  • notion://:Notion 页面
  • slack://:会话消息

URI 的所有权属于 Server:只有暴露这个资源的 Server 知道如何解析它。Host 只把 URI 当成不透明字符串,不会自己去解析、访问。Host 拿着 URI 调用 resources/read,由 Server 去读取对应内容。

2.2 Resource 对象完整字段

字段 类型 是否必填 说明
uri string ✅ 资源唯一标识 URI
name string ✅ 人类可读名称,给用户/模型识别
description string ❌ 资源描述,推荐填写,辅助 LLM 判断要不要读取
mimeType string ❌ MIME 类型,如 text/markdown,方便 Host 做渲染、截断

2.3 完整调用链路

资源相关标准 method:

  1. resources/list:列出所有可访问资源
  2. resources/read:根据 URI 读取资源内容
  3. resources/subscribe:订阅某个 URI 的变更(需要 resources.subscribe: true)
  4. resources/unsubscribe:取消订阅
  5. notifications/resources/updated:服务端推送变更通知(无 id)

读取示例:

1
2
3
4
5
6
{
"jsonrpc": "2.0",
"id": "res-read-01",
"method": "resources/read",
"params": { "uri": "file:///project/README.md" }
}

返回的核心是 contents 数组,一条资源可以返回多个内容块:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"jsonrpc": "2.0",
"id": "res-read-01",
"result": {
"contents": [
{
"uri": "file:///project/README.md",
"mimeType": "text/markdown",
"text": "# Demo Project\nThis is a test project."
}
]
}
}

内容类型分两种:text(文本)、blob(base64 二进制,图片、PDF)。同一个内容块不能同时存在 text 和 blob。

订阅成功之后,文件发生修改时 Server 主动推送一条通知,不需要 Client 轮询:

1
2
3
4
5
{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": { "uri": "file:///project/README.md" }
}

Host 收到通知后,按需再次调用 resources/read 拉取最新版本。

2.4 约束与安全边界

  1. 只读语义约定:协议层面约定 Resources 不应产生副作用;这是语义约束,不是强制技术锁,靠实现遵守;
  2. Roots 边界:如果协商开启 roots,Server 必须拒绝根以外的 URI 读取;
  3. 超大内容处理:几十 MB 的文件直接返回会瞬间耗尽上下文窗口;通常策略是截断 + 摘要 + 分页;
  4. 不可信数据:资源返回的文本属于外部不可信输入,存在提示注入风险,Host 需要做隔离、标记边界;
  5. 订阅是会话级:会话断开订阅自动失效,重连后需要重新订阅。

2.5 典型踩坑

  • 不要把动态查询塞进工具里,能用资源读就优先资源;但高频变更资源(实时日志)持续推送会压垮通道,适合分页拉取而不是订阅;
  • URI 不要硬编码、不要让 LLM 自由构造不受控的 URI。

三、Tools(工具)—— 带副作用的动作

Tools 是大家最熟悉、面试最高频的原语。工具代表”执行动作”:增删改、发送消息、创建工单、执行 SQL、部署服务。

3.1 Tool 对象完整字段

字段 类型 必填 说明
name string ✅ 唯一名称,LLM 依靠这个名字调用
description string ✅ 给 LLM 看的自然语言描述,写得差模型就不会正确调用
inputSchema object ✅ JSON Schema,严格定义入参结构、类型、必填项

注意:没有 outputSchema。协议不强制约束返回结构,返回内容由业务决定。

示例 Tool 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "create_ticket",
"description": "创建 IT 工单,用于提交故障申请,需要标题和描述,可选优先级",
"inputSchema": {
"type": "object",
"properties": {
"title": { "type": "string", "description": "工单标题" },
"content": { "type": "string", "description": "故障详细描述" },
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"default": "medium"
}
},
"required": ["title", "content"]
}
}

3.2 调用链路

标准 method:tools/list(列出工具)、tools/call(执行工具)、notifications/tools/list_changed(工具列表动态变更推送)。

调用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"jsonrpc": "2.0",
"id": "tool-call-01",
"method": "tools/call",
"params": {
"name": "create_ticket",
"arguments": {
"title": "服务无法启动",
"content": "后端服务启动时报端口占用",
"priority": "high"
}
}
}

返回的 content 数组支持多种类型:text、image、resource(内嵌一条资源)。失败返回走标准 JSON-RPC error。

3.3 工具审批流(非常关键)

MCP 没有内置”审批弹窗”报文,但规范定义了协作模式:

  1. Server 收到 tools/call,识别这是高危操作(删除、写库、发邮件);
  2. Server 不能直接弹出窗口,它只能正常返回结果或通过语义提示;
  3. 真正的审批权在 Host:Host 在发起 tools/call 之前,根据工具白名单、工具名称、参数内容决定是否展示确认窗口;
  4. 用户拒绝 → Host 直接不发起调用;用户同意 → 才发送 tools/call。

很多新手误区:以为 Server 可以通知 Host”请你弹确认框”。协议没有这个指令;审批策略是 Host 的业务安全层。

3.4 设计规范与最佳实践

  1. 工具粒度不要过大:拆成原子操作(创建工单、关闭工单、添加备注),方便审批、审计、权限控制;
  2. description 面向 LLM 写:写清楚什么时候适合调用、参数含义、限制条件;它不是给人类看的文档;
  3. 入参用 JSON Schema 严格约束:枚举、长度、正则,减少模型传错参数;
  4. 幂等优先:写类工具尽量设计成幂等,避免网络重试造成重复创建、重复发送;
  5. 审计埋点:所有 tools/call 都应记录调用时间、工具名、入参、返回、用户身份、是否人工确认。

3.5 Tools vs Resources 判定口诀

  • 只拿信息、查询、读取,不改变外部状态 → Resources(优先)
  • 新增、修改、删除、发送、触发任务 → Tools
  • 查询有复杂过滤、聚合、计算,参数动态、不方便映射成静态 URI → 放到 Tool

举例:读取单条工单详情用 resources/read;按关键词、时间、状态批量搜索工单用 search_ticket tool;创建工单用 tool;读取固定配置文件用 resource。

四、Prompts(提示模板)—— 预编排任务入口

Prompts 是最容易被忽略,但在企业落地里价值很高的原语。它不是发给 LLM 的一次性 prompt,而是可被 Host 拉取的任务模板,带参数占位。

它解决的问题:很多固定工作流(代码评审、周报生成、故障复盘、安全扫描)的 system prompt 很长、编排复杂,不适合每次让 LLM 自己从头生成。把模板托管在 MCP Server,所有接入的 Agent 都可以复用。

4.1 Prompt 对象字段

字段 类型 必填 说明
name string ✅ 模板唯一名称
description string ❌ 模板用途描述
arguments array ❌ 参数定义数组(name / description / required)

4.2 调用链路

标准 method:prompts/list、prompts/get。

1
2
3
4
5
6
7
8
9
10
11
12
{
"jsonrpc": "2.0",
"id": "prompt-get-01",
"method": "prompts/get",
"params": {
"name": "code_review",
"arguments": {
"code": "const x = eval(userInput);",
"language": "javascript"
}
}
}

返回结果里的 messages 是一组标准化对话消息(system / user / assistant),Host 可以直接送入 LLM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"jsonrpc": "2.0",
"id": "prompt-get-01",
"result": {
"description": "代码评审模板",
"messages": [
{
"role": "system",
"content": {
"type": "text",
"text": "你是安全代码评审专家,重点检查注入、权限、资源泄露。"
}
},
{
"role": "user",
"content": {
"type": "text",
"text": "评审下面 JS 代码:const x = eval(userInput);"
}
}
]
}
}

4.3 特点和边界

  1. 模板托管在 Server:更新评审模板只改 MCP Server,所有接入的 Host 自动拿到新版本;
  2. 返回的是消息序列,不是单纯字符串:支持多轮预设对话;
  3. Prompt 本身不执行推理:prompts/get 只是把模板渲染出来;真正调用 LLM 是 Host 的工作;
  4. 可以嵌入资源引用:模板内部可引用 Server 的 Resources,拉取参考文档作为上下文;
  5. 适合标准化团队规范:企业把审批、巡检、复盘模板收敛在内部 MCP Server。

4.4 什么时候不要用 Prompt 原语

  • 一次性、临时、用户随口输入的指令;
  • 动态性极强、每次结构完全不一样的任务;
  • 模板只是一行简短指令,收益很低。

五、原语组合实战:一个完整业务流程

场景:Agent 做工单复盘。

  1. Host 和工单 MCP Server 完成握手,协商:tools、resources、prompts、subscribe;
  2. Host 调用 prompts/list,找到 incident_summary 复盘模板;
  3. Host 调用 prompts/get,传入工单 id,拿到预定义 system prompt;
  4. Host 调用 tools/call,执行 search_ticket,拿到工单列表;
  5. Host 通过 resources/read,读取指定工单详情资源;
  6. 如果开启订阅,后续工单更新会收到 resources/updated 通知;
  7. Host 把模板 + 工单上下文一起送入 LLM,生成复盘报告;
  8. 需要保存复盘结果时,调用 create_note tool 写回工单。

这就是 MCP 区别于普通 function calling 的核心:读上下文、执行动作、任务模板三者有独立标准,可以自由编排组合。

六、校验规则与错误模型

除了标准 JSON-RPC 错误码,三大原语有固定的业务错误场景:

  • 调用 tools/call,但 Server capabilities 没有 tools → 非法请求;
  • inputSchema 校验失败(参数缺失、类型不对、枚举值非法)→ 参数错误;
  • resources/read 访问不在 roots 范围内的 URI → 拒绝;
  • prompts/get 缺失必填 argument → 返回错误;
  • 订阅一个不支持 subscribe 的资源 → 返回错误。

常见 JSON-RPC 标准错误码:

code 含义
-32601 Method not found:调用了不存在的方法
-32602 Invalid params:参数不符合 schema
-32600 Invalid Request:消息格式非法

业务自定义错误码建议从 -32000 开始分配,避免和 JSON-RPC 预留冲突。

七、第二篇小结与高频误区

核心复盘

  1. Resources:URI 寻址的只读上下文,支持订阅推送;Host 不解析 URI;
  2. Tools:带副作用动作,附带 JSON Schema;审批策略由 Host 控制;
  3. Prompts:可拉取、带参数的任务模板;模板渲染和模型推理分离;
  4. 三者是独立可插拔能力,靠握手协商开启。

高频误区

误区 正解
所有查询都做成 Tool 优先用 Resources 承载静态、可寻址的只读数据
Server 主动弹出确认窗口 高危操作确认权在 Host
Prompt 原语会直接调用模型 它只返回渲染后的消息,Host 负责送模型
订阅之后自动拿到新内容 订阅只发变更通知,读内容仍需手动 resources/read
inputSchema 只是文档 Server 有义务按 schema 校验入参,不能直接透传给业务

下一篇:《深入浅出 MCP 系列教程(三):传输层实战》,将详解 stdio 与 Streamable HTTP 两种传输的完整握手流程、鉴权与会话管理。