For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
メインナビゲーション

MCP サーバーへの UI の追加

選択した MCP ツールから任意の UI リソースを返します。

概要

カスタム UI の追加は任意です。プラグインのユースケースで、ユーザーが構造化された情報を調べる、比較する、編集する、確認する、またはその中を移動する必要がある場合に追加します。ChatGPT と Codex が UI なしでもワークフローを完了できるように、MCP ツールはコンポーネントがなくても有用に機能するようにしてください。

MCP サーバーは、選択したツール用の UI リソースを返します。 コンポーネントは ChatGPT 内の iframe で実行され、MCP Apps ブリッジ (postMessage 経由の JSON-RPC)を通じてホストと通信し、会話と並んで表示されます。 オープンな MCP Apps 標準により、互換性のある各ホストで同じ UI を実行できます。

MCP Apps からの導入

ChatGPT は、MCP サーバーが返す UI 向けに、オープンな MCP Apps 標準を実装しています。 MCP Apps は、サーバーがツールと UI リソースを関連付ける方法と、 iframe がホストと通信する方法を定義します。

新しい UI では、次のようにします。

  1. _meta.ui.resourceUri で UI リソースを宣言します。
  2. 初期化、通知、ツール呼び出し、メッセージ、モデルに公開するコンテキストには、 postMessage 経由の ui/* JSON-RPC ブリッジを使用します。
  3. コンポーネントを描画しないクライアントでもモデルがワークフローを完了できるように、ツールは UI なしでも有用に機能するようにしてください。

標準を優先したこの基盤により、ChatGPT と、MCP Apps に対応するほかのホストで同じ UI を実行できます。

標準の実装に進む際は、MCP Apps 仕様を参照してください。

ChatGPT 拡張機能の追加

MCP Apps のフローが動作するようになったら、 共通仕様でカバーされていない機能にのみ window.openai を使用します。 これらの任意の拡張機能は、移植可能な UI の基盤に組み込むことなく、 ChatGPT での使い勝手を向上させられます。

共通フィールドとメソッドの優先

共通仕様でカバーされている機能には、MCP Apps のフィールドやメソッドを使用してください。

目的MCP Apps 標準ChatGPT 互換エイリアス
ツールと UI リソースの関連付け_meta.ui.resourceUri_meta["openai/outputTemplate"]
ツール入力の受信ui/initialize + ui/notifications/tool-inputwindow.openai.toolInput
ツール結果の受信ui/notifications/tool-resultwindow.openai.toolOutput
UI からのツール呼び出しtools/callwindow.openai.callTool
フォローアップメッセージの送信ui/messagewindow.openai.sendFollowUpMessage

互換エイリアスは、既存の連携で引き続き利用できます。新しい UI では、中央の列にある共通フィールドとブリッジメソッドを使用してください。

例として、次のようなものがあります。

  • window.openai.requestCheckout による Instant Checkout
  • window.openai.uploadFilewindow.openai.selectFileswindow.openai.getFileDownloadUrl による ChatGPT のファイル処理
  • window.openai.requestModal によるホスト制御のモーダル
  • window.openai.widgetStatewindow.openai.setWidgetState によるウィジェット状態の永続化

各拡張機能が利用可能かを検出し、実用的な範囲でフォールバックを用意してください。

const openai = typeof window !== "undefined" ? window.openai : undefined;

if (openai?.requestModal) {
  await openai.requestModal({
    /* ... */
  });
} else {
  // Fallback behavior for hosts without this extension.
}

ホスト名や製品名による条件分岐は避けてください。UI に必要な機能が利用できるかを確認します。

拡張機能のシグネチャと使用例については、window.openai コンポーネントブリッジの リファレンスを参照してください。

任意で利用できる OpenAI コンポーネントライブラリ

@openai/apps-sdk-ui コンポーネントライブラリは、 ChatGPT のコンテナーに合う既製のボタン、カード、入力コントロール、 レイアウトの基本要素を提供します。 基本コンポーネントを作り直すことなく、 統一されたスタイルを実現したい場合に使用してください。

