08|资源发现:为大模型提供服务器端的数据内容

你好,我是黄佳。

从这节课开始,我们将通过理论讲解和代码示例相配合的方式,来深入探讨一系列MCP协议中的核心概念(MCP官方文档中把这些概念称为原语——Primitives,其实就是核心概念的意思)。

这节课将详细介绍什么是MCP中的资源(Resources),包括其定义、结构、类型、实现方式及最佳实践,帮你全面了解资源在增强AI驱动应用中的作用。

什么是 MCP 中的资源

资源是 Model Context Protocol(MCP)中的核心原语之一,用于将服务器端的数据内容通过可以被客户端读取的方式暴露给 LLM,用作对话或生成时的上下文。

这里的资源可以是任意类型的文本或二进制数据,包括文件内容、数据库记录、API 响应、日志、截图、音频、视频等。

每个资源都由唯一的 URI 确定,遵循通用的 [协议]://[主机]/[路径] 形式,例如:

file:///home/user/docs/report.pdf
postgres://db.example.com/customers/schema
screen://localhost/display

服务器可根据需要自定义 URI 方案,并在文档中说明各字段含义。

资源是应用控制的,意味着客户端应用程序可以决定如何以及何时使用这些资源。例如,应用可以树状或列表视图通过 UI 元素展示可选资源,或允许用户搜索和过滤可用资源。

不同的MCP客户端处理资源的方式可能不同,例如:

  • 像Claude Desktop这样的客户端要求用户明确选择资源。

  • 其他客户端可能根据启发式规则自动选择资源。

  • 某些实现甚至允许AI模型自行决定使用哪些资源。

因此,服务器开发者在实现资源支持时,需要意识到资源的交互模式是MCP Host确定的。如果需要服务端自动向模型暴露数据(而不是Host来决定),服务器应使用模型控制的原语(如Tools)而非资源。

资源的定义和发现

在 MCP 协议中,“服务端资源的定义和发现”是指服务器如何声明(定义)它能提供的各种数据内容,以及客户端如何探测(发现)并访问这些资源。

服务端资源的定义

服务器需要用一个统一的数据结构来描述每个资源对象,包括后面这几项内容。

  • URI:唯一标识符,形如 scheme://host/path(比如 file:///logs/app.log)。

  • name:人类可读的名称,如 “Application Logs”。

  • description(可选):对资源内容的简要说明。

  • mimeType(可选):资源的媒体类型(text/plain、application/json、image/png 等)。

  • size(可选):资源的字节数

支持资源功能的服务器必须声明 resources 能力:

{
  "capabilities": {
    "resources": {
      "subscribe": true,
      "listChanged": true
    }
  }
}

其中:

  • subscribe:客户端是否可以订阅单个资源的变更通知

  • listChanged:当资源列表变化时,服务器是否会发送通知

这两项均为可选,服务器可以只支持其中一个、两个都支持,也都不支持:

{
  "capabilities": {
    "resources": {} // 两项都不支持
  }
}

静态资源 vs 动态资源

静态资源是值在服务提供时路径已经明确的资源,此时一次性列出URI 即可,服务器返回一组具体的资源对象。

静态资源的示例如下:

{
  "uri": "file:///logs/app.log",
  "name": "Application Logs",
  "description": "实时日志文件",
  "mimeType": "text/plain"
}

动态资源的内容或可用集合会随参数变化而变化,此时服务器通过 URI 模板(遵循 RFC 6570)来定义资源,例如:

{
  "uriTemplate": "logs://recent?timeframe={duration}",
  "name": "最近日志",
  "description": "按时长获取日志",
  "mimeType": "text/plain"
}

在通信过程中,客户端只要填入模板参数就能构造出具体的资源 URI。

MCP 接口绑定

在代码层面,服务器通过装饰器或注册函数来实现资源定义接口。

# 使用装饰器注册资源列表处理函数
@app.list_resources()
async def list_resources() -> list[types.Resource]:
    """
    实现资源列表API,返回服务器提供的资源列表
    
    返回:
        list[types.Resource]: 资源对象列表
    """
    return [
        # 创建一个资源对象
        types.Resource(
            uri="file:///logs/app.log",  # 资源的唯一标识符
            name="Application Logs"    # 资源的显示名称
            mimeType="text/plain"
        )
    ]

当收到 resources/list 请求时,MCP 框架就会调用这个方法返回资源列表。

