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 のユーザー向けドキュメント ユースケース

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. ui/* の JSON-RPC ブリッジを postMessage 経由で使用し、初期化、通知、ツール呼び出し、メッセージの送受信、モデルが参照できるコンテキストの受け渡しを行います。
  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-input window.openai.toolInput
ツールの結果を受け取る ui/notifications/tool-result window.openai.toolOutput
UI からツールを呼び出す tools/call window.openai.callTool
フォローアップメッセージを送信する ui/message window.openai.sendFollowUpMessage

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

拡張機能の例を以下に示します。

  • window.openai.requestCheckout による Instant Checkout
  • window.openai.uploadFile、window.openai.selectFiles、window.openai.getFileDownloadUrl による ChatGPT でのファイル操作
  • window.openai.requestModal によるホスト側でのモーダルの制御
  • window.openai.widgetState と window.openai.setWidgetState によるウィジェットの状態の保存

拡張機能ごとに利用可能かどうかを判定し、可能な場合はフォールバックを用意してください。

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

if (openai?.requestModal) {
  await openai.requestModal({
    template: "ui://widget/details.html",
  });
} else {
  document.body.textContent = "This host does not support modals.";
}

ホスト名や製品名による条件分岐は避けてください。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 ツールで指定できるのは、市、価格、寝室数、浴室数といった大まかな条件だけで、学区では絞り込めません。

ユーザーが「この中でリッチモンド小学校の学区にある物件はどれですか?」と尋ねた場合、ツールを分離しておくと、次のように対応できます。

  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";
import { build } from "esbuild";
import { fileURLToPath } from "node:url";

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

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

// Bundle the same browser source that is type-checked and tested directly.
const widgetBundle = await build({
  entryPoints: [fileURLToPath(new URL("./dice-widget.ts", import.meta.url))],
  bundle: true,
  format: "esm",
  platform: "browser",
  write: false,
});
const widgetScript = widgetBundle.outputFiles[0].text.replace(
  /<\/script/gi,
  "<\\/script"
);
const widgetHtml = `<!doctype html><html lang="en"><meta charset="utf-8"><title>Dice widget</title><body><script type="module">${widgetScript}</script></body></html>`;

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;

esbuild をサーバーの依存パッケージとしてインストールしてから、次のブラウザー用コードを dice-widget.ts としてサーバーファイルと同じディレクトリに保存してください。

import { App } from "@modelcontextprotocol/ext-apps";

const container = document.createElement("div");
container.style.cssText = "font-family: system-ui; padding: 8px;";
const resultLine = document.createElement("div");
resultLine.style.cssText = "font-size: 20px; margin-bottom: 6px;";
const output = document.createElement("span");
output.id = "out";
output.textContent = "—";
resultLine.append("Result: ", output);
const rerollButton = document.createElement("button");
rerollButton.id = "reroll";
rerollButton.textContent = "Re-roll";
rerollButton.disabled = true;
container.append(resultLine, rerollButton);
document.body.append(container);

const app = new App({ name: "Dice widget", version: "1.0.0" });
let latestInput: Record<string, unknown> = {};
let latestOutput: Record<string, unknown> = {};

function render(result: Record<string, unknown>) {
  latestOutput = result;
  output.textContent = String(result.value ?? "—");
}

app.ontoolinput = ({ arguments: input }) => {
  latestInput = input ?? {};
};
app.ontoolresult = (result) => {
  render(result.structuredContent ?? {});
};

// Call the data tool without remounting the widget.
rerollButton.onclick = async () => {
  const candidate = latestOutput.sides ?? latestInput.sides;
  const sides =
    typeof candidate === "number" &&
    Number.isInteger(candidate) &&
    candidate >= 2
      ? candidate
      : 6;
  const next = await app.callServerTool({
    name: "roll_dice",
    arguments: { sides },
  });
  if (next.structuredContent) render(next.structuredContent);
};

await app.connect();
rerollButton.disabled = false;

状態管理

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,
}: {
  tasks: { id: string; title: string }[];
}) {
  const [state, setState] = useState(
    window.openai?.widgetState ?? { selectedId: null }
  );

  function selectTask(selectedId: string) {
    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
const selectedFiles = await window.openai.selectFiles();

window.openai.setWidgetState({
  modelContent: "Review the currently selected images.",
  privateContent: {
    currentView: "image-viewer",
    filters: ["crop", "sharpen"],
  },
  imageIds: selectedFiles.map((file) => file.fileId),
});

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

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

会話、デバイス、セッションが変わっても維持する必要がある設定やデータは、開発者が管理するストレージに保存します。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@^19 react-dom@^19
npm install -D typescript esbuild

ドラッグ&ドロップやグラフ表示などのライブラリが必要な場合は、この段階で追加します。バンドルサイズを抑えるため、依存パッケージは必要なものに絞ってください。

React コンポーネントの作成

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

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

Pizzaz コンポーネントギャラリー

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

  • Pizzaz List: お気に入りボタンやアクションボタンを備えたランキング形式のカード一覧
    Pizzaz List コンポーネントのスクリーンショット
  • Pizzaz Carousel: メディアを多用したレイアウトの例を示す、Embla を使った横スクロール表示
    Pizzaz Carousel コンポーネントのスクリーンショット
  • Pizzaz Map: 全画面での詳細表示とホストとの状態同期に対応した Mapbox 連携
    Pizzaz Map コンポーネントのスクリーンショット
  • Pizzaz Album: 1 か所を詳しく紹介するために画像を重ねて表示するギャラリー
    Pizzaz Album コンポーネントのスクリーンショット
  • Pizzaz Video: オーバーレイと全画面表示のコントロールを備えた、スクリプトで制御するプレーヤー

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

React のヘルパーフック

ui/notifications/tool-result を購読するための簡単なヘルパーフックです。

import { useEffect, useState } from "react";

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 にロケールを反映します。このロケールに基づいて翻訳を読み込み、日付や数値の表示形式を整えます。次の例を使用する前に、npm install react-intl を web/ ディレクトリで実行してください。

import { IntlProvider, FormattedMessage } from "react-intl";

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

export function PluginUI() {
  const locale = document.documentElement.lang || "en-US";
  return (
    <IntlProvider
      locale={locale}
      messages={messages[locale] ?? messages["en-US"]}
    >
      <FormattedMessage id="greeting" defaultMessage="Hello" />
    </IntlProvider>
  );
}

iframe 用のバンドル作成

React コンポーネントを作成したら、サーバーがインラインで埋め込めるように、単一の JavaScript モジュールにまとめてビルドします。

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

npm run build を実行して dist/component.js を生成します。esbuild で依存パッケージが見つからないというエラーが出た場合は、npm install を web/ ディレクトリで実行したかどうかを確認してください。また、インポート名がインストール済みのパッケージ名と一致しているかを確認します(例:@react-dnd/html5-server-side と react-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 のサーバーは、https://app.example.com を frameDomains に指定できます。申請時には埋め込みが必要な理由を記載し、共有ホスティングに関する制限や審査要件を含む 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 リファレンスを参照してください。