
MCP AppsのECショップにApps SDKの他の機能も試してみた
はじめに
前回の記事では、MCP Appsを使ってChatGPT上で動くECショップアプリを構築しました。今回はそのECショップをベースに、Apps SDK(window.openai)が提供する各種APIをひととおり組み込んで、実際にどう動くのかを検証してみます。
検証環境
- ChatGPT(Webブラウザ版)
- MCP Apps ECショップ(Vercelにデプロイ済み)
@modelcontextprotocol/ext-appsv1.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>
)}
動作確認

ボタンを押すと、チャット側に「テーラードジャケット(スプリング)について、どんなコーディネートに合うか教えてください。」のようなメッセージが送信され、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>
)}
動作確認

ポイント
- 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.locale→navigator.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でも発信していきますので是非フォローしてください!









