For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
기본 탐색 메뉴
Theme
개요 ChatGPT 및 Codex 사용자 문서 활용 사례

레퍼런스

ChatGPT 전용 UI 확장 기능과 메타데이터 레퍼런스입니다.

공개 표준부터 사용하세요. 공통 UI 필드와 브리지 메서드를 구현할 때는

MCP Apps 사양

문서를 따르세요. OpenAI 확장 기능은 선택 사항입니다. ChatGPT 전용 기능이 필요하면 window.openai에서 사용할 수 있습니다.

window.openai 컴포넌트 브리지

ChatGPT는 호환성을 위한 별칭과 선택적으로 사용할 수 있는 ChatGPT 확장 기능을 window.openai로 제공합니다. 새 UI를 개발할 때는 공통 사양에 해당 기능이 있으면 MCP Apps 브리지를 사용하고, ChatGPT 전용 기능에만 window.openai를 사용하세요.

단계별 구현 방법은 ChatGPT UI 만들기를 참고하세요.

사용자 확인이 필요한 도구라면 처음에 toolInput이 없는 것이 정상입니다. ChatGPT는 승인 전에 도구 인수를 위젯 값에 채우지 않습니다. 사용자가 호출을 승인하면 호스트가 ui/notifications/tool-input을 통해 인수를 전달합니다.

기능

기능 제공 항목 일반적인 용도
상태 및 데이터 window.openai.toolInput 도구 호출 시 전달된 인수입니다. 승인이 필요한 도구에서는 이 값이 null로 남아 있을 수 있습니다. 사용자가 승인한 뒤 호스트가 ui/notifications/tool-input을 보내면 인수가 전달됩니다.
상태 및 데이터 window.openai.toolOutput 도구가 반환한 structuredContent입니다. 모델이 필드 내용을 그대로 읽으므로 간결하게 작성하세요.
상태 및 데이터 window.openai.toolResponseMetadata 위젯에만 제공되는 도구 결과 메타데이터의 표준 형식입니다. ChatGPT에서는 status, call_tool_result, mcp_tool_result를 포함하며 숨겨진 _meta까지 MCP 결과 구조 전체를 보존합니다.
상태 및 데이터 window.openai.widgetState 렌더링 사이에 유지되는 UI 상태 스냅샷입니다.
상태 및 데이터 window.openai.setWidgetState(state) 새 스냅샷을 동기적으로 저장합니다. 의미 있는 UI 상호작용이 일어날 때마다 호출하세요.
위젯 런타임 API window.openai.callTool(name, args) 모델이 호출할 때와 같은 방식으로 위젯에서 다른 MCP 도구를 호출합니다.
위젯 런타임 API window.openai.sendFollowUpMessage({ prompt, scrollToBottom }) 컴포넌트가 작성한 메시지를 게시하도록 ChatGPT에 요청합니다. scrollToBottom은 선택 사항이며 기본값은 true입니다. 자동 스크롤을 막으려면 false로 설정하세요.
위젯 런타임 API window.openai.uploadFile(file, { library?: boolean }) 사용자가 선택한 파일을 업로드하고 fileId를 받습니다. 사용자가 ChatGPT 파일 라이브러리를 이용할 수 있는 경우 { library: true }를 전달하면 업로드한 파일을 라이브러리에도 저장합니다.
위젯 런타임 API window.openai.selectFiles() ChatGPT 파일 라이브러리의 파일 선택기를 열고 현재 플러그인이 접근할 권한이 있는 파일을 { fileId, fileName, mimeType }[] 형식으로 반환합니다. 사용자에 따라 파일 라이브러리를 이용할 수 없으므로 이 헬퍼의 지원 여부를 확인하세요.
위젯 런타임 API window.openai.getFileDownloadUrl({ fileId }) 위젯에서 업로드했거나 파일 라이브러리에서 선택한 파일, 파일 매개변수로 전달된 파일 또는 도구가 파일 참조로 반환한 파일의 임시 다운로드 URL을 가져옵니다.
위젯 런타임 API window.openai.requestDisplayMode(...) 화면 속 화면(PiP) 또는 전체 화면 모드를 요청합니다.
위젯 런타임 API window.openai.requestModal({ params, template }) ChatGPT가 관리하는 모달을 엽니다. template을 생략하면 현재 템플릿을 사용합니다. 모달 내용을 바꾸려면 등록된 템플릿 URI를 전달하세요.
위젯 런타임 API window.openai.requestClose() 현재 위젯을 닫도록 ChatGPT에 요청합니다.
위젯 런타임 API window.openai.notifyIntrinsicHeight(...) 위젯 높이가 바뀌면 호스트에 알려 스크롤 영역의 콘텐츠가 잘리지 않게 합니다.
위젯 런타임 API window.openai.openExternal({ href, redirectUrl }) 검증된 외부 링크를 사용자의 브라우저에서 엽니다. 승인된 리디렉션 대상에는 ChatGPT가 기본적으로 ?redirectUrl=...를 추가합니다. 생략하려면 redirectUrl: false로 설정하세요.
위젯 런타임 API window.openai.setOpenInAppUrl({ href }) 전체 화면에서 표시되는 외부 링크 대상을 바꿀 수 있습니다. 설정하지 않으면 ChatGPT는 기본 동작에 따라 컴포넌트의 현재 iframe 경로를 엽니다.
컨텍스트 window.openai.theme, window.openai.displayMode, window.openai.maxHeight, window.openai.safeArea, window.openai.view, window.openai.userAgent, window.openai.locale useOpenAiGlobal로 이러한 환경 값을 읽거나 변경 사항을 구독해 화면 구성과 문구를 조정할 수 있습니다.

