🔌 第2章 MCP核心原理
📄 一句话概括

一个 Server 能提供三样东西:工具、资源、提示词。 分界线只有一条:谁发起。 模型发起的是工具,应用发起的是资源,人发起的是提示词。 三样都得先在握手时声明,没声明的等于不存在。

📝 本章和前面几章的关系

上一章看的是「接上」这件事,用的是最小的一个工具。 这一节把 Server 能给的东西摊开:除了工具,它还能给资源、给提示词模板。 这三样的分别不是功能强弱,是谁来按下那个按钮。 想不清这条线的人,会把一份静态文档写成工具,然后奇怪模型为什么调它。

一、三样东西,三种发起人

协议给 Server 留了三个出口:tools、resources、prompts。 工具是能做事的东西——查天气、发消息、下单。有参数、有副作用、算完给结果。 资源是能读的东西——一份文档、一条规则、一个日志文件。有地址、没参数、读出来就是文本。

提示词是写好的模板——服务端替你填好参数,还你一段可以直接发出去的话。 区别不在内容,在谁先动手。 这一句记住,后面三个小节的细节都是它的推论。

二、能力要先报,没报的等于没有

握手的时候,服务端的回应里有一段 capabilities。 它说的不是「我有什么功能」,是「我这儿有哪几类出口」。 演示里那段 JSON 三块都在:tools 带 listChanged,resources 带 subscribe,prompts 带 listChanged。 这三个小字段各有含义:listChanged 是说「我的清单会变,变了我会通知你」; subscribe 是说「你可以订阅某一个资源,它改了我推给你」。

客户端只该用它报了的那些。 人家没报 tools,你偏要 tools/list,那是客户端的问题,不是服务端的。 这一条在排错时很有用:工具列表是空的,先看 capabilities 里有没有 tools,再看是不是清单为空。 两种情况的修法完全不同。

三、工具:有副作用、要参数、由模型判断

工具是这三样里唯一「做事」的。它有三个硬特征。 第一,有副作用。查一次天气是读,发一条消息是写。 写出去就收不回来,所以工具调用要人确认——第 3 章讲这个。 第二,要参数,而且参数要校验。 演示里那个 weather.forecast 要 city 和 day,少一个就该回参数错,不能让它在函数里抛异常。

第三,由模型判断该不该调。 这是它跟资源最大的区别:模型看着那段描述,自己决定「这句话要不要查天气」。 正因为第三点,描述写得好不好直接决定它会不会被调用。第 6 章会拿三个工具试一遍,把这件事量出来。

四、资源:一个有地址、没参数的东西

资源长得像一个 uri。演示里是 trave://policy/refund,读出来是一段退改规则。 它没有参数——你没法对一个地址说「给我第三段」。要参数才能取的东西,那是工具。 它也没有副作用。读一百遍跟读一遍结果一样,所以不需要人确认。

谁来读?应用在读,不是模型在读。 宿主决定「这句话可能要用到退改规则」, 于是先把这段读出来塞进上下文,再让模型回答。 这个区别很实际:资源可以整段塞进去,因为它就是给人看的资料;工具不行,工具是动作,动作要挑时候。

五、提示词:一封写好的信,服务端负责填空

提示词最容易被忽略,因为它看起来就是一段文字。 它的用法是:用户在一个菜单里点一下「按城市和天数生成行程框架」,填两个空(杭州、3 天), 服务端把模板填好还回来一段话。 这段话说出来之后,跟用户自己打字没有任何区别。它的价值是把好用的问法固化下来。

发起人是人,不是模型。模型不会自己去取一个提示词模板——它连这个菜单都看不见。 所以这三样的分界线可以一句话说完:【模型发起的叫工具,应用发起的叫资源,人发起的叫提示词。】 这本教材后面九个案例用的全是工具那一样。资源和提示词知道有、知道什么时候该用,就够了。

六、动手 Demo:一个 Server 能给的三样东西

造一个 Server,三样都给:两个工具、一个资源、一个提示词。 先看它握手时报了什么,再一样一样取出来,看形状差在哪。 这三段都是真跑:真起子进程,真发报文,真读回结果。

srv = Server('trave-tools')

# 一、工具:模型判断该不该调
srv.add_tool(Tool('weather.forecast', '查某城市某天的天气',
                   schema({'city': prop('string', '城市名'),
                           'day': prop('string', '今天/明天/后天')},
                          ['city', 'day']),
                   lambda a: forecast(a['city'], a['day'])))

# 二、资源:应用决定读不读,uri 就是地址
srv.add_res('trave://policy/refund', '退改规则', 'text/markdown',
            '起飞前 24 小时以上退票收 10%,24 小时以内收 20%。')

# 三、提示词:人点一下,服务端把模板填好
srv.add_prompt('plan_trip', '按城市和天数生成行程框架', ['city', 'days'],
                '帮我规划 {city} 的 {days} 天行程,先列景点再排顺序。')

print(srv.caps())          # 三块都在,客户端才知道能要什么
print(srv.list_tools())
print(srv.read_res('trave://policy/refund'))
print(srv.get_prompt('plan_trip', {'city': '杭州', 'days': 3}))

