写在前面
本文是,有关MCP Resources 的使用示例,原理见MCP 核心概念 -- Resources(上),官方原文分析
正如 MCP(模型上下文协议)去魅!深刻(客观)剖析MCP到底是什么?带来了什么改变 文章所述,MCP 包含了客户端和服务端。
因此本文中,
-
【客户端】使用官方python示例,略有修改。
-
【服务端】使用官方的 everything 服务,有关介绍如下。
This MCP server attempts to exercise all the features of the MCP protocol. It is not intended to be a useful server, but rather a test server for builders of MCP clients. It implements prompts, tools, resources, sampling, and more to showcase MCP capabilities.
可以看到,官方给的定义就是一个帮助使用者了解 MCP 的定位,用它来做例子简直不要太合适。
-
【功能上】以一个简单的测试脚本,了解如何列出资源、读取资源、处理错误以及订阅资源更新。
1. 资源操作接口概览
在测试脚本中,我们可以看到 self.session
提供了一系列接口,包括:
- 列出资源模板
:
list_resource_templates()
- 列出具体资源
:
list_resources()
- 读取资源内容
:
read_resource(uri)
- 订阅与取消订阅资源更新
:
subscribe_resource(uri)
和unsubscribe_resource(uri)
这些接口为 MCP 服务器提供了标准化的资源管理方法,确保客户端可以灵活地访问和处理服务器中的各种数据。
> .../python-sdk/examples/clients/simple-chatbot/mcp_simple_chatbot/main.py(120)list_tools() -> tools_response = await self.session.list_tools() (Pdb) dir(self.session) ['call_tool', 'complete', 'get_prompt', 'incoming_messages', 'initialize', 'list_prompts', 'list_resource_templates', 'list_resources', 'list_tools', 'read_resource', 'send_notification', 'send_ping', 'send_progress_notification', 'send_request', 'send_roots_list_changed', 'set_logging_level', 'subscribe_resource', 'unsubscribe_resource']
测试脚本片段:
async def list_resources(self) -> list[Any]: resource_templates_response = await self.session.list_resource_templates() print("resource_templates_response:") pprint(resource_templates_response) resources_response = await self.session.list_resources() print("resources_response:") pprint(resources_response) # --- 测试资源读取流程 --- for resource in resources_response.resources: print("\n读取资源:") pprint(resource.uri) try: resource_data = await self.session.read_resource(resource.uri) print("read_resource response:") pprint(resource_data) except Exception as e: print("读取资源时捕获到错误:") pprint(e) invalid_resource_uri = "test://static/resource/invalid" print("\n尝试读取不存在的资源:") pprint(invalid_resource_uri) try: invalid_resource = await self.session.read_resource(invalid_resource_uri) print("read_resource (invalid) response:") pprint(invalid_resource) except Exception as e: print("读取不存在资源时捕获到错误:") pprint(e) # --- 测试资源更新订阅机制 --- subscribe_resource_uri = resource.uri print("\n测试订阅资源更新:") pprint(subscribe_resource_uri) try: await self.session.subscribe_resource(subscribe_resource_uri) print("成功订阅资源更新:") pprint(subscribe_resource_uri) # 模拟等待时间以便接收更新通知(实际情况中应处理通知回调) await asyncio.sleep(2) print("incoming_messages 1:") pprint(self.session.incoming_messages) await asyncio.sleep(4) print("incoming_messages 2:") pprint(self.session.incoming_messages) await self.session.unsubscribe_resource(subscribe_resource_uri) print("成功取消订阅资源更新:") pprint(subscribe_resource_uri) except Exception as e: print("订阅或取消订阅资源时出错:") pprint(e) return tools
测试结果:
resource_templates_response: ListResourceTemplatesResult( meta=None, nextCursor=None, resourceTemplates=[ ResourceTemplate( uriTemplate='test://static/resource/{id}', name='Static Resource', description='A static resource with a numeric ID', mimeType=None, annotations=None ) ] ) resources_response: ListResourcesResult( meta=None, nextCursor='MTA=', resources=[ Resource=AnyUrl('test://static/resource/1'), name='Resource 1', description=None, mimeType='text/plain', size=None, annotations=None, text='Resource 1: This is a plaintext resource'), ... # 省略了不必要的打印 Resource=AnyUrl('test://static/resource/10'), name='Resource 10', description=None, mimeType='application/octet-stream', size=None, annotations=None, blob='UmVzb3VyY2UgMTA6IFRoaXMgaXMgYSBiYXNlNjQgYmxvYg==') ] ) 读取资源: AnyUrl('test://static/resource/1') read_resource response: ReadResourceResult( meta=None, nextCursor=[ TextResourceContents( uri=AnyUrl('test://static/resource/1'), mimeType='text/plain', text='Resource 1: This is a plaintext resource', name='Resource 1' ) ] ) ... 读取资源: AnyUrl('test://static/resource/10') read_resource response: ReadResourceResult( meta=None, nextCursor=[ BlobResourceContents( uri=AnyUrl('test://static/resource/10'), mimeType='application/octet-stream', blob='UmVzb3VyY2UgMTA6IFRoaXMgaXMgYSBiYXNlNjQgYmxvYg==', name='Resource 10' ) ] ) 尝试读取不存在的资源: 'test://static/resource/invalid' 读取不存在资源时捕获到错误: McpError('Unknown resource: test://static/resource/invalid') 测试订阅资源更新: AnyUrl('test://static/resource/10') 订阅或取消订阅资源时出错: McpError('MCP error -32600: Sampling not supported')
2. 测试脚本解析
资源模板和资源列表
首先,脚本通过调用 list_resource_templates()
获取资源模板。模板通常定义了一类资源的 URI 格式和一些基本属性,例如:
ResourceTemplate( uriTemplate='test://static/resource/{id}', name='Static Resource', description='A static resource with a numeric ID', mimeType=None )
这种模板允许客户端根据需要动态构造实际资源的 URI。
接着,通过 list_resources()
列出服务器当前可用的资源。测试结果显示服务器返回了 10 个资源,每个资源都有一个唯一的 URI 和相应的内容信息。部分资源以纯文本形式返回(如 Resource 1、Resource 3 等),而另外一些资源则以 base64 编码的二进制数据返回(如 Resource 2、Resource 4 等)。
资源读取
脚本通过遍历 resources_response.resources
来测试资源读取功能。对于每个资源,调用 read_resource(resource.uri)
能够成功获取资源内容。测试结果展示了:
- 文本资源
:返回
text
字段,内容为“Resource X: This is a plaintext resource” - 二进制资源
:返回
blob
字段,内容为 base64 编码后的字符串
这一部分展示了 MCP 如何根据资源类型返回合适的内容格式。
错误处理
当脚本尝试读取一个不存在的资源 URI(例如 test://static/resource/invalid
)时,服务器返回了一个 McpError
错误,提示 “Unknown resource”。这一机制确保了客户端在请求无效资源时能收到明确的错误信息,从而做出相应的处理。
资源更新订阅(小小的遗憾)
测试脚本还演示了订阅资源更新的过程:
-
使用
subscribe_resource(subscribe_resource_uri)
订阅指定资源更新。 -
通过
asyncio.sleep()
模拟等待,以便接收可能的更新通知。 -
查看
self.session.incoming_messages
检查是否有更新通知。 -
最后,调用
unsubscribe_resource(subscribe_resource_uri)
取消订阅。
在测试过程中,订阅资源更新时出现了一个错误:
McpError('MCP error -32600: Sampling not supported')
下面结合 TypeScript 源码解释一下错误原因:
在服务器端的订阅处理器中(SubscribeRequestSchema 的处理函数),代码如下:
server.setRequestHandler(SubscribeRequestSchema, async (request) => { const { uri } = request.params; subscriptions.add(uri); // Request sampling from client when someone subscribes await requestSampling("A new subscription was started", uri); return {}; });
可以看到,本质上是 everything
这个 MCP
没有针对资源订阅实现相关逻辑。不能在这里举例有点可惜,不过没关系,原理是类似的,就是提供了一个变化通知机制,类似 K8S
里的原理。具体的行为,比如客户端接收到后要做什么,这些还是需要开发者自己实现的。
3. 总结与最佳实践
通过这次测试,我们可以得到以下几点经验:
- 资源模板与资源列表
:清晰的模板定义和资源列表示例有助于客户端正确构造请求。确保资源 URI 格式统一,便于动态生成和访问。
- 读取资源
:应针对文本和二进制数据分别处理,服务器在返回数据时应明确指定
mimeType
与内容格式(text 或 blob)。 - 错误处理
:捕获异常并显示错误信息非常重要,有助于调试和提升用户体验。
- 订阅机制
:虽然订阅更新是一项有用的功能
如果你有更多问题或者需要进一步的说明,欢迎在评论区留言讨论!