客户端读取资源

客户端要使用服务器暴露的数据,必须先“发现”有哪些资源可用。

主要有两种方式:

  • 直接列出(Direct Listing): 客户端向服务器发送 resources/list 请求,服务器返回一组具体的资源对象。这个方式的优点是客户端一次就能拿到所有可用资源的完整信息,典型的场景为资源数量较少且变化不频繁时。

  • URI 模板(URI Templates): 对于需要根据参数动态生成的资源,客户端向服务器发送 resources/list 请求后,服务器先返回 URI 模板;客户端根据自己的需求填入模板参数(如时间范围、分页参数等),构造出真正的资源 URI,然后再发起 resources/read 请求读取内容。这样,就能避免在列表中暴露大量组合情况,只定义好了参数化方式。典型的场景为读取日志、监控指标、分页数据等动态内容。

列出资源

客户端发起 resources/list 请求以发现可用资源。

客户端请求格式如下:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "resources/list",
  "params": {
    "cursor": "optional-cursor-value"
  }
}

服务器响应格式如下:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resources": [
      {
        "uri": "file:///project/src/main.rs",
        "name": "main.rs",
        "description": "Primary application entry point",
        "mimeType": "text/x-rust"
      }
    ],
    "nextCursor": "next-page-cursor"
  }
}

读取资源

客户端通过 resources/read 请求指定 URI,服务器会返回包含一个或多个内容项的列表。

客户端请求格式如下:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "resources/read",
  "params": {
    "uri": "file:///logs/app.log",
  }
}

服务器响应格式如下:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "contents": [
      {
        "uri": "file:///logs/app.log",
        "mimeType": "text/plain",
        "text": "2025-05-11 10:00: INFO Server started\n..."
      }
    ]
  }
}

如果读取目录或参数化 URI,也可能返回多项结果,用于列出目录下的所有文件等场景 。

资源模板

资源模板(Resource Templates)允许服务器通过 URI 模板公开可参数化的资源,其参数可通过补全 API(completion API,这个内容我们后续文章中会介绍)自动填充。

客户端请求格式如下:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "resources/templates/list"
}

服务器响应格式如下:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "resourceTemplates": [
      {
        "uriTemplate": "file:///{path}",
        "name": "Project Files",
        "description": "Access files in the project directory",
        "mimeType": "application/octet-stream"
      }
    ]
  }
}

资源的动态更新

MCP 支持实时通知机制,让客户端保持对资源状态的感知。

  • 列表变更通知:当服务器端资源列表增删改时,发出notifications/resources/list_changed通知。

  • 内容更新订阅:客户端可通过 resources/subscribe 订阅某个 URI;服务器在资源变化时用 notifications/resources/updated 通知,客户端再调用 resources/read 拉取最新内容;必要时可调用 resources/unsubscribe 取消订阅 。

错误处理

服务器应对常见失败情况返回标准 JSON-RPC 错误:

  • 资源未找到(Resource not found):错误码 -32002

  • 内部错误(Internal error):错误码 -32603

示例:

{
  "jsonrpc": "2.0",
  "id": 5,
  "error": {
    "code": -32002,
    "message": "Resource not found",
    "data": {
      "uri": "file:///nonexistent.txt"
    }
  }
}

资源的交互流程

服务器和客户端的资源交互流程如下图所示。

MCP资源实战示例

下面我们结合示例来看看,如何在 MCP 服务器中定义并读取一系列本地知识库资源文件。(和之前示例一样,要在server和client目录中,通过 pyproject.toml 文件中的设置创建虚拟环境并安装依赖。)

首先,我们创建一系列非常简单的医学文档。

图片

然后,我们在服务器端实现资源的发现。

# 导入必要的库
import asyncio
import os
import mcp.types as types
from mcp.server import Server
from mcp.server.stdio import stdio_server

# 创建一个MCP服务器实例,名称为"example-server"
app = Server("example-server")

DOC_DIR = "你的路径/mcp-in-action/05-resource-资源发现/server/medical_docs"

# 注册资源列表
@app.list_resources()
async def list_resources() -> list[types.Resource]:
    files = [f for f in os.listdir(DOC_DIR) if f.endswith(".txt")]
    return [
        types.Resource(
            uri=f"file://{os.path.join(DOC_DIR, fname)}",
            name=fname,
            description="医学文档",
            mimeType="text/plain"
        )
        for fname in files
    ]

