何ができるのか — 一覧を書き留めればそのサーバー専用になる

5 分
目標MCP クライアント講座 2 編目。道具とリソースの一覧を問う。二行で終わるのだが、この二行があるかどうかが「そのサーバー専用のクライアント」と「どのサーバーの前にも立てるクライアント」を分ける。

1 編目で繋がった。今度は問う。

二行

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

人が使うクライアントならこれでボタンのラベルとツールチップを作る。モデルならこれを読んで何を呼ぶかを決める。サーバー 2 編目で「説明文は注釈ではなく動くコードだ」と言ったものが、ここで消費される。

リソースは uri で来る

resources: ui://desk, desk://waiting

ui:// は画面で desk:// は値だ。規約が定めたのではなく サーバーが決めた命名規則 である。クライアントはそれを知る必要が無く、4 編目で画面を読むときは一覧にある 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

持ち帰るもの

  1. listTools() / listResources() — 表面は実行時に来る
  2. 名前を書かない — 書いた瞬間、専用クライアントになる
  3. ハードコード禁止の検査 — この主張が静かに壊れるのを防ぐ

次の編

道具を呼ぶ。そしてサーバーが拒んだとき、それが例外ではないということ。

サンプルを実行する

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
GitHub で開く →