useOpenAiGlobal 헬퍼

많은 ChatGPT UI 프로젝트에서는 뷰를 테스트하기 쉽도록 작은 헬퍼 함수로 window.openai에 접근합니다. 다음 헬퍼는 호스트의 openai:set_globals 이벤트를 수신해 React 컴포넌트가 특정 전역 값의 변경 사항을 구독할 수 있게 합니다.

import { useSyncExternalStore } from "react";

type WebplusGlobals = {
  widgetState: { selectedId?: string | null; [key: string]: unknown } | null;
  toolInput: Record<string, unknown>;
  toolOutput: Record<string, unknown> | null;
  theme: "light" | "dark";
};
const SET_GLOBALS_EVENT_TYPE = "openai:set_globals";

export function useOpenAiGlobal<K extends keyof WebplusGlobals>(
  key: K
): WebplusGlobals[K] {
  return useSyncExternalStore(
    (onChange) => {
      const handleSetGlobal = (event: Event) => {
        if (!(event instanceof CustomEvent)) return;
        const value = event.detail?.globals?.[key];
        if (value === undefined) {
          return;
        }

        onChange();
      };

      window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal, {
        passive: true,
      });

      return () => {
        window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);
      };
    },
    () => {
      const globals = (window as Window & { openai?: WebplusGlobals }).openai;
      if (!globals) throw new Error("This hook requires a ChatGPT app host.");
      return globals[key];
    }
  );
}

UI 닫기

window.openai.requestClose()를 호출하면 ChatGPT에 현재 UI를 닫도록 요청합니다.

다른 표시 모드 요청하기

window.openai.requestDisplayMode로 인라인, 화면 속 화면 또는 전체 화면 모드를 요청하세요.

await window.openai?.requestDisplayMode({ mode: "fullscreen" });
// On mobile, picture-in-picture may be presented as fullscreen.

모달 열기

window.openai.requestModal을 사용하면 호스트가 제어하는 모달이 열립니다. 같은 MCP 서버가 등록한 다른 UI 템플릿의 URI를 전달하세요. template을 생략하면 현재 템플릿이 열립니다.

await window.openai.requestModal({
  template: "ui://widget/details.html",
});

파일 API

ChatGPT는 window.openai의 선택적 확장 기능으로 파일 업로드 및 다운로드 헬퍼를 제공합니다.