GitHub 上の UI サンプルリポジトリも参考にできます。

表示形式の選択

まずはインライン UI を使用し、ワークフローで必要な場合にのみ、より広い表示領域をリクエストしてください。ユーザーが結果を理解したり、タスクを完了したりできる範囲で、最もコンパクトな表示形式を選びます。

インラインカード

要点を絞った結果や確認事項、少数のアクションを表示するには、インラインカードを使用します。カード内で完結する構成にし、深い階層への移動は避けてください。

インラインカードの例

視覚情報が豊富な同種の選択肢を少数提示し、ユーザーがざっと見比べて選ぶ必要がある場合は、インラインカルーセルを使用します。

インラインカルーセルの例

全画面表示

地図、編集用キャンバス、詳細な閲覧など、より広い表示領域を必要とする高度なタスクには、全画面表示を使用します。全画面表示でも利用できる ChatGPT のコンポーザーと併用できるように設計してください。

全画面 UI の例

ピクチャーインピクチャー

ライブセッション、ゲーム、動画など、会話を続けている間も表示し続ける必要がある進行中のアクティビティには、ピクチャーインピクチャーを使用します。

ピクチャーインピクチャー UI の例

レイアウト、操作、ビジュアルデザイン、アクセシビリティに関する詳しいガイダンスは、 UI ガイドラインを参照してください。

データ処理と UI 描画の分離

分離パターン

すべてのツール呼び出しにウィジェットテンプレートを付けると、ChatGPT が iframe を過度に再描画する可能性があります。より適切なパターンは、次のようにデータ処理ツールと描画ツールを分離することです。

  • データツール は、データの取得、計算、変更を行い、ツール結果のみを返します。
  • 描画ツール は、最終的なデータを受け取り、ウィジェットテンプレートを返します。

この構成により、モデルは取得したデータに対して知的な処理を行ってから、ユーザーに UI を表示するかを判断できます。そのため、ユーザーが明示した具体的な目標を達成できる可能性が大幅に高まります。

このパターンは MCP Apps アーキテクチャの一部です。

実際に、多くの UI 連携では次のように役割を分けています。

  • 検索・取得ツール(データ優先): ウィジェットテンプレートを付けずに、 ID とメタデータを返します。
  • 描画ツール(例:render_listings_widget): 準備済みの ID リストを受け取り、 ウィジェットを描画します。

_meta.ui.resourceUri を含めるのは描画ツールのみにしてください。

処理を分離した呼び出しフロー

推奨する呼び出しフローは次のとおりです。

  1. モデルがデータツール(例:roll_dice)を呼び出します。
  2. モデルがデータツールから structuredContent を受け取ります。
  3. モデルがそのデータを使って描画ツールを呼び出します。
  4. モデルが確認した最終的なコンテキストを使って、ウィジェットを一度だけ描画します。

例:不動産検索での追加の質問

プラグインが物件カードと地図を表示する一方で、サーバー側の search ツールは 大まかな条件(市区町村、価格、寝室数、バスルーム数)での絞り込みにしか対応しておらず、 学区では絞り込めないとします。

ユーザーが「この中で Richmond Primary School の学区内にある物件はどれですか?」と質問した場合、処理を分離しておくと、次のように対応できます。

  1. search が広い条件で検索を実行し、候補となる物件の ID とメタデータを返します。
  2. モデルが追加の質問に合わせて候補を絞り込みます。
  3. モデルが絞り込み後の ID だけを指定して render_listings_widget を呼び出します。
  4. ウィジェットが最終的に絞り込まれた物件を描画します。