# 读取资源内容
@app.read_resource()
async def read_resource(uri: str) -> str:
    path = uri.replace("file://", "")
    with open(path, encoding="utf-8") as f:
        return f.read()

async def main():
    async with stdio_server() as streams:
        await app.run(
            streams[0],
            streams[1],
            app.create_initialization_options()
        )

if __name__ == "__main__":
    asyncio.run(main()) 

接下来,通过客户端发现服务器的资源,并读取它们。

import asyncio
import sys
from mcp import ClientSession, StdioServerParameters
from mcp.types import Notification, Resource
from mcp.client.stdio import stdio_client

async def main():
    # 检查命令行参数 - 必须通过两个参数启动此程序,一个当前客户端,一个服务器程序
    if len(sys.argv) < 2:
        print("Usage: python simple_client.py <path_to_server_script>")
        sys.exit(1)

    server_script = sys.argv[1]
    # 使用服务器的 Python 解释器来启动 Server,而不是当前客户端的Python环境
    params = StdioServerParameters(
        command="你的环境路径/bin/python3",
        args=[server_script],
        env=None
    )

    # 建立 stdio 传输并创建 Session
    async with stdio_client(params) as (reader, writer):
        async with ClientSession(reader, writer) as session:
            # 初始化握手
            await session.initialize()
            # 发送初始化完成通知
            notification = Notification(
                method="notifications/initialized",
                params={}
            )
            await session.send_notification(notification)

            # 列出资源
            response = await session.list_resources()
            print("资源列表:")
            # 打印返回格式
            print(f"返回类型: {type(response)}")
            print(f"返回内容: {response}")
            
            # 简化处理资源列表
            resources = getattr(response, 'resources', response)
            for res in resources:
                print(f"- URI: {res.uri}, Name: {res.name}")

if __name__ == "__main__":
    asyncio.run(main())

上述代码结合了标准输入/输出传输(stdio transport)来启动 MCP 服务器,并在初始化后响应资源列表与读取请求。

激活客户端Python环境后,运行下面的命令。

python client.py ../server/simple-resource-read.py

或直接运行

uv run client.py ../server/simple-resource-read.py

输出如下:

资源列表:
返回类型: <class 'mcp.types.ListResourcesResult'>
返回内容: meta=None nextCursor=None resources=[Resource(uri=AnyUrl('file:///home/mcp-in-action/05-resource/server/medical_docs/糖尿病.txt'), name='糖尿病.txt', description='医学文档', mimeType='text/plain', size=None, annotations=None), Resource(uri=AnyUrl('file:///home/mcp-in-action/05-resource/server/medical_docs/心脏病.txt'), name='心脏病.txt', description='医学文档', mimeType='text/plain', size=None, annotations=None), Resource(uri=AnyUrl('file:///home/mcp-in-action/05-resource/server/medical_docs/高血压.txt'), name='高血压.txt', description='医学文档', mimeType='text/plain', size=None, annotations=None)]
- URI: file:///home/mcp-in-action/05-resource/server/medical_docs/糖尿病.txt, Name: 糖尿病.txt
- URI: file:///home/mcp-in-action/05-resource/server/medical_docs/心脏病.txt, Name: 心脏病.txt
- URI: file:///home/mcp-in-action/05-resource/server/medical_docs/高血压.txt, Name: 高血压.txt

通过上述设计,我们把服务器中的知识库资源开放给客户端使用,成为了RAG知识库系统的一部分。

MCP资源最佳实践

MCP 协议的官方文档中,给出了一系列关于资源使用的最佳实践。

  • URI 设计:使用清晰可读的协议和路径,便于调试与文档化。

  • 名称与描述:资源列表中提供人性化 name 与详细 description,帮助客户端或用户界面展示上下文。

  • MIME 类型:尽量填写 mimeType,让客户端能够正确解析和展示内容。

  • 订阅机制:对于频繁变化的重要资源,结合订阅通知减少轮询开销。

  • 分页与缓存:当资源列表较大时,采用分页设计(允许服务器以较小的块形式生成结果,而不是一次性全部生成);对于大型二进制资源,可考虑本地缓存。

