window.openai는 어디로 갔나 — MCP 앱 UI가 JSON-RPC로 표준화된 이야기
MCP 앱의 화면과 호스트 통신이 window.openai 같은 전역 객체에서 JSON-RPC 2.0 over postMessage 표준으로 정리된 과정을 정리했다.
회사에서 MCP 프론트엔드를 붙이면서 알게 된 내용을 정리한다. 한 줄 요약: MCP 앱의 화면 ↔ 호스트 통신이
window.openai같은 전역 객체에서JSON-RPC 2.0 over postMessage표준으로 정리됐습니다.
들어가며
MCP tool 결과를 채팅 화면에 "그냥 텍스트"가 아니라 표·카드·대시보드 같은 UI로 보여주는 기능을 붙이려고 자료를 찾다가, 프론트 통신 규격이 최근에 표준화됐다는 걸 알게 됐습니다.
예전에 OpenAI Apps(ChatGPT Apps)를 볼 때는 iframe 안에서 window.openai.toolOutput 처럼 전역 객체로 데이터를 받아왔는데, 지금은 그게 "기본"이 아니게 됐습니다. 이 글은 그 변화가 무엇이고, 왜 그렇게 됐고, 코드가 어떻게 달라지는지를 정리한 것입니다.
1. 무엇이 바뀌었나
2025년 10월 OpenAI가 DevDay에서 Apps SDK를 내놓을 때, 앱은 ChatGPT 안에서만 돌았고 window.openai 브릿지로 호스트와 통신했습니다. 비슷한 시기에 커뮤니티에서는 MCP-UI라는 개방형 프로젝트가 같은 아이디어를 만들고 있었고요.
이 둘이 SEP-1865 라는 제안으로 병합되면서 공식 표준 MCP Apps(식별자 io.modelcontextprotocol/ui)가 됐습니다. 2026-01-26 사양으로 확정됐고, MCP의 첫 공식 UI 익스텐션입니다.
핵심 변화는 통신 방식입니다.
| 과거 (OpenAI Apps 초기) | 지금 (MCP Apps 표준) | |
|---|---|---|
| 데이터 받기 | window.openai.toolOutput | ui/notifications/tool-result 메시지 수신 |
| 툴 호출 | window.openai.callTool() | tools/call (JSON-RPC 요청) |
| 통신 방식 | 전역 객체 (호스트가 주입) | JSON-RPC 2.0 / postMessage |
| 이식성 | ChatGPT 전용 | Claude · ChatGPT · VS Code · Copilot · Goose 등 |
| 감사(audit) | 어려움 | 모든 메시지가 로그로 남음 |
오해 주의:
window.openai가 삭제된 게 아닙니다. OpenAI는 Apps SDK를 계속 지원하고 deprecate 계획도 없다고 밝혔습니다. 다만 기본(baseline)은 표준(JSON-RPC ui/ 브릿지)* 이고,window.openai는 ChatGPT 전용 기능이 필요할 때만 얹는 선택적 확장/호환 레이어로 위치가 바뀌었습니다. 결제·모달 같은window.openai전용 기능은 다른 호스트에서는 조용히 동작하지 않습니다.
2. 왜 전역 객체가 아니라 JSON-RPC인가
window.openai 방식의 근본 한계는 벤더 종속입니다. 전역 객체는 호스트(ChatGPT)가 iframe 안에 직접 주입해줘야 존재합니다. 그래서:
- 호스트마다 다른 전역 객체를 만들게 되어 이식성이 없습니다. (Claude에는
window.openai가 없습니다) - 외부 출처(다른 origin) iframe에는 호스트가 코드를 주입할 수 없습니다.
- 통신이 자바스크립트 객체 호출로 일어나 로그로 감사하기 어렵습니다.
반면 JSON-RPC over postMessage는 그냥 메시지를 주고받습니다. 그래서:
- 벤더 중립 — 어느 호스트든 같은 메시지 규격만 지키면 됩니다. "한 번 만들어 여러 호스트에서" 가 가능합니다.
- 감사 가능 — 모든 UI ↔ 호스트 통신이 로그 가능한 JSON-RPC 메시지로 남습니다.
- 사용자 동의 — 호스트가 UI에서 발생한 툴 호출에 대해 명시적 승인을 요구하거나, 수상하면 렌더 전에 차단할 수 있습니다.
즉, 브라우저의 벤더 전용 API가 표준으로 수렴하는 과정과 똑같습니다. 표준이 생기기 전엔 벤더 API로 빨리 출시하고, 표준이 생기면 표준을 기본으로 삼는 흐름이죠.
3. 어떻게 동작하나 (라이프사이클)
동작을 한 장으로 보면 이렇습니다. 두 개의 통신선이 있다는 점이 핵심입니다 — 서버→호스트는 기존 MCP(JSON-RPC), 호스트↔iframe은 postMessage 위의 JSON-RPC.
주요 메시지만 추리면:
| 방향 | 메서드 | 역할 |
|---|---|---|
| iframe → host | ui/initialize | 핸드셰이크. 테마·컨텍스트를 받음 |
| iframe → host | ui/notifications/initialized | 준비 완료 신호 |
| host → iframe | ui/notifications/tool-input | 툴 인자 전달 (승인 게이트가 있으면 승인 후) |
| host → iframe | ui/notifications/tool-result | 툴 결과 전달 (핵심) — content + structuredContent |
| iframe → host | tools/call | UI에서 툴 재호출 |
| iframe → host | ui/message | 대화에 메시지 전송 |
tool-result에서 데이터가 두 갈래로 나뉘는 게 중요합니다. structuredContent는 UI가 그릴 풍부한 데이터, content는 모델이 읽을 요약 텍스트입니다. 둘을 분리해서 모델 컨텍스트를 불필요하게 키우지 않습니다. (_meta는 위젯 전용으로, 모델에게는 노출되지 않고 컴포넌트로만 전달됩니다)
4. 코드로 보기
4-1. 서버 — tool 결과 반환
tool 정의가 _meta.ui.resourceUri로 자기 화면(HTML 리소스)을 가리키고, 결과는 structuredContent/content로 나눠 반환합니다.
server.tool("get_tasks", "할 일 목록 조회", async () => {
const tasks = [
{ id: 1, title: "MCP 서버 만들기", status: "done" },
{ id: 2, title: "UI Bridge 적용", status: "todo" },
]
return {
structuredContent: { tasks }, // UI가 그릴 데이터
content: [{ type: "text", text: "할 일 목록을 불러왔습니다." }], // 모델용 텍스트
_meta: { fetchedAt: new Date().toISOString() }, // 위젯 전용 (모델 비노출)
}
})4-2. 위젯(iframe) — 표준 메시지 수신
위젯은 전역 객체를 찾는 게 아니라 message 이벤트를 듣고 ui/notifications/tool-result를 골라 structuredContent를 렌더합니다.
window.addEventListener("message", (event) => {
// 부모 창이 보낸 메시지만
if (event.source !== window.parent) return
const msg = event.data
if (!msg || msg.jsonrpc !== "2.0") return
if (msg.method !== "ui/notifications/tool-result") return
const data = msg.params?.structuredContent // { tasks: [...] }
render(data) // 화면 갱신
}, { passive: true })4-3. Before / After
같은 "데이터 받기"를 예전 방식과 표준 방식으로 비교하면:
// 과거 — 벤더 전역 객체 (ChatGPT 전용)
const tasks = window.openai.toolOutput.tasks
// 지금 — 표준 메시지 (호스트 중립)
// (위 4-2의 리스너에서) const tasks = msg.params.structuredContent.tasks핵심 차이는 "어디서 왔는지 알 수 없는 전역 변수를 읽느냐" vs "명시적으로 도착한 메시지를 처리하느냐" 입니다. 후자는 어느 호스트에서도 동일하게 동작하고, 로그로 남습니다.
5. 그래서 무엇이 달라지나
- 커스텀 브릿지를 처음부터 새로 설계할 필요가 줄어듭니다. 이미 표준과 SDK(
@modelcontextprotocol/ext-apps)가 있어서, 메시지 규격을 표준에 맞추면 그대로 얹을 수 있습니다. - 한 번 만들면 여러 호스트에서 재사용됩니다. 표준 필드(
_meta.ui.resourceUri,ui://,ui/*)만 지키면 Claude·ChatGPT·VS Code·Copilot 등에서 동작합니다. - 만약 기존에
window.openai에 의존하는 코드가 있다면, 표준 메서드로 옮기되 ChatGPT 전용 기능은 feature-detect 후 없으면 우아하게 폴백하는 패턴이 권장됩니다. - 보안 관점에서도 이득입니다. 위젯은 sandboxed iframe에서 돌고, 서버가
_meta의 CSP로 필요한 외부 도메인만 선언하며, 모든 통신이 감사 가능한 메시지로 남습니다.
한 줄 정리
MCP 앱 UI는 이제 전역 객체가 아니라 JSON-RPC 메시지(ui/ over postMessage)* 로 호스트와 대화합니다.
window.openai는 사라진 게 아니라 "선택적 벤더 확장"으로 물러났고, 이식성·감사·보안을 얻기 위해 표준을 기본으로 삼는 흐름입니다.
참고 링크
- MCP Apps 발표 (Model Context Protocol 블로그): https://blog.modelcontextprotocol.io/posts/2026-01-26-mcp-apps/
- OpenAI Apps SDK — MCP Apps compatibility: https://developers.openai.com/apps-sdk/mcp-apps-in-chatgpt
- OpenAI Apps SDK — Quickstart / Build your ChatGPT UI: https://developers.openai.com/apps-sdk/quickstart
- MCP Apps 사양 (2026-01-26): https://github.com/modelcontextprotocol/ext-apps
프론트엔드 개발자이자 여행·기록·경제에 관심이 많은 사람. 직접 겪은 것만 씁니다.