+250 skills, dinamita para tu productividad 🧨Explorar →

OpenAI MCP Extensions: crea un plugin para ChatGPT

Tabla de contenidos

Hasta ahora, una MCP App en ChatGPT era un invitado: entraba cuando el modelo la llamaba, pintaba su iframe en mitad de la conversación y desaparecía con el scroll.

Con OpenAI MCP Extensions el invitado se queda a vivir. Tu plugin puede tener icono propio en la barra lateral, una pestaña fija junto al chat, una página de ajustes con controles nativos, autocompletado cuando el usuario escribe @ y formularios con miniaturas.

OpenAI publicó el repositorio openai/mcp-extensions con la especificación, un SDK para TypeScript y otro para Python, y un plugin de ejemplo bastante completo. La versión 0.1.0 del SDK de Node salió el 29 de septiembre de 2026, así que esto está recién horneado.

En este artículo vas a encontrar:

  • Qué añade cada extensión sobre MCP y MCP Apps, y en qué plataformas funciona
  • La arquitectura y el árbol de ficheros de un plugin real, con Open-Meteo como fuente de datos
  • El código justo para entender cada pieza (el código no es lo importante aquí)
  • Una secuencia de prompts para que un agente te lo construya por fases y con criterios de verificación
  • Los límites y trampas que vas a encontrarte en esta primera versión

Si es la primera vez que oyes hablar de MCP Apps, antes date una vuelta por qué son las MCP Apps y cómo se usan. Aquí parto de esa base.

Qué son las OpenAI MCP Extensions

Son un conjunto de extensiones específicas de ChatGPT que se montan encima de dos estándares abiertos: el protocolo MCP y la extensión MCP Apps. Su objetivo, en palabras del README, es que los plugins “se sientan como funciones nativas de primera clase”.

El punto de partida importa. MCP te da tools, recursos y prompts que funcionan en cualquier cliente compatible. MCP Apps añade interfaces HTML que se renderizan en un iframe dentro de la conversación. Pero ninguno de los dos sabe nada de la barra lateral de ChatGPT, de su página de ajustes ni de su compositor.

Ahí entran las extensiones.

Técnicamente no son magia. La mayoría son campos en _meta con el prefijo openai/, capacidades que se anuncian en el initialize y algunos métodos JSON-RPC nuevos como openai/resources/write u openai/files/open. Un cliente que no las entienda las ignora, y tu servidor sigue funcionando como un MCP normal.

🔑 La documentación oficial insiste en el orden: empieza por el estándar abierto de MCP Apps y añade las extensiones de OpenAI solo cuando la interfaz necesite algo que el estándar no cubre. Tus tools tienen que seguir siendo útiles sin interfaz.

El repositorio trae tres cosas:

  1. La especificación (docs/spec.md), que describe cada extensión a nivel de protocolo.
  2. Dos SDK: @openai/mcp-extensions para TypeScript (servidor y app) y openai-mcp-extensions para Python (solo servidor).
  3. Bits & Bolts, un plugin de ejemplo que gestiona piezas CAD con un visor 3D y usa casi todas las extensiones a la vez.

¿Qué extensiones hay y dónde funciona cada una?

La especificación recoge trece funciones y su soporte varía mucho según la plataforma. Esta es la tabla resumida que publica OpenAI para el lanzamiento (Web se refiere a ChatGPT Work en el navegador, no al ChatGPT clásico):

Extensión Desktop Web iOS Android
Entrypoint global (barra lateral) Sí Sí Sí Sí
Entrypoint de hilo (pestaña) Sí Sí Sí Sí
Entrypoint de fichero Sí No No No
Ajustes estructurados Sí Sí Sí Sí
Modos de visualización Sí Sí Sí Sí
Deep links Sí Sí Sí No
Mensajes (ui/message) Sí Sí Parcial Parcial
Onboarding del plugin Sí Sí Sí Sí
Contexto del modelo Sí Sí Parcial Sí
Abrir ficheros locales Sí No No No
Recursos de fichero Sí No No No
Menciones en el compositor Sí No No No
Formularios extendidos Sí Sí No No

La lectura rápida: Desktop lo tiene todo, la web se queda sin lo que toca el sistema de ficheros y sin menciones, y el móvil es el pariente pobre en formularios.

Cada extensión resuelve un problema concreto:

  • Entrypoints: abrir tu app sin que el modelo la invoque. Hasta tres por app: global (barra lateral), de hilo (pestaña dentro de una conversación) y de fichero (visor para extensiones como .stl o .ipynb). El SDK añade un cuarto tipo, settings, y un quickAction para el entrypoint global.
  • Ajustes estructurados: tu servidor describe un esquema de ajustes y ChatGPT lo pinta con controles nativos en la página del plugin.
  • Menciones: el usuario escribe @ y busca elementos de tu plugin (personas, ficheros, canales… o ciudades).
  • Formularios extendidos: la elicitación de MCP con miniaturas, valores sugeridos y selector de recursos.
  • Contexto del modelo: tu app adjunta información al compositor como elementos que el usuario puede quitar.
  • Onboarding: una skill que se ejecuta justo después de instalar el plugin.

