想象一位从未进过这间厨房的新主厨。推开门,眼前是一间规划齐整的后厨:左手是食材储藏间,每份食材都贴着唯一的储位号;正面墙上挂满了厨具,每把都贴着操作说明;抽屉里码着一沓菜谱卡,标注着适合的场景。主厨要做的第一件事,不是闷头开火,而是先搞清楚“这里有什么、每样东西怎么用、哪些能组合”。这间后厨的秩序,恰恰就是 MCP 协议在数据层做的事。 AI Agent 面对一个 MCP Server 时,处境和这位主厨一模一样。MCP 协议把服务端的能力切成三种原语:Resources(数据源)是储藏间里的食材,只能被取用而不能被改变;Tools(可执行操作)是墙上的厨具,一经调用就产生真实动作;Prompts(提示模板)是菜谱卡,把常见任务的套路固化成可复用的模板。三类东西各归其位,Agent 才能按需取用、自由组合,最终端出一道完整的“菜”。 这篇文章就带你逛一遍这间后厨:先认三类原语的职责边界,再看数据格式规范(JSON Schema)与暴露机制(list/read),接着聊 Agent 如何按需组合,最后讲讲设计 MCP Server 时怎么避免“菜刀插进米缸”的混乱。

岗位关联

对 AI 从业者而言,理解 MCP 数据层是设计生产级 Agent 服务端的基本功——它决定了模型能否稳定、安全、高效地接入真实业务的数据与操作,是 Agent 架构师绕不开的必修课。

目录
  1. 可视化演示
  2. 三大原语概览
  3. Resources数据源
  4. Tools工具设计
  5. Prompts模板设计
  6. 数据格式规范
  7. 暴露方式机制
  8. Agent组合策略
  9. 设计避坑指南
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态演示MCP数据层的核心机制,通过总览/传输/转换/输出四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

MCP数据层:Reso Resources与Tools JSON Schema数据格式 核心机制可视化 MCP数据层整体架构与关键组件

三大原语概览

一位新主厨第一次走进后厨,最怕的不是锅铲不顺手,而是“不知道东西在哪”。还好这间后厨有规矩:左手储藏间只放食材,墙上只挂厨具,抽屉里只摆菜谱卡。他环顾一圈,心里便有了底——做任何菜,无非是“取食材、动厨具、照菜谱”三件事的组合。

MCP 协议在设计数据层时做了完全相同的抽象。它把服务端能提供给 Agent 的能力,整齐地切成三种原语(Primitive):Resources、Tools、Prompts。所谓原语,就是最基础、不可再分的能力单元。一个 MCP Server 无论业务多复杂,它的能力都可以用这三类原语完整描述,不多不少。

从实现角度看,三类原语各管一摊:Resources 是数据源,负责“被读取”,例如数据库记录、文件内容、外部 API 返回的快照;Tools 是可执行操作,负责“被调用”,例如查余额、发通知、修改订单状态;Prompts 是模板,负责“被复用”,把高频任务的执行套路固化成可填参的文本模板。Agent 眼中看到的服务端全景,就是这三类原语的并集。

理解了这一点,再看任何 MCP Server 都不会慌:主厨心里有分类,出菜才快;Agent 心里有分类,任务才不乱。接下来要做的,就是把每一类原语的细节逐个摸清。

C=R∪T∪P\displaystyle C = R \cup T \cup P
公式含义:Agent 通过 MCP 获得的完整能力空间 C,等于 Resources(数据源集合)、Tools(可执行操作集合)与 Prompts(模板集合)三者的并集。三者各司其职、互不重叠。
  1. R 集合(Resources)表示 MCP Server 上所有可读数据源的集合,包括数据库记录、文档内容等“可以被取用的食材”:
    R={ρ1,ρ2,…,ρn}R = \{ \rho_1, \rho_2, \dots, \rho_n \}
  2. T 集合(Tools)表示所有可执行操作的集合,每个元素都是一个带输入输出约束的函数:
    T={τ1,τ2,…,τm}T = \{ \tau_1, \tau_2, \dots, \tau_m \}
  3. P 集合(Prompts)表示所有提示模板的集合,每个元素是一份可填入参数的消息模板:
    P={π1,π2,…,πk}P = \{ \pi_1, \pi_2, \dots, \pi_k \}
  4. 并集 \cup把三类集合合成一个整体,形成 Agent 可发现、可调用、可复用的全部能力:
    C=R∪T∪PC = R \cup T \cup P

Resources数据源

