The Screen as a Resource — a Document the Server Hands Out, Not Code

5 min
GoalFourth in the mcp_server course. The server holds the screen as a file and hands it over. Edit the file and the next connection is different, with no rebuild and no reinstall — as long as it is read on every request.

So far the server answered with values. Now it answers with a screen.

The screen is a file

{ "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 } } } ] } } }

Six lines. A value lands where {{waiting}} is, and the button calls the tool built in part 3.

The server hands it over

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 has to be on too.

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

Reading it on every request is the point

readAsStringSync() sits inside the handler. Reading once at boot into a variable is obviously faster, and that is not what this does.

Hold it and the server has to restart every time the screen changes. At that moment the claim this piece makes — edit the file, get a different app — becomes false.

/// 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.

When performance does become the problem, cache it — but invalidate on file change. Simply holding it is losing the feature.

What changes

Once the screen is outside the code:

in the code served as a document
Change a font size build → deploy → install one line in a file
Kinds of client an implementation each all receive and draw the same document
Add a screen modify the app one more resource

Verification — it fails if the screen is in the code

There is one place where this piece most easily becomes a lie: putting the screen in the code and writing that the server hands it out. The response alone cannot tell them apart.

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"

One occurrence of fontSize in the server code and it fails.

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

Run it yourself

dart run bin/step4.dart
bash verify.sh

Change fontSize in ui/desk.json from 88 to 120 and ask again — the changed document comes back. The server was never stopped.

What to take

  1. addResource plus the capability — both, or it does not appear
  2. Read inside the handler — cache it and it stops being install-free
  3. A no-screen-in-code check — the only place this claim could go false

Next

The waiting count is a variable. It dies with the process. Make it survive.

Run the sample

cd course-server
dart pub get
dart run bin/step4.dart