MCP AppsのECショップにApps SDKの他の機能も試してみた

MCP AppsのECショップにApps SDKの他の機能も試してみた

2026.03.26

はじめに

https://devio2025-ecs-main-v3.developers.io/articles/mcp-apps-ec-shops/
前回の記事では、MCP Appsを使ってChatGPT上で動くECショップアプリを構築しました。今回はそのECショップをベースに、Apps SDK(window.openai)が提供する各種APIをひととおり組み込んで、実際にどう動くのかを検証してみます。

検証環境

  • ChatGPT(Webブラウザ版)
  • MCP Apps ECショップ(Vercelにデプロイ済み)
  • @modelcontextprotocol/ext-apps v1.x

今回試した機能一覧

# 機能 API / 設定 概要
1 Tool Annotations annotations ツールの性質(読み取り専用、破壊的など)をホストに伝える
2 モーダル表示 View内オーバーレイ 商品詳細をモーダルで表示
3 sendFollowUpMessage window.openai.sendFollowUpMessage() ウィジェットからチャットにメッセージを送信
4 openExternal window.openai.openExternal() 外部URLを開く
5 locale window.openai.locale ホストのロケール情報を取得してUIをi18n対応

1. Tool Annotations — ツールの性質をホストに伝える

概要

Tool Annotationsは、MCPツールの性質をホスト(ChatGPT)に伝えるためのメタデータです。ホストはこの情報をもとに、ツールの実行に確認を求めるかどうかなどの判断に利用します。

設定可能なAnnotation

Annotation 説明 用途
readOnlyHint: true データを読み取るだけで変更しない ホストが自動実行しやすくなる
readOnlyHint: false データを変更する可能性がある 実行前に確認を求める可能性
destructiveHint: true 削除など取り消せない操作 より慎重な確認UIを表示
openWorldHint: true 外部APIやネットワークにアクセスする リスク判断に使用

ECショップでの設定例

各ツールの性質に応じて以下のように設定しました。

ツール readOnlyHint destructiveHint openWorldHint 理由
search_products true false false 検索は読み取り専用
get_product true false false 商品詳細の取得
list_categories true false false カテゴリ一覧の取得
add_to_cart false false false カートへの追加(変更あり、非破壊)
remove_from_cart false true false カートからの削除(取り消し困難)
checkout false true false 注文確定(取り消し困難)
get_cart true false false カート内容の表示
get_order true false false 注文詳細の表示

実装コード

registerAppTool(
  server,
  "search_products",
  {
    title: "商品検索",
    description: "キーワード、カテゴリ、タグで商品を検索します",
    inputSchema: { /* ... */ },
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      openWorldHint: false,
    },
  },
  async ({ keyword, category, tags }) => { /* ... */ },
);
registerAppTool(
  server,
  "checkout",
  {
    title: "注文確定",
    description: "カートの商品を注文確定します",
    annotations: {
      readOnlyHint: false,
      destructiveHint: true,   // 取り消せない操作
      openWorldHint: false,
    },
  },
  async () => { /* ... */ },
);

2. モーダル表示 — 商品詳細のオーバーレイ表示

概要

商品カードをクリックすると、商品の詳細情報をモーダル(オーバーレイ)で表示します。

ChatGPT Apps SDKにはrequestModalというAPIがありますが、これはホスト側のモーダルを開くもので、任意のコンテンツを表示する用途には向きません。そのため、View内にオーバーレイとして実装しました。

ユースケース

商品一覧から商品をタップすると、商品画像・価格・タグ・在庫状況に加え、ChatGPT専用のアクションボタン(AIに相談、外部サイト)を含むモーダルが表示されます。

実装コード

