画面をリソースに — コードではなくサーバーが差し出す文書

5 分
目標MCP サーバー講座 4 編目。画面をサーバーがファイルとして持ち、差し出す。ファイルを直せば再ビルドも再インストールも無しに次の接続で変わる — ただし毎リクエスト読んでこそ、その言葉が真になる。

ここまでサーバーは値を答えてきた。今度は 画面を答える。

画面はファイルだ

{ "type": "page", "title": "Desk",
  "content": { "type": "center",
    "child": { "type": "linear", "direction": "vertical", "spacing": 14, "alignment": "center", "children": [
      { "type": "text", "text": "WAITING", "style": { "fontSize": 16, "letterSpacing": 4, "color": "#6b7280" } },
      { "type": "text", "text": "{{waiting}}", "style": { "fontSize": 88, "fontWeight": "bold", "color": "#111827" } },
      { "type": "button", "label": "Admit one", "onTap": { "type": "tool", "tool": "desk.admit", "params": { "count": 1 } } } ] } } }

六行だ。{{waiting}} のところに値が入り、ボタンが 3 編目で作った道具を呼ぶ。

サーバーが差し出す

server.addResource(
  uri: 'ui://desk',
  name: 'Desk screen',
  description: 'The desk screen, served as a document',
  mimeType: 'application/json',
  handler: (uri, params) async => ReadResourceResult(contents: [
    ResourceContentInfo(
      uri: 'ui://desk',
      mimeType: 'application/json',
      text: File(_screenPath).readAsStringSync(),
    )
  ]),
);

ResourcesCapability も入れねばならない。

capabilities: ServerCapabilities(
  tools: ToolsCapability(listChanged: true),
  resources: ResourcesCapability(listChanged: true),
),

毎リクエスト読むことが要点だ

readAsStringSync() がハンドラの中にある。起動時に一度読んで変数に持てば当然速いのに、そうしなかった。

持ってしまえば 画面を直すたびにサーバーを再起動せねばならない。 その瞬間、この編が主張すること — ファイルを直せば別のアプリになる — が嘘になる。

/// Read fresh on every request, deliberately. A screen cached at boot is a
/// screen you have to restart the server to change, and then the claim above
/// stops being true.

性能が問題になる地点が来たらキャッシュしてよいが、ファイルの変更を検知して無効化 せねばならない。ただ持つのは機能を失うことだ。

何が変わるか

画面がコードの外に出ると、こうなる。

コードにあるとき 文書として差し出すとき
文字の大きさ変更 ビルド → 配布 → インストール ファイル一行
クライアントの種類 種類ごとに実装 同じ文書を受け取って描く
画面の追加 アプリを修正 リソース一行

検証 — 画面がコードの中にあれば失敗

この編がいちばん簡単に嘘になる場所がある。コードの中に画面を入れておいて「サーバーが差し出す」と書くこと。 応答だけでは区別が付かない。

ask step4 '{"jsonrpc":"2.0","id":2,"method":"resources/read","params":{"uri":"ui://desk"}}'
grep -q 'ui://desk' captures/s4.txt || die "resource not served"
grep -q 'page'      captures/s4.txt || die "served document is not a screen"

# The claim is that the screen is not in the code.
grep -q 'fontSize' bin/step4.dart && die "step4: the screen is inside the code"

サーバーのコードに fontSize が一度でも出れば失敗する。

step4  ui://desk served from ui/desk.json (6 lines), not from the code

自分で動かす

dart run bin/step4.dart
bash verify.sh

ui/desk.json の fontSize を 88 から 120 に変えてもう一度問えば、変わった文書が出てくる。サーバーは落としていない。

持ち帰るもの

  1. addResource + capability — 両方入れねば一覧に出ない
  2. ハンドラの中で読む — キャッシュした瞬間、無インストールでなくなる
  3. コード内画面の禁止検査 — この主張が嘘になりうる唯一の場所

次の編

いまの待ち人数は変数だ。プロセスが死ねば消える。それを残す。

サンプルを実行する

makemind-academy/course_server/
git clone https://github.com/makemind-academy/course_server
cd course_server
dart pub get
dart run bin/step4.dart
GitHub で開く →