在安全与合规方面,则需要遵循下列原则。

  • 输入验证:校验所有 URI,防止目录遍历和注入攻击。

  • 访问控制:对敏感路径或数据实施访问控制,在执行操作前应检查资源权限。

  • 数据清理:二进制数据需要正确的编码。

  • 传输加密:在跨网络通信时使用 TLS 等安全协议。

  • 速率限制:防止恶意或错误的高频读取请求。

通过以上内容,你就可以在 MCP 服务端完整地实现资源原语(Resources),并以清晰、可维护的方式将各类静态或动态数据暴露给客户端与 LLM,构建丰富的上下文增强能力。

总结一下

MCP中的“资源”允许服务器向客户端暴露数据和内容,这些数据可作为LLM交互的上下文。

  • 定义:服务器通过 resources/list 接口,声明自己提供了哪些 URI(或 URI 模板),并给出名称、描述、MIME 类型等元信息。

  • 发现:客户端调用 resources/list 拿到资源清单(或模板),再根据清单中的 URI 调用 resources/read 获取实际内容;必要时,还可借助订阅机制保持数据的实时性。

这样,MCP 就在客户端(Consumer)与服务器(Provider) 之间,建立了一套灵活、可扩展的“资源管理”机制,使得 LLM 在生成时能够方便、安全地引用外部数据。

思考题

  1. MCP协议是如何通过资源模板实现访问动态资源,以及如何支持资源订阅和更新通知的?

  2. 在实际生产系统中,要实现真实的资源读取逻辑、添加访问控制、实现真实的资源更新机制、增强错误与安全检查,需要注意哪些要点?

欢迎你在留言区记录自己的收获或者疑问。如果这节课对你有启发,别忘了分享给身边更多朋友。

