화면을 리소스로 — 코드가 아니라 서버가 내주는 문서

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에서 열기 →