储藏间管理的核心,是“货架标签”。每一份食材都有一个唯一的储位号:db://orders/2024 是指向订单库的坐标,file://reports/q3.pdf 是指向报告文件的坐标。主厨不关心食材是怎么进货的,他只需要知道“按这个号码就能拿到那样东西”。

在 MCP 中,Resources 就是这个储藏间:一种只读的数据源抽象。设计 Resources 要回答三个问题:数据放哪(URI)、数据长什么样(MIME 与结构)、怎么让 Agent 知道它存在(暴露)。URI 是资源的唯一地址,协议约定它必须能稳定定位同一份内容;mimeType 声明内容格式,告诉 Agent 该用 JSON 解析还是当作纯文本。

技术实现上,Server 通过 list_resources 返回资源清单(URI + 名称 + 描述 + MIME),通过 read_resource 接受 URI 并返回具体内容。数据量大的资源还可以分页读取。需要强调的是:Resources 不含任何“动作”。主厨打开储位号只能取出食材,不能顺手把食材加工成菜——那是厨具的事。

一间好储藏间的标准是“标签清晰、取用顺手”。同样,设计 Resources 时,给每个 URI 起一个可读的名字、写一句人能看懂的描述,比堆砌字段重要得多。Agent 是语言模型,它读清单时靠的就是这些描述来“猜”该取哪份食材。

Resource=(uri,  mimeType,  description)↦content\displaystyle Resource = (uri, \; mimeType, \; description) \mapsto content
公式含义:一个资源由 URI 地址、MIME 类型和描述信息共同定位,最终映射到一份具体的数据内容。URI 是钥匙,MIME 是标签,description 是给 Agent 看的“人话”。
  1. uri 资源坐标资源的唯一地址,相当于储藏间的储位号,Agent 凭它取货:
    uri∈Uuri \in U
  2. mimeType 类型标签声明内容的媒体类型(如 application/json),决定 Agent 如何解析这份数据:
    mimeType∈{application/json,  text/plain,  … }mimeType \in \{ \text{application/json},\; \text{text/plain},\; \dots \}
  3. description 人话说明用一句自然语言描述资源用途,帮助 LLM 在清单里做出选择:
    description=自然语言描述\text{description} = \text{自然语言描述}
  4. 映射到 content三元组(uri、mimeType、description)共同决定最终读到的具体内容:
    Resource(uri)=contentResource(uri) = content

Tools工具设计

工具墙上的每把厨具都贴着一张说明书:名字、用途、怎么用、用了之后会发生什么。主厨拿起“高温喷枪”,知道它需要接燃气(输入),知道它能烤焦糖(输出),也知道它有烫伤风险(副作用)。知道这三件事,他才敢放心使用。

Tools 就是 MCP 里的“厨具说明书 + 厨具本身”。它是对可执行函数的封装,元数据包含四要素:name(动词开头的工具名)、description(用途说明)、inputSchema(输入参数的 JSON Schema)、outputSchema(返回结构的 JSON Schema)。Agent 看到这份说明书,就知道“这个工具解决什么问题、我该传什么参数”。

与 Resources 的“只读”不同,Tools 天然带有副作用的可能:写入数据库、发送通知、修改状态。因此设计 Tools 时要显式声明副作用,让 Agent 在调用前有能力评估风险。比如 query_balance 是无副作用的查询,send_notice 会真实发出消息——描述里写清楚,模型才能做负责任的选择。

工具命名也讲究:统一用动词开头,query_balance、update_address、restart_service,风格一致的工具清单能让 Agent 的“模式匹配”更快。想象一下,如果储藏间的储位号一会儿是数字一会儿是汉字,主厨找东西就得挨个翻——命名规范,本质上是降低 Agent 的选择成本。

Tool=(name,  inputSchema,  outputSchema)↦f\displaystyle Tool = (name, \; inputSchema, \; outputSchema) \mapsto f
公式含义:一个工具由名字、输入约束、输出约束三部分定义,它们共同指向一个实际执行函数 f。元数据让 Agent 理解工具,函数体让工具真正干活。
  1. name 工具名动词短语命名,如 query_balance,一眼看出工具干什么:
    name∈动词短语\text{name} \in \text{动词短语}
  2. inputSchema 输入契约用 JSON Schema 声明参数结构,标明字段类型与必填项:
    inputSchema={field:{type,  required}}\text{inputSchema} = \{ \text{field} : \{ \text{type},\; \text{required} \} \}
  3. outputSchema 输出契约声明返回数据的结构,让 Agent 知道调用后能拿到什么:
    outputSchema={field:type}\text{outputSchema} = \{ \text{field} : \text{type} \}
  4. f 执行体被调用的实际函数,负责完成真实业务动作:
    f:inputSchema→outputSchemaf: \text{inputSchema} \rightarrow \text{outputSchema}