精选留言

  • 悟空聊架构

    2025-07-06 12:40:23

    🤔 思考题第二题:在实际生产系统中,要实现真实的资源读取逻辑、添加访问控制、实现真实的资源更新机制、增强错误与安全检查,需要注意哪些要点?

    - 2.1 真实的资源读取逻辑
    - 对接真实数据源:如数据库、对象存储,而不是硬编码内容。
    - 参数校验:防止 SQL 注入
    - 分页与过滤数据:避免一次返回大量数据造成服务器压力。
    - 2.2 添加访问控制
    - 统一入口校验,对请求进行身份验证,如利用 token,api key 等。
    - 对资源进行细粒度权限校验,如使用 RBAC 方案。
    - 2.3 真实的资源更新机制
    - 变更后及时推送,如果资源变更频繁或订阅的客户端很多,可以用消息队列来接收变更消息,缓解 MCP 服务端的压力。
    - 在客户端收到资源更新通知后,如果客户端还需要做其他操作,则还需要保证幂等性。
    - 2.4 增强错误与安全检查
    - 异常捕获。
    - 限制调用获取资源的频率。
    - 对输入输出内容进行校验,防止 SQL 注入、XSS 攻击。
    - 资源隔离。
    - 避免返回敏感信息。
    - 监控告警。
    作者回复

    大家还有没有什么要补充的地方,我这边没有了

    2025-07-17 20:58:01

  • 悟空聊架构

    2025-07-06 12:39:37

    🤔 思考题 第一题、MCP 协议是如何通过资源模板实现访问动态资源,以及如何支持资源订阅和更新通知的?

    🌈1.1、MCP 协议是如何通过资源模板实现访问动态资源

    - 1.1.1、MCP 底层是如何注册资源模板的:文件:src/mcp/server/fastmcp/server.py。通过 @server.resource("resource://{param}") 装饰器注册资源模板,底层会调用 ResourceManager.add_template,并最终创建 ResourceTemplate 实例。
    - 1.1.2、开发者是如何注册资源模板的:开发者用 @server.resource("resource://{param}") 注册模板,服务端保存模板和参数定义。类似 Java 中的 MVC 模式中定义的 API,通过路径匹配到 controller 定义的方法。

    @mcp.resource("resource://users/{user_id}/posts/{post_id}")
    def get_user_post(user_id: str, post_id: str) -> str:
    return f"Post {post_id} by user {user_id}"

    - 1.1.3、MCP Client 如何请求资源模板列表:MCP client 通过发送 resources/templates/list 请求,获取所有可用的资源模板。服务端收到后,返回所有注册的资源模板。

    - 1.4 开发者拼接 URI:将参数填充到模板 URI 中。

    ```
    # 假设你要访问 resource://users/{user_id}/posts/{post_id}
    user_id = "123"
    post_id = "456"
    uri = f"resource://users/{user_id}/posts/{post_id}"
    ```
    - 1.1.5 开发者发送获取资源的请求,resources/read
    - 1.1.6 MCP 服务端参数提取与资源生成。
    - 服务端遍历所有注册的资源模板,调用 matches() 方法用正则表达式匹配 URI 并提取参数。
    - 用提取的参数调用注册的函数,生成实际资源内容并返回。这里才是真正调用注册的函数。
    ☀️ 1.2、MCP 如何支持资源订阅和更新通知的?
    - 1.2.1 在 src/mcp/client/session.py 可以找到 subscribe_resource、unsubscribe_resource 支持资源的订阅和取消订阅。
    - 1.2.2 服务端会在资源变更时,向客户端推送 resources/updated 通知,客户端可在 _received_notification 方法中处理。
    作者回复

    悟空的答案即为标准答案
    小总结:相比传统轮询方式,这种方式具备更高的实时性和效率,适合实时日志、仪表盘、代码监控等场景。

    2025-07-17 20:55:38

  • coderlee

    2025-07-04 17:48:50

    1.MCP 协议是如何通过资源模板实现访问动态资源,以及如何支持资源订阅和更新通知的?
    (1)通过资源模板实现访问动态资源
    1)资源模板的注册发现:
    Server在Client初始化session时候向Host注册支持的资源模板;Host维护Server注册的资源模板,并通过ist_resource_templates原语向 Client提供这些模板;Client,通过调用list_resource_templates获取可用模板列表,了解哪些动态资源可以通过模板访问。
    2)动态资源请求
    Client根据资源模板构造具体的URI,通过read_resource原语发送请求给Host;Host解析Client发送的URI,匹配对应的资源模板,并根据模板路由请求到适当的Server;Server接收Host转发的请求,解析URI中的动态参数,执行相应的操作,并将结果通过Host返回给Client。
    3)数据响应:
    Server将处理结果封装为JSON-RPC 2.0响应,返回给Host;Host将Server的响应转发给 Client;Client接收响应数据。
    (2)如何支持资源订阅和更新通知,应该与实现访问动态资源类似:
    1)client发送订阅请求
    2)host转发给server
    3)server确认订阅并进行订阅管理监控
    4)server监控资源变化
    5)server在资源发生变化时候通知订阅者
    6)client接收到通知,更新数据。
    2. 资源模板的标准化、性能、身份认证、权限控制、加密、标准的错误码、版本的兼容性(目前还没实操,只能想到这些)
    3. 最后,看完这一章,也明白了高德的MCP为什么list_resources是空的,原来是不开放出来
    作者回复

    谢谢兄弟梳理了完整的“资源模板 → 动态资源请求 → 数据响应”流程。值得商榷之处,参见和悟空的讨论。我的理解:Host 只转发和路由资源模板信息,并不持久化维护模板列表。

    2025-07-17 20:54:21

  • YSL

    2025-07-03 11:33:32

    资源的本质是 “数据载体”,解决 “LLM 需要什么数据” 的问题;
    工具的本质是 “能力载体”,解决 “LLM 需要执行什么操作” 的问题。
    可以理解为资源是“GET”专注获取,工具是“PSOT”专注提交?
    作者回复

    可以。挺好。或者也可以说:资源关注读取已有内容,工具关注执行行为并产出内容。

    2025-07-17 20:23:53

  • 小风

    2025-06-27 08:09:28

    老师,资源和工具的使用场景有什么不同呢?感觉调用工具也可以返回内容,比如数据表结构等信息。
    作者回复

    简单说一个是客户端应用程序驱动,一个是模型驱动。我解释一下哈。

    “资源”是服务器中的文件、数据库记录、日志、图像等。是用户或 Client的UI 控制何时读取,模型本身并不会主动调用它们。是用户来控制的。这叫做Application-Driven。
    “工具调用”是典型的模型主动执行操作。Tools 被定义成可被模型调用的函数接口,模型可基于上下文决定调用工具。工具不仅能读取,还可能修改状态或发送命令,如执行 API 请求、写数据库、发送消息等。

    上面是最重要的二者的不同。一个是客户端固定的基于程序逻辑读取资源;一个通过大模型来决定做什么,如果工具调用的过程中有提供资源给客户端,当然也可以。

    2025-06-27 11:32:54