ベストプラクティス:

  • データツールは再利用できるようにします。後続の処理につなげられるよう、完全な structuredContent を返してください。
  • 描画ツールの役割は表示に限定します。描画ハンドラーにビジネスロジックを混在させないでください。
  • 描画ツールの説明に依存関係を明記します(例: 「必ず最初に roll_dice を呼び出してください」)。
  • 再実行は意図したときだけ行われるようにします。「もう一度振る」などの UI 内の操作では、ウィジェットを再マウントせずに、UI からデータツールを直接呼び出せるようにしてください。

処理を分離した実装例

例(処理を分離したサイコロツール):

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod/v3";

const TEMPLATE_URI = "ui://widget/dice.html";

const server = new McpServer(
  { name: "Decoupled dice", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

// The widget only renders the latest tool result.
// Re-roll calls the data tool directly to avoid remounting the widget.
const widgetHtml = `
  <div style="font-family: system-ui; padding: 8px;">
    <div style="font-size: 20px; margin-bottom: 6px;">
      Result: <span id="out">—</span>
    </div>
    <button id="reroll">Re-roll</button>
  </div>

  <script>
    const outputEl = document.getElementById("out");
    const rerollButton = document.getElementById("reroll");
    const pendingRequests = new Map();
    let nextRequestId = 1;
    let latestToolInput;
    let latestToolOutput;

    function render(result) {
      outputEl.textContent = String(result?.value ?? "—");
    }

    function request(method, params) {
      const id = nextRequestId++;
      window.parent.postMessage({ jsonrpc: "2.0", id, method, params }, "*");
      return new Promise((resolve, reject) => {
        pendingRequests.set(id, { resolve, reject });
      });
    }

    window.addEventListener(
      "message",
      (event) => {
        if (event.source !== window.parent) return;
        const message = event.data;
        if (!message || message.jsonrpc !== "2.0") return;

        if (message.id !== undefined && pendingRequests.has(message.id)) {
          const pending = pendingRequests.get(message.id);
          pendingRequests.delete(message.id);
          if (message.error) pending.reject(message.error);
          else pending.resolve(message.result);
          return;
        }

        if (message.method === "ui/notifications/tool-input") {
          latestToolInput = message.params;
        }

        if (message.method === "ui/notifications/tool-result") {
          latestToolOutput = message.params?.structuredContent;
          render(latestToolOutput);
        }
      },
      { passive: true }
    );

    rerollButton.onclick = async () => {
      const sides = latestToolOutput?.sides ?? latestToolInput?.sides ?? 6;
      const next = await request("tools/call", {
        name: "roll_dice",
        arguments: { sides },
      });
      if (next?.structuredContent) {
        render(next.structuredContent);
      }
    };
  </script>
`.trim();

server.registerResource("dice-widget", TEMPLATE_URI, {}, async () => ({
  contents: [
    {
      uri: TEMPLATE_URI,
      mimeType: "text/html;profile=mcp-app",
      text: widgetHtml,
      _meta: { ui: { prefersBorder: true } },
    },
  ],
}));

// 1) Data tool: no output template, returns chainable structuredContent.
server.registerTool(
  "roll_dice",
  {
    title: "Roll dice",
    description: "Roll an N-sided die and return { sides, value }.",
    inputSchema: { sides: z.number().int().min(2) },
    outputSchema: {
      sides: z.number().int().min(2),
      value: z.number().int().min(1),
    },
    _meta: {
      "openai/toolInvocation/invoking": "Rolling…",
      "openai/toolInvocation/invoked": "Rolled.",
    },
  },
  async ({ sides }) => {
    const value = 1 + Math.floor(Math.random() * sides);
    return {
      structuredContent: { sides, value },
      content: [{ type: "text", text: `Rolled ${value} on ${sides} sides.` }],
    };
  }
);

// 2) Render tool: owns the template and requires data from roll_dice.
server.registerTool(
  "render_dice_widget",
  {
    title: "Render dice widget",
    description:
      "Render the dice widget from roll data. First call roll_dice, then pass its sides and value to this tool.",
    inputSchema: {
      sides: z.number().int().min(2),
      value: z.number().int().min(1),
    },
    outputSchema: {
      sides: z.number().int().min(2),
      value: z.number().int().min(1),
    },
    _meta: {
      ui: { resourceUri: TEMPLATE_URI },
      "openai/toolInvocation/invoking": "Rendering…",
      "openai/toolInvocation/invoked": "Rendered.",
    },
  },
  async ({ sides, value }) => ({
    structuredContent: { sides, value },
    content: [
      {
        type: "text",
        text: `Showing a ${sides}-sided roll: ${value}.`,
      },
    ],
  })
);

export default server;

状態の管理

MCP サーバーが提供する UI では、次の 3 種類の状態を扱います。

状態の種類管理主体保持期間
業務データ(正本)MCP サーバーまたは外部サービス長期タスク、チケット、ドキュメント
UI の状態(一時的)UI インスタンスUI インスタンスが有効な間選択された行、展開されたパネル、並び順
セッションをまたぐ状態(永続的)自分で管理するストレージセッションや会話をまたいで保持保存したフィルター、表示モード、ワークスペース

各値は、その管理を担うシステムに保持します。UI では、ツールの結果から正本となるデータを描画し、その上に一時的な表示状態を反映してください。

MCP server or external service

├── Authoritative business data


UI

├── Ephemeral presentation state

└── Rendered view = business data + UI state

サーバーでの業務データの保持

業務データは信頼できる情報源です。UI だけに保存しないでください。ユーザーが操作すると、次の処理が行われます。

  1. UI が MCP ツールを呼び出します。
  2. サーバーがリクエストを検証し、データを更新します。
  3. サーバーが更新後の正本データのスナップショットを返します。
  4. UI が、引き続き適用できる表示状態を保持しながら、スナップショットを描画します。

モデルと UI の両方が新しい状態を把握できるよう、十分な構造化コンテンツを返してください。これにより、UI を読み込めない場合でも、会話を通じて有用な情報を提供できます。

UI 内での一時的な状態の保持

選択中の項目、開いているパネル、編集中のフィルターなど、表示だけに影響する値には、フレームワークの状態管理を使います。描画された UI インスタンスは、それぞれ独自の状態を持ちます。

選択内容や確定前の編集内容をモデルに伝える必要がある場合は、 ui/update-model-context を通じてその情報を送信します。これは、モデルが参照できるコンテキストを更新するための、 ホストを問わず利用できる MCP Apps の仕組みです。

ChatGPT には、ウィジェット単位で状態を保持するオプション機能もあります。

  • window.openai.widgetState から現在のスナップショットを読み取ります。
  • window.openai.setWidgetState(state) で新しいスナップショットを書き込みます。

setWidgetState は同期処理です。UI の状態に意味のある変更が生じるたびに呼び出してください。 await で待機する処理はありません。

import { useState } from "react";

export function TaskList({ tasks }) {
  const [state, setState] = useState(
    window.openai?.widgetState ?? { selectedId: null }
  );

  function selectTask(selectedId) {
    const nextState = { ...state, selectedId };
    setState(nextState);
    window.openai?.setWidgetState?.(nextState);
  }

  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id}>
          <button
            type="button"
            aria-pressed={state.selectedId === task.id}
            onClick={() => selectTask(task.id)}
          >
            {task.title}
          </button>
        </li>
      ))}
    </ul>
  );
}

