Ghosty
Blog

Parte 3 — cómo se estandariza la UI de un agente

Llevábamos meses emitiendo 16 tipos de bloque desde el agente al chat. Al leer MCP Apps y A2UI descubrimos que nuestros bloques se parten exactamente en las dos categorías que el estándar nuevo define, sin que nadie se lo hubiera propuesto. Y que aun así no son portables.

11 min de lecturaFixter
  • agentes
  • MCP
  • MCP Apps
  • A2UI
  • generative UI
  • protocolos
  • Ghosty Teams
Pintura trampantojo de 1670 de un gabinete de curiosidades entreabierto, con papeles, una pluma y objetos guardados detrás de la reja
Cornelius Norbertus Gijsbrechts, Gabinete de curiosidades (1670) — SMK, Galería Nacional de Dinamarca (CC0)

En los dos posts anteriores fuimos a implementar estándares ajenos. Con A2A escribimos 250 líneas de traductor para hablar un protocolo que ya existía; con ACP escribimos un relé para conectarnos a un ecosistema que ya estaba armado. En los dos casos el trabajo fue adaptarnos.

Este post cuenta el caso contrario. Llevamos meses emitiendo bloques estructurados desde el agente hacia el chat porque nos hacían falta. La semana pasada leímos la spec de MCP Apps y el post conjunto de Google sobre A2UI, y la sensación no fue "hay que implementar esto": fue reconocer nuestro propio diagrama escrito por otras nueve personas.

Lo interesante no es el halago. Es lo que descubrimos al intentar responder la pregunta honesta que sigue: ¿nuestros bloques son portables? La respuesta corta es que no, y la razón por la que no lo son es lo mejor de este post.

El problema es viejo y siempre se resolvió igual

Un agente que sólo devuelve párrafos obliga a un humano a hacer el trabajo mecánico: copiar, pegar, abrir otra pestaña, volver a capturar. En cuanto un agente hace algo útil, alguien quiere un botón.

La industria ya pasó por esto sin agentes de por medio:

SistemaAñoFormaAlcance
Slack Block Kit2019JSON declarativosólo Slack
Adaptive Cards (Microsoft)2017JSON declarativoTeams, Outlook, Webex
Google Chat cards2018JSON declarativosólo Google Chat

Adaptive Cards es el que más cerca estuvo de ser un estándar multi-vendor de verdad: la misma tarjeta se pinta en Teams, en Outlook y en Webex. Y aun así, los tres comparten la misma limitación estructural: son vocabularios cerrados. Hay una lista de bloques —texto, imagen, columna, input, botón— y lo que no está en la lista no se puede expresar.

La lección histórica, que vale para lo que viene: un vocabulario fijo nunca alcanza. Siempre falta un bloque. Siempre hay alguien intentando dibujar una gráfica con columnas de texto.

Lo que construimos nosotros, antes de saber que era eso

Nuestro agente escribe en el chat de Ghosty Teams usando fences de markdown con etiqueta. El agente emite un bloque; la plataforma decide cómo se pinta. Hoy hay 16 tipos en el código:

eb-doc      documento vivo con versiones     eb-artifact  página web con estado propioeb-sheet    hoja de cálculo                  eb-audio     nota de voz sintetizadaeb-patch    edición quirúrgica de un bloque  eb-file      archivo adjuntoeb-insert   inserción por posición           gt-pr        tarjeta de pull requesteb-remove   borrado señalado                 gt-task      tarjeta de tareagt-steps    pasos con palomita               gt-tests     resultado de una corridagt-tools    herramientas que corrió          gt-perm      solicitud de permisogt-alert    aviso                            gt-ask       pregunta con opciones

El parser vive en un solo archivo (src/lib/ebdoc.ts, ~1,100 líneas) y se ejecuta en cada render del mensaje, no una vez al guardarlo. Eso tiene una consecuencia que descubrimos por las malas: el cuerpo del mensaje guarda el fence crudo, así que cualquier cambio al parser reinterpreta retroactivamente todos los mensajes viejos de la base. No puedes cambiar la gramática, sólo ampliarla.

Los dos que no se parecen entre sí

Si miras la lista de arriba con atención, gt-alert y eb-artifact no son la misma clase de cosa. Uno es un aviso de tres líneas; el otro es una página completa.

eb-artifact es HTML que escribe el agente y que servimos en un iframe con sandbox="allow-scripts allow-forms allow-popups" — deliberadamente sin allow-same-origin. Esa ausencia es toda la seguridad del asunto: el iframe puede correr JavaScript, pero su Origin es literalmente null, no alcanza cookies, no alcanza localStorage, no alcanza nuestra sesión. Es código que escribió un modelo corriendo con cero credenciales.

