For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
Navegación principal
Theme
Descripción general Documentación para usuarios de ChatGPT y Codex Casos de uso

Agrega una interfaz de usuario a tu servidor MCP

Devuelve recursos de interfaz opcionales desde herramientas MCP específicas.

Descripción general

La interfaz de usuario personalizada es opcional. Agrégala cuando un caso de uso de tu complemento requiera que las personas examinen, comparen, editen o confirmen información estructurada, o naveguen por ella. Asegúrate de que las herramientas MCP sigan siendo útiles sin un componente para que ChatGPT y Codex puedan completar el flujo de trabajo sin la interfaz.

El servidor MCP devuelve recursos de interfaz para determinadas herramientas. En ChatGPT, los componentes se ejecutan dentro de un iframe, se comunican con el host a través del puente de MCP Apps (JSON-RPC mediante postMessage) y se muestran junto a la conversación. El estándar abierto MCP Apps permite que la interfaz funcione en distintos hosts compatibles.

Empieza con MCP Apps

ChatGPT implementa el estándar abierto MCP Apps para las interfaces que devuelve un servidor MCP. MCP Apps define cómo el servidor asocia herramientas con recursos de interfaz y cómo se comunica el iframe con su host.

Para crear una interfaz:

  1. Declara el recurso de interfaz con _meta.ui.resourceUri.
  2. Usa el puente JSON-RPC ui/* mediante postMessage para la inicialización, las notificaciones, las llamadas a herramientas, los mensajes y el contexto visible para el modelo.
  3. Asegúrate de que las herramientas sigan siendo útiles sin la interfaz para que el modelo pueda completar el flujo de trabajo en clientes que no renderizan componentes.

Al usar este estándar como base, la misma interfaz puede funcionar en ChatGPT y en otros hosts compatibles con MCP Apps.

Cuando vayas a implementar el estándar, consulta la especificación de MCP Apps.

Para crear apps de barra lateral, visores de archivos y formularios más completos, consulta Extensiones.

Agrega extensiones de ChatGPT

Una vez que funcione el flujo de MCP Apps, usa window.openai solo para las funciones que la especificación compartida no cubre. Estas extensiones opcionales pueden mejorar la experiencia en ChatGPT sin que la base portátil de la interfaz dependa de ellas.

Da preferencia a los campos y métodos compartidos

Usa el campo o método de MCP Apps siempre que la especificación compartida cubra la función que necesitas:

Objetivo Estándar MCP Apps Alias de compatibilidad de ChatGPT
Vincular una herramienta a un recurso de interfaz _meta.ui.resourceUri _meta["openai/outputTemplate"]
Recibir los datos de entrada de una herramienta ui/initialize + ui/notifications/tool-input window.openai.toolInput
Recibir los resultados de una herramienta ui/notifications/tool-result window.openai.toolOutput
Llamar a una herramienta desde la interfaz tools/call window.openai.callTool
Enviar un mensaje de seguimiento ui/message window.openai.sendFollowUpMessage

Los alias de compatibilidad siguen disponibles para las integraciones existentes. Las interfaces nuevas deben usar los campos compartidos y los métodos del puente que aparecen en la columna central.

Algunos ejemplos de extensiones:

  • Pago instantáneo con window.openai.requestCheckout.
  • Manejo de archivos en ChatGPT con window.openai.uploadFile, window.openai.selectFiles y window.openai.getFileDownloadUrl.
  • Ventanas modales controladas por el host con window.openai.requestModal.
  • Persistencia del estado del widget con window.openai.widgetState y window.openai.setWidgetState.

Comprueba si cada extensión está disponible y, cuando sea viable, ofrece una alternativa:

const openai = typeof window !== "undefined" ? window.openai : undefined;

if (openai?.requestModal) {
  await openai.requestModal({
    template: "ui://widget/details.html",
  });
} else {
  document.body.textContent = "This host does not support modals.";
}

Evita condicionar la lógica al nombre del host o del producto. Comprueba si está disponible la función que necesita tu interfaz.

Para consultar las firmas de las extensiones y ver ejemplos, visita la referencia del puente de componentes window.openai.

Biblioteca opcional de componentes de OpenAI

La biblioteca de componentes @openai/apps-sdk-ui ofrece botones, tarjetas, controles de entrada y elementos básicos de diseño listos para usar que se ajustan al contenedor de ChatGPT. Úsala si quieres mantener un estilo uniforme sin tener que volver a crear los componentes básicos.

También puedes explorar el repositorio de ejemplos de interfaces en GitHub.

Elige cómo presentar la interfaz

Empieza con una interfaz integrada en la conversación y solicita más espacio solo cuando el flujo de trabajo lo requiera. Elige el formato más pequeño que permita entender el resultado o completar la tarea.

Tarjeta integrada

Usa una tarjeta integrada para mostrar un resultado concreto, una confirmación o un conjunto pequeño de acciones. Procura que la tarjeta se pueda usar por sí sola y evita la navegación con muchos niveles.

Ejemplos de tarjetas integradas

Carrusel integrado

Usa un carrusel integrado cuando las personas necesiten revisar de un vistazo unas pocas opciones similares con abundante contenido visual y elegir entre ellas.

Ejemplo de un carrusel integrado

Pantalla completa

Usa la pantalla completa para tareas complejas que necesiten más espacio, como consultar mapas, trabajar con lienzos de edición o explorar contenido en detalle. Diseña la experiencia para que funcione con el editor de ChatGPT, que sigue disponible en pantalla completa.

Ejemplo de interfaz en pantalla completa

Imagen en imagen

Usa el modo de imagen en imagen para una actividad en curso que deba permanecer visible mientras continúa la conversación, como una sesión en vivo, un juego o un video.

Ejemplo de interfaz en modo de imagen en imagen

Para obtener orientación detallada sobre la distribución de los elementos, la interacción, el diseño visual y la accesibilidad, consulta las pautas de interfaz de usuario.

Separa el procesamiento de datos del renderizado de la interfaz

Patrón desacoplado

Si adjuntas una plantilla de widget a cada llamada a una herramienta, ChatGPT puede volver a renderizar el iframe con demasiada frecuencia. Es preferible separar las herramientas de procesamiento de datos de las de renderizado:

  • Las herramientas de datos obtienen, calculan o modifican datos y devuelven solo los resultados de la herramienta.
  • Las herramientas de renderizado reciben los datos finales y devuelven la plantilla del widget.

Así, el modelo puede razonar sobre los datos que obtuvo antes de decidir si debe mostrar una interfaz al usuario. Esto aumenta considerablemente la probabilidad de que el modelo cumpla el objetivo específico que expresó el usuario.

Este patrón forma parte de la arquitectura de MCP Apps.

En la práctica, muchas integraciones de interfaz aplican esta separación:

  • Herramientas de búsqueda y obtención de datos (primero los datos): devuelven identificadores y metadatos sin adjuntar una plantilla de widget.
  • Herramientas de renderizado (por ejemplo, render_listings_widget): reciben una lista preparada de identificadores y renderizan el widget.

Solo la herramienta de renderizado debe incluir _meta.ui.resourceUri.

Flujo de llamadas desacopladas

Flujo de llamadas recomendado:

  1. El modelo llama a la herramienta de datos (por ejemplo, roll_dice).
  2. El modelo recibe structuredContent de la herramienta de datos.
  3. El modelo llama a la herramienta de renderizado con esos datos.
  4. El widget se renderiza una sola vez con el contexto final que verificó el modelo.

Ejemplo: preguntas de seguimiento sobre propiedades

Supón que tu complemento muestra tarjetas de propiedades y un mapa, pero la herramienta search de tu servidor solo admite filtros generales (ciudad, precio, número de habitaciones y baños) y no permite filtrar por zona escolar.

Si un usuario pregunta “¿Cuáles de estas propiedades están en la zona escolar de Richmond Primary School?”, el desacoplamiento ayuda de la siguiente manera:

  1. search realiza una búsqueda amplia y devuelve los identificadores de las propiedades candidatas junto con sus metadatos.
  2. El modelo filtra ese conjunto de propiedades según la pregunta de seguimiento.
  3. El modelo llama a render_listings_widget solo con los identificadores filtrados.
  4. El widget muestra el conjunto final de propiedades filtradas.

Buenas prácticas:

  • Diseña las herramientas de datos para que se puedan reutilizar. Devuelve structuredContent completo para poder encadenar las llamadas.
  • Limita las herramientas de renderizado a la presentación. Evita incorporar lógica de negocio en la función que maneja el renderizado.
  • Indica la dependencia en la descripción de la herramienta de renderizado (por ejemplo, “Siempre llama primero a roll_dice”).
  • Repite la ejecución solo cuando sea necesario. Permite que la interfaz llame directamente a las herramientas de datos para interacciones locales como “Volver a lanzar”, sin volver a montar el widget.

Ejemplo de desacoplamiento

Ejemplo con herramientas desacopladas para lanzar un dado:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod/v3";
import { build } from "esbuild";
import { fileURLToPath } from "node:url";

const TEMPLATE_URI = "ui://widget/dice.html";

const server = new McpServer(
  { name: "Decoupled dice", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

// Bundle the same browser source that is type-checked and tested directly.
const widgetBundle = await build({
  entryPoints: [fileURLToPath(new URL("./dice-widget.ts", import.meta.url))],
  bundle: true,
  format: "esm",
  platform: "browser",
  write: false,
});
const widgetScript = widgetBundle.outputFiles[0].text.replace(
  /<\/script/gi,
  "<\\/script"
);
const widgetHtml = `<!doctype html><html lang="en"><meta charset="utf-8"><title>Dice widget</title><body><script type="module">${widgetScript}</script></body></html>`;

server.registerResource("dice-widget", TEMPLATE_URI, {}, async () => ({
  contents: [
    {
      uri: TEMPLATE_URI,
      mimeType: "text/html;profile=mcp-app",
      text: widgetHtml,
      _meta: { ui: { prefersBorder: true } },
    },
  ],
}));

// 1) Data tool: no output template, returns chainable structuredContent.
server.registerTool(
  "roll_dice",
  {
    title: "Roll dice",
    description: "Roll an N-sided die and return { sides, value }.",
    inputSchema: { sides: z.number().int().min(2) },
    outputSchema: {
      sides: z.number().int().min(2),
      value: z.number().int().min(1),
    },
    _meta: {
      "openai/toolInvocation/invoking": "Rolling…",
      "openai/toolInvocation/invoked": "Rolled.",
    },
  },
  async ({ sides }) => {
    const value = 1 + Math.floor(Math.random() * sides);
    return {
      structuredContent: { sides, value },
      content: [{ type: "text", text: `Rolled ${value} on ${sides} sides.` }],
    };
  }
);

// 2) Render tool: owns the template and requires data from roll_dice.
server.registerTool(
  "render_dice_widget",
  {
    title: "Render dice widget",
    description:
      "Render the dice widget from roll data. First call roll_dice, then pass its sides and value to this tool.",
    inputSchema: {
      sides: z.number().int().min(2),
      value: z.number().int().min(1),
    },
    outputSchema: {
      sides: z.number().int().min(2),
      value: z.number().int().min(1),
    },
    _meta: {
      ui: { resourceUri: TEMPLATE_URI },
      "openai/toolInvocation/invoking": "Rendering…",
      "openai/toolInvocation/invoked": "Rendered.",
    },
  },
  async ({ sides, value }) => ({
    structuredContent: { sides, value },
    content: [
      {
        type: "text",
        text: `Showing a ${sides}-sided roll: ${value}.`,
      },
    ],
  })
);

export default server;

Instala esbuild junto con las dependencias del servidor. Luego, guarda el siguiente código para el navegador como dice-widget.ts en el mismo directorio que el archivo del servidor:

import { App } from "@modelcontextprotocol/ext-apps";

const container = document.createElement("div");
container.style.cssText = "font-family: system-ui; padding: 8px;";
const resultLine = document.createElement("div");
resultLine.style.cssText = "font-size: 20px; margin-bottom: 6px;";
const output = document.createElement("span");
output.id = "out";
output.textContent = "—";
resultLine.append("Result: ", output);
const rerollButton = document.createElement("button");
rerollButton.id = "reroll";
rerollButton.textContent = "Re-roll";
rerollButton.disabled = true;
container.append(resultLine, rerollButton);
document.body.append(container);

const app = new App({ name: "Dice widget", version: "1.0.0" });
let latestInput: Record<string, unknown> = {};
let latestOutput: Record<string, unknown> = {};

function render(result: Record<string, unknown>) {
  latestOutput = result;
  output.textContent = String(result.value ?? "—");
}

app.ontoolinput = ({ arguments: input }) => {
  latestInput = input ?? {};
};
app.ontoolresult = (result) => {
  render(result.structuredContent ?? {});
};

// Call the data tool without remounting the widget.
rerollButton.onclick = async () => {
  const candidate = latestOutput.sides ?? latestInput.sides;
  const sides =
    typeof candidate === "number" &&
    Number.isInteger(candidate) &&
    candidate >= 2
      ? candidate
      : 6;
  const next = await app.callServerTool({
    name: "roll_dice",
    arguments: { sides },
  });
  if (next.structuredContent) render(next.structuredContent);
};

await app.connect();
rerollButton.disabled = false;

Administra el estado

La interfaz de un servidor MCP trabaja con tres tipos de estado:

Tipo de estado Responsable Duración Ejemplos
Datos de negocio (fuente de verdad) Servidor MCP o servicio externo Largo plazo Tareas, tickets, documentos
Estado de la interfaz (efímero) Instancia de la interfaz Mientras la instancia esté activa Fila seleccionada, panel expandido, criterio de ordenación
Estado entre sesiones (persistente) Almacenamiento que tú controlas Se conserva entre sesiones y conversaciones Filtros guardados, modo de visualización, espacio de trabajo

Mantén cada valor en el sistema responsable de él. La interfaz debe mostrar los datos de referencia que devuelven las herramientas y aplicarles el estado temporal de presentación.

MCP server or external service
│
├── Authoritative business data
│
▼
UI
│
├── Ephemeral presentation state
│
└── Rendered view = business data + UI state

Mantén los datos de negocio en el servidor

Los datos de negocio son la fuente de verdad. Evita almacenarlos únicamente en la interfaz. Cuando un usuario realiza una acción:

  1. La interfaz llama a una herramienta MCP.
  2. El servidor valida la solicitud y actualiza los datos.
  3. El servidor devuelve una instantánea actualizada de los datos de referencia.
  4. La interfaz muestra la instantánea y conserva el estado de presentación compatible.

Devuelve suficiente contenido estructurado para que tanto el modelo como la interfaz puedan interpretar el nuevo estado. Así, la conversación seguirá siendo útil incluso si la interfaz no se carga.

Mantén el estado temporal de la interfaz en la propia interfaz

Usa el estado del framework para los valores que solo afectan la presentación, como un elemento seleccionado, un panel abierto o un filtro que aún se está editando. Cada instancia renderizada de la interfaz tiene su propio estado.

Cuando el modelo necesite conocer una selección o un cambio pendiente de aplicar, envíale esa información mediante ui/update-model-context. Este es el mecanismo portátil de MCP Apps para actualizar el contexto visible para el modelo.

ChatGPT también ofrece persistencia opcional para cada widget:

  • Lee la instantánea actual desde window.openai.widgetState.
  • Escribe una nueva instantánea con window.openai.setWidgetState(state).

setWidgetState es síncrono. Llámalo después de cada cambio relevante en el estado de la interfaz. No hace falta usar await.

import { useState } from "react";

export function TaskList({
  tasks,
}: {
  tasks: { id: string; title: string }[];
}) {
  const [state, setState] = useState(
    window.openai?.widgetState ?? { selectedId: null }
  );

  function selectTask(selectedId: string) {
    const nextState = { ...state, selectedId };
    setState(nextState);
    window.openai?.setWidgetState?.(nextState);
  }

  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id}>
          <button
            type="button"
            aria-pressed={state.selectedId === task.id}
            onClick={() => selectTask(task.id)}
          >
            {task.title}
          </button>
        </li>
      ))}
    </ul>
  );
}

El estado del widget pertenece a una sola instancia renderizada de la interfaz. No lo uses como fuente de verdad para los datos de negocio ni como almacenamiento persistente.

Haz que las imágenes sean visibles para el modelo

Si tu interfaz trabaja con imágenes, usa el siguiente formato estructurado para el estado del widget:

  • modelContent: texto o JSON que el modelo debe ver.
  • privateContent: estado exclusivo de la interfaz que el modelo no debe ver.
  • imageIds: identificadores de archivos que el modelo debe recibir en turnos posteriores.
const selectedFiles = await window.openai.selectFiles();

window.openai.setWidgetState({
  modelContent: "Review the currently selected images.",
  privateContent: {
    currentView: "image-viewer",
    filters: ["crop", "sharpen"],
  },
  imageIds: selectedFiles.map((file) => file.fileId),
});

Incluye únicamente identificadores de archivos que se hayan subido con window.openai.uploadFile o seleccionado con window.openai.selectFiles, o identificadores que se hayan recibido mediante parámetros de archivo en la entrada de una herramienta o devuelto mediante referencias a archivos en sus resultados.

Guarda en tu servidor el estado que debe persistir entre sesiones

Guarda las preferencias y los datos que deban conservarse entre conversaciones, dispositivos o sesiones en un sistema de almacenamiento que tú controles. Autentica al usuario para que el servidor MCP pueda asociar cada solicitud con la cuenta correcta.

Al agregar almacenamiento persistente:

  • Mantén la latencia lo suficientemente baja para que la interfaz responda a las interacciones.
  • Protege los datos privados mediante controles de autorización en el servidor.
  • Ten en cuenta los requisitos de residencia de datos y de cumplimiento normativo.
  • Aplica límites de solicitudes al tráfico que provenga de reintentos o de instancias simultáneas de la interfaz.
  • Asigna versiones a los objetos almacenados para poder migrarlos sin interrumpir las conversaciones existentes.

Evita usar localStorage para el estado principal. La interfaz se ejecuta en un iframe aislado y el almacenamiento del navegador no ofrece una capa de datos confiable entre dispositivos o sesiones.

Crea la estructura del proyecto del componente

Ahora que conoces el puente de MCP Apps y las extensiones opcionales de ChatGPT, puedes crear la estructura del proyecto de tu componente.

Como buena práctica, mantén el código del componente separado de la lógica del servidor. Esta es una estructura habitual:

plugin-ui/
  server/            # MCP server (Python or Node)
  web/               # Component bundle source
    package.json
    tsconfig.json
    src/component.tsx
    dist/component.js   # Build output

Crea el proyecto e instala las dependencias (se recomienda Node 18 o posterior):

cd plugin-ui/web
npm init -y
npm install react@^19 react-dom@^19
npm install -D typescript esbuild

Si tu componente necesita bibliotecas para arrastrar y soltar elementos, mostrar gráficos u otras funciones, agrégalas ahora. Limita las dependencias a las necesarias para reducir el tamaño del paquete.

Escribe el componente de React

El archivo de entrada debe montar un componente en un elemento root y renderizarlo a partir del último resultado de herramienta recibido a través del puente de MCP Apps (por ejemplo, mediante ui/notifications/tool-result).

La página de ejemplos incluye interfaces de muestra, como la lista de pizzerías de Pizzaz.

Explora la galería de componentes de Pizzaz

Los ejemplos de interfaces incluyen componentes que puedes usar como referencia al diseñar tu propia interfaz:

  • Lista de Pizzaz: lista de tarjetas ordenadas por relevancia, con favoritos y botones de llamada a la acción.
    Captura de pantalla del componente de lista de Pizzaz
  • Carrusel de Pizzaz: componente con desplazamiento horizontal basado en Embla que muestra cómo distribuir contenido multimedia abundante.
    Captura de pantalla del componente de carrusel de Pizzaz
  • Mapa de Pizzaz: integración con Mapbox que incluye un inspector en pantalla completa y sincroniza el estado con el host.
    Captura de pantalla del componente de mapa de Pizzaz
  • Álbum de Pizzaz: galería con vistas apiladas para explorar un lugar en detalle.
    Captura de pantalla del componente de álbum de Pizzaz
  • Video de Pizzaz: reproductor controlado por código con elementos superpuestos y controles de pantalla completa.

Cada ejemplo muestra cómo empaquetar recursos, conectar las API del host y estructurar el estado para conversaciones reales. Copia el que más se acerque a tu caso de uso y adapta la capa de datos a las respuestas de tus herramientas.

Hooks auxiliares de React

Este pequeño hook permite suscribirse a ui/notifications/tool-result:

import { useEffect, useState } from "react";

type ToolResult = { structuredContent?: unknown } | null;

export function useToolResult() {
  const [toolResult, setToolResult] = useState<ToolResult>(null);

  useEffect(() => {
    const onMessage = (event: MessageEvent) => {
      if (event.source !== window.parent) return;
      const message = event.data;
      if (!message || message.jsonrpc !== "2.0") return;
      if (message.method !== "ui/notifications/tool-result") return;
      setToolResult(message.params ?? null);
    };

    window.addEventListener("message", onMessage, { passive: true });
    return () => window.removeEventListener("message", onMessage);
  }, []);

  return toolResult;
}

Renderiza la interfaz a partir de toolResult?.structuredContent y trata su contenido como datos de entrada no confiables.

Localización del widget

El host refleja la configuración regional en document.documentElement.lang. Úsala para cargar traducciones y dar formato a fechas y números. Ejecuta npm install react-intl en el directorio web/ antes de usar este ejemplo:

import { IntlProvider, FormattedMessage } from "react-intl";

const messages: Record<string, Record<string, string>> = {
  "en-US": { greeting: "Hello" },
  "es-ES": { greeting: "Hola" },
};

export function PluginUI() {
  const locale = document.documentElement.lang || "en-US";
  return (
    <IntlProvider
      locale={locale}
      messages={messages[locale] ?? messages["en-US"]}
    >
      <FormattedMessage id="greeting" defaultMessage="Hello" />
    </IntlProvider>
  );
}

Empaqueta el componente para el iframe

Cuando termines de escribir el componente de React, puedes compilarlo en un único módulo de JavaScript que el servidor pueda insertar directamente:

// package.json
{
  "scripts": {
    "build": "esbuild src/component.tsx --bundle --format=esm --jsx=automatic --outfile=dist/component.js"
  }
}

Ejecuta npm run build para generar dist/component.js. Si esbuild indica que faltan dependencias, verifica que hayas ejecutado npm install en el directorio web/ y que los nombres de las importaciones coincidan con los de los paquetes instalados (por ejemplo, @react-dnd/html5-server-side frente a react-dnd-html5-server-side).

Inserta el componente en la respuesta del servidor

Expón el componente como un recurso MCP con el tipo MIME de interfaz de MCP Apps (text/html;profile=mcp-app). Si usas @modelcontextprotocol/ext-apps/server, da preferencia a RESOURCE_MIME_TYPE en lugar de insertar la cadena literal:

import {
  registerAppResource,
  RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import { readFileSync } from "node:fs";

const component = readFileSync("web/dist/component.js", "utf8");

registerAppResource(
  server,
  "project-board",
  "ui://project-board/v1.html",
  {},
  async () => ({
    contents: [
      {
        uri: "ui://project-board/v1.html",
        mimeType: RESOURCE_MIME_TYPE,
        text: `<div id="root"></div><script type="module">${component}</script>`,
        _meta: {
          ui: {
            prefersBorder: true,
            domain: "https://example.com",
            csp: {
              connectDomains: ["https://api.example.com"],
              resourceDomains: ["https://static.example.com"],
            },
          },
        },
      },
    ],
  })
);

Asocia el URI del recurso solo con las herramientas que deban renderizar el componente. Para lograr una mayor compatibilidad con MCP Apps, usa _meta.ui.resourceUri. ChatGPT también admite _meta["openai/outputTemplate"] como alias de compatibilidad.

Trata el URI del recurso como una clave de caché. Cuando hagas un cambio incompatible en el HTML, JavaScript o CSS, publica un nuevo URI y actualiza todas las herramientas que hagan referencia a él.

Política de seguridad de contenido (CSP)

Declara los dominios exactos a los que se conecta el componente o desde los que carga recursos:

  • connectDomains para las solicitudes a las API.
  • resourceDomains para scripts, estilos, imágenes y otros recursos.
  • frameDomains solo cuando el componente deba insertar iframes de orígenes específicos.

Los marcos anidados están bloqueados de forma predeterminada. Limita cada lista de dominios permitidos a lo estrictamente necesario. Durante la revisión del complemento se comprueba que el comportamiento de la interfaz coincida con la política declarada.

Puedes insertar un editor o una interfaz de administración existente que se aloje bajo el mismo dominio registrable que tu servidor MCP. Por ejemplo, un servidor en https://api.example.com/mcp puede declarar https://app.example.com en frameDomains. Incluye la justificación requerida al enviar el complemento y sigue la política de iframes, incluidas las restricciones sobre el alojamiento compartido y los requisitos de revisión.

Para producción, se recomienda usar plantillas de interfaz de componentes.

Durante el desarrollo, puedes volver a compilar el paquete del componente cada vez que cambie el código de React y recargar el servidor en caliente.

Ofrece un proceso de pago en tu interfaz

Si quieres que los usuarios puedan completar compras a través de la interfaz de tu complemento, usa el componente para mostrar los productos, los precios, las condiciones y las opciones de pago antes de la confirmación. Asegúrate de que las herramientas de catálogo y pedidos también sean útiles sin la interfaz. Luego, elige un proceso de pago externo o, si está disponible, una opción de pago integrada.

Usa el pago externo de forma predeterminada

El pago externo es la opción recomendada y está disponible de forma general. Desde el componente, agrega un enlace al proceso de pago que alojas como comercio en tu propio dominio. Allí te encargas de lo siguiente:

  • Fijación de precios y cobro de pagos.
  • Impuestos, descuentos y cargos.
  • Envíos y gestión de pedidos.
  • Reembolsos, soporte y cumplimiento normativo.

Por el momento, la aprobación se limita a complementos para la compra de bienes físicos. No ofrezcas otras categorías de comercio a menos que OpenAI las haya habilitado explícitamente para tu complemento.

Usa métodos de pago guardados

Para las compras de bienes físicos que cumplan los requisitos, puedes ofrecer una interfaz opcional que permita a los clientes seleccionar un método de pago que ya hayan guardado en tu servicio. Este flujo puede mostrar los métodos guardados que cumplan los requisitos, pero no permite recopilar nuevas credenciales de pago. Tu servidor MCP procesa la compra y devuelve el resultado definitivo del pedido.

Usa el formulario de pago de ChatGPT

El proceso de pago integrado con el formulario de pago de ChatGPT está en beta privada para determinados marketplaces. Su disponibilidad es limitada, tanto para desarrolladores como para usuarios.

En las integraciones habilitadas, window.openai.requestCheckout abre el formulario de pago de ChatGPT:

const order = await window.openai.requestCheckout(checkoutSession);

El proceso de pago consta de cuatro partes:

  1. Una herramienta MCP devuelve una sesión de pago en structuredContent.
  2. El componente muestra las líneas del pedido, los totales, las condiciones y las opciones de entrega.
  3. El componente llama a requestCheckout(checkoutSession) después de que el usuario elige pagar.
  4. ChatGPT envía el token de pago seleccionado a la herramienta complete_checkout del servidor MCP, que efectúa el cargo en el método de pago y devuelve el pedido completado.

La sesión de pago debe incluir:

  • Un identificador único de sesión.
  • Las líneas del pedido y sus cantidades.
  • Los totales expresados como cantidades enteras en la unidad mínima de la moneda.
  • Los metadatos del proveedor de pagos y del comercio.
  • Los enlaces requeridos a información legal, de privacidad, de reembolsos y de soporte.

Usa el servidor como fuente de verdad para los precios y el estado del pedido. Verifica el token de pago, asegúrate de que la operación sea idempotente, guarda el pedido de forma persistente y devuelve un comprobante con los datos definitivos. Nunca confíes en totales calculados únicamente en el componente.

Usa payment_mode: "test" para probar el flujo de principio a fin sin transferir fondos reales. Maneja en el componente las cancelaciones, los pagos rechazados y los errores del proveedor de pagos.

Para consultar todos los campos de la sesión de pago, el comportamiento del proveedor de pagos, el formato del resultado de complete_checkout y los requisitos de los pagos delegados, consulta la referencia de la API de pago.