API 용도 참고 사항
window.openai.uploadFile(file, { library?: boolean }) 사용자가 선택한 파일을 업로드하고 fileId를 받습니다. { library: true }를 전달하면 현재 사용자가 ChatGPT 파일 라이브러리를 이용할 수 있는 경우 업로드한 파일을 라이브러리에도 저장합니다.
window.openai.selectFiles() 파일 라이브러리에서 기존 파일을 선택할 수 있는 창을 엽니다. [{ fileId, fileName, mimeType }]를 반환합니다. 사용자에 따라 파일 라이브러리를 이용할 수 없으므로 이 헬퍼의 지원 여부를 확인하세요.
window.openai.getFileDownloadUrl({ fileId }) 파일의 임시 다운로드 URL을 요청합니다. 위젯에서 업로드했거나 파일 라이브러리에서 선택한 파일, 파일 매개변수로 전달된 파일 또는 도구가 파일 참조로 반환한 파일에 사용할 수 있습니다.

ChatGPT 파일 라이브러리는 선택적 기능으로, 사용자에 따라 제공되지 않을 수 있습니다. window.openai.selectFiles()를 사용할 수 있다면 현재 플러그인에는 이 헬퍼가 반환한 파일에 접근할 권한이 이미 있습니다. 반환된 fileId를 window.openai.getFileDownloadUrl({ fileId })에 전달하거나 파일 매개변수를 사용하는 도구의 입력에 사용하세요.

사용자가 선택한 파일을 업로드하는 예시입니다.

const { fileId } = await window.openai.uploadFile(file, {
  library: true,
});

사용자가 이미 ChatGPT에 업로드한 파일을 선택하는 예시입니다.

if (window.openai?.selectFiles) {
  const files = await window.openai.selectFiles();
  // [{ fileId, fileName, mimeType }]
}

window.openai.selectFiles의 지원 여부를 확인하고, 파일 라이브러리를 이용할 수 없으면 window.openai.uploadFile을 사용하세요.

임시 다운로드 URL을 요청하는 예시입니다.

const { downloadUrl } = await window.openai.getFileDownloadUrl({ fileId });

파일 입력 정의하기

ChatGPT가 도구에 파일을 전달할 수 있도록 _meta["openai/fileParams"]에 각 최상위 파일 입력 필드를 나열하세요. 나열된 각 필드의 값은 파일 객체 또는 파일 객체 배열이어야 합니다.

모든 파일 객체 스키마에는 지원되는 속성 네 가지를 모두 선언해야 합니다.

속성 유형 properties에 선언 required에 포함
download_url string 예 예
file_id string 예 예
mime_type string 예 아니요
file_name string 예 아니요

mime_type과 file_name은 값이 없어도 되지만 스키마에는 두 속성을 선언해야 합니다. 도구 스캔 단계와 플러그인 제출 시에는 다음 중 하나라도 해당하면 파일 스키마가 거부됩니다. 네 가지 속성 중 하나라도 누락한 경우, download_url과 file_id 중 하나라도 필수로 지정하지 않은 경우, 두 선택적 속성 중 하나라도 필수로 지정한 경우, 또는 download_url과 file_id 이외의 속성을 필수로 지정한 경우입니다. 선택적 속성은 추가로 선언할 수 있습니다.

다음은 필수 파일 입력 하나를 받는 도구 설명자 전체 예시입니다.

{
  "name": "analyze_file",
  "title": "Analyze file",
  "description": "Analyzes a user-provided file without modifying it.",
  "inputSchema": {
    "type": "object",
    "$defs": {
      "OpenAIFile": {
        "type": "object",
        "properties": {
          "download_url": { "type": "string" },
          "file_id": { "type": "string" },
          "mime_type": { "type": "string" },
          "file_name": { "type": "string" }
        },
        "required": ["download_url", "file_id"],
        "additionalProperties": false
      }
    },
    "properties": {
      "file": { "$ref": "#/$defs/OpenAIFile" }
    },
    "required": ["file"]
  },
  "annotations": {
    "readOnlyHint": true,
    "openWorldHint": false,
    "destructiveHint": false
  },
  "_meta": {
    "openai/fileParams": ["file"]
  }
}