Prompts模板设计

菜谱卡是个神奇的东西:它不是食材(不能直接吃),也不是厨具(不能切菜),但主厨离不开它。一张“蒜蓉西兰花”的菜谱,把“焯水→炝锅→翻炒→调味”的套路写清楚,换掉主料就变成蒜蓉菠菜。菜谱的本质是可复用的套路。

Prompts 在 MCP 中扮演的正是菜谱卡的角色:一套可复用、可填参的提示模板。它由两部分组成:arguments(参数声明,如 {task}、{problem})和 messages(消息骨架,包含角色与带占位符的内容模板)。Agent 调用 get_prompt 拿到模板,填入实际参数,就得到一份高质量的任务指令或回复草稿。

Prompts 与 Tools 的根本区别在于:Tools 执行动作,Prompts 只产出文本。weekly_report 模板不会替你写周报,它只是把“写周报的正确姿势”固定下来。因此 Prompts 尤其适合高频、有固定结构的任务:客服回复、周报生成、代码审查清单、故障上报格式。它让团队的最佳实践沉淀为模板,而不是每次靠模型自由发挥。

值得注意的是,Prompts 常常与 Tools 配合:Agent 先 read 数据、call 工具拿到结果,再套用模板把结果组织成规范输出。菜谱提供了装盘的角度,但菜本身还是要靠食材和厨具做出来——模板负责“表达”,不负责“生产”。

Prompt=(arguments,  messages)↦composedPrompt\displaystyle Prompt = (arguments, \; messages) \mapsto \text{composedPrompt}
公式含义:一个提示模板由参数表和消息骨架构成,填入参数后合成一份完整的、可直接发送给 LLM 的消息。
  1. arguments 参数表声明模板中的占位参数,如 {task}、{question}:
    arguments={a1,a2,… }\text{arguments} = \{ a_1, a_2, \dots \}
  2. messages 消息骨架定义角色(system/user)与带占位符的内容模板:
    messages={(role,  contenttemplate)}\text{messages} = \{ (\text{role},\; \text{content}_{\text{template}}) \}
  3. 参数填充将实际值替换进占位符,得到具体内容:
    content=replace(contenttemplate,  ai↦vi)\text{content} = \text{replace}(\text{content}_{\text{template}},\; a_i \mapsto v_i)
  4. 合成结果填充完成后的消息集合就是可直接使用的完整提示:
    Prompt(v1,v2,… )=composedPrompt\text{Prompt}(v_1, v_2, \dots) = \text{composedPrompt}

数据格式规范

如果储藏间里每份食材的标签格式都不一样——有的写日期、有的写"fresh"、有的干脆不贴——主厨根本没法判断能不能用。标签的价值在于统一。在 MCP 世界里,这个统一标签就是 JSON Schema。

JSON Schema 是描述数据结构的标准格式,它声明字段名称、类型、是否必填、取值范围、默认值等信息。Tools 的 inputSchema、Resources 的内容结构、Prompts 的参数定义,都可以用 JSON Schema 来表达。有了它,参数校验就是一次确定性的运算,而不是靠“猜”。

校验流程在 Server 端执行:Agent 发起调用请求 → Server 按 Schema 逐字段校验 → 校验通过才进入业务逻辑,失败则立刻返回错误。错误返回也要遵循统一协议:isError: true 加上结构化错误信息,让 Agent 能读懂“哪里错了、怎么改”。想象后厨的食材变质会直接丢弃而不是硬炒——快速失败,是对 Agent 最大的保护。

Schema 里还要写好描述字段。LLM 不是普通程序,它通过阅读描述来理解“这个参数该填什么”。一个字段的 description 写得清晰,模型第一次就能填对;写得含糊,模型就开始瞎猜。好的 Schema 既是约束,也是说明书。

validate(x,S)=true  ⟺  x∈D(S)\displaystyle validate(x, S) = true \iff x \in D(S)
公式含义:参数校验函数 validate 对输入 x 与约束定义 S 进行判定,当且仅当 x 属于 S 所定义的合法值域 D(S) 时,校验通过。
  1. x 输入值Agent 调用工具时传入的参数对象,如 {"user_id": 1}:
    x={field:value}x = \{ \text{field} : \text{value} \}
  2. S 约束定义JSON Schema 描述的规则集合,包含类型、必填项、属性约束:
    S={type,  required,  properties}S = \{ \text{type},\; \text{required},\; \text{properties} \}
  3. D(S) 合法值域所有满足 Schema 约束的取值构成合法值域:
    D(S)={x∣x 满足 S 的全部约束}D(S) = \{ x \mid x \text{ 满足 } S \text{ 的全部约束} \}
  4. validate 判定校验通过等价于参数落在合法值域内:
    validate(x,S)=true  ⟺  x∈D(S)\text{validate}(x, S) = true \iff x \in D(S)

