调用并接住 — 拒绝不是异常
5 分钟目标MCP 客户端课程第三篇。调用工具并读回答案。结果不是一个值而是一份内容列表;而服务端拒绝时,它不会以 throw 的形式到来——所以要读,不是去捕获。
第二篇知道了有什么。现在去调。
结果不是一个值
final ok = await client.callTool('desk.admit', {'count': 1});回来的不是数字,而是 一份内容列表。可能是文本,可能是图片,也可能有好几项。所以把取值这件事收在一处。
Map<String, dynamic> decode(CallToolResult r) {
final first = r.content.first;
if (first is! TextContent) {
throw StateError('expected text content, got ${first.runtimeType}');
}
return jsonDecode(first.text) as Map<String, dynamic>;
}去掉 is! TextContent 直接强转,等哪天服务端混进一张图片,它会在 完全不相干的地方 炸。在这里拦住,栈才读得懂。
admit 1 -> {waiting: 2}拒绝不会以 throw 到来
调一下服务端第三篇里做的那个拒绝。
final refused = await client.callTool('desk.admit', {'count': 99});
final text = (refused.content.first as TextContent).text;
stdout.writeln('admit 99 -> isError=${refused.isError} "$text"');admit 99 -> isError=true "only 2 waiting"没有抛异常。 await 正常完成,结果上带着 isError: true,理由在文本里。
不知道这一点,就会写成:
try {
await client.callTool('desk.admit', {'count': 99});
// 走到这儿了,说明成功
} catch (e) {
// 以为拒绝会掉到这里……其实不会
}拒绝进不了 catch,于是 被当成成功处理。 屏上写着「放进 99 人」,而服务端一个人也没放。
为什么它不是异常
异常的意思是「这次调用没有成立」——连接断了,或者没有这个工具。
拒绝的意思是 调用成立了,服务端做了判断。 工具正常跑完,答案是「不行」。而这个答案带着理由——要用上那个理由,就必须以结果的形式接住它。
读到 only 2 waiting,就能改成 2 以内重发。当异常捕获掉,那句话就埋进栈里了。
校验
run step3 > captures/s3.txt || die "step3: failed (a refusal should not throw)"
grep -q 'admit 1 -> {waiting: 2}' captures/s3.txt || die "the call did not go through"
grep -q 'admit 99 -> isError=true' captures/s3.txt || die "the refusal was not surfaced"
grep -q 'only 2 waiting' captures/s3.txt || die "the reason did not survive"第一行很要紧。拒绝要是以异常到来,程序就会死,退出码不为 0 —— || die 会在那儿抓住它。
第三条检查(only 2 waiting)真的抓到了一个缺陷。服务端 4 到 6 级把第 3 级分开的两种拒绝又揉回了一个。只跑服务端的话,第 3 级检查会过,而后面几级不会看那条消息,于是永远发现不了。 是把两个样例咬在一起跑,才露出来的。
自己跑一遍
dart run bin/step3.dart
bash verify.sh可以带走的
- 一处
decode— 内容类型的检查放在这里 - 去读
isError— 拒绝是一个结果 - 检查拒绝不会抛异常 — 退出码能确认
下一篇
接住画面。而客户端里一行画面都没有。
运行示例
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/step3.dart