여러 파일을 받으려면 최상위 필드를 배열로 정의하고 items에 동일한 파일 객체 스키마를 사용하세요. 도구는 각 파일 객체 내부의 필수 속성과 별개로 최상위 파일 필드를 필수로 지정할 수 있습니다.

런타임에 ChatGPT는 필드 이름이 스네이크 케이스인 파일 값을 전달합니다.

{
  "download_url": "https://...",
  "file_id": "file_...",
  "mime_type": "image/png",
  "file_name": "input.png"
}

ChatGPT는 항상 download_url과 file_id를 포함하며 mime_type과 file_name은 생략할 수 있습니다. 위젯에 새 임시 다운로드 URL이 필요하면 file_id를 fileId 값으로 사용해 window.openai.getFileDownloadUrl({ fileId })를 호출하세요.

후속 대화에서 모델이 이미지 ID를 볼 수 있게 하려면 위젯 상태를 저장할 때 구조화된 형식(modelContent, privateContent, imageIds)을 사용하세요.

호스트와 연동되는 탐색

샌드박스 런타임은 iframe의 탐색 기록을 ChatGPT UI에 반영합니다. React Router와 같은 표준 라우팅 API를 사용하면 호스트의 탐색 컨트롤이 컴포넌트 UI와 동기화됩니다.

React Router의 BrowserRouter를 사용한 라우터 설정 예시입니다.

import {
  BrowserRouter,
  Routes,
  Route,
  Link,
  useParams,
} from "react-router-dom";
function PizzaListPlugin() {
  const { placeId } = useParams();
  return (
    <main>
      <Link to="/place/pizzeria">Example Pizzeria</Link>
      <p>{placeId ?? "Choose a place"}</p>
    </main>
  );
}

export default function PizzaListRouter() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<PizzaListPlugin />}>
          <Route path="place/:placeId" element={<PizzaListPlugin />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

코드에서 다른 경로로 이동하는 예시입니다.

import { createElement } from "react";
import { BrowserRouter, useNavigate } from "react-router-dom";
function NavigationExample() {
  const navigate = useNavigate();

  function openDetails(placeId: string) {
    navigate(`place/${placeId}`, { replace: false });
  }

  function closeDetails() {
    navigate("..", { replace: true });
  }

  return createElement(
    "div",
    null,
    createElement(
      "button",
      { onClick: () => openDetails("pizzeria") },
      "Open details"
    ),
    createElement("button", { onClick: closeDetails }, "Close details")
  );
}

도구 설명자 매개변수

기본적으로 도구 설명에는 MCP 사양에 나열된 필드가 포함되어야 합니다.

outputSchema는 structuredContent를 반환하는 모든 도구에 선언하세요. 스키마는 도구가 반환하는 객체를 정확하게 기술해야 합니다. 그래야 클라이언트가 결과를 검증하고 모델이 후속 도구 호출을 판단할 수 있습니다.

도구 설명자의 _meta 필드

도구 설명자에는 아래 _meta 필드를 사용하세요. 도구를 UI 템플릿에 연결할 때는 MCP Apps 표준 키인 _meta.ui.resourceUri를 우선 사용하세요. ChatGPT는 호환성과 선택적 확장 기능을 위해 OpenAI 전용 메타데이터를 지원합니다.

키 위치 유형 제한 용도
_meta["securitySchemes"] 도구 설명자 array 없음 _meta만 읽는 클라이언트와의 하위 호환성을 위해 보안 스킴을 동일하게 담습니다.
_meta.ui.resourceUri 도구 설명자 string (URI) 없음 UI 템플릿의 표준 리소스 URI입니다.
_meta.ui.visibility 도구 설명자 string[] 기본값 ["model", "app"] 모델과 UI 중 어느 쪽에서 도구를 사용할 수 있는지 지정합니다. 양쪽 모두에 허용할 수도 있습니다. app은 MCP Apps 프로토콜에서 UI를 나타내는 식별자입니다.
_meta["openai/outputTemplate"] 도구 설명자 string (URI) 없음 ChatGPT에서 _meta.ui.resourceUri와의 호환성을 위해 선택적으로 사용할 수 있는 OpenAI 전용 별칭입니다.
_meta["openai/profile"] 도구 설명자 boolean 선택 사항. true일 때만 프로필 도구로 지정 인증을 거쳐 현재 프로필을 반환하는 읽기 전용 도구를 식별합니다. 이 도구를 구현하면 사용자가 연결된 여러 계정을 구분하고 관리하기 쉽습니다. 이 도구 없이도 여러 계정을 연결할 수 있습니다. 여러 계정 지원을 참고하세요.
_meta["openai/widgetAccessible"] 도구 설명자 boolean 기본값 false 기존 UI 통합에서 사용하는 OpenAI 전용 호환성 필드입니다. _meta.ui.visibility와 tools/call을 함께 사용하는 방식을 권장합니다.
_meta["openai/visibility"] 도구 설명자 string public(기본값) 또는 private 기존 UI 통합에서 사용하는 OpenAI 전용 호환성 필드입니다. _meta.ui.visibility를 우선 사용하세요.
_meta["openai/toolInvocation/invoking"] 도구 설명자 string 64자 이하 도구 실행 중 표시할 짧은 상태 문구입니다.
_meta["openai/toolInvocation/invoked"] 도구 설명자 string 64자 이하 도구 실행이 끝난 후 표시할 짧은 상태 문구입니다.
_meta["openai/fileParams"] 도구 설명자 string[] 없음 파일을 나타내는 최상위 입력 필드 목록입니다. 각 필드는 { download_url, file_id, mime_type?, file_name? } 형식의 값을 받습니다.

예시:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ name: "docs-example", version: "1.0.0" });

