Empieza por el estándar abierto. Usa la
especificación de MCP Apps
para los campos de interfaz y los métodos del puente compartidos.
Las extensiones de OpenAI son opcionales y están en window.openai
para cuando necesites capacidades específicas de ChatGPT.
Puente de componentes window.openai
ChatGPT proporciona window.openai para alias de compatibilidad y extensiones opcionales
de ChatGPT. Las interfaces nuevas deben usar el puente de MCP Apps siempre que la especificación compartida
ofrezca un equivalente, y usar window.openai solo para
capacidades específicas de ChatGPT.
Consulta Crear una interfaz de ChatGPT para ver guías de implementación paso a paso.
Si tu herramienta requiere confirmación, considera normal que toolInput no esté disponible
al principio. ChatGPT no carga en los valores del widget los argumentos que requieren aprobación
antes de que esta se otorgue; el host los entrega a través de
ui/notifications/tool-input una vez que el usuario aprueba la llamada.
Capacidades
| Capacidad | Qué hace | Uso habitual |
|---|---|---|
| Estado y datos | window.openai.toolInput | Argumentos proporcionados al invocar la herramienta. En las herramientas que requieren aprobación, este valor puede permanecer en null hasta que el host envíe ui/notifications/tool-input después de la aprobación. |
| Estado y datos | window.openai.toolOutput | Tu structuredContent. Mantén los campos concisos; el modelo los lee tal como están. |
| Estado y datos | window.openai.toolResponseMetadata | Metadatos canónicos del resultado de la herramienta, exclusivos del widget. En ChatGPT, esto incluye status, call_tool_result y mcp_tool_result, y conserva la estructura completa del resultado de MCP, incluido el campo oculto _meta. |
| Estado y datos | window.openai.widgetState | Instantánea del estado de la interfaz que se conserva entre renderizados. |
| Estado y datos | window.openai.setWidgetState(state) | Guarda una nueva instantánea de forma síncrona; llama a este método después de cada interacción significativa con la interfaz. |
| API del entorno de ejecución del widget | window.openai.callTool(name, args) | Invoca otra herramienta MCP desde el widget (replica las llamadas iniciadas por el modelo). |
| API del entorno de ejecución del widget | window.openai.sendFollowUpMessage({ prompt, scrollToBottom }) | Pide a ChatGPT que publique un mensaje redactado por el componente. scrollToBottom es opcional, su valor predeterminado es true y puede establecerse en false para evitar el desplazamiento automático. |
| API del entorno de ejecución del widget | window.openai.uploadFile(file, { library?: boolean }) | Carga un archivo seleccionado por el usuario y recibe un fileId. Pasa { library: true } para guardar también el archivo cargado en la biblioteca de archivos de ChatGPT del usuario cuando esa biblioteca esté disponible. |
| API del entorno de ejecución del widget | window.openai.selectFiles() | Abre el selector de la biblioteca de archivos de ChatGPT y devuelve los archivos autorizados para el complemento como { fileId, fileName, mimeType }[]. Comprueba si esta función auxiliar está disponible, ya que la biblioteca de archivos podría no estar disponible para todos los usuarios. |
| API del entorno de ejecución del widget | window.openai.getFileDownloadUrl({ fileId }) | Obtén una URL de descarga temporal de un archivo cargado por el widget, seleccionado de la biblioteca de archivos, pasado mediante parámetros de archivo o devuelto mediante referencias a archivos de herramientas. |
| API del entorno de ejecución del widget | window.openai.requestDisplayMode(...) | Solicita los modos PiP o pantalla completa. |
| API del entorno de ejecución del widget | window.openai.requestModal({ params, template }) | Abre una ventana modal administrada por ChatGPT. Omite template para usar la plantilla actual o pasa el URI de una plantilla registrada para cambiar el contenido de la ventana modal. |
| API del entorno de ejecución del widget | window.openai.requestClose() | Pide a ChatGPT que cierre el widget actual. |
| API del entorno de ejecución del widget | window.openai.notifyIntrinsicHeight(...) | Informa las alturas dinámicas del widget para evitar que se recorte el contenido al desplazarse. |
| API del entorno de ejecución del widget | window.openai.openExternal({ href, redirectUrl }) | Abre un enlace externo verificado en el navegador del usuario. Para los destinos de redirección aprobados, ChatGPT agrega ?redirectUrl=... de forma predeterminada; configura redirectUrl: false para omitirlo. |
| API del entorno de ejecución del widget | window.openai.setOpenInAppUrl({ href }) | Reemplaza de forma opcional el destino externo que se muestra en pantalla completa. Si no se configura, ChatGPT conserva el comportamiento predeterminado y abre la ruta actual del iframe del componente. |
| Contexto | window.openai.theme, window.openai.displayMode, window.openai.maxHeight, window.openai.safeArea, window.openai.view, window.openai.userAgent, window.openai.locale | Señales del entorno que puedes leer o a las que puedes suscribirte mediante useOpenAiGlobal para adaptar los elementos visuales y los textos. |
Función auxiliar useOpenAiGlobal
Muchos proyectos de interfaz de ChatGPT encapsulan el acceso a window.openai en pequeñas funciones auxiliares
para que las vistas se puedan seguir probando. Esta función auxiliar de ejemplo escucha los eventos
openai:set_globals del host y permite que los componentes de React se suscriban a un único
valor global:
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]
);
}
Cerrar la interfaz
Llama a window.openai.requestClose() para pedirle a ChatGPT que cierre la interfaz actual.
Solicitar otro modo de presentación
Usa window.openai.requestDisplayMode para solicitar la presentación integrada, de imagen en imagen
o en pantalla completa:
await window.openai?.requestDisplayMode({ mode: "fullscreen" });
// On mobile, picture-in-picture may be presented as fullscreen.
Abrir una ventana modal
Usa window.openai.requestModal para abrir una ventana modal controlada por el host. Proporciona el
URI de otra plantilla de interfaz registrada por el mismo servidor MCP u omite
template para abrir la plantilla actual:
await window.openai.requestModal({
template: "ui://widget/checkout.html",
});
API de archivos
ChatGPT admite funciones auxiliares de carga y descarga de archivos como extensiones opcionales de
window.openai.
| API | Propósito | Notas |
|---|---|---|
window.openai.uploadFile(file, { library?: boolean }) | Carga un archivo seleccionado por el usuario y recibe un fileId. | Pasa { library: true } para guardar también el archivo cargado en la biblioteca de archivos de ChatGPT del usuario cuando esa biblioteca esté disponible para el usuario actual. |
window.openai.selectFiles() | Abre el selector de la biblioteca de archivos para elegir archivos existentes. | Devuelve [{ fileId, fileName, mimeType }]. Comprueba si esta función auxiliar está disponible, ya que la biblioteca de archivos podría no estar disponible para todos los usuarios. |
window.openai.getFileDownloadUrl({ fileId }) | Solicita una URL de descarga temporal para un archivo. | Funciona con archivos cargados por el widget, seleccionados de la biblioteca de archivos, pasados mediante parámetros de archivo o devueltos mediante referencias a archivos de herramientas. |
La biblioteca de archivos de ChatGPT es opcional y podría no estar disponible para todos los usuarios.
Los archivos devueltos por window.openai.selectFiles() ya están autorizados para
el complemento actual cuando la función auxiliar está disponible. Usa el fileId devuelto con
window.openai.getFileDownloadUrl({ fileId }) o en una entrada de herramienta que use
parámetros de archivo.
Carga un archivo seleccionado por el usuario:
const { fileId } = await window.openai.uploadFile(file, {
library: true,
});
Selecciona archivos que el usuario ya haya cargado en ChatGPT:
if (window.openai?.selectFiles) {
const files = await window.openai.selectFiles();
// [{ fileId, fileName, mimeType }]
}
Comprueba si window.openai.selectFiles está disponible y recurre a
window.openai.uploadFile cuando la biblioteca de archivos no esté disponible.
Solicita una URL de descarga temporal:
const { downloadUrl } = await window.openai.getFileDownloadUrl({ fileId });
Definir archivos de entrada
Para que ChatGPT pueda pasar archivos a una herramienta, enumera cada campo de archivo de entrada de nivel superior en
_meta["openai/fileParams"]. Cada campo enumerado debe resolverse en un objeto de archivo o
un arreglo de objetos de archivo.
Todo esquema de objeto de archivo debe declarar las cuatro propiedades admitidas:
| Propiedad | Tipo | Declarar en properties | Incluir en required |
|---|---|---|---|
download_url | string | Sí | Sí |
file_id | string | Sí | Sí |
mime_type | string | Sí | No |
file_name | string | Sí | No |
mime_type y file_name son valores opcionales, pero debes declarar sus
propiedades en el esquema. Tanto en el paso Analizar herramientas como al enviar el complemento, se rechaza cualquier
esquema de archivo que omita alguna de las cuatro propiedades, que no exija
download_url y file_id, que marque cualquiera de las propiedades opcionales como obligatoria o
que exija una propiedad distinta de download_url o file_id. Puedes declarar
propiedades opcionales adicionales.
Este descriptor de herramienta completo acepta un archivo de entrada obligatorio:
{
"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"]
}
}
Para aceptar más de un archivo, define el campo de nivel superior como un arreglo y usa el
mismo esquema de objeto de archivo en items. La herramienta puede exigir el campo de archivo de nivel superior
independientemente de las propiedades obligatorias dentro de cada objeto de archivo.
En tiempo de ejecución, ChatGPT pasa los valores de archivo con campos en snake case:
{
"download_url": "https://...",
"file_id": "file_...",
"mime_type": "image/png",
"file_name": "input.png"
}
ChatGPT siempre incluye download_url y file_id; puede omitir mime_type
y file_name. Usa file_id como valor de fileId para
window.openai.getFileDownloadUrl({ fileId }) cuando un widget necesite una nueva
URL de descarga temporal.
Al guardar de forma persistente el estado del widget, usa el formato estructurado (modelContent, privateContent, imageIds) si quieres que el modelo vea los ID de las imágenes en los turnos posteriores.
Navegación con soporte del host
El entorno de ejecución del sandbox refleja el historial de navegación del iframe en la interfaz de ChatGPT. Usa API de enrutamiento estándar, como React Router, y el host mantendrá sus controles de navegación sincronizados con tu interfaz.
Configuración del enrutador con BrowserRouter de React Router:
export default function PizzaListRouter() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<PizzaListPlugin />}>
<Route path="place/:placeId" element={<PizzaListPlugin />} />
</Route>
</Routes>
</BrowserRouter>
);
}
Navegación programática:
const navigate = useNavigate();
function openDetails(placeId: string) {
navigate(`place/${placeId}`, { replace: false });
}
function closeDetails() {
navigate("..", { replace: true });
}
Parámetros del descriptor de herramienta
De forma predeterminada, la descripción de una herramienta debería incluir los campos enumerados aquí.
Declara outputSchema para cualquier herramienta que devuelva structuredContent. El
esquema debería describir el objeto exacto que devuelve tu herramienta para que los clientes puedan
validar los resultados y el modelo pueda razonar sobre las llamadas posteriores a herramientas.
Campos _meta del descriptor de herramienta
Usa estos campos _meta en el descriptor de herramienta. Da preferencia a la clave estándar de MCP Apps
_meta.ui.resourceUri para vincular una herramienta con una plantilla de interfaz. ChatGPT admite
metadatos específicos de OpenAI para compatibilidad y extensiones opcionales.
| Clave | Ubicación | Tipo | Límites | Propósito |
|---|---|---|---|---|
_meta["securitySchemes"] | Descriptor de herramienta | array | Ninguno | Copia para mantener la compatibilidad con versiones anteriores de clientes que solo leen _meta. |
_meta.ui.resourceUri | Descriptor de herramienta | string (URI) | Ninguno | URI de recurso estándar para la plantilla de interfaz. |
_meta.ui.visibility | Descriptor de herramienta | string[] | valor predeterminado ["model", "app"] | Controla si una herramienta está disponible para el modelo, la interfaz o ambos. El valor app es el identificador de la interfaz en el protocolo MCP Apps. |
_meta["openai/outputTemplate"] | Descriptor de herramienta | string (URI) | Ninguno | Alias opcional de compatibilidad específico de OpenAI para _meta.ui.resourceUri en ChatGPT. |
_meta["openai/profile"] | Descriptor de herramienta | boolean | Opcional; solo true designa una herramienta de perfil | Identifica la herramienta autenticada de solo lectura que devuelve el perfil actual. Impleméntala para ayudar a los usuarios a reconocer y administrar varias cuentas conectadas. Los usuarios pueden conectar varias cuentas sin esta herramienta. Consulta Compatibilidad con varias cuentas. |
_meta["openai/widgetAccessible"] | Descriptor de herramienta | boolean | valor predeterminado false | Campo de compatibilidad específico de OpenAI que usan las integraciones de interfaz existentes; da preferencia a _meta.ui.visibility + tools/call. |
_meta["openai/visibility"] | Descriptor de herramienta | string | public (predeterminado) o private | Campo de compatibilidad específico de OpenAI que usan las integraciones de interfaz existentes; usa preferentemente _meta.ui.visibility. |
_meta["openai/toolInvocation/invoking"] | Descriptor de la herramienta | string | ≤ 64 caracteres | Texto breve de estado mientras se ejecuta la herramienta. |
_meta["openai/toolInvocation/invoked"] | Descriptor de la herramienta | string | ≤ 64 caracteres | Texto breve de estado cuando finaliza la herramienta. |
_meta["openai/fileParams"] | Descriptor de la herramienta | string[] | Ninguno | Lista de campos de entrada de nivel superior que representan archivos. Cada campo recibe { download_url, file_id, mime_type?, file_name? }. |
Ejemplo:
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.` }],
};
}
);
Anotaciones
Para etiquetar una herramienta como “de solo lectura”, usa los siguientes
campos de
ToolAnnotations
en el descriptor de la herramienta:
| Clave | Tipo | Obligatorio | Notas |
|---|---|---|---|
readOnlyHint | boolean | Obligatorio | Indica que la herramienta solo recupera o calcula información y no crea, actualiza, elimina ni envía datos fuera de la conversación. |
destructiveHint | boolean | Obligatorio | Declara que la herramienta puede eliminar o sobrescribir datos del usuario para que el host sepa que debe solicitar aprobación explícita primero. |
openWorldHint | boolean | Obligatorio | Declara que la herramienta accede a la internet pública o a entidades externas de alcance abierto, incluso mediante acciones de solo lectura, como la búsqueda web. Una cuenta o un espacio de trabajo privado de alcance delimitado no se considera de mundo abierto solo por estar alojado externamente. |
idempotentHint | boolean | Opcional | Declara que llamar a la herramienta con los mismos argumentos no tiene efectos adicionales en su entorno. |
Estas indicaciones solo influyen en cómo ChatGPT o Codex presenta la llamada a la herramienta al usuario; los servidores deben seguir aplicando su propia lógica de autorización.
Ejemplo:
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() },
})
);
Campos _meta del recurso del componente
Configura estas claves en la plantilla de recursos que sirve tu componente (registerResource). Ayudan a ChatGPT a describir y presentar el iframe renderizado sin filtrar metadatos a otros clientes.
| Clave | Ubicación | Tipo | Propósito |
|---|---|---|---|
_meta.ui.prefersBorder | Contenido del recurso | boolean | Indica que el componente debería renderizarse dentro de una tarjeta con borde cuando se admita esta opción. |
_meta.ui.csp | Contenido del recurso | object | Ubicación de metadatos preferida para los campos CSP estándar del widget: connectDomains, resourceDomains y, opcionalmente, frameDomains. |
_meta.ui.domain | Contenido del recurso | string (origen) | Origen dedicado para los componentes alojados (obligatorio al enviar un complemento con interfaz; debe ser único para cada complemento). El valor predeterminado es https://web-sandbox.oaiusercontent.com. |
_meta["openai/widgetDescription"] | Contenido del recurso | string | Resumen legible para las personas que se proporciona al modelo cuando se carga el componente y reduce las explicaciones redundantes del asistente. |
_meta["openai/widgetPrefersBorder"] | Contenido del recurso | boolean | Alias de compatibilidad específico de OpenAI para _meta.ui.prefersBorder en ChatGPT. |
_meta["openai/widgetCSP"] | Contenido del recurso | object | Clave de compatibilidad heredada de ChatGPT para los metadatos CSP del widget. _meta.ui.csp reemplaza los campos CSP estándar, pero redirect_domains sigue siendo obligatorio para los destinos de confianza de openExternal. |
_meta["openai/widgetDomain"] | Contenido del recurso | string (origen) | Alias de compatibilidad específico de OpenAI para _meta.ui.domain en ChatGPT. |
ChatGPT admite la clave de compatibilidad heredada _meta["openai/widgetCSP"] con los siguientes nombres de campo en snake_case:
connect_domains:string[]resource_domains:string[]frame_domains?:string[]redirect_domains?:string[]. Extensión de ChatGPT para los destinos de redirección dewindow.openai.openExternal.
En general, se prefiere el objeto estándar _meta.ui.csp para las nuevas interfaces de usuario. Admite lo siguiente:
connectDomains:string[]. Dominios con los que el widget puede comunicarse mediante fetch/XHR.resourceDomains:string[]. Dominios para recursos estáticos (imágenes, fuentes, scripts, estilos).frameDomains?:string[]. Lista opcional de orígenes permitidos para contenido incrustado en iframes. De forma predeterminada, los widgets no pueden renderizar marcos secundarios. Los complementos pueden incrustar contenido de su propio dominio, incluidos editores e interfaces de administración existentes, conforme a la política de iframes. Se requiere una justificación al realizar el envío, y el uso de iframes puede requerir una revisión adicional o demorar la aprobación.
Sin embargo, _meta.ui.csp no admite redirect_domains para los enlaces de window.openai.openExternal(...). Para agregar destinos de redirección a la lista de permitidos, sigue siendo necesario configurar _meta["openai/widgetCSP"].redirect_domains.
Resultados de herramientas
Los resultados de herramientas pueden contener los siguientes campos. En particular:
| Clave | Tipo | Obligatorio | Notas |
|---|---|---|---|
structuredContent | object | Opcional | Se muestra al modelo y al componente. Debe ajustarse al outputSchema declarado, si se proporciona. |
content | string o Content[] | Opcional | Se muestra al modelo y al componente. |
_meta | object | Opcional | Se entrega únicamente al componente. Se oculta al modelo. |
Solo structuredContent y content aparecen en la transcripción de la conversación. El host reenvía _meta al componente para que puedas hidratar la interfaz de usuario sin exponer los datos al modelo.
Metadatos de resultados de herramientas proporcionados por el host:
| Clave | Ubicación | Tipo | Propósito |
|---|---|---|---|
_meta["openai/widgetSessionId"] | _meta del resultado de la herramienta (proporcionado por el host) | string | ID estable de la instancia del widget montada actualmente; úsalo para correlacionar registros y llamadas a herramientas hasta que el widget se desmonte. |
Ejemplo:
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])
),
},
};
}
);
Resultado de herramienta con error
Para devolver un error en el resultado de la herramienta, usa la siguiente clave de _meta:
| Clave | Propósito | Tipo | Notas |
|---|---|---|---|
_meta["mcp/www_authenticate"] | Resultado de error | string o string[] | Desafíos WWW-Authenticate de RFC 7235 para iniciar OAuth. |
Campos de _meta que proporciona el cliente
| Clave | Cuándo se proporciona | Tipo | Propósito |
|---|---|---|---|
_meta["openai/locale"] | Inicialización + llamadas a herramientas | string (BCP 47) | Configuración regional solicitada (los clientes más antiguos pueden enviar _meta["webplus/i18n"]). |
_meta["openai/userAgent"] | Llamadas a herramientas | string | Indicación opcional del agente de usuario, proporcionada en la medida de lo posible, para análisis o formato. |
_meta["openai/userLocation"] | Llamadas a herramientas | object | Indicación de ubicación aproximada (city, region, country, timezone, longitude, latitude). |
_meta["openai/subject"] | Llamadas a herramientas | string | ID de usuario anonimizado que se envía a los servidores MCP para limitar las solicitudes e identificar al usuario |
_meta["openai/session"] | Llamadas a herramientas | string | ID de conversación anonimizado para correlacionar llamadas a herramientas dentro de la misma sesión de ChatGPT. |
_meta["openai/organization"] | Llamadas a herramientas | string | ID anonimizado de la organización asociado con la organización actual de ChatGPT, cuando está disponible. |
Durante la fase de operación, _meta["openai/userAgent"] y _meta["openai/userLocation"] son solo datos orientativos; los servidores nunca deben basarse en ellos para tomar decisiones de autorización y deben tolerar su ausencia. Trata _meta["openai/userAgent"] como metadatos opcionales que se proporcionan en la medida de lo posible, no como una forma estable de detectar desde qué interfaz del host se llama a tu servidor.
Ejemplo:
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 },
};
}
);