Mira la tabla antes de diseñar nada. Si tu caso de uso depende de las menciones, hoy solo lo tendrás en el escritorio.

Curso gratis · paso a paso

Antes de pedirle un plugin a tu agente, dale una spec

Trece funciones y cuatro plataformas son muchas piezas para improvisar. En el curso recorres el ciclo de SDD con OpenSpec: proposal, spec, diseño y lista de tareas antes de tocar código.

Entra en el curso gratis →

El plugin que vamos a construir: Tiempo, con Open-Meteo

Bits & Bolts es un gran ejemplo, pero tiene un visor 3D con Three.js, un importador de STEP en WebAssembly y más de mil líneas en el controlador de la app. Para entender la arquitectura sobra la mitad.

Vamos a diseñar Tiempo, un plugin que consulta la previsión meteorológica con la API gratuita de Open-Meteo. Si ya seguiste el tutorial de MCP Apps con mcp-use, allí construimos un widget inline que aparecía cuando el modelo lo llamaba. Aquí el objetivo es otro: que el tiempo sea una parte fija de ChatGPT.

Open-Meteo encaja por tres razones. No pide API key, tiene un endpoint de previsión y otro de geocodificación, y devuelve JSON limpio. Una llamada a https://api.open-meteo.com/v1/forecast con current=temperature_2m,weather_code,wind_speed_10m te devuelve la temperatura actual de Zaragoza sin registro ni clave.

Esto es lo que hará cada extensión en el plugin:

Extensión Qué hace en Tiempo
Entrypoint global Panel “Mis ciudades” en la barra lateral, a pantalla completa
Entrypoint de hilo Pestaña “Previsión” junto a la conversación
Ajustes estructurados Unidades de temperatura, de viento y días de previsión
Menciones Escribes @Zarag… y aparecen ciudades del geocoder
Formulario extendido Elegir ciudad con sugerencias o texto libre
Contexto del modelo Adjunta “Zaragoza, 21,8 °C, nublado” al compositor
Onboarding Pregunta tus unidades preferidas al instalar
Entrypoint de fichero (extra) Abre un .gpx y muestra la previsión a lo largo de la ruta

El último es opcional y solo funciona en Desktop. Lo dejo como extra porque obliga a tocar el sistema de ficheros, y eso trae su propia ración de seguridad.

Plugins, MCP y extensiones cambian cada pocas semanas. Cada domingo +7.200 developers compartimos lo que vamos probando con IA y 12 recursos para no perder el hilo.

Suscríbete gratis →

¿Cómo es la arquitectura del plugin?

Tres procesos hablan entre sí: ChatGPT (el host), tu servidor MCP y tu app, que vive en un iframe aislado. Open-Meteo solo habla con el servidor.

Arquitectura del plugin Tiempo: ChatGPT como host en el centro, a la izquierda la app en un iframe con CSP sin dominios externos, a la derecha el servidor MCP en Node que es el único que llama a Open-Meteo (forecast y geocoding). La app pide datos al servidor con tools/call a través del host, y el host añade por su cuenta la barra lateral, la pestaña del hilo, la página de ajustes y el compositor con menciones

La decisión de diseño más importante está en esa flecha que falta: la app nunca llama a Open-Meteo por su cuenta. Cuando necesita datos, llama a una tool del servidor (weather.forecast) a través del host, y el servidor hace el fetch.

¿Por qué tanto rodeo? Por tres motivos:

  1. El CSP del iframe se queda cerrado. Bits & Bolts declara csp: { connectDomains: [], resourceDomains: [] } en su recurso de UI. Si tu app hiciera fetch a Open-Meteo, tendrías que abrir ese dominio y justificarlo en la revisión.
  2. El modelo puede usar las mismas tools sin interfaz. weather.forecast sirve igual cuando el usuario pregunta “¿llueve mañana en Bilbao?” sin abrir nada.
  3. Controlas el límite de peticiones en un solo sitio. Open-Meteo gratis admite menos de 10.000 llamadas al día, 5.000 por hora y 600 por minuto. Con una caché en el servidor, cien pestañas abiertas no son cien llamadas.

El flujo de una apertura desde la barra lateral es así:

  1. El usuario pulsa el icono de Tiempo en la barra lateral.
  2. ChatGPT llama a la tool weather.library con {} como argumentos (la especificación obliga a aceptar el objeto vacío).
  3. El servidor devuelve structuredContent con las ciudades guardadas y su previsión.
  4. ChatGPT lee el recurso ui://tiempo/app-v1, monta el iframe a pantalla completa y le pasa ese primer resultado.
  5. La app pinta con ese resultado sin volver a llamar a la tool.

