まずはオープンな標準仕様から始めましょう。
MCP Apps 仕様
を共通の UI フィールドとブリッジメソッドに使用してください。
OpenAI の拡張機能は任意です。ChatGPT 固有の機能が必要な場合は、
window.openai で利用できます。
window.openai コンポーネントブリッジ
ChatGPT は、互換性のためのエイリアスと任意の ChatGPT 拡張機能を window.openai で提供しています。
新しい UI では、共通仕様に同等の機能がある場合は MCP Apps ブリッジを使用してください。
window.openai は、
ChatGPT 固有の機能にのみ使用してください。
実装手順については、ChatGPT UI の構築をご覧ください。
確認が必要なツールでは、初期状態で toolInput がないことは想定どおりの動作です。
ChatGPT は、承認が必要な引数を承認前にウィジェットの値に読み込みません。
ユーザーが呼び出しを承認すると、
ホストが ui/notifications/tool-input を通じて引数を渡します。
できること
| 機能 | 動作 | 主な用途 |
|---|---|---|
| 状態とデータ | window.openai.toolInput | ツールの呼び出し時に渡された引数です。承認が必要なツールでは、承認後にホストが ui/notifications/tool-input を送信するまで null のままになる場合があります。 |
| 状態とデータ | 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 を受け取ります。{ library: true } を渡すと、ユーザーの ChatGPT ファイルライブラリが利用可能な場合、アップロードしたファイルをそこにも保存できます。 |
| ウィジェットランタイム 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 コンポーネントが単一のグローバル値の変更を購読できるようにしています。
export function useOpenAiGlobal<K extends keyof WebplusGlobals>(
key: K
): WebplusGlobals[K] {
return useSyncExternalStore(
(onChange) => {
const handleSetGlobal = (event: SetGlobalsEvent) => {
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);
};
},
() => window.openai[key]
);
}
UI を閉じる操作
window.openai.requestClose() を呼び出して、現在の UI を閉じるよう ChatGPT に要求します。
別の表示モードの要求
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/checkout.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"] に列挙します。列挙した各フィールドの値は、ファイルオブジェクト、
またはファイルオブジェクトの配列である必要があります。
すべてのファイルオブジェクトのスキーマで、サポートされている 4 つのプロパティをすべて宣言する必要があります:
| プロパティ | 型 | properties で宣言 | required に含める |
|---|---|---|---|
download_url | string | はい | はい |
file_id | string | はい | はい |
mime_type | string | はい | いいえ |
file_name | string | はい | いいえ |
mime_type と file_name の値は省略可能ですが、スキーマには両方の
プロパティを宣言する必要があります。 ツールをスキャン のステップとプラグインの提出時には、
4 つのプロパティのいずれかが欠けている、
download_url と file_id を必須にしていない、省略可能なプロパティのいずれかを必須にしている、または
download_url と file_id 以外のプロパティを必須にしているファイルスキーマは拒否されます。
省略可能なプロパティを追加で宣言することはできます。
次の完全なツール記述子は、1 つの必須ファイル入力を受け付けます:
{
"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 が必要な場合は、
window.openai.getFileDownloadUrl({ fileId }) の fileId の値に
file_id を使用してください。
ウィジェットの状態を永続化する際、後続のターンでモデルが画像 ID を参照できるようにするには、構造化された形式(modelContent、privateContent、imageIds)を使用してください。
ホスト連携のナビゲーション
サンドボックスのランタイムは、iframe のナビゲーション履歴を ChatGPT の UI に反映します。React Router などの標準的なルーティング API を使用すると、ホスト側のナビゲーションコントロールが UI と同期されます。
React Router の BrowserRouter を使ったルーターのセットアップ:
export default function PizzaListRouter() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<PizzaListPlugin />}>
<Route path="place/:placeId" element={<PizzaListPlugin />} />
</Route>
</Routes>
</BrowserRouter>
);
}
プログラムによるナビゲーション:
const navigate = useNavigate();
function openDetails(placeId: string) {
navigate(`place/${placeId}`, { replace: false });
}
function closeDetails() {
navigate("..", { replace: true });
}
ツール記述子のパラメーター
基本的に、ツールの記述にはこちらに記載されているフィールドを含めてください。
structuredContent を返すすべてのツールで、outputSchema を宣言してください。
クライアントが結果を検証でき、モデルが後続のツール呼び出しについて推論できるように、
スキーマにはツールが返すオブジェクトを正確に記述してください。
ツール記述子の _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 { 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(),
})
),
},
securitySchemes: [
{ type: "noauth" },
{ type: "oauth2", scopes: ["search.read"] },
],
_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 { 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 () => ({
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/widgetDescription"] | リソースの内容 | string | コンポーネントの読み込み時にモデルに渡される、人が読める形式の概要です。アシスタントによる重複した説明を減らします。 |
_meta["openai/widgetPrefersBorder"] | リソースの内容 | boolean | ChatGPT における _meta.ui.prefersBorder の、OpenAI 固有の互換エイリアスです。 |
_meta["openai/widgetCSP"] | リソースの内容 | object | ウィジェットの CSP メタデータ用の、従来の ChatGPT 互換性キーです。標準 CSP フィールドは _meta.ui.csp に置き換えられていますが、openExternal の信頼済みのリンク先を指定するには、引き続き redirect_domains が必要です。 |
_meta["openai/widgetDomain"] | リソースの内容 | string(オリジン) | ChatGPT における _meta.ui.domain の、OpenAI 固有の互換エイリアスです。 |
ChatGPT は、次の snake_case 形式のフィールド名を持つ、従来の互換性キー _meta["openai/widgetCSP"] をサポートしています。
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 は window.openai.openExternal(...) のリンクに対する redirect_domains をサポートしていません。リダイレクト先を許可リストに登録するには、引き続き _meta["openai/widgetCSP"].redirect_domains を設定する必要があります。
ツールの結果
ツールの結果には、次のフィールドを含めることができます。主なフィールドは以下のとおりです。
| キー | 型 | 必須 | 備考 |
|---|---|---|---|
structuredContent | object | 任意 | モデルとコンポーネントに渡されます。outputSchema が宣言されている場合は、そのスキーマに一致する必要があります。 |
content | string または Content[] | 任意 | モデルとコンポーネントに渡されます。 |
_meta | object | 任意 | コンポーネントにのみ渡されます。モデルには公開されません。 |
会話の記録に含まれるのは structuredContent と content のみです。ホストが _meta をコンポーネントに転送するため、モデルにデータを公開せずに UI にデータを反映できます。
ホストが提供するツール結果のメタデータ:
| キー | 設定場所 | 型 | 用途 |
|---|---|---|---|
_meta["openai/widgetSessionId"] | ツール結果の _meta(ホストから提供) | string | 現在マウントされているウィジェットインスタンスの固定 ID です。ウィジェットがアンマウントされるまで、ログとツール呼び出しの関連付けに使用します。 |
例:
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 { 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 location = _meta?.["openai/userLocation"]?.city;
const cafes = await findNearbyCafes(location);
return {
content: [{ type: "text", text: formatIntro(locale, location) }],
structuredContent: { cafes },
};
}
);