Como el agente escribe Tailwind y no podemos meter un CDN dentro de ese iframe, el servidor hornea el CSS al publicar (bakeTailwind): compila las clases que realmente aparecen en el HTML y las inyecta. El artefacto queda autocontenido y con una URL compartible propia.

gt-alert, en cambio, no trae HTML. Trae datos, y nuestro componente de React decide cómo se ven. Nunca lo pensamos como una decisión de arquitectura; simplemente un aviso no amerita un iframe.

Esa distinción accidental es, exactamente, la que el estándar formalizó.

MCP Apps: el iframe, estandarizado

MCP-UI era un proyecto de comunidad con una idea simple: un servidor MCP devuelve un recurso HTML o remote-DOM, y el host lo pinta en un iframe aislado; los dos hablan por postMessage. Funcionaba, tenía adoptantes reales —Postman, Hugging Face, Shopify, Goose, ElevenLabs— y no era un estándar.

En paralelo, OpenAI lanzó su Apps SDK en noviembre de 2025 para construir aplicaciones dentro de ChatGPT, también sobre MCP. Ahí estaba el riesgo obvio: dos formas incompatibles de hacer lo mismo, y cada desarrollador manteniendo un adaptador por host.

No divergieron. SEP-1865, "MCP Apps", se creó el 21 de noviembre de 2025, firmado por nueve autores encabezados por los creadores de MCP-UI —Ido Salomon y Liad Yosef— y anunciado en el blog de Model Context Protocol. La spec dice con todas sus letras que la arquitectura de los dos proyectos —Apps SDK y MCP-UI— informó el diseño, y que el objetivo es unificar los dos enfoques en un solo estándar abierto, para que nadie tenga que mantener un adaptador por host. Hoy el SEP está en estado Final, en el Extensions Track.

Las piezas que define:

  • Recursos de UI predeclarados bajo el esquema ui://, no incrustados en el resultado de la tool. El motivo es interesante: predeclarar permite al host precargar la plantilla antes de ejecutar nada, cachear presentación aparte de datos, y revisar el HTML antes de pintarlo.
  • text/html;profile=mcp-app como único tipo de contenido del MVP.
  • JSON-RPC de MCP para el diálogo iframe↔host, en vez de un protocolo propio de mensajes. Así todo lo que cruza es loggeable y auditable.
  • Sandbox obligatorio, y consentimiento explícito del usuario para las tools que dispare la UI.

Anthropic lo encendió en Claude el 26 de enero de 2026 con socios de arranque —Amplitude, Asana, Box, Canva, Clay, Figma, Hex, monday.com, Slack— y hoy la matriz de soporte incluye Claude web y escritorio, ChatGPT, VS Code Copilot, Microsoft 365 Copilot, Cursor, Goose, Postman y MCPJam. El spec RC del 28 de julio de 2026 formalizó el framework de Extensions bajo el que vive.

A2UI: lo declarativo, estandarizado

Google publicó A2UI casi en paralelo, y la primera reacción de cualquiera es asumir guerra de estándares. El post conjunto dice lo contrario de forma explícita.

A2UI es un payload JSON que define qué renderizar, y la aplicación anfitriona lo pinta con sus propios componentes nativos. No hay iframe. El resultado se ve como el host, no como el servidor — que es justo lo que MCP Apps sacrifica al dar libertad creativa dentro de un iframe: consistencia visual y rendimiento.

El reparto que proponen:

  • A2UI → formularios, datos estructurados, visualizaciones. Lo que quieres que se vea integrado.
  • MCP Apps → experiencias muy a la medida y con lógica de cliente compleja. Juegos, flujos intrincados, cosas con estado propio.

Y se pueden anidar en las dos direcciones: servir payloads A2UI desde un servidor MCP, incrustar una MCP App dentro de un componente A2UI, o meter un renderizador de A2UI dentro del iframe de una MCP App. La cita que resume la intención: aprovechar el renderizado nativo para lo estándar y reservar el iframe para lo verdaderamente a la medida.

Con esto, el mapa de protocolos queda limpio por primera vez:

        MCP              A2A               ACP              MCP Apps / A2UI  agente ↔ tools   agente ↔ agente   persona ↔ agente     agente ↔ interfaz

Nuestros 16 bloques se parten solos

Aquí está el hallazgo que nos hizo escribir el post. Si clasificas nuestra lista con el criterio del estándar nuevo, no queda ni un caso ambiguo:

Serían A2UI (declarativos)Serían MCP Apps (iframe con estado)
gt-steps, gt-tools, gt-alerteb-artifact
gt-ask, gt-perm, gt-testseb-doc, eb-sheet
gt-pr, gt-task

