실행 가능한 MCP Apps 예제 저장소: Claude 채팅 흐름 내 풍부한 HTML "앱" UI
요약
Claude 채팅 내에서 대화형 HTML 위젯을 렌더링할 수 있는 MCP Apps 예제 저장소와 구현 가이드를 소개합니다. 모델의 컨텍스트 비용을 절감하기 위해 데이터 페이로드를 분리하는 '참조 및 가져오기' 패턴을 핵심 전략으로 제시합니다.
핵심 포인트
- MCP Apps를 통해 Claude 내부에 샌드박스화된 HTML 위젯 구현 가능
- 참조 및 가져오기(reference-and-fetch) 패턴으로 모델 토큰 비용 절감
- visibility: ['app'] 설정을 활용해 모델 컨텍스트에서 데이터 분리
- 보안을 위해 서버 측 입력값 검증 필수 (visibility는 접근 제어가 아님)
Title: 실행 가능한 MCP Apps 예제 저장소: Claude 채팅 흐름 내 풍부한 HTML "앱" UI.
저는 채팅(Claude 웹 및 데스크톱) 내부에 대화형 위젯을 렌더링하는 MCP App을 구축했습니다. 직접 실행 가능한 작은 예제 저장소와 명세(spec)만으로는 파악하는 데 시간이 걸렸던 부분들을 다루는 문서를 정리했습니다. 도움이 될까 하여 여기에 게시합니다.
저장소: https://github.com/iamneilroberts/mcp-apps-interactive-ui (MIT)
아직 Apps 확장을 사용해 보지 않으셨다면 간단한 배경 설명입니다: MCP Apps (io.modelcontextprotocol/ui, SEP-1865)는 서버가 텍스트 대신 샌드박스화된 HTML 위젯을 반환할 수 있게 해줍니다. 모델이 도구(tool)를 호출하면, 호스트가 iframe 내에 위젯을 렌더링하며, 위젯은 postMessage 브릿지를 통해 서버와 통신합니다. 명세 및 SDK: modelcontextprotocol/ext-apps.
예제
저장소의 예제 앱은 피자 빌더(Pizza Builder)입니다. 크기, 도우, 토핑을 선택하고 가격이 실시간으로 업데이트되는 것을 확인한 뒤, "주문하기(place order)"를 누르면 선택 사항이 모델로 전달됩니다. 실행 방법은 (npm install && npm run build && npm start)이며, stdio를 통해 Claude Desktop에 연결됩니다. 또한 빌드된 위젯은 호스트 없이도 확인할 수 있도록 모의 데이터(mock data)와 함께 브라우저에서 직접 열립니다. 신뢰하기 전에 검증하고 싶다면 유닛 테스트(unit tests)와 CI 워크플로우(CI workflow)가 포함되어 있습니다.
[IMG:pizza-builder.png]
MCP Apps의 메커니즘에만 집중할 수 있도록 의도적으로 지루한 도메인을 선택했습니다.
복사할 가치가 있는 패턴: 페이로드(payload)를 모델의 컨텍스트(context)에서 분리하기
이것은 MCP App을 시작하는 모든 사람에게 제가 해주고 싶은 말입니다. 풍부한 위젯은 많은 데이터를 필요로 합니다. 만약 모델이 호출한 도구로부터 그 데이터를 직접 반환한다면, 매번 모델의 컨텍스트에 데이터가 쌓이게 되어 비용이 많이 발생합니다.
해결책은 참조 및 가져오기(reference-and-fetch) 분리 방식입니다:
- 모델이 호출하는 런처 도구(launcher tool)는 아주 작은 참조값, 즉 ID만 반환합니다.
- 위젯은 브릿지를 통해 두 번째 도구를 호출함으로써 전체 페이로드를 직접 가져옵니다.
_meta.ui.visibility: ["app"]를 사용하여 모델로부터 이 두 번째 도구를 숨깁니다. 이렇게 하면 모델의 도구 목록에 나타나지 않으며 토큰 비용도 발생하지 않습니다.
저를 당황하게 했던 한 가지는 visibility: ["app"]이 도구 전체를 숨기는 것이지, 결과별 콘텐츠를 제어하는 것이 아니라는 점입니다. 단일 도구 결과 내에 앱 전용 채널이 따로 존재하지 않습니다. 따라서 모델에게 보이는 참조(ref)를 반환하는 도구 하나와, 앱 전용 데이터 도구 하나, 이렇게 실제로 두 개의 도구가 필요합니다. 제가 만든 한 앱에서는 이 방식을 통해 모델에게 보이는 페이로드(payload)를 약 9,000 토큰에서 약 130 토큰으로 줄일 수 있었습니다.
관련된 주의 사항: visibility: ["app"]은 호스트가 모델의 목록에서 도구를 제외하도록 하는 힌트(hint)입니다. 이것은 접근 제어(access control)가 아닙니다. 도구는 여전히 프로토콜 내에 존재하므로, 로우(raw) MCP 클라이언트는 이를 나열하고 호출할 수 있습니다. 따라서 서버 측에서 입력값을 반드시 검증해야 합니다.
호스트가 실제로 무엇을 허용하는지 조사하기
기능(Capabilities)과 컨텍스트(Context)는 호스트마다 다르며, 셸(shell)에서는 읽을 수 없고 오직 라이브 세션 내부에서만 확인할 수 있습니다. 그래서 저는 getHostCapabilities()와 getHostContext()를 덤프(dump)하여 렌더링하는 조사용 앱(probe app)을 만들어 실행해 보았습니다.
Claude Desktop (Claude/1.569.0, 2026-06-13):
[IMG:1]
| 기능(Capability) | 결과(Result) |
|---|---|
| openLinks | yes |
| downloadFile | yes |
| logging (sendLog) | yes |
| updateModelContext | yes ({text, image}) |
| message (sendMessage) | yes ({text}) |
| serverTools / serverResources | yes |
| sampling | no |
| app-registered tools (model→app) | no (onlisttools/oncalltool never fired) |
데스크톱 컨텍스트: 인라인(inline) 및 전체 화면(fullscreen) 디스플레이 모드(PiP 없음), 너비 736px로 고정된 컨테이너 및 최대 5000px 높이, 다크 테마, 약 76개의 CSS 테마 변수 세트 및 호스트 폰트. 아직 claude.ai 웹의 호스트 내 기능 수치는 캡처하지 않았으므로 이를 단정 짓지는 않겠습니다.
주의 사항(The gotchas)
전체 내용은 저장소의 docs/08-gotchas.md에 있습니다. 요약하자면 다음과 같습니다:
[IMG:2]
- Claude는
ui://리소스를 URI별로 캐싱하며 재연결 시 다시 가져오지 않습니다. 새로운 위젯을 배포해도 연결된 웹 클라이언트는 계속 이전 것을 렌더링합니다. 새로운 빌드 시 URI가 변경되도록 위젯 번들의 해시(hash)를 URI에 포함시키세요. 웹에서는 여전히 새로운 채팅이 필요하지만, 데스크톱은 다시 가져옵니다. - 도구 카탈로그(tool catalog)는 세션별로 잠겨 있습니다. 새로운 도구를 등록하면 기존 세션은 재연결할 때까지 이를 볼 수 없습니다.
claude.ai에는 tools/list_changed가 없습니다.
updateModelContext는 조용히 실행됩니다. 이는 모델의 다음 턴을 위해 상태를 준비(stage)할 뿐, 턴을 트리거하지 않으며 마지막 업데이트만 유지합니다. 실제로 제어권을 다시 넘기려면 그 후에 sendMessage를 호출해야 합니다. 또한, capabilities는 getHostContext()가 아니라 getHostCapabilities()에 있습니다. 이 부분 때문에 시간을 낭비했습니다.
claude.ai 웹에서 sendMessage를 호출하면 문구와 상관없이 매번 빨간색 "주의해서 사용하십시오(use caution)" 배너가 표시됩니다. 이는 호스트의 안전 UX(host safety UX)이며 끌 수 없습니다. 이 배너가 항상 나타난다고 가정하고 흐름을 설계하세요.
claude.ai로부터 progressToken은 오지 않습니다. MCP 진행 상황 알림(progress notifications)은 절대 도착하지 않습니다. 유일하게 실시간으로 확인할 수 있는 "작업 중(working on it)" 피드백은 모델이 설명하는 내용뿐입니다.
claude.ai는 앱 전용 도구(app-only tools)에 전달된 중첩된 객체 파라미터(nested object params)를 문자열로 변환(stringifies)합니다. 서버 측에서 이를 강제 변환(coerce)해야 합니다(저는 JSON.parse와 함께 z.preprocess를 사용합니다).
기본 샌드박스 CSP는 외부 이미지를 차단합니다 (img-src 'self' data:). 필요한 정확한 오리진(origins)을 _meta.ui.csp.resourceDomains에 선언하세요. claude.ai 웹과 데스크톱 앱 모두 이를 준수합니다.
샌드박스된 iframe은 window.open을 사용할 수 없습니다. app.openLink를 사용하세요.
인라인 너비(Inline width)는 약 736px로 고정되어 있습니다. 이에 맞춰 설계하세요. 높이는 유연합니다.
작은 팁
스펙 저장소(modelcontextprotocol/ext-apps)에는 전체 SDK 소스, 스키마(schema), 그리고 약 12개의 예제 서버가 있습니다. 저는 호스트 API 표면(API surface)을 기억하는 것보다 소스에서 읽는 것이 더 쉽기 때문에, 이를 로컬에 클론하여 빌드하는 동안 계속 열어둡니다. examples 디렉토리는 올바른 최소 서버(minimal server)를 확인하는 가장 빠른 방법입니다.
질문이 있다면 기꺼이 답변해 드리겠습니다. 만약 claude.ai 웹의 capability 수치를 파악하셨다면, 서로 비교해 보고 싶습니다.
제출자: /u/tribat
[link] [comments]
AI 자동 생성 콘텐츠
본 콘텐츠는 r/ClaudeAI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기