// Local example data; replace this with your application data.
async function performSearch(query: string) {
  const documents = [
    {
      id: "intro",
      title: "Introduction to Acme",
      url: "https://example.com/intro",
    },
  ];
  return documents.filter((document) =>
    document.title.toLowerCase().includes(query.toLowerCase())
  );
}

import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
import { z } from "zod";

registerAppTool(
  server,
  "search",
  {
    title: "Public Search",
    description: "Search public documents.",
    inputSchema: { q: z.string() },
    outputSchema: {
      results: z.array(
        z.object({
          id: z.string(),
          title: z.string(),
          url: z.string(),
        })
      ),
    },
    _meta: {
      securitySchemes: [
        { type: "noauth" },
        { type: "oauth2", scopes: ["search.read"] },
      ],
      ui: { resourceUri: "ui://widget/story.html" },
      // Optional compatibility alias (ChatGPT only):
      // "openai/outputTemplate": "ui://widget/story.html",
      "openai/toolInvocation/invoking": "Searching…",
      "openai/toolInvocation/invoked": "Results ready",
    },
  },
  async ({ q }) => {
    const results = await performSearch(q);

    return {
      structuredContent: { results },
      content: [{ type: "text", text: `Found ${results.length} results.` }],
    };
  }
);

어노테이션

도구를 ‘읽기 전용’으로 표시하려면 도구 설명자에 다음 ToolAnnotations 필드를 사용하세요.

키 유형 필수 여부 참고 사항
readOnlyHint boolean 필수 도구가 정보 조회나 계산만 수행하며 대화 외부에서 데이터를 생성, 수정, 삭제하거나 외부로 전송하지 않음을 나타냅니다.
destructiveHint boolean 필수 도구가 사용자 데이터를 삭제하거나 덮어쓸 수 있음을 선언해 호스트가 실행 전에 사용자에게 명시적인 승인을 요청해야 함을 알 수 있게 합니다.
openWorldHint boolean 필수 도구가 공개 인터넷 또는 접근 범위가 한정되지 않은 외부 대상에 접근함을 선언합니다. 웹 검색과 같은 읽기 전용 작업도 포함됩니다. 접근 범위가 한정된 비공개 계정이나 워크스페이스는 외부에서 호스팅된다는 이유만으로 이 범주에 속하지 않습니다.
idempotentHint boolean 선택 동일한 인수로 도구를 다시 호출해도 실행 환경에 추가적인 영향이 없음을 선언합니다.

