Tutoriales
Las apps de ChatGPT ahora son plugins: crea su interfaz con MCP Apps
Casi todos los tutoriales de widgets del Apps SDK de ChatGPT siguen enseñando window.openai y text/html+skybridge. OpenAI ya recomienda construir primero sobre el estándar MCP Apps. Esto es lo que ha cambiado y cómo migrar.
8 min de lectura
#La respuesta corta
Si vas a crear widgets para ChatGPT en octubre de 2026 (lo que antes era un widget del Apps SDK y ahora es la interfaz de un plugin de ChatGPT), constrúyelos como MCP Apps: la tool apunta a su interfaz con _meta.ui.resourceUri, el HTML se sirve con el tipo MIME text/html;profile=mcp-app y la vista habla con el host por el puente de MCP Apps (App, de @modelcontextprotocol/ext-apps). Deja window.openai solo para lo poco que el estándar no cubre, como el pago o la subida de ficheros. Es lo que recomienda la propia OpenAI, y te deja un único widget que se pinta en ChatGPT, en Claude y en cualquier otro host de MCP Apps.
En esta guía tienes qué ha cambiado, una tabla campo a campo del Apps SDK a MCP Apps, un ejemplo de código antes y después, y las deprecaciones que conviene conocer.
#Qué ha cambiado: las apps de ChatGPT son ahora plugins
En 2026 se han movido tres cosas:
- ChatGPT es compatible del todo con MCP Apps. El changelog para desarrolladores de OpenAI lo dice sin rodeos el 22 de febrero de 2026: "ChatGPT is now fully compatible with the MCP Apps spec" (changelog). MCP Apps es la extensión abierta de MCP para interfaces interactivas, estable desde el 26 de enero de 2026 y escrita entre gente de OpenAI, de Anthropic y del proyecto MCP-UI (especificación). Si el término te suena nuevo, empieza por qué son las MCP Apps.
- Las apps pasaron a llamarse plugins. En julio de 2026 OpenAI renombró las apps de ChatGPT como plugins y sustituyó el App Directory por el Plugin Directory. El centro de ayuda de OpenAI fecha el cambio el 9 de julio de 2026; no hemos podido abrir esa página directamente, así que toma el día exacto como publicado, no como comprobado.
- La documentación para desarrolladores se ha mudado. La de interfaz vive ahora bajo
developers.openai.com/plugins, por ejemplo en Build your ChatGPT UI, que habla de "plugin" en todo momento y tiene su propio changelog de interfaz.
Por eso, al buscar "ChatGPT apps SDK widget" o "crear app para ChatGPT", salen mezclados el vocabulario viejo y el nuevo. Muchos de los primeros resultados son de finales de 2025, cuando window.openai y text/html+skybridge eran la única forma de pintar una interfaz en ChatGPT. Lo que cuentan sigue funcionando, pero ya no es el camino recomendado.
#Qué recomienda OpenAI ahora
La página de OpenAI sobre MCP Apps en ChatGPT es clara: "Use the MCP Apps field or method whenever the shared specification covers the capability". Una interfaz nueva usa los campos y los métodos del puente compartido y, cuando el flujo de MCP Apps ya funciona, se añade window.openai solo para lo que la especificación no cubre, detectando antes si existe y con una alternativa cuando se pueda.
A octubre de 2026, las extensiones exclusivas de ChatGPT que lista OpenAI son estas:
| Extensión | Para qué sirve |
|---|---|
window.openai.requestCheckout | Abre una hoja de pago integrada |
window.openai.requestModal | Abre un modal que controla el host |
window.openai.uploadFile, selectFiles, getFileDownloadUrl | Ficheros |
window.openai.widgetState, setWidgetState | Guarda el estado del widget |
Todo lo demás (recibir la entrada y el resultado de la tool, llamar a tools del servidor, abrir enlaces, mandar mensajes, avisar del tamaño, leer el tema y el idioma) tiene su equivalente en el estándar.
#Del Apps SDK a MCP Apps: la tabla de campos
La guía de migración oficial del proyecto ext-apps traduce cada pieza. En el servidor:
| Apps SDK | MCP Apps | Notas |
|---|---|---|
_meta["openai/outputTemplate"] | _meta.ui.resourceUri | La URI ui:// de la vista |
_meta["openai/widgetAccessible"] | _meta.ui.visibility | true es que la lista incluye "app" |
_meta["openai/visibility"] | _meta.ui.visibility | "private" es que la lista no incluye "model" |
text/html+skybridge | text/html;profile=mcp-app | Exportado como RESOURCE_MIME_TYPE |
_meta["openai/widgetCSP"] | _meta.ui.csp | resource_domains, connect_domains, frame_domains pasan a resourceDomains, connectDomains, frameDomains |
_meta["openai/widgetDomain"] | _meta.ui.domain | Origen propio del sandbox |
_meta["openai/widgetPrefersBorder"] | _meta.ui.prefersBorder | Preferencia de borde |
En la vista:
| Apps SDK | MCP Apps |
|---|---|
window.openai.toolInput | app.ontoolinput = (params) => params.arguments |
window.openai.toolOutput | app.ontoolresult = (params) => params.structuredContent |
window.openai.callTool(name, args) | app.callServerTool({ name, arguments: args }) |
window.openai.notifyIntrinsicHeight(height) | app.sendSizeChanged({ width, height }) |
window.openai.openExternal({ href }) | app.openLink({ url: href }) |
window.openai.theme, locale, displayMode | app.getHostContext()?.theme, locale, displayMode |
La _meta.ui.visibility por defecto es ["model", "app"]: puede llamar a la tool el modelo y también la vista. Con ["app"] la escondes del modelo y tu interfaz la sigue pudiendo llamar.
#Antes y después: el servidor
Una tool que enseña una lista de pedidos, primero con los metadatos del Apps SDK. El handler es el mismo en las dos versiones.
const handler = async ({ status }: { status: string }) => {
const orders = await listOrders(status)
return {
content: [{ type: 'text', text: `${orders.length} ${status} orders` }],
structuredContent: { orders },
}
}
// Before: Apps SDK metadata
server.registerResource('Orders view', 'ui://view/orders.html', { mimeType: 'text/html+skybridge' }, async () => ({
contents: [{
uri: 'ui://view/orders.html',
mimeType: 'text/html+skybridge',
text: ordersHtml,
_meta: { 'openai/widgetCSP': { resource_domains: ['https://cdn.example.com'] } },
}],
}))
server.registerTool('show_orders', {
title: 'Orders',
description: 'Shows the orders with a given status',
inputSchema: z.object({ status: z.string() }),
_meta: { 'openai/outputTemplate': 'ui://view/orders.html', 'openai/widgetAccessible': true },
}, handler)La misma tool con MCP Apps y los helpers de servidor de @modelcontextprotocol/ext-apps/server:
import { RESOURCE_MIME_TYPE, registerAppResource, registerAppTool } from '@modelcontextprotocol/ext-apps/server'
registerAppResource(server, 'Orders view', 'ui://view/orders.html', { description: 'Orders UI' }, async () => ({
contents: [{
uri: 'ui://view/orders.html',
mimeType: RESOURCE_MIME_TYPE, // text/html;profile=mcp-app
text: ordersHtml,
_meta: { ui: { csp: { resourceDomains: ['https://cdn.example.com'] } } },
}],
}))
registerAppTool(server, 'show_orders', {
title: 'Orders',
description: 'Shows the orders with a given status',
inputSchema: z.object({ status: z.string() }),
_meta: { ui: { resourceUri: 'ui://view/orders.html' } }, // default visibility already includes "app"
}, handler)Cuida el texto de content. Un host que no pinta MCP Apps enseña la tool como texto plano, y esa línea es lo que ve quien lo use.
#Antes y después: la vista
Con el Apps SDK la vista leía variables globales que inyectaba ChatGPT:
// Before: globals injected by ChatGPT
render(window.openai.toolOutput)
refreshButton.onclick = () => window.openai.callTool('show_orders', { status: 'open' })Con MCP Apps la vista crea un App, le pone sus handlers y después se conecta:
import { App } from '@modelcontextprotocol/ext-apps'
const app = new App({ name: 'Orders', version: '1.0.0' })
app.ontoolresult = (params) => render(params.structuredContent)
await app.connect()
refreshButton.onclick = async () => {
const result = await app.callServerTool({ name: 'show_orders', arguments: { status: 'open' } })
render(result.structuredContent)
}Hay dos detalles que pillan a casi todo el mundo. Los handlers van antes de connect(), o te puedes perder el primer resultado. Y el import necesita un bundler (el quickstart oficial usa Vite con vite-plugin-singlefile) o un módulo cargado desde un CDN, como hace el quickstart de Claude. El recorrido completo está en crear una MCP App en TypeScript.
Si aun así necesitas algo exclusivo de ChatGPT, compruébalo antes de usarlo:
if (window.openai?.requestModal) {
detailsButton.hidden = false
}#Qué está deprecado
openai/visibilityestá deprecado desde el 21 de julio de 2026. El changelog explica el motivo: su valorprivateescondía la tool del modelo sin decir si la app la podía seguir llamando. Usa_meta.ui.visibility.openai/outputTemplatesigue funcionando como alias de compatibilidad. La documentación de OpenAI dice que ChatGPT "also honors" ese campo, y que_meta.ui.resourceUries lo que te da compatibilidad con más hosts. Migra la próxima vez que toques la tool: hoy no se rompe nada.- El tipo MIME de la vista en la documentación actual de OpenAI es
text/html;profile=mcp-app. La propuesta inicial de MCP Apps usabatext/html+mcp; la especificación estable exigetext/html;profile=mcp-app, y la constanteRESOURCE_MIME_TYPEdel SDK siempre trae el valor vigente. - Las variables de tema también son estándar. Desde el 28 de mayo de 2026 ChatGPT entrega las variables CSS del host de MCP Apps en
hostContext.styles.variables, así que un único juego de estilos vale para ChatGPT y para Claude. Lo tienes en temas en MCP Apps.
Ojo con un nombre: "Skybridge" es también un framework de código abierto de Alpic para crear MCP Apps y apps de ChatGPT. Dejar el tipo MIME text/html+skybridge no tiene nada que ver con ese proyecto.
#Un solo widget para ChatGPT y Claude
En cuanto la vista habla MCP Apps, el mismo servidor y el mismo HTML sirven para los dos hosts. La documentación de Claude dice que App.connect() sin transporte "detects the host and picks the transport", y que registerAppTool y registerAppResource generan los metadatos de cada host por ti. Lo que sigue cambiando de un host a otro:
- La Content Security Policy. Los dos bloquean los orígenes que el recurso no declara. Claude restringe
frameDomainsmientras pasa una revisión de seguridad (guía de diseño), y ChatGPT bloquea los iframes anidados por defecto y revisa la política declarada al aprobar el plugin. Declara solo lo que cargas. El detalle está en la CSP de MCP Apps. ui.domain. Su valor depende del host: ponlo por host o no lo pongas.- Hosts de solo texto. Algunos clientes MCP llaman a tu tool sin pintar la interfaz. Claude Code en la terminal es uno. Qué host pinta qué lo tienes en clientes de MCP Apps.
- Las extensiones de ChatGPT. Lo que va por
window.openaisolo existe en ChatGPT. Detéctalo y dale a quien use Claude un camino que funcione sin ello.
Si tienes mucho código del Apps SDK, el proyecto ext-apps publica una Agent Skill, migrate-oai-app, que guía a un agente de código por esta misma tabla (ext-apps en GitHub). Para ver cómo encajan MCP Apps, el Apps SDK y MCP-UI en general, lee MCP Apps vs MCP-UI vs OpenAI Apps SDK.
#Dónde encaja Widgetry
Widgetry hace widgets que son MCP Apps desde el principio: un recurso ui:// servido como text/html;profile=mcp-app, una tool que apunta a él con _meta.ui.resourceUri y su CSP en _meta.ui.csp. No llevan window.openai, así que el mismo widget se pinta en ChatGPT y en Claude. Eliges una de las plantillas (un tablero, indicadores, gráficos, una tabla, un globo 3D), cambias los datos y el diseño, y lo compruebas en una vista previa que habla el mismo protocolo que los chats.
Para usarlo desde tu servidor, enlazas el widget publicado o descargas su HTML y su manifiesto y los registras con el SDK oficial de TypeScript; añadir una interfaz a tu servidor MCP explica los dos caminos. Widgetry no cubre las extensiones exclusivas de ChatGPT: si tu plugin necesita pago o subida de ficheros, esa parte la escribes tú encima.
#Lista para migrar
- Cambia
openai/outputTemplatepor_meta.ui.resourceUri(el alias sigue funcionando mientras migras). - Sirve la vista con
RESOURCE_MIME_TYPEen lugar detext/html+skybridge. - Pasa
openai/widgetCSPa_meta.ui.cspcon las claves en camelCase. - Sustituye
openai/visibilityyopenai/widgetAccessiblepor_meta.ui.visibility. - En la vista, cambia lecturas y llamadas a
window.openaipor los handlers y métodos deApp, puestos antes deconnect(). - Deja
window.openaisolo para pago, modales, ficheros y estado del widget, detrás de una comprobación. - Prueba en ChatGPT y en Claude, y lee una vez el texto alternativo como si fueras un host de solo texto.