它能做什么 — 把清单写下来,客户端就只属于那一个服务端
5 分钟目标MCP 客户端课程第二篇。去问工具和资源的清单。两行就完了,而有没有这两行,决定了它是「那台服务端专用的客户端」还是「站到任何服务端面前都行的客户端」。
第一篇接上了。现在去问。
两行
final tools = await client.listTools();
stdout.writeln('tools: ${tools.map((t) => t.name).join(", ")}');
for (final t in tools) {
stdout.writeln(' ${t.name} — ${t.description}');
}
final resources = await client.listResources();
stdout.writeln('resources: ${resources.map((r) => r.uri).join(", ")}');tools: desk.admit
desk.admit — Admit a number of people from the queue
resources: ui://desk, desk://waiting写下来,它就成了专用的
也可以这么写:
// 这台服务端有 desk.admit
await client.callTool('desk.admit', {'count': 1});能跑。而 就在那一刻,这个客户端成了那台服务端专用的。 只要有一个工具名嵌进代码,它就没法站到别的服务端面前。
问来的就不一样。清单是运行时到的,所以客户端 不需要知道自己接的是什么。 接开发板就来 led.set,接柜台就来 desk.admit。同一份代码。
说明文在这里被用上
listTools() 给的不只是名字。说明文和输入模式会一起来。
desk.admit — Admit a number of people from the queue给人用的客户端拿它做按钮文案和提示。模型则读它来决定调什么。服务端第二篇说的「说明文不是注释,是会跑的代码」,在这里被消费掉。
资源是以 uri 来的
resources: ui://desk, desk://waitingui:// 是画面,desk:// 是值。这不是契约规定的,而是 服务端自己定的命名规矩。 客户端不需要知道,第四篇读画面时直接用清单里的 uri 就行。
校验 —— 硬编码就失败
这一篇的主张是「客户端不是被告知,而是去问」。所以检查会去看客户端的代码。
grep -q 'tools: .*desk.admit' captures/s2.txt || die "tools not discovered"
grep -q 'resources: .*ui://desk' captures/s2.txt || die "resources not discovered"
# The claim is that the client was not told; it asked.
grep -q "'desk.admit'" bin/step2.dart && die "the tool name is hardcoded in the client"bin/step2.dart 里出现一次工具名就失败。
自己跑一遍
dart run bin/step2.dart
bash verify.sh可以带走的
listTools()/listResources()— 表面在运行时才到- 不要写下名字 — 写了就成了专用客户端
- 禁止硬编码的检查 — 防止这个主张被悄悄破掉
下一篇
调用工具。以及服务端拒绝时,那并不是异常。
运行示例
makemind-academy/course_client/git clone https://github.com/makemind-academy/course_client cd course_client dart pub get (cd course-server && dart pub get) dart run bin/step2.dart