이 힌트는 ChatGPT나 Codex가 사용자에게 도구 호출을 설명하는 방식에만 영향을 줍니다. 서버는 자체 권한 검사 로직을 반드시 적용해야 합니다.

예시:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ name: "docs-example", version: "1.0.0" });

// Local example data; replace this with your application data.
async function fetchSavedRecipes() {
  return [{ id: "pasta", title: "Tomato pasta" }];
}

import { z } from "zod";

server.registerTool(
  "list_saved_recipes",
  {
    title: "List saved recipes",
    description: "Returns the user’s saved recipes without modifying them.",
    inputSchema: {},
    outputSchema: {
      recipes: z.array(
        z.object({
          id: z.string(),
          title: z.string(),
        })
      ),
    },
    annotations: { readOnlyHint: true },
  },
  async () => ({
    content: [],
    structuredContent: { recipes: await fetchSavedRecipes() },
  })
);

컴포넌트 리소스의 _meta 필드

컴포넌트를 제공하는 리소스 템플릿(registerResource)에 다음 키를 설정하세요. ChatGPT는 이 키를 사용해 렌더링된 iframe을 설명하고 표시 방식을 조정할 수 있으며, 메타데이터는 다른 클라이언트에 노출되지 않습니다.

키 위치 유형 용도
_meta.ui.prefersBorder 리소스 콘텐츠 boolean 지원되는 환경에서는 테두리가 있는 카드 안에 컴포넌트를 렌더링하도록 권장합니다.
_meta.ui.csp 리소스 콘텐츠 object 표준 위젯 CSP 필드인 connectDomains, resourceDomains와 선택적 필드인 frameDomains를 설정할 때 권장하는 메타데이터 키입니다.
_meta.ui.domain 리소스 콘텐츠 string (오리진) 호스팅되는 컴포넌트의 전용 오리진입니다. UI가 있는 플러그인을 제출할 때 필수이며 플러그인마다 고유해야 합니다. 기본값은 https://web-sandbox.oaiusercontent.com입니다.
_meta["openai/ui"].availableDisplayModes 리소스 콘텐츠 string[] 지원되는 표시 모드는 inline, fullscreen, pip입니다. ChatGPT가 컴포넌트를 로드하기 전에 표시 모드를 선택할 수 있게 합니다.
_meta["openai/widgetDescription"] 리소스 콘텐츠 string 컴포넌트를 로드할 때 모델에 제공하는 사람이 읽을 수 있는 요약입니다. 어시스턴트가 같은 내용을 반복해서 설명하는 일을 줄여 줍니다.
_meta["openai/widgetPrefersBorder"] 리소스 콘텐츠 boolean ChatGPT에서 _meta.ui.prefersBorder와의 호환성을 위해 제공하는 OpenAI 전용 별칭입니다.
_meta["openai/widgetCSP"] 리소스 콘텐츠 object 위젯 CSP 메타데이터를 위한 기존 ChatGPT 호환성 키입니다. 표준 CSP 필드에는 이제 _meta.ui.csp를 사용하지만, redirect_domains는 신뢰할 수 있는 openExternal 대상을 지정할 때 여전히 필요합니다.
_meta["openai/widgetDomain"] 리소스 콘텐츠 string (오리진) ChatGPT에서 _meta.ui.domain과의 호환성을 위해 제공하는 OpenAI 전용 별칭입니다.

ChatGPT는 기존 호환성 키 _meta["openai/widgetCSP"]에서 다음 snake_case 필드 이름을 지원합니다.

  • connect_domains: string[]
  • resource_domains: string[]
  • frame_domains?: string[]
  • redirect_domains?: string[]. window.openai.openExternal의 리디렉션 대상을 지정하는 ChatGPT 확장 기능입니다.

