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

Agrega una interfaz de usuario a tu servidor MCP

Devuelve recursos opcionales de interfaz de usuario desde herramientas MCP seleccionadas.

Descripción general

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

El servidor MCP devuelve recursos de interfaz de usuario para herramientas seleccionadas. Los componentes se ejecutan dentro de un iframe en ChatGPT, se comunican con el host mediante el puente de MCP Apps (JSON-RPC sobre postMessage) y se renderizan junto a la conversación. El estándar abierto MCP Apps permite que la interfaz de usuario se ejecute en distintos hosts compatibles.

Comienza con MCP Apps

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

Para interfaces de usuario nuevas:

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

Esta base que prioriza los estándares permite que la misma interfaz de usuario se ejecute en ChatGPT y otros hosts compatibles con MCP Apps.

Cuando estés listo para implementar el estándar, usa la especificación de MCP Apps.

Agrega extensiones de ChatGPT

Una vez que el flujo de MCP Apps funcione, usa window.openai solo para las capacidades que la especificación compartida no cubre. Estas extensiones opcionales pueden mejorar la experiencia en ChatGPT sin que tengas que incorporarlas a la base de la interfaz de usuario portable.

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 capacidad:

ObjetivoEstándar MCP AppsAlias de compatibilidad de ChatGPT
Vincular una herramienta con un recurso de interfaz de usuario_meta.ui.resourceUri_meta["openai/outputTemplate"]
Recibir la entrada de la herramientaui/initialize + ui/notifications/tool-inputwindow.openai.toolInput
Recibir los resultados de la herramientaui/notifications/tool-resultwindow.openai.toolOutput
Llamar a una herramienta desde la interfaz de usuariotools/callwindow.openai.callTool
Enviar un mensaje de seguimientoui/messagewindow.openai.sendFollowUpMessage

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

Algunos ejemplos son:

  • Pago instantáneo con window.openai.requestCheckout.
  • Manejo de archivos de 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.

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

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

if (openai?.requestModal) {
  await openai.requestModal({
    /* ... */
  });
} else {
  // Fallback behavior for hosts without this extension.
}

Evita usar el nombre del host o del producto para decidir qué ruta sigue el código. Comprueba si está disponible la capacidad que tu interfaz de usuario necesita.

Para consultar las firmas de las extensiones y ver ejemplos, consulta 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 cuando quieras mantener un estilo uniforme sin volver a crear los componentes básicos.

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

Elige una presentación

Comienza con una interfaz de usuario integrada en la conversación y solicita más espacio solo cuando el flujo de trabajo lo necesite. Elige la presentación más pequeña que permita a las personas comprender el resultado o completar la tarea.

Tarjeta integrada

Usa una tarjeta integrada para un resultado específico, una confirmación o un conjunto pequeño de acciones. Haz que sea autosuficiente y evita la navegación con muchos niveles.

Ejemplos de tarjetas integradas

Usa un carrusel integrado cuando las personas necesiten revisar rápidamente y elegir entre un conjunto pequeño de opciones similares con abundante contenido visual.

Ejemplo de un carrusel integrado

pantalla completa

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

Ejemplo de interfaz de usuario 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 de usuario en modo de imagen en imagen

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

Separa el procesamiento de datos del renderizado de la interfaz de usuario

Patrón desacoplado

Si adjuntas una plantilla de widget a cada llamada a una herramienta, ChatGPT puede volver a renderizar tu iframe con demasiada frecuencia. Un patrón más adecuado consiste en separar las herramientas de procesamiento de datos de las herramientas de renderizado:

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

Esto permite que el modelo aplique su inteligencia a los datos que obtuvo antes de decidir renderizar una interfaz de usuario, lo que aumenta considerablemente la probabilidad de que 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 de usuario usan esta división:

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

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

Flujo de llamadas desacoplado

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, verificado por el modelo.

Ejemplo: consultas de seguimiento sobre propiedades inmobiliarias

Supongamos que tu complemento muestra tarjetas de anuncios inmobiliarios y un mapa, pero la herramienta search de tu servidor solo admite filtros generales (ciudad, precio, habitaciones, baños) y no puede filtrar por zona escolar.

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

  1. search realiza una búsqueda amplia y devuelve los ID de los anuncios candidatos junto con sus metadatos.
  2. El modelo refina ese conjunto de candidatos para responder a la pregunta de seguimiento.
  3. El modelo llama a render_listings_widget solo con los ID filtrados.
  4. El widget renderiza el conjunto filtrado final.

Prácticas recomendadas:

  • Mantén las herramientas de datos reutilizables. Devuelve structuredContent completo para encadenar las llamadas.
  • Mantén las herramientas de renderizado enfocadas en la presentación. No mezcles la lógica de negocio con el controlador de renderizado.
  • Indica la dependencia en la descripción de la herramienta de renderizado (por ejemplo, “Siempre llama primero a roll_dice”).
  • Asegúrate de que las ejecuciones repetidas sean intencionales. 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 desacoplado

Ejemplo (herramientas de dados desacopladas):

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod/v3";

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

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