ウィジェットの状態は、描画された 1 つの UI インスタンスに属します。業務データの正本や永続ストレージとして使わないでください。

モデルへの画像の共有

画像を扱う UI では、次の構造化されたウィジェット状態の形式を使います。

  • modelContent:モデルに渡すテキストまたは JSON
  • privateContent:モデルには渡さない、UI 専用の状態
  • imageIds:後続のターンでモデルに渡すファイル ID
window.openai.setWidgetState({
  modelContent: "Review the currently selected images.",
  privateContent: {
    currentView: "image-viewer",
    filters: ["crop", "sharpen"],
  },
  imageIds: ["file_123", "file_456"],
});

含めるファイル ID は、window.openai.uploadFile でアップロードしたファイル、 window.openai.selectFiles で選択したファイル、ツール入力のファイルパラメーターを通じて受け取ったファイル、または ツール結果のファイル参照を通じて返されたファイルのものに限定してください。

セッションをまたぐ状態のサーバーへの保存

会話、デバイス、セッションが変わっても保持する必要がある設定やデータは、自分で管理するストレージに保存します。MCP サーバーが各リクエストを正しいアカウントに対応付けられるよう、ユーザーを認証してください。

永続ストレージを追加する際は、次の点に注意してください。

  • 対話型 UI の操作に支障がないよう、レイテンシを十分に低く抑えてください。
  • サーバー側の認可で非公開データを保護します。
  • データレジデンシーとコンプライアンスの要件を考慮して計画します。
  • 再試行や同時に動作する複数の UI インスタンスからのトラフィックにレート制限を適用します。
  • 既存の会話を壊さずに移行できるよう、保存するオブジェクトにバージョンを付けます。