새 UI에는 일반적으로 표준 _meta.ui.csp 객체를 권장합니다. 다음 필드를 지원합니다.

  • connectDomains: string[]. 위젯이 fetch/XHR로 요청을 보낼 수 있는 도메인입니다.
  • resourceDomains: string[]. 이미지, 글꼴, 스크립트, 스타일 등 정적 리소스를 제공하는 도메인입니다.
  • frameDomains?: string[]. iframe에 삽입할 수 있는 오리진의 목록으로, 선택 사항입니다. 기본적으로 위젯은 하위 프레임을 렌더링할 수 없습니다. 플러그인은 iframe 정책에 따라 기존 편집기나 관리 인터페이스를 비롯해 자체 도메인의 페이지를 삽입할 수 있습니다. 제출 시 iframe을 사용하는 이유를 설명해야 하며, 추가 검토가 필요하거나 승인에 더 오래 걸릴 수 있습니다.

다만 _meta.ui.csp는 redirect_domains를 지원하지 않으므로 window.openai.openExternal(...) 링크의 리디렉션 대상을 허용 목록에 추가하려면 여전히 _meta["openai/widgetCSP"].redirect_domains를 설정해야 합니다.

UI를 로드하기 전에 표시 모드 선언하기

서버가 반환하는 리소스 콘텐츠에 _meta["openai/ui"].availableDisplayModes를 설정하세요. 전체 화면만 지원하는 컴포넌트라면 ChatGPT가 인라인 로딩 화면을 먼저 표시하지 않고 바로 전체 화면으로 열 수 있습니다.

리소스의 contents 배열에 다음 객체를 담아 반환하세요. html에는 컴포넌트의 HTML을 지정합니다.

const resource = {
  uri: "ui://widget/viewer.html",
  mimeType: "text/html;profile=mcp-app",
  text: html,
  _meta: {
    "openai/ui": {
      availableDisplayModes: ["fullscreen"],
    },
  },
};

["inline", "fullscreen"]처럼 컴포넌트가 지원하는 모드를 모두 나열하세요. MCP Apps 초기화 단계에서도 계속 availableDisplayModes를 선언하세요. 서버 측 선언은 초기화 플로우를 보완하며, 초기화 플로우에서는 동적 표시 모드도 지원합니다.

도구 결과

도구 결과에는 MCP 사양의 도구 결과 필드가 포함될 수 있습니다. 주요 필드는 다음과 같습니다.

키 유형 필수 여부 참고 사항
structuredContent object 선택 모델과 컴포넌트에 제공됩니다. outputSchema를 선언한 경우 해당 스키마와 일치해야 합니다.
content string 또는 Content[] 선택 모델과 컴포넌트에 제공됩니다.
_meta object 선택 컴포넌트에만 전달되며 모델에는 표시되지 않습니다.

대화 기록에는 structuredContent와 content만 나타납니다. 호스트는 _meta를 컴포넌트에 전달하므로 모델에 데이터를 노출하지 않고 UI에 필요한 데이터를 채울 수 있습니다.

호스트가 제공하는 도구 결과 메타데이터는 다음과 같습니다.

키 위치 유형 용도
_meta["openai/widgetSessionId"] 도구 결과의 _meta(호스트 제공) string 현재 마운트된 위젯 인스턴스의 고정 ID입니다. 위젯이 언마운트될 때까지 로그와 도구 호출을 연결하는 데 사용하세요.

예시:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ name: "docs-example", version: "1.0.0" });

// Local example data; replace this with your application data.
function generateZooAnimals(count: number) {
  return Array.from({ length: count }, (_, index) => ({
    id: `animal-${index + 1}`,
    name: `Penguin ${index + 1}`,
    species: "Penguin",
  }));
}

import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
import { z } from "zod";