// The widget only renders the latest tool result.
// Re-roll calls the data tool directly to avoid remounting the widget.
const widgetHtml = `
  <div style="font-family: system-ui; padding: 8px;">
    <div style="font-size: 20px; margin-bottom: 6px;">
      Result: <span id="out">—</span>
    </div>
    <button id="reroll">Re-roll</button>
  </div>

  <script>
    const outputEl = document.getElementById("out");
    const rerollButton = document.getElementById("reroll");
    const pendingRequests = new Map();
    let nextRequestId = 1;
    let latestToolInput;
    let latestToolOutput;

    function render(result) {
      outputEl.textContent = String(result?.value ?? "—");
    }

    function request(method, params) {
      const id = nextRequestId++;
      window.parent.postMessage({ jsonrpc: "2.0", id, method, params }, "*");
      return new Promise((resolve, reject) => {
        pendingRequests.set(id, { resolve, reject });
      });
    }

    window.addEventListener(
      "message",
      (event) => {
        if (event.source !== window.parent) return;
        const message = event.data;
        if (!message || message.jsonrpc !== "2.0") return;

        if (message.id !== undefined && pendingRequests.has(message.id)) {
          const pending = pendingRequests.get(message.id);
          pendingRequests.delete(message.id);
          if (message.error) pending.reject(message.error);
          else pending.resolve(message.result);
          return;
        }

        if (message.method === "ui/notifications/tool-input") {
          latestToolInput = message.params;
        }

        if (message.method === "ui/notifications/tool-result") {
          latestToolOutput = message.params?.structuredContent;
          render(latestToolOutput);
        }
      },
      { passive: true }
    );

    rerollButton.onclick = async () => {
      const sides = latestToolOutput?.sides ?? latestToolInput?.sides ?? 6;
      const next = await request("tools/call", {
        name: "roll_dice",
        arguments: { sides },
      });
      if (next?.structuredContent) {
        render(next.structuredContent);
      }
    };
  </script>
`.trim();

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;

Gestiona el estado

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

Tipo de estadoResponsableDuraciónEjemplos
Datos de negocio (fuente de verdad)Servidor MCP o servicio externoLarga duraciónTareas, tickets, documentos
Estado de la interfaz (efímero)Instancia de la interfazMientras la instancia de la interfaz esté activaFila seleccionada, panel expandido, orden de clasificación
Estado entre sesiones (persistente)Almacenamiento que tú controlasEntre sesiones y conversacionesFiltros guardados, modo de vista, espacio de trabajo

Mantén cada valor en el sistema responsable de él. La interfaz debe renderizar los datos de los resultados de las herramientas que constituyen la fuente de verdad y aplicar sobre ellos 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. No los almacenes ú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 la instantánea actualizada que constituye la fuente de verdad.
  4. La interfaz renderiza la instantánea y conserva el estado de presentación compatible.

Devuelve suficiente contenido estructurado para que tanto el modelo como la interfaz comprendan el nuevo estado. Esto también permite que la conversación siga siendo útil si la interfaz no puede cargarse.

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 en borrador. Cada instancia renderizada de la interfaz tiene su propio estado.

Cuando el modelo necesite conocer una selección o una edición pendiente de aplicar, envía esa información a través de ui/update-model-context. Este es el mecanismo portable de MCP Apps para actualizar el contexto visible para el modelo.

ChatGPT también ofrece persistencia opcional limitada al widget:

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

setWidgetState es una función síncrona. Llámala después de cada cambio significativo en el estado de la interfaz; no hay nada que esperar con await.

import { useState } from "react";

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

  function selectTask(selectedId) {
    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

Para las interfaces que trabajan con imágenes, usa el formato estructurado del 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: ID de archivos que el modelo debe recibir en turnos posteriores.
window.openai.setWidgetState({
  modelContent: "Review the currently selected images.",
  privateContent: {
    currentView: "image-viewer",
    filters: ["crop", "sharpen"],
  },
  imageIds: ["file_123", "file_456"],
});

Incluye únicamente los ID de archivos cargados con window.openai.uploadFile, seleccionados con window.openai.selectFiles, recibidos mediante parámetros de archivo en la entrada de una herramienta o devueltos mediante referencias a archivos en el resultado de una herramienta.

Almacena en tu servidor el estado que se conserva entre sesiones

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

Cuando agregues almacenamiento persistente:

  • Mantén la latencia lo suficientemente baja para que la interfaz sea interactiva.
  • Protege los datos privados con autorización del lado del servidor.
  • Prepara el sistema para cumplir los requisitos de residencia de datos y cumplimiento normativo.
  • Aplica límites de solicitudes al tráfico generado por reintentos o instancias simultáneas de la interfaz.
  • Asigna versiones a los objetos almacenados para poder migrarlos sin afectar las conversaciones existentes.

Evita 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 inicial del proyecto del componente

Ahora que entiendes el puente de MCP Apps (y las extensiones opcionales de ChatGPT), es hora de crear la estructura inicial del proyecto de tu componente.

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

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+):

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

Si tu componente requiere bibliotecas para arrastrar y soltar, crear 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

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

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