主要な状態の保存には localStorage を使わないでください。UI は分離された iframe 内で動作します。 ブラウザストレージでは、デバイス間やセッション間でデータを確実に共有・保持できません。

コンポーネントプロジェクトのひな形作成

MCP Apps ブリッジと任意で利用できる ChatGPT 拡張機能を理解したところで、コンポーネントプロジェクトのひな形を作成します。

ベストプラクティスとして、コンポーネントのコードはサーバーロジックから分離します。一般的な構成は次のとおりです。

plugin-ui/
  server/            # MCP server (Python or Node)
  web/               # Component bundle source
    package.json
    tsconfig.json
    src/component.tsx
    dist/component.js   # Build output

プロジェクトを作成し、依存関係をインストールします(Node 18 以降を推奨)。

cd plugin-ui/web
npm init -y
npm install react@^18 react-dom@^18
npm install -D typescript esbuild

コンポーネントにドラッグ&ドロップ、グラフなどのライブラリが必要な場合は、この段階で追加します。バンドルサイズを抑えるため、依存関係は必要最小限にとどめてください。

React コンポーネントの作成

エントリーファイルでは、コンポーネントを root 要素にマウントし、 MCP Apps ブリッジ経由(例:ui/notifications/tool-result)で届く 最新のツール結果を基にレンダリングします。

サンプルページには、 ピザ店を一覧表示する Pizzaz などの UI サンプルがあります。

UI サンプルには、コンポーネントの実装例が含まれています。独自の UI を設計する際のひな形として活用してください。

  • Pizzaz List: お気に入り機能とアクションを促すボタンを備えた、順位付きのカードリスト
    Pizzaz List コンポーネントのスクリーンショット
  • Pizzaz Carousel: メディアを多用するレイアウトの例となる、Embla を使った横スクロール表示
    Pizzaz Carousel コンポーネントのスクリーンショット
  • Pizzaz Map: 全画面表示のインスペクターとホストとの状態同期を備えた Mapbox 連携
    Pizzaz Map コンポーネントのスクリーンショット
  • Pizzaz Album: 特定の場所を詳しく見るための、画像を重ねて表示するギャラリービュー
    Pizzaz Album コンポーネントのスクリーンショット
  • Pizzaz Video: オーバーレイと全画面表示の操作機能を備えた、スクリプトで制御するプレーヤー

各サンプルでは、実際の会話で使えるようにアセットをバンドルし、ホスト API と接続し、状態を構成する方法を示しています。ユースケースに最も近いものをコピーし、ツールのレスポンスに合わせてデータ層を調整してください。

React ヘルパーフック

ui/notifications/tool-result を購読するための小さなヘルパーです。

type ToolResult = { structuredContent?: unknown } | null;