registerAppTool(
  server,
  "get_zoo_animals",
  {
    title: "get_zoo_animals",
    inputSchema: { count: z.number().int().min(1).max(20).optional() },
    outputSchema: {
      animals: z.array(
        z.object({
          id: z.string(),
          name: z.string(),
          species: z.string(),
        })
      ),
    },
    _meta: { ui: { resourceUri: "ui://widget/widget.html" } },
  },
  async ({ count = 10 }) => {
    const animals = generateZooAnimals(count);

    return {
      structuredContent: { animals },
      content: [{ type: "text", text: `Here are ${animals.length} animals.` }],
      _meta: {
        allAnimalsById: Object.fromEntries(
          animals.map((animal) => [animal.id, animal])
        ),
      },
    };
  }
);

도구 결과로 오류 반환하기

도구 결과에 오류를 담아 반환하려면 다음 _meta 키를 사용하세요.

키 용도 유형 참고 사항
_meta["mcp/www_authenticate"] 오류 결과 string 또는 string[] OAuth를 시작하기 위한 RFC 7235 WWW-Authenticate 챌린지입니다.

클라이언트가 제공하는 _meta 필드

키 제공 시점 유형 용도
_meta["openai/locale"] 초기화 및 도구 호출 시 string (BCP 47) 요청된 로캘입니다. 이전 버전의 클라이언트는 _meta["webplus/i18n"]을 보낼 수 있습니다.
_meta["openai/userAgent"] 도구 호출 시 string 분석이나 표시 형식 조정에 참고할 수 있도록 가능한 경우에만 제공되는 사용자 에이전트 정보입니다.
_meta["openai/userLocation"] 도구 호출 시 object 대략적인 위치 정보입니다(city, region, country, timezone, longitude, latitude).
_meta["openai/subject"] 도구 호출 시 string 요청 한도 적용 및 사용자 식별을 위해 MCP 서버로 전송되는 익명화된 사용자 ID입니다.
_meta["openai/session"] 도구 호출 시 string 동일한 ChatGPT 세션 내에서 도구 호출을 연결하기 위한 익명화된 대화 ID입니다.
_meta["openai/organization"] 도구 호출 시 string 현재 ChatGPT 조직과 연결된 익명화된 조직 ID로, 사용 가능한 경우에만 제공됩니다.

실행 단계에서 제공되는 _meta["openai/userAgent"]와 _meta["openai/userLocation"]은 참고 정보일 뿐입니다. 서버는 권한을 판단할 때 이 값에 의존해서는 안 되며, 값이 없어도 동작해야 합니다. _meta["openai/userAgent"]는 가능한 경우에만 제공되는 선택적 메타데이터입니다. 어떤 호스트 인터페이스에서 서버를 호출했는지 이 값으로 안정적으로 판별할 수는 없습니다.

예시:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ name: "docs-example", version: "1.0.0" });

// Local example data; replace this with your application data.
async function findNearbyCafes(city: unknown) {
  return city === "San Francisco"
    ? [{ name: "Example Cafe", address: "123 Example Street, San Francisco" }]
    : [];
}

function formatIntro(locale: unknown, city: unknown) {
  const location = typeof city === "string" ? city : "your area";
  return locale === "es" ? `Cafés en ${location}` : `Cafes in ${location}`;
}

import { z } from "zod";

server.registerTool(
  "recommend_cafe",
  {
    title: "Recommend a cafe",
    inputSchema: {},
    outputSchema: {
      cafes: z.array(
        z.object({
          name: z.string(),
          address: z.string(),
        })
      ),
    },
  },
  async (_args, { _meta }) => {
    const locale = _meta?.["openai/locale"] ?? "en";
    const userLocation = _meta?.["openai/userLocation"];
    const location =
      typeof userLocation === "object" &&
      userLocation !== null &&
      "city" in userLocation
        ? userLocation.city
        : undefined;
    const cafes = await findNearbyCafes(location);

    return {
      content: [{ type: "text", text: formatIntro(locale, location) }],
      structuredContent: { cafes },
    };
  }
);