暴露方式机制

主厨不需要搬空储藏间才知道有哪些食材。他先看一眼门上的清单——“土豆 × 3、牛排 × 2、西兰花 × 1”——然后决定取哪一份。清单和取货是两道工序,先看单、再动手,省时又省力。

MCP 的暴露机制正是这个思路:list 与 read(或 call)分离。list_resources 返回轻量目录,只含 URI、名称、描述等元数据,不携带实际内容;read_resource 拿到具体 URI 后才返回完整数据。Tools 同理:list_tools 返回工具清单和参数 Schema,call_tool 才真正执行。Prompts 则是 list_prompts 与 get_prompt。

为什么非要拆成两步?首先是效率:把全量数据直接塞给模型,上下文会爆炸,而清单通常只有几百字节。其次是自主性:Agent 可以在运行时动态发现能力,而不是把工具列表硬编码在系统提示词里。Server 新增一个工具,Agent 下次 list 就能看到,无需重新部署——这就是动态发现的价值。

这种机制还天然支持权限控制:list 阶段给 Agent 看“有哪些能力”,read/call 阶段校验“这次操作是否被允许”。主厨能看清单,不代表能取走保险柜里的东西。两段式的暴露,让安全策略有了清晰的挂载点。

discover(S)=list(S)∪⋃r∈list(S)read(r)\displaystyle discover(S) = list(S) \cup \bigcup_{r \in list(S)} read(r)
公式含义:Agent 对一个服务 S 的完整发现过程,等于先获得轻量清单,再对清单中的每一项逐个读取详细内容,两者并集构成全部可知信息。
  1. list(S) 清单发现返回服务 S 的轻量目录,只含元信息:
    list(S)={(uri,  desc)}\text{list}(S) = \{ (\text{uri},\; \text{desc}) \}
  2. read(r) 内容读取对清单中的某个条目 r 发起读取,得到具体内容:
    read(r)=content(r)\text{read}(r) = \text{content}(r)
  3. 对全部清单项求和对清单中每个条目逐一执行读取,得到所有内容的并集:
    ⋃r∈list(S)read(r)\bigcup_{r \in list(S)} \text{read}(r)
  4. discover 总发现清单与内容相并,便是 Agent 对服务的完整认知:
    discover(S)=list(S)∪⋃r∈list(S)read(r)\text{discover}(S) = \text{list}(S) \cup \bigcup_{r \in list(S)} \text{read}(r)

Agent组合策略

今晚的主厨要做一道黑椒牛排。他先去储藏间取出牛排(read),再用平底锅煎制(call),最后对照摆盘手册装盘(fill)。三步缺一不可,顺序也不能乱。做菜如此,Agent 处理任务也一样——读数据 → 执行动作 → 套模板是最高频的组合链路。

Agent 组合策略的第一步是按需选择:根据用户任务判断需要哪类原语。查天气只需要 call 一个工具;生成周报则先 read 工作数据,再套用周报模板。选择的原则是“能读不写、能模板不自由发挥、能分工不包办”。数据取用走 Resources,动作执行走 Tools,表达组织走 Prompts。

第二步是多步编排。一个复杂任务往往需要串起多个原语:先 list_resources 发现订单表 → read_resource 读取用户订单 → call_tool 调用 check_overdue 判断状态 → fill 套用催单模板生成话术。前一步的输出作为后一步的输入,这就是上下文的流转与注入。

第三步是优先级判断。Agent 应该优先尝试显式的原语而非让模型凭空推理。Resoning(模型自己思考)成本低但不可靠,原语调用有确定语义但成本高。靠谱的 Agent 会先问:“这个信息服务端有没有?”——有,就 read;没有,才考虑让模型发挥。

Action=read(R∗)→call(T∗)→fill(P∗)\displaystyle Action = read(R^*) \rightarrow call(T^*) \rightarrow fill(P^*)
公式含义:一个完整的 Agent 动作链,是从资源集合中选取子集读取数据,再到工具集合中选取子集执行操作,最终套用模板组织输出。
  1. read(R\*) 数据获取从 Resources 中选出需要的资源子集并读取数据:
    R∗⊆R,data=read(R∗)R^* \subseteq R, \quad \text{data} = read(R^*)
  2. call(T\*) 动作执行基于已读数据,从 Tools 中选出合适的工具执行:
    T∗⊆T,result=call(T∗,data)T^* \subseteq T, \quad \text{result} = call(T^*, \text{data})
  3. fill(P\*) 模板套用把执行结果填入 Prompts 模板,生成结构化输出:
    P∗⊆P,answer=fill(P∗,result)P^* \subseteq P, \quad \text{answer} = fill(P^*, \text{result})
  4. 串联执行三个阶段按序编排,前一步输出即后一步输入:
    Action=read→call→fillAction = read \rightarrow call \rightarrow fill