El paso 5 tiene truco y el README del SDK lo avisa: registra app.ontoolresult antes de app.connect(), porque si llamas otra vez a la tool para el primer render, la app tarda más y parpadea.

El árbol de ficheros que se genera

Este es el plugin completo. Sigue la estructura de Bits & Bolts, que es también el layout que genera @plugin-creator:

tiempo/
├── .codex-plugin/
│   └── plugin.json            # manifiesto: nombre, skills, mcpServers, onboarding
├── .mcp.json                  # cómo arrancar el servidor (stdio → node dist/server.js)
├── package.json               # type: module, node >= 22
├── tsconfig.json
├── assets/
│   ├── icon.svg               # monocromo, 20×20, currentColor
│   └── icon-dark.svg
├── skills/
│   ├── tiempo/SKILL.md        # cuándo usar cada tool
│   └── onboarding/SKILL.md    # se ejecuta al instalar
├── scripts/
│   └── build.mjs              # esbuild (servidor) + vite (app en un solo HTML)
├── src/
│   ├── shared/
│   │   └── contracts.ts       # esquemas zod compartidos servidor ↔ app
│   ├── server/
│   │   ├── index.ts           # McpServer + transporte stdio
│   │   ├── open-meteo.ts      # fetch a forecast y geocoding + caché
│   │   ├── store.ts           # ciudades guardadas y ajustes (JSON local)
│   │   └── register.ts        # tools, settings, mentions, formulario, recurso UI
│   └── app/
│       ├── index.html         # plantilla con <!-- APP_SCRIPT -->
│       └── index.ts           # App + OpenAIExtensions, render y model context
└── dist/                      # generado: server.js + app.html

Cuatro cosas que conviene mirar en este árbol:

  • src/shared/contracts.ts es la frontera. El servidor valida con esos esquemas lo que devuelve y la app los usa para tipar lo que recibe. Bits & Bolts hace lo mismo con publicCadPartSchema.
  • register.ts concentra todas las extensiones. El index.ts del servidor de Bits & Bolts tiene 30 líneas: crea el McpServer, llama a registerCadServer() y conecta el transporte. Todo lo demás vive en el registro.
  • La app se compila a un único HTML. El script de build de Bits & Bolts falla a propósito si Vite genera más de un chunk o algún asset suelto: el iframe recibe un solo documento con el JavaScript embebido.
  • dist/ es lo que distribuyes. El build de ejemplo copia manifiesto, skills, assets y dist/ a una carpeta que solo necesita Node para funcionar, sin instalar dependencias.

El manifiesto queda así:

{
  "name": "tiempo",
  "version": "0.1.0",
  "description": "Previsión meteorológica con Open-Meteo dentro de ChatGPT.",
  "skills": "./skills/",
  "mcpServers": "./.mcp.json",
  "extensions": {
    "com.openai": {
      "onboardingSkill": "./skills/onboarding/SKILL.md"
    }
  },
  "interface": {
    "displayName": "Tiempo",
    "shortDescription": "Previsión de tus ciudades",
    "category": "Productivity",
    "capabilities": ["Interactive", "Read"],
    "composerIcon": "./assets/icon.svg",
    "logo": "./assets/icon.svg",
    "logoDark": "./assets/icon-dark.svg",
    "defaultPrompt": ["@Tiempo ¿qué tiempo hará este finde?"]
  }
}

Y .mcp.json apunta al servidor compilado:

{
  "mcpServers": {
    "tiempo": { "command": "node", "args": ["./dist/server.js"], "cwd": "." }
  }
}

💡 La guía oficial de empaquetado ya recomienda un formato portable: plugin.json en la raíz con el esquema de Agent Plugins, mcp.json con un type de transporte por servidor, y todo lo específico de OpenAI bajo extensions.com.openai. El layout .codex-plugin/ sigue soportado como compatibilidad. Si quieres la versión portable, en qué es Agent Plugins tienes el manifiesto campo a campo.

Cada extensión, pieza a pieza

Vamos con el código mínimo de cada extensión. No es un tutorial para copiar y pegar: es lo justo para que sepas qué pedirle al agente y cómo revisar lo que te entregue.

Entrypoints: barra lateral y pestaña del hilo

Un entrypoint es una tool con _meta["openai/ui"].entrypoints. Bits & Bolts registra dos tools casi idénticas, cad.library (global) y cad.tray (hilo), que devuelven lo mismo. Tiempo hace igual:

const UI = "ui://tiempo/app-v1";
const ui = (entrypoints: unknown[] = []) => ({
  ui: { resourceUri: UI },
  "openai/ui": { entrypoints },
});

server.registerTool(
  "weather.library",
  {
    title: "Mis ciudades",
    inputSchema: z.object({}),
    annotations: { readOnlyHint: true },
    _meta: ui([{ type: "global" }]),
  },
  async () => view({ page: "library", cities: await store.listCities() }),
);