实跑输出:

--------------------------------------------------------------------
一个 Server 能给的三样东西
--------------------------------------------------------------------


  ① 握手时先报家底
  --------------------------------------------------------------------
initialize 的回应里有 capabilities 一段,它说明这个 Server 能提供哪几类东西:
    {
      "tools": {
        "listChanged": true
      },
      "resources": {
        "subscribe": false,
        "listChanged": false
      },
      "prompts": {
        "listChanged": false
      }
    }
  三块都在,说明这个 Server 既能给工具、也能给资源、还能给提示词模板。
  【客户端只该用它报了的那些。】 没报 tools 却去 tools/list,是客户端的问题。

  ② 工具:模型自己去调
  --------------------------------------------------------------------

| 名字 | 描述 | 必填参数 |
|---|---|---|
| weather.forecast | 查某城市某天的天气 | city、day |
| poi.search | 按城市和类型找地点 | city |


调一次,回来的是一段文本:
  {"day": "明天", "text": "小雨", "low": 17, "high": 23}
  工具的特征:【有副作用、要参数、由模型判断该不该调。】

  ③ 资源:应用去读,模型只是看见
  --------------------------------------------------------------------

| uri | 名字 | 类型 |
|---|---|---|
| trave://policy/refund | 退改规则 | text/markdown |


读出来是文本,和工具返回的形状完全不一样:
    {
      "contents": [
        {
          "uri": "trave://policy/refund",
          "mimeType": "text/markdown",
          "text": "起飞前 24 小时以上退票收 10%,24 小时以内收 20%。"
        }
      ]
    }
  资源没有参数、没有副作用,【它就是一个能被读的地址。】
  谁来读?宿主(应用)在读,读完塞进上下文。模型不发起这个动作。

  ④ 提示词:用户点一下就用
  --------------------------------------------------------------------

| 名字 | 描述 | 参数 |
|---|---|---|
| plan_trip | 按城市和天数生成行程框架 | city、days |


带参数取一次,服务端把模板填好还给你:
  帮我规划 杭州 的 3 天行程,先列景点再排顺序。
  它就是一封写好的信,服务端负责填空。【发起人是人,不是模型。】

  ⑤ 三样东西的分界线
  --------------------------------------------------------------------

|  | 谁发起 | 有没有参数 | 典型用途 |
|---|---|---|---|
| 工具 tools | 模型判断 | 有,要校验 | 查数据、发消息、下单 |
| 资源 resources | 应用决定 | 无 | 文档、规则、日志 |
| 提示词 prompts | 用户点选 | 有,填空用 | 固定套路的一问 |

  记这一句就够:【模型发起的叫工具,应用发起的叫资源,人发起的叫提示词。】
  三样都要在 capabilities 里先声明,没声明的等于不存在。
  ! 这本教材的九个案例,用的全是工具那一样。资源和提示词知道有就行。

①第一段是握手时的那段 capabilities。它只报「有哪几类」,不报「具体有什么」。 具体清单要另外列一次,这是两步。 ②工具表只有两行,注意最后一列:必填参数。这两个字段是给模型看的,模型据此决定填什么、能不填什么。 ③资源那段返回的形状跟工具完全不同:一个 contents 数组,里面带 uri、mimeType、text。 没有 isError,因为它不会失败——读一个固定地址而已。 ④提示词取回来的是一句人话,方括号被填掉了。 它跟用户自己打字的结果一模一样,区别只在这句话是被谁写好的。 ⑤最后那张三行的表是本节要背下来的。三样的区别不在功能,在「谁先动手」——模型、应用、人。

30 秒实验

  1. 把演示里那个资源的 uri 改成 trave://policy/refund/2,想想它会返回什么、该不该报错。
  2. 判断三件事各属于哪一样:查快递、读公司规章制度、把一段话改写成公文。
  3. 如果你的 Server 只有一个工具,capabilities 里那三块会是什么样。

常见误区

  1. 「资源就是只读的工具。」不只看读写,要看谁发起。资源没有参数,是应用去读; 工具要参数、由模型决定调。
  2. 「提示词是给模型看的。」提示词是给人点的。模型看不见那个菜单,它只看见被填好之后的那段话。
  3. 「capabilities 里报了就一定有。」它报的是「我有这类出口」,清单可能是空的。排错要分两步看。
自测三问(点开对答案) 1. 工具、资源、提示词的分界线是什么? —— 谁发起。模型发起的是工具,应用发起的是资源,人发起的是提示词。 2. 为什么读一份规则文档不该做成工具? —— 它没有副作用、不要参数、也不需要模型判断该不该读。 做成资源,由应用决定什么时候塞进上下文,更省事也更安全。 3. capabilities 里报了 tools,但工具列表是空的,问题在哪? —— 报的是「有这类出口」,清单可以是空的。 要么服务端没注册工具,要么注册了但列不出来,两步分开查。

三样东西分清了,下一个问题是它们各自归谁管。 下一节把 Host、Client、Server 三个角色摆开,看一条连接到底是谁跟谁连。

相关笔记