Los ejemplos de interfaces incluyen componentes de ejemplo. Úsalos como base al diseñar tu propia interfaz:

  • Pizzaz List: lista de tarjetas ordenadas por clasificación, con favoritos y botones de llamada a la acción.
    Captura de pantalla del componente de lista de Pizzaz
  • Pizzaz Carousel: carrusel horizontal basado en Embla que muestra diseños con abundante contenido multimedia.
    Captura de pantalla del componente de carrusel de Pizzaz
  • Pizzaz Map: integración con Mapbox con un inspector en pantalla completa y sincronización del estado con el host.
    Captura de pantalla del componente de mapa de Pizzaz
  • Pizzaz Album: vista de galería apilada diseñada para explorar un solo lugar en detalle.
    Captura de pantalla del componente de álbum de Pizzaz
  • Pizzaz Video: reproductor controlado por scripts 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

Una pequeña función auxiliar para suscribirse a ui/notifications/tool-result:

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 a partir de toolResult?.structuredContent y trátalo como datos de entrada no confiables.

Localización del widget

El host copia la configuración regional a document.documentElement.lang. Usa esa configuración regional para cargar traducciones y dar formato a fechas y números. Un patrón habitual con react-intl:

import { IntlProvider } from "react-intl";
import en from "./locales/en-US.json";
import es from "./locales/es-ES.json";

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

export function PluginUI() {
  const locale = document.documentElement.lang || "en-US";
  return (
    <IntlProvider
      locale={locale}
      messages={messages[locale] ?? messages["en-US"]}
    >
      {/* Render UI with <FormattedMessage> or useIntl() */}
    </IntlProvider>
  );
}

Empaqueta el código para el iframe

Una vez que termines de escribir tu componente de React, puedes compilarlo en un único módulo de JavaScript que el servidor pueda incluir directamente:

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

Ejecuta npm run build para generar dist/component.js. Si esbuild informa que faltan dependencias, confirma que ejecutaste npm install en el directorio web/ y que tus importaciones coinciden con los nombres 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 de texto:

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 una mayor compatibilidad con MCP Apps, usa _meta.ui.resourceUri. ChatGPT también reconoce _meta["openai/outputTemplate"] como alias de compatibilidad.

Trata el URI del recurso como una clave de caché. Cuando hagas un cambio que rompa la compatibilidad en el HTML, JavaScript o CSS, publica un nuevo URI y actualiza todas las herramientas que lo referencien.

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 solicitudes a 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 permitidos tanto como sea posible. El proceso de revisión de complementos verifica que la política declarada coincida con el comportamiento de la interfaz.

Puedes insertar un editor o una interfaz de administración existentes desde el dominio registrable de tu propio servidor MCP. Por ejemplo, un servidor en https://api.example.com/mcp puede declarar https://app.example.com en frameDomains. Proporciona la justificación requerida al hacer el envío y cumple con la política de iframes, incluidas sus restricciones sobre el alojamiento compartido y sus requisitos de revisión.

Las plantillas de interfaz de componentes son la opción recomendada para producción.

Durante el desarrollo, puedes volver a compilar el paquete del componente cada vez que cambie tu 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 mediante los flujos de la interfaz de tu complemento, usa el componente para presentar productos, precios, condiciones y opciones de pago antes de la confirmación. Mantén la utilidad de las herramientas subyacentes de catálogo y pedidos sin una interfaz; luego, elige un flujo de pago externo o, cuando esté disponible, una opción de pago integrada.

Usa un proceso de pago externo de forma predeterminada

El proceso de pago externo es la opción recomendada y de disponibilidad general. Incluye en el componente un enlace a un flujo de pago alojado por el comerciante en tu propio dominio, donde te encargas de:

  • Precios y cobros.
  • Impuestos, descuentos y cargos.
  • Envío y gestión de pedidos.
  • Reembolsos, soporte y cumplimiento normativo.

La aprobación actual se limita a complementos para compras de bienes físicos. No ofrezcas otras categorías comerciales 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, una interfaz opcional puede permitir que los clientes seleccionen un método de pago que hayan guardado previamente en tu servicio. Este flujo puede mostrar los métodos guardados que cumplan los requisitos, pero no puede recopilar nuevas credenciales de pago. Tu servidor MCP procesa la compra y devuelve el resultado del pedido que sirve como fuente de verdad.

Usa el panel de pago de ChatGPT

El proceso de pago integrado con el panel de pago de ChatGPT está en beta privada para determinados marketplaces y no está disponible para todos los desarrolladores o usuarios.

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

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

El flujo de pago consta de cuatro partes:

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

La sesión de pago debe incluir:

  • Un ID de sesión único.
  • Artículos desglosados y cantidades.
  • Totales expresados como números enteros en las unidades menores de la moneda.
  • Metadatos del proveedor de pagos y del comercio.
  • Enlaces obligatorios 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, haz que la operación sea idempotente, guarda el pedido de forma persistente y devuelve un comprobante oficial. Nunca confíes en los totales calculados únicamente en el componente.

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

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