拒む道具 — スキーマは約束であって保証ではない
5 分目標MCP サーバー講座 3 編目。入力スキーマを付け、そのスキーマを信じないハンドラを書く。0 人を入れよという要求と 99 人を入れよという要求が、それぞれ違う理由で拒まれるところまで。
2 編目の道具は引数を取らなかった。今回は取る。そして 受け取ったものを信じない。
スキーマ
inputSchema: const {
'type': 'object',
'properties': {
'count': {'type': 'integer', 'description': 'How many to admit, 1 or more'},
},
'required': ['count'],
},これを付ければ呼ぶ側が何を送ればよいか分かる。モデルならここに合わせて引数を作る。
ところがスキーマは保証ではない
'type': 'integer' と書いたから整数が来るのではない。スキーマは 呼ぶ側への案内 であり、その側が守らないかもしれないし守れないかもしれない。
だからハンドラがもう一度見る。
handler: (args) async {
final n = args['count'];
// The schema says integer; the caller may still send anything.
if (n is! int || n < 1) {
return CallToolResult(
content: [TextContent(text: 'desk.admit: count must be 1 or more')],
isError: true,
);
}
// And the desk cannot admit people who are not there.
if (n > waiting) {
return CallToolResult(
content: [TextContent(text: 'only $waiting waiting')],
isError: true,
);
}
waiting -= n;
return CallToolResult(
content: [TextContent(text: jsonEncode({'waiting': waiting}))],
);
},二つの拒否は性質が違う
検査が二つあり、理由が違う。
n < 1 — 値そのものが成り立たない。0 人を入れよは常に誤りだ。入力の問題。
n > waiting — 値は正常だが今の状態では不可能だ。3 人待ちで 99 人は入れられない。だが 100 人待ちなら 99 は正常だ。状態の問題。
これを分けねばならない理由はメッセージにある。
count must be 1 or more ← 送り直しても同じく誤り
only 3 waiting ← 3 以下で送ればよいisError: true でひとまとめにすれば、呼ぶ側は再試行すべきか諦めるべきか分からない。
黙って受けてはいけない理由
拒む代わりにこう書くこともできる。
if (n > waiting) n = waiting; // 黙って合わせてやる動きはする。ところが 呼んだ側は 99 人入れたと信じる。 画面に 99 が出て、帳簿に 3 が残り、締めが合わない。そしてどこでずれたかを探す術が無い。
計測の編の 12V クランプと同じ場所だ。切ったなら 切ったと言わねばならない。
検証
三つの要求を続けて送る。
ask step3 \
'...{"name":"desk.admit","arguments":{"count":0}}}' \
'...{"name":"desk.admit","arguments":{"count":99}}}' \
'...{"name":"desk.admit","arguments":{"count":1}}}'
grep -q 'count must be 1 or more' captures/s3.txt || die "zero was not refused"
grep -q 'only 3 waiting' captures/s3.txt || die "over-count was not refused"
grep -q 'waiting.*2' captures/s3.txt || die "a valid admit did not go through"step3 refused 0 and 99, admitted 1 -> waiting 2三つ目の検査が重要だ。 前の二つだけなら「全部拒む道具」も通ってしまう。拒めるかと通せるかを一緒に見る必要がある。
自分で動かす
dart run bin/step3.dart
bash verify.sh持ち帰るもの
- スキーマ+ハンドラの検査 — スキーマは案内、検査は防御
- 拒否を二種類に — 入力の問題と状態の問題はメッセージが違うべきだ
- 拒否の検査+通過の検査 — 両方あって検証が成立する
次の編
画面を付ける。コードではなく、サーバーが差し出す文書として。
サンプルを実行する
makemind-academy/course_server/git clone https://github.com/makemind-academy/course_server cd course_server dart pub get dart run bin/step3.dart