function ProductModal({ product, onClose, onAddToCart, loading }: ProductModalProps) {
  const inChatGPT = isChatGPT();

  const askAI = () => {
    sendFollowUpMessage(`「${product.name}」について教えてください`);
  };

  const openProductPage = () => {
    openExternal(`https://www.google.com/search?q=${encodeURIComponent(product.name)}`);
  };

  return (
    <div style={{
      position: "fixed", inset: 0,
      background: "rgba(0,0,0,0.5)",
      display: "flex", alignItems: "center", justifyContent: "center",
      zIndex: 50,
    }}>
      <div style={{
        background: "var(--color-background-primary)",
        borderRadius: "var(--border-radius-lg)",
        padding: 16, maxWidth: 360, width: "90%",
      }}>
        {/* 商品画像 */}
        <ProductThumb productId={product.id} imageUrl={product.imageUrl} tags={product.tags} />

        {/* 商品情報 */}
        <h2>{product.name}</h2>
        <p className="price">{product.price}</p>

        {/* アクションボタン */}
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          <button className="btn btn--primary btn--sm" onClick={() => onAddToCart(product.id)}>
            + Cart
          </button>
          {inChatGPT && (
            <button className="btn btn--sm" onClick={askAI}>{"AIに相談"}</button>
          )}
          {inChatGPT && (
            <button className="btn btn--sm" onClick={openProductPage}>{"外部サイト"}</button>
          )}
        </div>
      </div>
    </div>
  );
}

動作確認

モーダル表示

ポイント

  • requestModal(ホスト側モーダル)ではなく、View内のオーバーレイとして実装
  • モーダル内にChatGPT専用ボタン(sendFollowUpMessage、openExternal)を配置することで、複数のSDK機能を組み合わせた体験を実現
  • isChatGPT()で判定し、ChatGPT以外の環境ではChatGPT専用ボタンを非表示にする

3. sendFollowUpMessage — ウィジェットからチャットにメッセージ送信

概要

sendFollowUpMessageは、ウィジェット(View)内のボタン操作などをきっかけに、チャットにメッセージを送信するAPIです。ユーザーの代わりにメッセージを投稿し、ChatGPTにアクションを起こさせることができます。

ユースケース

ECショップでは、商品詳細モーダルに「AIに相談」ボタンを設置しました。ボタンを押すと、その商品についてChatGPTに質問するメッセージが自動送信されます。

実装コード