Los de la izquierda son datos; nuestro React decide la forma. Los de la derecha son superficies con vida propia: un eb-doc tiene versiones, un parámetro ?v para elegir cuál estás viendo, exportación a .docx y PDF, y edición quirúrgica por eb-patch/eb-insert/eb-remove con marcatextos sobre lo que cambió.

Nadie diseñó esa frontera. Emergió de la pregunta práctica de siempre: ¿esto tiene estado propio o no? Que dos equipos independientes lleguen al mismo corte es la señal de que el corte es real y no una preferencia de arquitectura.

Ahora la parte incómoda: no son portables

Es fácil terminar aquí con una palmada en la espalda. La realidad es que hoy ninguno de nuestros bloques viaja a un host ajeno, y vale la pena desglosar por qué usando el caso más difícil.

gt-pr es la tarjeta de revisión de un pull request: el agente cierra su análisis con un fence y la plataforma pinta Aprobar · Pedir cambios · Rechazar · Comentar · Mergear. Tres razones por las que no es portable, y ninguna es cosmética:

1. El nombre es una convención nuestra. gt-pr no significa nada fuera de nuestro parser. Esto es lo único que se arregla renombrando, y es la parte fácil.

2. El fence es un puntero, no contenido. No lleva el estado del PR adentro: lleva la referencia. El estado —si está aprobado, si CI está roja, qué métodos de merge acepta el repo— se lee de GitHub en el momento de pintar. Fue una decisión explícita: la primera versión guardaba el estado en localStorage y sólo lo veía quien había hecho clic. Un host ajeno que recibiera nuestro fence recibiría una referencia a un estado que no sabe resolver.

3. Los botones llaman a nuestra API con la sesión de quien hace clic. Y esto es lo más difícil de soltar. Los botones no mandan texto al chat: cada uno es una server function que corre una tool de conector con las credenciales de la persona que hizo clic, no con las del agente. Aprobar un PR tiene que ser atribuible a un humano.

Ese tercer punto tiene un corolario de seguridad que nos costó pensar bien: la lista de acciones (CARD_ACTIONS) es cerrada y vive en el servidor. Si el modelo pudiera declarar el nombre de la acción, cualquiera con sesión podría hacer que se invocara cualquier tool suya con los argumentos que quisiera. De ahí la regla que aplicamos a todas las tarjetas: el modelo emite datos, la plataforma pone los botones. Si el modelo declarara los botones, inventaría un "Mergear" que no existe.

Lo que realmente cuesta adoptar el estándar

La conclusión práctica, y es la que no esperábamos: adoptar MCP Apps no es un renombre, es decidir qué parte del estado estás dispuesto a soltar.

Nuestros bloques valen precisamente por lo que los ata a nuestro servidor: la sesión de quien hace clic, la lista cerrada de acciones, el estado que se lee en vivo de la fuente. Un bloque portable de verdad tiene que llevar consigo su contexto de autorización, y ahí el estándar todavía no llega — MCP Apps resuelve el transporte, el sandbox y la auditoría, pero el "con qué credenciales corre esta acción" sigue siendo del host.

Por eso el camino que vemos es asimétrico y honesto:

  • Los declarativos son portables casi de inmediato. gt-steps, gt-alert, gt-tests son datos puros. Ahí A2UI encaja sin pelear.
  • eb-artifact ya es una MCP App en todo menos el nombre. HTML autocontenido, CSS horneado, iframe sandbox sin allow-same-origin, URL propia. Lo que falta es declarar el recurso bajo ui:// y hablar JSON-RPC en vez de nuestro postMessage.
  • gt-pr y gt-task probablemente nunca viajen enteros, y está bien. Lo que viaja es la vista; los botones se quedan donde están las credenciales.

El veredicto

En A2A el estándar llegó primero y nos costó un traductor. En ACP el estándar llegó primero y nos costó un relé. Aquí llegamos nosotros primero, y el costo es distinto: tener que separar, en código que ya funciona, la parte que es UI de la parte que es autoridad.

Si estás construyendo la capa visual de un agente hoy, la única decisión que importa tomar bien desde el principio es esa frontera. Todo lo demás —el nombre del fence, el formato del payload, el mecanismo de transporte— es reversible. Dónde vive el estado y con qué credenciales se ejecuta una acción, no.

Fuentes

  • — la spec, en estado Final
  • — el anuncio conjunto
  • — el post de Google sobre el reparto
  • — WorkOS
  • — MindStudio
  • — OpenUI
  • — Inkeep

Pruébalo gratis hoy

Sin código. Sin complicaciones. Crea tu agente y ponlo a trabajar en minutos.

Ghosty ® 2026. Todos los derechos reservados.