server.registerTool(
  "weather.tray",
  {
    title: "Previsión",
    inputSchema: z.object({}),
    annotations: { readOnlyHint: true },
    _meta: ui([{ type: "thread" }]),
  },
  async () => view({ page: "library", cities: await store.listCities() }),
);

Dos detalles de la especificación que conviene respetar. El título de cada entrypoint debería describir la vista y ser distinto del nombre del plugin (“Previsión”, no “Tiempo”). Y el icono de cada tool debería ser un SVG monocromo de 20×20 con trazos de 1,33 px que use currentColor, para que cambie con el tema.

Fíjate también en el -v1 del URI. Bits & Bolts va por ui://bits-and-bolts/app-v14: versionar el recurso evita que el host sirva un HTML viejo de caché después de un cambio.

Ajustes estructurados con controles nativos

Con los ajustes, ChatGPT pinta un formulario nativo en la página del plugin y tu servidor solo describe el esquema. El SDK registra por ti las dos tools que pide la especificación, settings.read y settings.update:

const extensions = new OpenAIExtensions(server);

extensions.settings?.register({
  fields: {
    temperatureUnit: { schema: z.enum(["celsius", "fahrenheit"]), title: "Temperatura" },
    windSpeedUnit: { schema: z.enum(["kmh", "ms", "mph", "kn"]), title: "Viento" },
    forecastDays: { schema: z.number().int().min(1).max(16), title: "Días de previsión" },
  },
  layout: [
    {
      kind: "group",
      title: "Unidades",
      items: [
        { kind: "property", property: "temperatureUnit" },
        { kind: "property", property: "windSpeedUnit" },
      ],
    },
  ],
  read: () => store.readSettings(),
  update: (set) => store.updateSettings(set),
});

Los valores del enum son los mismos que acepta Open-Meteo en temperature_unit y wind_speed_unit, y 16 es el máximo de forecast_days. Así el servidor pasa los ajustes tal cual a la API sin traducir nada.

forecastDays no aparece en el layout, así que ChatGPT lo coloca en un grupo “Other settings” al final. Y la persistencia es cosa tuya. ChatGPT solo te envía los campos que cambian. En Bits & Bolts los ajustes viven en memoria y se reinician con el servidor; para Tiempo basta un JSON en disco dentro de store.ts.

Menciones en el compositor

Esta es la que más luce. El usuario escribe @Zarag y ve “Zaragoza, España” en el desplegable:

extensions.mentions.setHandler(async ({ query }) => ({
  items: (await geocode(query)).slice(0, 10).map((city) => ({
    type: "resource_link" as const,
    uri: `tiempo://city/${city.id}`,
    name: city.name,
    title: `${city.name}, ${city.country}`,
  })),
}));

El SDK registra una tool llamada search_mentions con visibility: ["app"], que la especificación exige para que el modelo no la llame por su cuenta. geocode() llama a https://geocoding-api.open-meteo.com/v1/search?name=…&count=10&language=es.

⚠️ Las menciones son búsqueda por tecleo. Si cada pulsación dispara una llamada al geocoder, un usuario rápido se come una parte del límite de 600 por minuto. Mete una caché por prefijo en open-meteo.ts y no devuelvas nada con menos de dos caracteres.

Formulario con ciudades sugeridas

Los formularios extendidos amplían la elicitación de MCP. Para elegir ciudad, x-openai-suggestions ofrece opciones y deja escribir texto libre:

const result = await extensions.elicitInput({
  mode: "form",
  message: "¿De qué ciudad quieres la previsión?",
  requestedSchema: {
    type: "object",
    required: ["city"],
    properties: {
      city: {
        type: "string",
        minLength: 2,
        "x-openai-suggestions": saved.map((c) => ({ const: c.name, title: c.name })),
      },
    },
  },
});

Si quisieras que cada opción llevara imagen, x-openai-thumbnail acepta una URL HTTPS o un data URI. Bits & Bolts lo usa en cad.pickFile con las vistas isométricas de cada pieza.

⚠️ Aquí hay un aviso que te puede parar en seco. Según la especificación, los servidores MCP registrados en OpenAI necesitan la versión 2026-07-28 del protocolo con peticiones multi-ida-y-vuelta (MRTR) para usar formularios. El README del SDK aclara que elicitInput no implementa MRTR: solo funciona en conexiones directas, como el servidor local por stdio de este plugin.

Contexto del modelo desde la app

Cuando el usuario toca una ciudad en el panel, la app la adjunta al compositor. Así el siguiente mensaje (“¿y mañana?”) ya tiene contexto:

const app = new App({ name: "tiempo", version: "0.1.0" });
const extensions = new OpenAIExtensions(app);

app.ontoolresult = (result) => render(result.structuredContent);
await app.connect();

