MCP Apps — ui://를 통한 상호작용 UI 리소스
텍스트만 반환하는 도구 출력에는 에이전트가 보여줄 수 있는 것에 한계가 있습니다. MCP Apps(SEP-1724, 2026년 1월 26일 공식화)는 도구가 샌드박스 처리된 상호작용 HTML(sandboxed interactive HTML)을 반환하게 하며, Claude Desktop, ChatGPT, Cursor, Goose, VS Code 안에 인라인으로 렌더링됩니다. 대시보드, 폼, 지도, 3D 장면을 하나의 확장(extension)으로 모두 제공할 수 있습니다. 이 lesson은 ui:// 리소스 스킴(resource scheme), text/html;profile=mcp-app MIME, iframe 샌드박스 postMessage 프로토콜, 그리고 서버가 HTML을 렌더링하게 할 때 생기는 보안 표면(security surface)을 살펴봅니다.
유형: Build
언어: Python (표준 라이브러리, UI 리소스 발행기), HTML (샘플 앱)
선수 학습: Phase 13 · 07 (MCP 서버), Phase 13 · 10 (리소스)
소요 시간: 약 75분
학습 목표
- 도구 호출에서
ui:// 리소스를 반환하고 올바른 MIME과 메타데이터(metadata)를 설정합니다.
_meta.ui.resourceUri, _meta.ui.csp, _meta.ui.permissions로 도구의 관련 UI를 선언합니다.
- UI에서 호스트(host)로 통신하기 위한 iframe 샌드박스 postMessage JSON-RPC를 구현합니다.
- UI에서 시작되는 공격을 방어하는 CSP와 permissions-policy 기본값을 적용합니다.
문제
2025년 무렵의 visualize_timeline 도구는 "다음은 시간순으로 정리된 노트 14개입니다: ..."와 같은 응답을 반환할 수 있었습니다. 그러나 이것은 결국 한 문단의 텍스트일 뿐이고, 사용자가 실제로 원하는 것은 직접 조작 가능한 상호작용 타임라인입니다. MCP Apps 이전에는 선택지가 두 가지뿐이었습니다. 클라이언트별 위젯 API(Claude artifacts, OpenAI Custom GPT HTML)를 따로 구현하거나, UI 자체를 제공하지 않는 것입니다.
MCP Apps(SEP-1724, 2026년 1월 26일 배포)는 이 계약을 표준화합니다. 도구 결과에는 URI가 ui://...이고 MIME이 text/html;profile=mcp-app인 resource가 들어갑니다. 호스트는 제한된 CSP와 함께 샌드박스된 iframe 안에서 이를 렌더링하며, 명시적으로 허용하지 않는 한 네트워크 접근을 막습니다. iframe 안의 UI는 작은 postMessage JSON-RPC 방언(dialect)을 통해 호스트에 메시지를 보냅니다.
호환되는 모든 클라이언트(Claude Desktop, ChatGPT, Goose, VS Code)는 같은 ui:// 리소스를 같은 방식으로 렌더링합니다. 서버 하나, HTML 번들 하나, 범용 UI입니다.
개념
ui:// 리소스 스킴
도구는 다음을 반환합니다.
{
"content": [
{"type": "text", "text": "Here is your notes timeline:"},
{"type": "ui_resource", "uri": "ui://notes/timeline"}
],
"_meta": {
"ui": {
"resourceUri": "ui://notes/timeline",
"csp": {
"defaultSrc": "'self'",
"scriptSrc": "'self' 'unsafe-inline'",
"connectSrc": "'self'"
},
"permissions": []
}
}
}
그 뒤 호스트는 ui://notes/timeline URI에 대해 resources/read를 호출하고 다음을 받습니다.
{
"contents": [{
"uri": "ui://notes/timeline",
"mimeType": "text/html;profile=mcp-app",
"text": "<!doctype html>..."
}]
}
Iframe 샌드박스
호스트는 샌드박스된 <iframe> 안에서 HTML을 렌더링합니다.
sandbox="allow-scripts allow-same-origin"을 사용합니다. 또는 서버 선언에 따라 더 엄격하게 설정합니다.
- 서버가 선언한 CSP(Content-Security-Policy)를 응답 헤더로 적용합니다.
- 호스트 출처(origin)의 쿠키나
localStorage에 접근할 수 없습니다.
- 네트워크 접근은 CSP의
connectSrc로 제한됩니다.
postMessage 프로토콜
Iframe은 window.postMessage를 통해 호스트와 통신합니다. 아주 작은 JSON-RPC 2.0 방언입니다.
항상 targetOrigin을 상대(peer)의 정확한 출처(origin)로 고정하고, 받는 쪽에서는 본문(payload)을 처리하기 전에 event.origin을 허용 목록(allowlist)과 비교해 검증합니다. 이 채널의 어느 쪽에서도 절대 "*"를 사용하지 않습니다. 메시지 본문에는 도구 호출과 리소스 읽기가 실려 오기 때문입니다.
window.parent.postMessage({
jsonrpc: "2.0",
id: 1,
method: "host.callTool",
params: { name: "notes_update", arguments: { id: "note-14", title: "..." } }
}, "https://host.example.com");
iframe.contentWindow.postMessage({
jsonrpc: "2.0",
id: 1,
result: { content: [...] }
}, "https://iframe.example.com");
window.addEventListener("message", (event) => {
if (event.origin !== "https://expected-peer.example.com") return;
});
UI가 호출할 수 있는 호스트 측 메서드는 다음과 같습니다.
host.callTool(name, arguments) — 서버 도구를 호출합니다.
host.readResource(uri) — MCP 리소스를 읽습니다.
host.getPrompt(name, arguments) — 프롬프트 템플릿을 가져옵니다.
host.close() — UI를 닫습니다.
모든 호출은 여전히 MCP 프로토콜을 거치며 서버의 권한을 상속합니다.
권한
_meta.ui.permissions 목록은 추가 기능을 요청합니다.
camera — 사용자의 카메라에 접근합니다. 문서 스캔 UI에 사용됩니다.
microphone — 음성 입력입니다.
geolocation — 위치입니다.
network:* — connectSrc만으로 허용되는 것보다 넓은 네트워크 접근입니다.
각 권한은 UI가 렌더링되기 전에 사용자에게 한 번씩 동의 프롬프트(prompt)로 표시됩니다.
보안 위험
iframe 안의 HTML도 여전히 HTML입니다. 새로운 공격 표면이 생깁니다.
- UI를 통한 프롬프트 인젝션(Prompt-injection via UI). 악의적인 서버 UI는 시스템 메시지처럼 보이는 텍스트를 표시해 사용자를 속일 수 있습니다. 호스트 렌더링은 서버 UI와 호스트 UI를 눈에 띄게 구분해야 합니다.
connectSrc를 통한 유출(Exfiltration). CSP가 connect-src: *를 허용하면 UI는 데이터를 어디로든 보낼 수 있습니다. 기본값은 엄격해야 합니다.
- 클릭재킹(Clickjacking). UI가 호스트의 화면 구성 요소(chrome) 위에 겹쳐집니다. 호스트는
z-index 조작을 막고 불투명도(opacity) 규칙을 강제해야 합니다.
- 포커스 탈취(Steal focus). UI가 키보드 포커스를 가져가 다음 메시지를 캡처합니다. 호스트가 이를 가로채야 합니다.
Phase 13 · 15는 MCP 보안의 일부로 이를 깊이 다룹니다. 이 lesson에서는 소개만 합니다.
ui/initialize 핸드셰이크
iframe이 로드된 뒤 postMessage로 ui/initialize를 보냅니다.
{"jsonrpc": "2.0", "id": 0, "method": "ui/initialize",
"params": {"theme": "dark", "locale": "en-US", "sessionId": "..."}}
호스트는 기능(capabilities)과 세션 토큰(session token)으로 응답합니다. UI는 이후 모든 호스트 호출에 세션 토큰을 사용합니다.
AppRenderer / AppFrame SDK 프리미티브
ext-apps SDK는 두 가지 편의 프리미티브를 제공합니다.
AppRenderer(서버 측) — React / Vue / Solid 컴포넌트를 감싸 올바른 MIME과 메타데이터를 가진 ui:// 리소스를 내보냅니다.
AppFrame(클라이언트 측) — 리소스를 받고, iframe을 마운트하며, postMessage를 중재합니다.
이를 사용할 수도 있고, HTML과 JSON-RPC를 직접 구현할 수도 있습니다.
생태계 상태
MCP Apps는 2026년 1월 26일 배포되었습니다. 2026년 4월 기준 클라이언트 지원은 다음과 같습니다.
- Claude Desktop. 2026년 1월부터 완전 지원합니다.
- ChatGPT. Apps SDK를 통해 완전 지원합니다. 밑바탕은 같은 MCP Apps 프로토콜입니다.
- Cursor. 베타입니다. 설정에서 활성화합니다.
- VS Code. Insider 빌드에서만 지원합니다.
- Goose. 완전 지원합니다.
- Zed, Windsurf. 로드맵에 있습니다.
프로덕션 서버 사례로는 대시보드, 지도 시각화, 데이터 테이블, 차트 빌더, 샌드박스 IDE 프리뷰가 있습니다.
사용해 보기
code/main.py는 노트 서버에 visualize_timeline 도구를 추가합니다. 이 도구는 ui://notes/timeline 리소스를 반환하고, 해당 URI에 대한 resources/read 핸들러는 SVG 타임라인을 포함한 작지만 완전한 HTML 번들을 반환합니다. HTML은 표준 라이브러리로 템플릿 처리됩니다. 빌드 시스템은 없습니다. 표준 라이브러리만으로 브라우저를 구동할 수는 없으므로 postMessage는 JS 주석으로 스케치되어 있습니다.
살펴볼 지점은 다음과 같습니다.
- 도구 응답의
_meta.ui는 resourceUri, CSP, 권한을 담습니다.
- HTML은 네트워크 접근 없이 렌더링됩니다. 모든 데이터는 인라인으로 들어갑니다.
- JS는
window.parent.postMessage를 통해 host.callTool을 호출합니다. 이 표준 라이브러리 데모에서는 문서화만 되어 있고 실제로 동작하지는 않습니다.
산출물 만들기
이 lesson은 outputs/skill-mcp-apps-spec.md를 만듭니다. 상호작용 UI가 도움이 될 도구가 주어지면, 이 스킬(skill)은 전체 MCP Apps 계약을 만듭니다. 여기에는 ui:// URI, CSP, 권한, postMessage 진입점(entrypoint), 보안 체크리스트가 포함됩니다.
연습문제
-
code/main.py를 실행하고 발행된 HTML을 검사합니다. HTML을 브라우저에서 직접 열고 SVG가 렌더링되는지 확인합니다. 그런 다음 UI가 host.callTool("notes_update", ...)를 호출할 때 사용할 postMessage 계약을 스케치합니다.
-
CSP를 더 엄격하게 만듭니다. 'unsafe-inline'을 제거하고 nonce 기반 스크립트 정책을 사용합니다. HTML 생성 코드에서 무엇이 바뀌어야 할까요?
-
노트를 제자리에서 편집하기 위한 폼을 가진 두 번째 UI 리소스 ui://notes/editor를 추가합니다. 사용자가 제출하면 iframe이 host.callTool("notes_update", ...)를 호출합니다.
-
UI의 공격 표면을 감사(audit)합니다. 악의적인 서버는 어디에 콘텐츠를 주입할 수 있을까요? iframe 샌드박스는 무엇을 방어하고 무엇을 방어하지 못할까요?
-
SEP-1724 스펙을 읽고, 이 장난감 구현이 사용하지 않는 MCP Apps SDK 기능 하나를 찾습니다. 힌트: 컴포넌트 수준 상태 동기화(component-level state sync)를 보세요.
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| MCP Apps | "상호작용 UI 리소스" | 2026-01-26에 배포된 SEP-1724 확장 |
ui:// | "앱 URI 스킴" | UI 번들을 위한 리소스 스킴 |
text/html;profile=mcp-app | "그 MIME" | MCP App HTML의 콘텐츠 타입 |
| iframe 샌드박스(iframe sandbox) | "렌더 컨테이너" | CSP와 권한을 적용한 UI의 브라우저 샌드박싱 |
| postMessage JSON-RPC | "UI에서 호스트로 가는 선" | 호스트 호출을 위한 작은 JSON-RPC-over-postMessage 방언 |
_meta.ui | "도구-UI 바인딩" | 도구 결과와 UI 리소스를 연결하는 메타데이터 |
| CSP | "Content-Security-Policy" | 스크립트, 네트워크, 스타일의 허용 출처를 선언함 |
| AppRenderer | "서버 SDK 프리미티브" | 프레임워크 컴포넌트를 ui:// 리소스로 변환함 |
| AppFrame | "클라이언트 SDK 프리미티브" | postMessage를 중재하는 iframe 마운트 헬퍼 |
ui/initialize | "핸드셰이크" | UI가 호스트로 보내는 첫 postMessage |
더 읽을거리