1.3 内存客户端验证:不写一行协议就调通


1.3 内存客户端验证:不写一行协议就调通

本节摘要:上一节的服务端只是定义了能力,还没「跑起来」。本节我们用 SDK 自带的第一类 Client,在进程内直接连上它、调用工具、读取资源,全程不碰网络、不写一行协议帧。这是全书所有示例的测试骨架——Client(mcp) 这种「内存连接」是开发期验证「我的服务端真的能用」最轻的方式。我们会讲清它的写法、为什么不需要传输、以及如何读到工具调用与资源读取的结果。读完本节,你将亲手完成第一次端到端的工具调用,建立「真的能用」的信心。

一、第一类 Client:连接即对象

v2 把客户端升级成了第一类对象——意思是它不再是个底层会话的工具函数,而是一个独立、完整的抽象。它的用法极其简洁:

import asyncio from mcp import Client from server import mcp # 上一节写的服务端 async def main() -> None: async with Client(mcp) as client: result = await client.call_tool("add", {"a": 1, "b": 2}) print(result) asyncio.run(main())

注意这里发生了什么:Client(mcp) 把上一节写的服务端对象 mcp 直接当参数传进去。没有 URL、没有端口、没有命令——客户端在构造时识别出「这是一个服务端对象」,于是用内存传输连上它。

💡 技巧:这是 Client 最聪明的设计——按参数类型自动选传输。传服务端对象→内存;传 URL 字符串→流式 HTTP;传传输对象→直用。第 9 章会详细讲这个分发机制,本节先用最简单的内存形态。

二、为什么内存连接不需要传输

回顾第 8 章会讲的内容:传输层(Transport)的本质是「产出一对读写流」。stdio、HTTP、SSE 这些传输,都是把消息从一端搬到另一端的方式。

但内存连接的场景里,客户端与服务端在同一个进程,根本不需要「搬运」——消息可以直接从客户端的「发」函数,递到服务端的「收」函数。所以内存传输用的是一种直接分发(Direct Dispatcher) 机制,连 JSON-RPC 帧都省了:

普通传输(stdio / HTTP) ┌─────────┐ JSON-RPC 帧 ┌─────────┐ │ 客户端 │ ──────────────► │ 服务端 │ │ │ ◄────────────── │ │ └─────────┘ 序列化/反序列化 └─────────┘ 内存传输(Client(服务端对象)) ┌─────────┐ 直接调用函数 ┌─────────┐ │ 客户端 │ ──────────────► │ 服务端 │ │ │ ◄────────────── │ │ └─────────┘ 无帧、无序列化 └─────────┘

这个差异解释了为什么内存连接极快零配置,是开发期验证的首选。

⚠️ 注意:内存连接只是「开发与测试」用。生产环境里,客户端与服务端通常分属不同进程甚至不同机器,那时才需要真正的传输。但你的服务端代码完全不变——这是传输与业务解耦的功劳,第 8 章详讲。

三、调用工具:call_tool

连上之后,核心动作是调用工具:

result = await client.call_tool("add", {"a": 1, "b": 2})

call_tool 接两个参数:工具名("add")、参数字典({"a": 1, "b": 2})。返回的 result 是一个结构化对象,包含两块内容:

字段 内容 给谁看
content 文本形式的返回值(给模型) 模型据此继续对话
structured_content 类型化的结构化数据(给应用) 应用据此做后续逻辑

add 来说,content 会是类似 "3" 的文本,structured_content 会是 {"result": 3}。第 4 章会讲清为什么标量返回会被包成 {"result": ...},这里先记住:调用工具拿到的不是裸返回值,而是一个含两块内容的响应对象

# 看看返回的结构 print(result.content) # 给模型的文本 print(result.structured_content) # 给应用的结构化数据 print(result.is_error) # 是否出错(第 4 章详讲)

四、读取资源:read_resource

调用工具之外,另一类操作是读取资源。回顾上一节我们注册的资源模板 greeting://{name}:

content = await client.read_resource("greeting://World") print(content) # "Hello, World!"

注意这里传的是完整的 URI(greeting://World),客户端据此找到对应的资源模板处理器,把 World 填进 {name},调用 greeting("World"),返回 "Hello, World!"

💡 技巧:read_resource 接的是完整 URI,不是模板模式。如果你想先知道「有哪些资源模板可读」,用 client.list_resource_templates()(第 9 章详讲)。这套「先列后读」的模式,和 Web API 的「先 GET 列表再 GET 详情」一模一样。

五、看看服务端声明了什么能力

内存客户端还有个开发期特别有用的用法——看服务端声明了什么能力:

async with Client(mcp) as client: print(client.server_capabilities.model_dump(exclude_none=True))

输出大致是:

{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}

这个字典就是服务端的能力声明(Capability Declaration)——它告诉客户端「我会回应 tools、resources、prompts 这三类请求」。客户端据此决定该问什么、不该问什么。第 3 章会讲清这套机制,这里先建立一个直觉:能力是自动声明的,你注册了处理器,能力就出现;没注册就不声明,客户端就不会问

注意输出里没有 completions——因为我们没注册补全处理器。这就是「不写就不声明」的体现。

六、这个骨架会反复出现

这一节的代码模式(async with Client(mcp) as client: ...)是全书所有示例的测试骨架。从第 4 章的工具契约,到第 7 章的引导填写,再到第 11 章的认证,几乎所有特性验证都用这个内存客户端做。它的价值在于:

开发循环 ┌─────────────────────────────────────────┐ │ 1. 写/改一个工具、资源、提示词 │ │ 2. 用 Client(mcp) 内存连上 │ │ 3. call_tool / read_resource 验证行为 │ │ 4. 不对 → 回 1 改;对 → 继续下一个 │ └─────────────────────────────────────────┘

这个循环极快(毫秒级),不依赖网络,让你能像写单元测试一样迭代服务端。等你服务端验证没问题了,再考虑用 mcp.run() 跑成 stdio 或 HTTP 服务对外(第 8 章)。

本节要点回顾

  1. Client(mcp) 是内存连接,把服务端对象直接当参数,客户端据此选内存传输。
  2. Client 按参数类型自动选传输:服务端对象→内存、URL→HTTP、传输对象→直用。
  3. 内存连接用直接分发,无 JSON-RPC 帧,所以极快、零配置,是开发期验证首选。
  4. call_tool 返回含两块内容:content(给模型)与 structured_content(给应用)。
  5. read_resource 接完整 URI,不是模板模式;想先知道有哪些用 list_resource_templates
  6. server_capabilities 展示服务端声明的能力,注册才声明,没注册就不出现。
  7. 这个内存客户端骨架是全书示例的测试基础,几乎所有特性都用它验证。

服务端能在内存里跑通了,但它还是个「黑盒」——你看不到模型视角的工具表单长什么样。下一节我们启动 MCP 检查器,用浏览器可视化地体验这一切。


作者与出处
原作者: 灏天文库
来源:modelcontextprotocol
许可证:MIT
整理: 灏天文库整理
由灏天文库结构化整理,提供目录导航、全文检索与在线阅读,便于系统化学习
发布者: 作者: 灏天文库 转发
评论区 (0)
U