async function selectCity(city: CityForecast) {
  await extensions.modelContext?.update({
    content: [
      {
        type: "text",
        text: `${city.name}: ${city.current.temperature} ${city.units.temperature}, ${city.summary}`,
        _meta: { "openai/title": city.name },
      },
    ],
    structuredContent: { cityId: city.id },
  });
}

Cada llamada a update reemplaza el contexto anterior de esa instancia de la app. El texto con openai/title aparece como un adjunto con nombre que el usuario puede quitar. Si quieres contexto que el usuario no vea, la especificación permite marcar el bloque con annotations.audience: ["assistant"].

Las extensiones de la app (modelContext, message, files, resources) son undefined hasta que termina la inicialización, y pueden seguir así si el host no las soporta. De ahí el ?. en todas partes.

Onboarding con una skill

El onboarding es la extensión más barata: un campo en el manifiesto y un SKILL.md. La skill de Bits & Bolts es un buen molde: lee los ajustes con settings.read, pregunta de una en una y llama a settings.update solo con lo que cambia.

---
name: onboarding
description: Configura las unidades de Tiempo y abre Mis ciudades cuando el usuario elige configurar el plugin.
---

Llama a `settings.read` con `{}`. Pregunta, de una en una:

1. "¿Prefieres grados Celsius o Fahrenheit?" → `temperatureUnit`
2. "¿Viento en km/h, m/s, mph o nudos?" → `windSpeedUnit`

Llama a `settings.update` con un objeto `set` que contenga solo los cambios.
Confirma con los valores que devuelva la tool; si falla, no digas que se guardó.
Termina llamando a `weather.library` con `{}`.

Mapa de superficies de ChatGPT donde aparece el plugin Tiempo: barra lateral con el entrypoint global Mis ciudades, pestaña Previsión dentro del hilo, compositor con la mención @Zaragoza y un adjunto de contexto, página de ajustes con los controles de unidades y un formulario con sugerencias de ciudades. Cada superficie lleva una etiqueta con las plataformas donde funciona: las menciones solo en Desktop y los formularios en Desktop y Web

Los prompts para construirlo por fases

Llegamos a los prompts. La idea es construir por capas, de dentro hacia fuera, y no pasar a la siguiente hasta que la anterior se verifica. Si le pides todo de golpe a un agente, te entrega 1.500 líneas y no hay forma razonable de revisar qué extensión rompe qué.

Secuencia de siete prompts para construir el plugin Tiempo: manifiesto y skill, servidor headless con Open-Meteo, ajustes estructurados, menciones, UI con entrypoints, contexto del modelo y marketplace local. Cada paso tiene debajo su verificación, y solo cuando pasa se avanza al siguiente

Antes del primero, dale contexto al agente. Clona el repo en una carpeta temporal y apúntalo ahí:

Tienes clonado github.com/openai/mcp-extensions en /tmp/mcp-extensions.
Lee docs/spec.md, typescript/README.md y plugins/bits-and-bolts/ entero.
No escribas código todavía. Resúmeme en 10 líneas qué extensiones usa
Bits & Bolts, en qué fichero se registra cada una y qué hace scripts/build.mjs.

Verificación: el resumen menciona register.ts, createSettings, createMentions, elicitCadForm y el chunk único del build. Si no, no ha leído el código.

Prompt 1: esqueleto del plugin

Crea el plugin "tiempo" en ./tiempo siguiendo la estructura de Bits & Bolts:
.codex-plugin/plugin.json, .mcp.json (stdio, node ./dist/server.js),
package.json (type module, node >= 22), tsconfig.json, assets/icon.svg
(monocromo, 20x20, currentColor) y skills/tiempo/SKILL.md.
Dependencias: @modelcontextprotocol/sdk, @modelcontextprotocol/ext-apps,
@openai/mcp-extensions, zod. Sin código de servidor todavía.

Verificación: plugin.json es JSON válido, apunta a ./skills/ y ./.mcp.json, y el icono usa currentColor.

Prompt 2: servidor headless con Open-Meteo

Implementa src/server/open-meteo.ts con dos funciones:
- geocode(query): GET https://geocoding-api.open-meteo.com/v1/search
  con name, count=10, language=es.
- forecast(lat, lon, settings): GET https://api.open-meteo.com/v1/forecast
  con current=temperature_2m,weather_code,wind_speed_10m,
  daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,
  timezone=auto y las unidades de settings.
Caché en memoria de 10 minutos por URL. Define los esquemas en
src/shared/contracts.ts. Registra las tools weather.search y weather.forecast
(readOnlyHint, outputSchema). Sin UI ni extensiones de OpenAI.

Verificación: con el MCP Inspector o un script, weather.forecast para Zaragoza devuelve temperatura y tres días; una segunda llamada idéntica no sale a la red.

Prompt 3: ajustes estructurados y persistencia