export function useToolResult() {
  const [toolResult, setToolResult] = useState<ToolResult>(null);

  useEffect(() => {
    const onMessage = (event: MessageEvent) => {
      if (event.source !== window.parent) return;
      const message = event.data;
      if (!message || message.jsonrpc !== "2.0") return;
      if (message.method !== "ui/notifications/tool-result") return;
      setToolResult(message.params ?? null);
    };

    window.addEventListener("message", onMessage, { passive: true });
    return () => window.removeEventListener("message", onMessage);
  }, []);

  return toolResult;
}

toolResult?.structuredContent を基にレンダリングし、その内容は信頼できない入力として扱います。

ウィジェットのローカライズ

ホストはロケールを document.documentElement.lang に反映します。 そのロケールを使って翻訳を読み込み、日付や数値の書式を設定します。 react-intl を使った一般的なパターンは次のとおりです。

import { IntlProvider } from "react-intl";
import en from "./locales/en-US.json";
import es from "./locales/es-ES.json";

const messages: Record<string, Record<string, string>> = {
  "en-US": en,
  "es-ES": es,
};

export function PluginUI() {
  const locale = document.documentElement.lang || "en-US";
  return (
    <IntlProvider
      locale={locale}
      messages={messages[locale] ?? messages["en-US"]}
    >
      {/* Render UI with <FormattedMessage> or useIntl() */}
    </IntlProvider>
  );
}

iframe 向けのバンドル

React コンポーネントの作成が終わったら、サーバーでインライン化できる単一の JavaScript モジュールにビルドできます。

// package.json
{
  "scripts": {
    "build": "esbuild src/component.tsx --bundle --format=esm --outfile=dist/component.js"
  }
}

npm run build を実行して dist/component.js を生成します。esbuild が依存関係の不足を報告する場合は、web/ ディレクトリで npm install を実行したか、インポート先がインストール済みのパッケージ名と一致しているかを確認してください(例:@react-dnd/html5-server-sidereact-dnd-html5-server-side の違い)。

サーバーレスポンスへのコンポーネントの埋め込み

MCP Apps UI の MIME タイプ(text/html;profile=mcp-app)を指定し、 コンポーネントを MCP リソースとして公開します。 @modelcontextprotocol/ext-apps/server を使用する場合は、文字列を直接埋め込まず、 RESOURCE_MIME_TYPE を使うことを推奨します。