设计避坑指南

厨房的混乱,往往源于“东西放错地方”:菜刀插在米缸里,菜谱被垫了锅底,冰箱里存着没点着的煤气灶。每样东西只要离开自己的位置,整个流程就卡壳。MCP Server 设计的第一原则,就是别让三类原语互相越界。

最常见的越界是把数据塞进 Tools。比如把“查询订单”做成一个工具,表面上能用,但数据被锁死在工具内部:另一个工具想复用这批数据就只能重复调用,无法直接 read。反过来,把动作做成 Resources 更危险——Agent read 一个资源后以为“事情办完了”,实际上没产生任何效果。判断标准很简单:被取用的进 Resources,被执行的进 Tools,被复用的进 Prompts。

第二个坑是过度暴露。把内部函数全部注册成 Tool,Agent 的选择空间变大,但选错的概率和安全隐患也同步上升。一个订单服务没必要暴露“直接改数据库字段”的工具,暴露 update_order_status(带权限校验)就够了。最小权限原则在 MCP 世界里同样成立:能不开的门就不开。

此外还有命名与安全。Resources 用名词 URI(db://orders),Tools 用动词(query_balance),风格统一才能让模型快速理解。而对敏感数据,read 要有权限校验,Tools 要有操作审计,Prompts 要防止模板注入——食材再新鲜,食品安全出了问题,整间厨房都得关门。

边界清晰  ⟺  (R∩T=∅)∧(T∩P=∅)∧(R∩P=∅)\displaystyle \text{边界清晰} \iff (R \cap T = \emptyset) \land (T \cap P = \emptyset) \land (R \cap P = \emptyset)
公式含义:设计良好的 MCP Server,三类原语的集合两两交集为空——没有一样东西同时属于两类。边界互不越界,职责自然清晰。
  1. 交集为空之义任意两类原语之间没有共享成员,即每个能力条目只属于一个类别:
    R∩T=∅R \cap T = \emptyset
  2. 逻辑与联结三个“两两不相交”的条件必须同时成立,缺一不可:
    A∧B∧CA \land B \land C
  3. 边界清晰判定当数据、动作、模板三者互不越界时,服务端职责即达到清晰状态:
    边界清晰  ⟺  (R∩T=∅)∧(T∩P=∅)∧(R∩P=∅)\text{边界清晰} \iff (R \cap T = \emptyset) \land (T \cap P = \emptyset) \land (R \cap P = \emptyset)

AI 应用场景

智能客服工单 Agent

阶段:Agent 编排与工具调用

解决的问题:传统客服需要人工查库、翻话术手册,响应慢且口径不一。通过 MCP 连接客服 Server,Resources 暴露用户档案与订单库,Tools 提供改地址、退差价等真实操作,Prompts 内置标准话术模板,Agent 端到端完成“查—判—答—办”全流程。

具体案例:用户问“我的订单 1234 什么时候到?”Agent 先 list_resources 发现订单库,read_resource 读取该订单状态,再套用 Prompts 的物流话术模板生成回复;若用户要求修改收货地址,则 call_tool 调用 update_address 完成变更并确认。全程无需人工介入,且动作类操作全部有权限校验和审计记录。

企业数据分析 Agent

阶段:数据接入与查询编排

解决的问题:分析师取数要写 SQL、等排期、手工核对口径,效率极低。MCP 数据层把表结构元数据放入 Resources(表名、字段、业务含义),把“执行只读查询”“导出报表”封装成 Tools,把“图表生成规范”做成 Prompts,业务人员用自然语言即可自助取数。

具体案例:销售总监问“华南区本月销售额”。Agent read 表结构资源确认字段语义,call 查询工具执行聚合 SQL,再套用报表模板生成结论和图表。所有查询只读、走统一权限网关,杜绝了业务人员直连生产库的裸 SQL 风险。

AIOps 运维 Agent

阶段:可观测与自动化操作

解决的问题:故障处置依赖运维专家经验,半夜告警往往人手不足。MCP 把监控指标放入 Resources,把重启、扩容、回滚等操作封装成 Tools(带审批流),把“故障排查手册”做成 Prompts,让 Agent 按预案自动处置常见故障。

具体案例:午夜告警“订单服务内存飙升”。Agent 先 read 监控指标定位进程,套用排查模板生成诊断步骤,确认是内存泄漏后 call_tool 执行灰度重启,并自动向值班群推送处置记录。预案在手,动作可控——这正是负责任地使用自动化。

Python 演示

python94 行
# -*- coding: utf-8 -*-"""mini_mcp.py - 模拟 MCP 数据层:Resources / Tools / Prompts 三类原语的登记与调用。""" import jsonimport re  class MiniMCPServer:    """迷你 MCP 服务端:像一间后厨,分门别类管理食材、厨具和菜谱。"""     def __init__(self):        self.resource_map = {}   # uri -> {"desc": "...", "content": ...}        self.tool_map = {}       # name -> {"desc": "...", "schema": ..., "fn": ...}        self.prompt_map = {}     # name -> {"desc": "...", "template": "..."}     # ---------- Resources:食材储藏间 ----------    def add_resource(self, uri, desc, content):        self.resource_map[uri] = {"desc": desc, "content": content}     def list_resources(self):        return [{"uri": u, "desc": v["desc"]} for u, v in self.resource_map.items()]     def read_resource(self, uri):        if uri not in self.resource_map:            raise ValueError("Resource not found: %s" % uri)        return self.resource_map[uri]["content"]     # ---------- Tools:工具墙 ----------    def add_tool(self, name, desc, schema, fn):        self.tool_map[name] = {"desc": desc, "schema": schema, "fn": fn}     def list_tools(self):        return [{"name": n, "desc": v["desc"]} for n, v in self.tool_map.items()]     def call_tool(self, name, **args):        if name not in self.tool_map:            raise ValueError("Tool not found: %s" % name)        tool = self.tool_map[name]        for field, spec in tool["schema"].items():            if spec.get("required") and args.get(field) is None:                raise ValueError("missing arg: %s" % field)        return tool["fn"](**args)     # ---------- Prompts:菜谱盒 ----------    def add_prompt(self, name, desc, template):        self.prompt_map[name] = {"desc": desc, "template": template}     def list_prompts(self):        return [{"name": n, "desc": v["desc"]} for n, v in self.prompt_map.items()]     def read_prompt(self, name, **kwargs):        if name not in self.prompt_map:            raise ValueError("Prompt not found: %s" % name)        tpl = self.prompt_map[name]["template"]        return re.sub(r"\{(\w+)\}", lambda m: str(kwargs.get(m.group(1), m.group(0))), tpl)  def main():    server = MiniMCPServer()     # 1. 登记 Resources:把数据放进储藏间    server.add_resource("db://orders", "最新订单表", [{"id": 1, "amount": 99.5}])    server.add_resource("db://users", "用户档案库", [{"id": 1, "name": "Alice"}])     # 2. 登记 Tools:把操作挂上工具墙    server.add_tool("query_balance", "查询账户余额",                    {"user_id": {"required": True}},                    lambda user_id: {"user_id": user_id, "balance": 88.0})    server.add_tool("send_notice", "发送通知",                    {"target": {"required": True}, "content": {"required": True}},                    lambda target, content: "sent -> %s: %s" % (target, content))     # 3. 登记 Prompts:把常用套路收进菜谱盒    server.add_prompt("weekly_report", "周报模板", "本周完成:{task};遇到的问题:{problem}")    server.add_prompt("customer_service", "客服话术模板", "亲,您的问题我们已经收到:{question}")     # 4. Agent 先"翻清单"再动手    print("== 发现阶段 ==")    print(json.dumps(server.list_resources(), ensure_ascii=False))    print(json.dumps(server.list_tools(), ensure_ascii=False))    print(json.dumps(server.list_prompts(), ensure_ascii=False))     # 5. Agent 组合使用:读数据 + 调工具 + 套模板    print("== 组合阶段 ==")    orders = server.read_resource("db://orders")    balance = server.call_tool("query_balance", user_id=1)    reply = server.read_prompt("customer_service", question="我的余额是多少?")    print(orders)    print(balance)    print(reply)  if __name__ == "__main__":    main()
目的:用不到 80 行实现一个迷你 MCP Server,演示三类原语的登记、发现(list)与组合使用(read → call → fill),对应正文中“主厨的后厨”完整流程。
  1. self.resource_map = {}用字典模拟储藏间,键是 URI 储位号,值是数据内容。
  2. def list_resources(self)翻清单操作,只返回 uri 和描述,不返回具体数据。
  3. def read_resource(self, uri)按储位号取货,URI 不存在时抛错,对应快速失败原则。
  4. def add_tool(self, name, desc, schema, fn)挂上工具墙:名字、说明、参数契约、执行函数四件套。
  5. for field, spec in tool["schema"].items()调用前按 Schema 校验必填参数,对应 JSON Schema 契约思想。
  6. def read_prompt(self, name, **kwargs)读取模板并用 re.sub 把 {question} 这类占位符替换成实际值。
  7. print(json.dumps(server.list_resources(), ...))Agent 的“发现阶段”输出,先看清单再动手。

常见误区

误区很多初学者觉得 Tools 最“万能”,把数据、动作、模板一股脑全注册成工具,例如把“查订单”和“订单数据”都做成 Tool,认为这样 Agent 什么都能干。

正解Tools 应该只承载可执行的动作。数据放进 Tools 会失去复用性——别的工具想读取同一份数据时,只能重复调用,无法直接 read。正确的划分是:数据进 Resources、动作进 Tools、套路进 Prompts。就像把食材全塞进烤箱,主厨想生吃沙拉时就没东西可用了。

为何常见:因为 Tools 在概念上最贴近“函数调用”,而函数在传统编程里什么都能干,容易让人忽略 MCP 对三类原语的职责切分。

误区看到"Prompts"这个词,就以为是用户输入框里的提示语,或者是写进系统提示词的 system prompt,觉得把它暴露给用户就行。

正解MCP 里的 Prompts 是服务端预置的任务模板,服务对象是 Agent 而非最终用户。它是“菜谱”而不是“菜单”:主厨(Agent)取走菜谱卡,填入参数,照着套路执行;食客(用户)只看到最后端上来的菜。模板的价值在于沉淀最佳实践,让 Agent 的高频任务不走样。

为何常见:"Prompt"一词在大模型领域太常用了,提示工程的概念先入为主,让人忽略了 MCP 场景下它是服务端复用的结构化资源。

误区有人觉得工具参数用自然语言描述清楚就够了,写 JSON Schema 是多余的负担,“反正 LLM 很聪明,能理解我的话”。

正解JSON Schema 是机器可校验的结构化契约。没有它,参数校验就只能靠运气,类型错误在运行时才暴露,而且每个客户端(不同模型、不同版本)猜的结果都不一样。就像食材标签缺保质期,主厨只能靠鼻子闻——闻错了就要出食品安全事故。Schema 让“能传什么、必须传什么”成为确定性的规则。

为何常见:LLM 对话接口的成功让很多人觉得“描述清楚就行”,忽视了生产系统对确定性校验的刚需。

误区倾向于把内部函数全部注册成 Tool,觉得能力越全,Agent 发挥空间越大,多暴露几个“备用工具”总没坏处。

正解过度暴露会带来三重风险:安全面扩大(内部操作可能被误调用)、选择混乱(工具太多导致模型选错)、上下文膨胀(工具清单占满上下文窗口)。正确做法是最小权限暴露,只开放业务允许的操作,并配上描述和权限校验。后厨的灭火器不会开放给顾客把玩,工具墙只挂今天用得上的厨具。

为何常见:功能枚举的思维惯性——把“提供更多选项”等同于“更强的能力”,而没有考虑 Agent 在大量工具中选错的概率会随数量上升。

面试题

Q1. MCP 数据层中,对应“厨房储藏间里食材”的原语是哪一类?

单选题 · easy

  • A.Tools,负责可执行的操作
  • B.Prompts,负责生成提示模板
  • C.Resources,负责供应只读数据← 正确答案
  • D.JSON Schema,负责约束参数格式

答案:C。Resources 就是数据源,像储藏间只存放食材一样,它只承载可被读取的数据、不产生任何动作。Tools 是厨具负责执行,Prompts 是菜谱负责模板,JSON Schema 只是描述规格的工具,并非原语本身。

Q2. Agent 想知道 MCP Server“有哪些数据可以读”,应该调用哪个操作?

单选题 · easy

  • A.read_resource 读取具体数据
  • B.list_resources 查看资源清单← 正确答案
  • C.call_tool 执行某个业务工具
  • D.read_prompt 读取提示模板

答案:B。list 系列操作返回的是轻量“目录”,只含 URI 和描述,不含实际内容——就像主厨先翻储藏清单,而不是把每份食材都拆开。A 是拿到清单后才做的事,C 属于 Tools 的执行操作,D 属于 Prompts 的模板操作。

Q3. Tools 工具的输入参数约束,通常用什么格式来描述?

单选题 · easy

  • A.JSON Schema 结构化定义← 正确答案
  • B.Markdown 文档描述段落
  • C.自然语言自由文本描述
  • D.私有二进制协议格式

答案:A。JSON Schema 是机器可读、可校验的结构化契约,定义字段类型、必填项、取值范围等。就像厨具标签写明功率和适用锅具,程序才能自动校验;而自然语言描述无法被程序确定性验证,容易在运行时出错。

Q4. Prompts 与 Tools 最根本的区别是什么?

单选题 · mid

  • A.Prompts 有副作用而 Tools 没有
  • B.Tools 执行动作而 Prompts 是模板← 正确答案
  • C.Prompts 面向用户而 Tools 面向开发
  • D.Tools 必须联网而 Prompts 离线可用

答案:B。Tools 被调用会产生真实动作(查余额、发通知、改状态),Prompts 只是文本模板,填入参数生成消息,本身不产生任何副作用。就像菜谱卡只是套路说明,下锅开火才是厨具(Tools)的事。副作用的有无,正是区分二者的关键。

Q5. 调用 read_resource("db://orders/1") 返回的是什么?

单选题 · mid

  • A.资源的完整 JSON Schema 结构
  • B.该 URI 对应的具体数据内容← 正确答案
  • C.Server 上全部资源的清单列表
  • D.该资源关联的可执行函数体

答案:B。read 操作按 URI 取出具体内容,对应“去储藏间把标好储位号的食材取出来”。A 是 Schema 定义的范畴,C 是 list 操作的返回值,D 属于 Tools 的信息。记住口诀:list 看目录,read 取内容。

Q6. 设计“查询用户余额”功能时,最合理的拆分方式是?

单选题 · mid

  • A.全部封装进一个大而全的 Tool
  • B.全部静态存进 Resources 数据里
  • C.余额数据进 Resources,查询动作用 Tool← 正确答案
  • D.用 Prompts 模板写死余额数值

答案:C。数据与动作分离是 MCP 的基本准则。余额数据由 Resources 提供,可被多个工具复用;“查询”这个动作用 Tool 封装以响应实时变化。全部塞进 Tool 会让数据失去复用性,静态存进 Resources 无法反映余额变化,写死在模板里更是荒谬。

Q7. 下列哪些属于 MCP 数据层的三大原语?

多选题 · mid

  • A.Resources 数据源抽象← 正确答案
  • B.Tools 可执行操作抽象← 正确答案
  • C.Prompts 提示模板抽象← 正确答案
  • D.Plugins 插件市场抽象

答案:ABC。MCP 协议只定义三类原语:Resources(读数据)、Tools(执行动作)、Prompts(套模板)。Plugins 是生态层面的扩展概念,不属于协议层原语。类比后厨:储藏间、工具墙、菜谱盒是三个固定区域,市场则是厨房之外的商业设施。

Q8. 以下哪些场景适合设计成 Resources?

多选题 · mid

  • A.用户档案数据库内容← 正确答案
  • B.发送邮件的通知动作
  • C.商品文档与手册内容← 正确答案
  • D.历史销售报表数据内容← 正确答案

答案:ACD。Resources 放“可以被读取的数据”:用户档案、文档、报表都是只读数据。B 是会产生副作用的动作,应封装成 Tools。判断标准一句话:主厨是“取用”还是“操作”?取用进储藏间,操作上工具墙。

Q9. 以下哪些做法有助于避免 MCP 的职责混乱?

多选题 · hard

  • A.数据统一放进 Resources← 正确答案
  • B.动作操作封装成 Tools← 正确答案
  • C.通用套路做成 Prompts← 正确答案
  • D.所有能力一律做成 Tools

答案:ABC。三类原语各司其职、两两交集为空,是避免混乱的唯一正解。数据归 Resources、动作归 Tools、套路归 Prompts。D 是典型反模式——把数据塞进工具会让数据失去复用性,也模糊了“读”与“做”的界限,就像把食材全塞进烤箱。

Q10. Agent 完成“读取订单→判断逾期→自动催单”全流程,最合理的组合是?

单选题 · hard

  • A.只调用一个大而全的 Tool 完成所有步骤
  • B.先 read 订单数据,再 call 催单工具← 正确答案
  • C.只 read 订单数据,不做任何后续动作
  • D.只套用 Prompts 模板,不读取真实数据

答案:B。按需组合是 Agent 的核心能力:先用 read_resource 取订单数据(取食材),判断逾期后调用 call_tool 发催单通知(开火),必要时再套模板组织话术。大而全 Tool 破坏职责边界,只读不做或只套模板都完不成业务闭环。