Añade ajustes con OpenAIExtensions(server).settings.register:
temperatureUnit (celsius|fahrenheit), windSpeedUnit (kmh|ms|mph|kn),
forecastDays (entero 1-16). Agrupa las unidades en "Unidades".
Persiste en un JSON dentro de la carpeta de datos del plugin (store.ts).
weather.forecast debe leer los ajustes guardados.
Añade skills/onboarding/SKILL.md calcada de la de Bits & Bolts y
declárala en extensions.com.openai.onboardingSkill.

Verificación: settings.read con {} devuelve schema, values con un valor para cada propiedad, y layout. Tras settings.update con {"set":{"temperatureUnit":"fahrenheit"}}, reinicias el servidor y el valor sigue ahí.

Prompt 4: menciones

Registra el handler de menciones con extensions.mentions.setHandler.
Usa geocode() y devuelve resource_links con uri tiempo://city/{id}.
Ignora consultas de menos de 2 caracteres. Registra también un recurso
MCP para tiempo://city/{id} que devuelva la previsión en markdown.

Verificación: tools/list incluye search_mentions con visibility: ["app"], y leer tiempo://city/3104324 devuelve texto con la previsión de Zaragoza.

Prompt 5: la app y los entrypoints

Crea src/app/index.html e index.ts siguiendo el patrón de Bits & Bolts
(App de ext-apps + OpenAIExtensions de @openai/mcp-extensions/app).
Registra ontoolresult ANTES de connect() y pinta con el primer resultado.
Aplica tema y variables del host (applyDocumentTheme,
applyHostStyleVariables) e incluye app/styles.css del SDK.
Registra el recurso ui://tiempo/app-v1 (text/html;profile=mcp-app,
CSP sin dominios) y las tools weather.library (entrypoint global,
fullscreen) y weather.tray (entrypoint thread), ambas aceptando {}.
scripts/build.mjs: esbuild para el servidor y vite en un único HTML;
falla si sale más de un chunk. Añade un pie visible en la app:
"Datos: Open-Meteo.com (CC BY 4.0)", con enlace a la licencia.

Verificación: el build genera dist/server.js y dist/app.html sin ficheros sueltos; el HTML no hace ninguna petición a api.open-meteo.com y el pie de atribución se ve en las dos vistas.

Prompt 6: contexto del modelo

Al seleccionar una ciudad en la app, llama a
extensions.modelContext?.update con un bloque de texto con
_meta["openai/title"] = nombre de la ciudad y structuredContent {cityId}.
Escucha hostcontextchanged para marcar como no seleccionada la ciudad
si el usuario quita el adjunto.

Verificación: al tocar una ciudad aparece un único adjunto en el compositor; al tocar otra, se sustituye, no se acumula.

Prompt 7: marketplace local

Crea .agents/plugins/marketplace.json en la raíz del repo con una entrada
"tiempo" (source local, path ./tiempo, installation AVAILABLE,
authentication ON_INSTALL, category Productivity).
Explícame cómo instalarlo en la app de escritorio de ChatGPT.

Verificación: después de reiniciar la app de escritorio, el marketplace aparece como fuente en el directorio de plugins y Tiempo se instala desde ahí.

🔑 Con el servidor headless primero, las fases 3 a 6 son incrementos pequeños sobre algo que ya funciona. Si algo se rompe, sabes en qué capa ha sido.

Esta forma de trabajar (especificar, dividir en tareas pequeñas, verificar cada una) es justo lo que se formaliza con spec-driven development. El prompt 0 es tu “lee el contexto”, los siete siguientes son tus tareas y cada verificación es tu criterio de aceptación.

El otro medio plugin

Las skills que viajan dentro de tu plugin, bien escritas

Tiempo lleva dos SKILL.md: la que dice qué tool usar y la de onboarding. En la guía vas a ver cómo escribir skills que el agente carga solo cuando hacen falta y que funcionan en más de 25 agentes.

Abrir la guía →

Plantillas SKILL.md descargables incluidas

¿Cómo pruebas el plugin en local?

Con la app de escritorio de ChatGPT y un marketplace local. La guía oficial de empaquetado lo explica en tres pasos:

  1. Pon el plugin en una carpeta (por ejemplo ./plugins/tiempo en tu repo o ~/.codex/plugins/tiempo).
  2. Crea .agents/plugins/marketplace.json en la raíz del repo, o ~/.agents/plugins/marketplace.json para uno personal, con una entrada que apunte a esa carpeta con una ruta relativa que empiece por ./.
  3. Reinicia la app de escritorio de ChatGPT, abre el directorio de plugins, elige tu marketplace e instala.

Si prefieres la terminal, Codex CLI tiene un comando para registrar marketplaces:

codex plugin marketplace add ./mi-marketplace
codex plugin marketplace list