View側(chatgpt-apis.ts

export function sendFollowUpMessage(prompt: string, scrollToBottom = true): boolean {
  const api = getOpenAI();
  if (!api?.sendFollowUpMessage) return false;
  api.sendFollowUpMessage({ prompt, scrollToBottom });
  return true;
}

商品詳細モーダルでの使用(App.tsx

const askAI = () => {
  sendFollowUpMessage(
    `「${product.name}」について、どんなコーディネートに合うか教えてください。`
  );
};

// モーダル内のボタン
{inChatGPT && (
  <button className="btn btn--sm" onClick={askAI}>
    {"AIに相談"}
  </button>
)}

動作確認

sendFollowUpMessage
ボタンを押すと、チャット側に「テーラードジャケット(スプリング)について、どんなコーディネートに合うか教えてください。」のようなメッセージが送信され、ChatGPTがそれに回答します。ウィジェット自体のUIは変化しません。

ポイント

  • ウィジェットのUIは変化せず、チャット側にメッセージが投稿される
  • scrollToBottom: trueを指定すると、チャットが自動スクロールされる
  • ChatGPTランタイム外(Claude Desktopなど)ではfalseを返し、何も起きない

4. openExternal — 外部URLを開く

概要

openExternalは、ウィジェットから外部URLを開くためのAPIです。ChatGPTがホスト側でURLを検証(vetting)した上で、ブラウザの新しいタブで開きます。

ユースケース

ECショップでは、商品詳細モーダルに「外部サイト」ボタンを設置し、商品名でGoogle検索するページを開くようにしました。実際のプロダクトでは、ECサイトの商品ページURLなどを設定することを想定しています。

実装コード

View側(chatgpt-apis.ts

export function openExternal(href: string, redirectUrl?: string): boolean {
  const api = getOpenAI();
  if (!api?.openExternal) return false;
  api.openExternal({ href, redirectUrl });
  return true;
}

商品詳細モーダルでの使用

const openProductPage = () => {
  openExternal(
    `https://www.google.com/search?q=${encodeURIComponent(product.name)}`
  );
};

{inChatGPT && (
  <button className="btn btn--sm" onClick={openProductPage}>
    {"外部サイト"}
  </button>
)}

動作確認

openExternal

ポイント

  • URLは自由に設定可能だが、ChatGPTホスト側でURLの検証が行われる
  • redirectUrlを指定すると、外部サイトから戻ってきたときのリダイレクト先を指定できる
  • セキュリティ上、ウィジェットから直接window.open()するのではなく、ホスト経由で開く設計になっている

5. locale — ホストのロケール情報でUIをi18n対応

概要

ChatGPTのwindow.openaiからホストのロケール情報を取得し、ウィジェットのUI表示言語を切り替える機能です。

ユースケース

ECショップでは、在庫表示ラベルをロケールに応じて日本語/英語で切り替えています。

実装コード

View側(chatgpt-apis.ts

export function getLocale(): string {
  const env = getOpenAI() as Record<string, unknown> | null;
  if (env) {
    const locale = env?.locale;
    if (typeof locale === "string") return locale;
  }
  return navigator.language || "ja-JP";
}

商品カタログでの使用

const locale = getLocale();
const isEn = !locale.startsWith("ja");

// 在庫表示の切り替え
<span className={`badge ${outOfStock ? "badge--error" : "badge--success"}`}>
  {isEn
    ? outOfStock ? "Out of stock" : "In stock"
    : product.stock}
</span>

ポイント

  • window.openai.localenavigator.language"ja-JP" の順でフォールバック
  • ChatGPTの言語設定が英語の場合は英語表示、日本語の場合は日本語表示になる
  • 在庫の有無判定やバッジ表示自体は既存機能で、locale機能で新しいのは表示言語の切り替え部分のみ

まとめ

ChatGPT Apps SDKの各機能を実際に試してみた結果をまとめます。

機能 動作結果 所感
Tool Annotations 設定は反映されるが、現時点でUI上の顕著な変化は確認できず 今後のChatGPT側の対応に期待。設定しておいて損はない
モーダル 動作OK View内オーバーレイとして実装。SDK機能の組み合わせに最適
sendFollowUpMessage 動作OK ウィジェットからチャットへの橋渡しとして便利
openExternal 動作OK URLは自由に設定可能。ホスト側でのURL検証あり
locale 動作OK フォールバック込みで安定して動作

ChatGPT Apps SDK全体の印象

ChatGPT Apps SDKは、MCP Appsのウィジェットをよりリッチなアプリケーションにするための拡張機能群です。特に以下の点が印象的でした。

  • sendFollowUpMessageが強力: ウィジェット上のユーザーアクションをチャットにブリッジできるため、「UIで選択 → AIが応答」という自然な体験を作れる
  • グレースフルデグラデーション: window.openaiの存在チェックだけで、ChatGPT以外の環境(Claude Desktopなど)でもエラーなく動作する。ChatGPT専用機能はisChatGPT()で出し分ければ良い

一方で、Tool AnnotationsやrequestDisplayMode(フルスクリーン)など、設定しても現時点では目に見える変化が少ない機能もありました。これらはSDKとして仕様は定義されているものの、ChatGPT側の対応が今後進むことで効果が出てくると思われます。

MCP Appsでウィジェットを開発する際は、まずホスト非依存の部分を @modelcontextprotocol/ext-apps で実装し、その上にChatGPT Apps SDKの機能を任意で載せる、というレイヤード構成がおすすめです。

さいごに

マッハチームではMCP Appsをはじめとした様々な発信を行っています!Xでも発信していきますので是非フォローしてください!
https://x.com/mach_asset_pro

この記事をシェアする

FacebookHatena blogX

関連記事