import {
  registerAppResource,
  RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import { readFileSync } from "node:fs";

const component = readFileSync("web/dist/component.js", "utf8");

registerAppResource(
  server,
  "project-board",
  "ui://project-board/v1.html",
  {},
  async () => ({
    contents: [
      {
        uri: "ui://project-board/v1.html",
        mimeType: RESOURCE_MIME_TYPE,
        text: `<div id="root"></div><script type="module">${component}</script>`,
        _meta: {
          ui: {
            prefersBorder: true,
            domain: "https://example.com",
            csp: {
              connectDomains: ["https://api.example.com"],
              resourceDomains: ["https://static.example.com"],
            },
          },
        },
      },
    ],
  })
);

コンポーネントをレンダリングする必要があるツールにのみ、リソース URI を関連付けます。 MCP Apps との互換性を広く確保するには、_meta.ui.resourceUri を使います。 ChatGPT は、互換性のためのエイリアスとして _meta["openai/outputTemplate"] も認識します。

リソース URI はキャッシュキーとして扱います。HTML、JavaScript、CSS に破壊的変更を加える場合は、新しい URI で公開し、そのリソースを参照するすべてのツールを更新してください。

コンテンツセキュリティポリシー(CSP)

コンポーネントの接続先やリソースの読み込み元となるドメインを、正確に宣言します。

  • connectDomains:API リクエスト用
  • resourceDomains:スクリプト、スタイル、画像、その他のアセット用
  • frameDomains:特定のオリジンの iframe をコンポーネントに埋め込む必要がある場合のみ

ネストされたフレームはデフォルトでブロックされます。各許可リストの範囲はできる限り狭くしてください。プラグインのレビュープロセスでは、宣言されたポリシーと UI の動作を照合します。

MCP サーバーと同じ 登録可能ドメインにある既存のエディターや管理インターフェースを埋め込むことができます。たとえば、https://api.example.com/mcp のサーバーは、 frameDomainshttps://app.example.com を宣言できます。申請時には、必要な 理由説明を添え、 iframe ポリシーに従ってください。これには、 共有ホスティングに関する制限とレビュー要件も含まれます。

本番環境では、コンポーネント UI テンプレートの使用を推奨します。

開発中は、React コードを変更するたびにコンポーネントのバンドルを再ビルドし、サーバーをホットリロードできます。

UI での購入手続きの提供

プラグインの UI フローでユーザーが購入手続きを行えるようにする場合は、確定前にコンポーネントで商品、価格、規約、支払いの選択肢を提示します。基盤となるカタログや注文のツールは UI なしでも役立つものにしたうえで、外部の購入フローか、利用可能な場合は埋め込み型の決済を選択してください。

外部の購入手続きを基本とする構成

外部の購入手続きは、一般提供されている推奨の方式です。コンポーネントから、販売者である自社のドメインでホストする購入フローにリンクし、そこで次の項目を処理します。

  • 料金の設定と代金の回収
  • 税金、割引、手数料
  • 配送とフルフィルメント
  • 返金、サポート、コンプライアンス

現在、承認の対象は物理的な商品の購入用プラグインに限られています。OpenAI がそのプラグインで明示的に有効にしていない限り、他の商取引カテゴリーを提供しないでください。

保存済みの支払い方法の利用

対象となる物理的な商品の購入では、任意で UI を用意し、顧客がサービスに保存済みの支払い方法を選べるようにできます。このフローでは、利用可能な保存済みの支払い方法を表示できますが、新しい支払い認証情報は収集できません。MCP サーバーが購入を処理し、正式な注文結果を返します。

ChatGPT 決済シートの利用

ChatGPT 決済シートによる埋め込み型の購入手続きは、一部のマーケットプレイスを対象としたプライベートベータ版です。すべての開発者やユーザーが利用できるわけではありません。

この機能が有効になっている連携では、 window.openai.requestCheckout で ChatGPT 決済シートが開きます。

const order = await window.openai.requestCheckout(checkoutSession);

購入フローは次の 4 つのパートで構成されます。

  1. MCP ツールが structuredContent 内に購入セッションを返します。
  2. コンポーネントが明細項目、合計金額、規約、フルフィルメントの選択肢を表示します。
  3. ユーザーが支払いを選択した後、 コンポーネントが requestCheckout(checkoutSession) を呼び出します。
  4. ChatGPT が、選択された支払いトークンを MCP サーバーの complete_checkout ツールに送信します。 このツールは、その支払い方法に対して請求を行い、 完了した注文を返します。

購入セッションには、次の情報を含める必要があります。

  • 一意のセッション ID
  • 明細項目と数量
  • 通貨の最小単位で整数として表した合計金額
  • 決済プロバイダーと加盟店のメタデータ
  • 法務、プライバシー、返金、サポートに関する必須のリンク

価格と注文ステータスは、サーバーのデータを正としてください。決済トークンを検証し、操作の冪等性を確保し、注文を永続化したうえで、確定した取引情報を含む領収書を返します。コンポーネント内だけで計算された合計金額は、決して信頼しないでください。

payment_mode: "test" を使用すると、実際の資金を動かさずに フロー全体をテストできます。キャンセル、決済の拒否、決済プロバイダーのエラーは、 コンポーネント内で処理してください。

チェックアウトセッションの全フィールド、決済プロバイダーの動作、 complete_checkout の結果の構造、決済委任の要件については、 チェックアウト API リファレンスを参照してください。