La instalación y las pruebas siguen haciéndose desde la app de escritorio. Y cada vez que cambies algo del plugin, toca reiniciar la app para que recargue los ficheros.

Para ver Bits & Bolts en acción antes de construir nada, lo más corto es instalar Bits & Bolts Remote desde el directorio de plugins de ChatGPT. Si quieres la versión local, el repo incluye su propio .agents/plugins/marketplace.json (con el nombre mcp-extensions-early-access) que apunta a ./plugins/bits-and-bolts. Compila el SDK y el ejemplo en su sitio, sin --plugin-dir, para que dist/ quede donde el marketplace lo busca:

pnpm install --frozen-lockfile
pnpm build
cd plugins/bits-and-bolts
node scripts/build.mjs

💡 Si tu servidor va a vivir en remoto, el camino es otro: activas el modo desarrollador en Settings → Security and login, registras la URL del servidor en la página de plugins de ChatGPT y le pasas el ID plugin_asdk_app… a @plugin-creator. Para eso, el tutorial de MCP desde cero te ayuda con la parte del servidor HTTP.

Si te gusta construir por capas y verificar cada paso, en la newsletter contamos cada domingo cómo lo hacemos con agentes en el día a día. Gratis, desde 2018.

Quiero esa dinamita 🧨

Límites y trampas de esta primera versión

Esto es una 0.1.0 y el marketplace del propio repo se llama early access. Funciona, pero hay aristas.

La plataforma decide por ti. Las menciones, los entrypoints de fichero y la apertura de ficheros locales solo existen en Desktop. Los formularios no llegan a iOS ni Android. Si tu usuario objetivo está en el móvil, la mitad de este artículo no le afecta.

Los formularios y MRTR. Si publicas un servidor remoto registrado en OpenAI, la elicitación necesita el protocolo 2026-07-28 con MRTR, y el helper elicitInput del SDK no lo implementa. Para un plugin local por stdio no hay problema.

Licencia de Open-Meteo. El uso gratuito es para fines no comerciales, los datos van bajo licencia CC BY 4.0 (hay que citar la fuente y enlazar la licencia) y tiene esos límites de 10.000 llamadas diarias. Para un plugin personal o de equipo va de sobra. Si piensas publicarlo en el directorio público, revisa sus condiciones: un plugin público con miles de usuarios puede no encajar en “no comercial”.

Dos APIs de cliente conviven. La referencia oficial de plugins documenta window.openai, que reúne alias de compatibilidad y extensiones propias de ChatGPT como uploadFile o requestModal. La recomendación oficial es usar el puente estándar de MCP Apps (y este SDK) siempre que cubra lo que necesitas, y recurrir a window.openai solo para lo que no tenga equivalente.

El sistema de ficheros, con pinzas. Si añades el extra del .gpx, la app recibe un URI opaco, nunca una ruta. El servidor sí recibe la ruta real en _meta["openai/resource"].path y el README muestra cómo leer ficheros hermanos: resolver con realpath y comprobar que no te sales del directorio. Copia ese patrón tal cual, porque un ../../ mal controlado te expone el disco del usuario.

Riesgo Cuándo te afecta Qué hacer
Extensión no soportada en la plataforma Siempre que uses menciones, ficheros o formularios Tools útiles sin UI; comprobar ?. en la app
Formularios sin MRTR Servidor remoto registrado en OpenAI Mantener local o esperar soporte en el SDK
Límite de Open-Meteo Menciones con muchos usuarios Caché por prefijo y mínimo de caracteres
HTML viejo en caché Tras cambiar la app Versionar el URI ui://…-vN
Parpadeo en el primer render Si la app llama a la tool al montar ontoolresult antes de connect()
Acceso a ficheros fuera de sitio Entrypoint de fichero realpath + comprobación de directorio

¿Merece la pena construir con esto hoy?

Si ya tienes un servidor MCP con interfaz, sí, al menos los entrypoints y los ajustes. Son unas pocas líneas de _meta y cambian por completo cómo se percibe el plugin: deja de ser algo que aparece si el modelo se acuerda y pasa a tener sitio fijo en la barra lateral.

Las menciones y los formularios merecen la pena si tu público trabaja en Desktop. Para el resto, espera a que la tabla de plataformas se llene.

Lo que no haría es construir el plugin alrededor de las extensiones. La propia documentación de OpenAI lo dice: estándar abierto primero, extensiones encima. Un servidor MCP con tools que funcionan sin interfaz sigue sirviendo en Claude, en VS Code y en cualquier cliente que llegue mañana. Las extensiones son la capa que hace que en ChatGPT, además, parezca de la casa.

¿Qué servicio que usas a diario te gustaría tener en la barra lateral de ChatGPT?

TL;DR

  • 🧩 OpenAI MCP Extensions son campos _meta y métodos con prefijo openai/ que añaden barra lateral, pestañas, ajustes nativos, menciones, formularios y onboarding encima de MCP Apps
  • 🗂️ Un plugin son un manifiesto, un .mcp.json, skills y un servidor que concentra el registro de todas las extensiones en un fichero
  • 🌦️ Con Open-Meteo, la app nunca llama a la API: el servidor hace el fetch, cachea y mantiene el CSP del iframe cerrado
  • 🪜 Siete prompts por capas, cada uno con su verificación: servidor headless primero, extensiones después
  • ⚠️ Menciones y ficheros solo en Desktop, formularios sin móvil y sin MRTR en elicitInput: revisa la tabla antes de diseñar

Preguntas frecuentes

¿Qué es OpenAI MCP Extensions?

Es un conjunto de extensiones al protocolo MCP y a MCP Apps que añade funciones específicas de ChatGPT: entrypoints en la barra lateral, pestañas en el hilo, ajustes con controles nativos, menciones en el compositor, formularios extendidos y onboarding. OpenAI publica la especificación y SDK para TypeScript y Python en github.com/openai/mcp-extensions.

¿En qué se diferencian las MCP Apps de las OpenAI MCP Extensions?

MCP Apps es un estándar abierto para mostrar interfaces HTML de un servidor MCP dentro de cualquier cliente compatible. Las OpenAI MCP Extensions son una capa opcional encima que solo entiende ChatGPT. Un servidor con extensiones sigue funcionando como MCP App normal en otros clientes, que ignoran los campos openai/.

¿Qué paquete npm necesito para usarlas en TypeScript?

@openai/mcp-extensions, junto a @modelcontextprotocol/sdk y, si tienes interfaz, @modelcontextprotocol/ext-apps. Se importa desde @openai/mcp-extensions/server en el servidor y desde @openai/mcp-extensions/app en la app. Requiere Node.js 22 o superior.

¿Cómo añado mi app a la barra lateral de ChatGPT?

Registra una tool que acepte {} como argumentos y añade en su _meta el campo "openai/ui": { entrypoints: [{ type: "global" }] } junto al ui.resourceUri de tu recurso HTML. ChatGPT la mostrará en la barra lateral con el título y el icono de la tool.

¿Funcionan las menciones con @ en ChatGPT web o en el móvil?

No. Según la tabla de soporte de la especificación, las menciones del compositor solo funcionan en ChatGPT Desktop en el lanzamiento. Lo mismo ocurre con los entrypoints de fichero y la apertura de ficheros locales.

¿Quién guarda los ajustes de mi plugin?

Tu servidor. ChatGPT pinta los controles a partir del esquema que devuelve la tool de lectura y llama a la tool de actualización solo con los campos que cambian. Dónde y cómo los persistes (memoria, JSON local, base de datos) es responsabilidad tuya.

¿Por qué no llamar a Open-Meteo directamente desde la app?

Porque obligaría a abrir el CSP del iframe a un dominio externo, duplicaría la lógica que el modelo ya usa a través de las tools y repartiría el consumo del límite de peticiones entre todas las pestañas abiertas. Llamar desde el servidor permite cachear y controlar el límite en un solo sitio.

¿Open-Meteo es gratis para un plugin de ChatGPT?

Es gratis para uso no comercial con atribución, por debajo de 10.000 llamadas al día, 5.000 por hora y 600 por minuto, sin API key. Para un uso comercial tiene planes de pago con clave propia, así que revisa las condiciones antes de publicar un plugin abierto al público.

¿Cómo instalo un plugin local en ChatGPT Desktop?

Crea un marketplace.json en .agents/plugins/ dentro de tu repo, o en ~/.agents/plugins/ para uno personal, con una entrada que apunte a la carpeta del plugin. Reinicia la app de escritorio, abre el directorio de plugins, selecciona tu marketplace e instálalo.

¿Puedo usar formularios extendidos en un servidor remoto?

Sí, pero los servidores registrados en OpenAI necesitan la versión 2026-07-28 de MCP con peticiones multi-ida-y-vuelta (MRTR). El helper elicitInput del SDK no implementa MRTR, así que hoy solo sirve para conexiones directas como un servidor local por stdio.

Fuentes

🧨 Última oprtunidad para recibir la dinamita que mereces sobre programación con IA el próximo domingo: Suscríbete gratis a Web Reactiva en https://webreactiva.com/newsletter

Imagen de Daniel Primo
Claude, IA de Anthropic

Escrito con la ayuda de la IA generativa de Claude, fuentes fidedignas y con un human in the loop:
Dani Primo.

CEO en pantuflas de Web Reactiva. Programador y formador en tecnologías que cambian el mundo y a las personas. Activo en linkedin, en substack y canal @webreactiva en telegram

12 recursos para developers cada domingo en tu bandeja de entrada

Además de una skill práctica bien explicada, trucos para mejorar tu futuro profesional y una pizquita de humor útil para el resto de la semana. Gratis.