Lulu Ads
MCP de conserjería para editores del SDK de monetización de Lulu Ads: guía de integración autogestionada, registro de editores y verificación para cualquier servidor MCP o herramienta de agente. 70% de participación en ingresos por CPA, campo de datos patrocinados divulgado, nunca una instrucción de visualización.
Documentación
lulu-ads
La capa de monetización para la economía de agentes.
Monetiza tu servidor MCP o herramienta de agente con una línea patrocinada etiquetada.
Inicio rápido · Integraciones · Hosts compatibles · Superficies compatibles · Servidores stdio · Garantías · Contrato de API · Docs alojadas · Blog · Conviértete en editor
70% to publishers · CPA only · 800ms fail-open · 0 prompt injections, by design
↑ tarjeta de patrocinio renderizada en vivo — demanda publicitaria rotativa real, se actualiza cada ~60s. Cualquier listado reclamado puede incrustar esto en su propio README.
Lulu Ads adjunta un campo de datos divulgado y etiquetado al resultado propio de tu herramienta. El modelo anfitrión — Claude, Cursor, cualquier agente — decide por su propio criterio si es lo suficientemente relevante para mostrarlo. Nunca le instruimos a hacerlo.
| Lo que el SDK incluye (un campo de datos) | Lo que el host renderiza (su elección) |
|---|---|
|
|
Inicio sin fricción — agrega el servidor MCP y deja que tu agente haga el resto:
claude mcp add --transport http lulu-ads https://ads.getlulu.dev/mcp
monetiza mi servidor
Obtendrá la guía de integración correcta para tu stack, registrará un editor (con tu consentimiento), configurará la línea única y verificará que un espacio haya entrado en vivo.
Si se renderiza y se hace clic, ganas 70% en CPA. Si no — nadie paga, nada se rompe.
Sin inyección de prompts — enviamos un campo de datos; el host decide.
Inicio rápido
Python
pip install lulu-ads
# or: uv add lulu-ads
# or: poetry add lulu-ads
from lulu_ads import LuluAds
ads = LuluAds(publisher_id="pub_123", api_key="lk_...")
result = search_flights("TLV", "BKK", dates)
result["sponsored"] = await ads.sponsored_slot(
context={"tool": "search_flights", "category": "travel.flights"},
)
return result
Los servidores FastMCP lo obtienen en una sola llamada — las credenciales vienen del entorno,
y cada herramienta (presente y futura) obtiene tanto el campo de datos sponsored simple
Y, en hosts que lo soporten (p. ej. Claude.ai), el widget de tarjeta
patrocinada renderizado, automáticamente:
export LULU_ADS_PUBLISHER_ID=pub_123
export LULU_ADS_API_KEY=lk_...
from lulu_ads.enable import enable_lulu_ads
enable_lulu_ads(mcp, endpoint_url="https://my-server.example.com/mcp")
¿Solo quieres el campo de datos, sin widget? El middleware simple sigue funcionando por sí solo:
mcp.add_middleware(LuluAdsMiddleware())
TypeScript
npm install lulu-ads
# or: pnpm add lulu-ads
# or: yarn add lulu-ads
# or: bun add lulu-ads
import { LuluAds } from "lulu-ads";
const ads = new LuluAds({ publisherId: "pub_123", apiKey: "lk_..." });
result.sponsored = await ads.sponsoredSlot({ context: { tool: "search_flights" } });
Los servidores MCP construidos sobre el SDK oficial de TS reciben el mismo tratamiento de una llamada — campo de datos Y widget en cada herramienta, automáticamente:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { enableLuluAds } from "lulu-ads/mcp";
const server = new McpServer({ name: "my-server", version: "1.0.0" });
await enableLuluAds(server, { endpointUrl: "https://my-server.example.com/mcp" });
¿Aún no tienes ID de editor? Consulta docs/quickstart.md — tres
formas de obtener uno, ninguna depende de las demás.
¿Precios escalonados (anuncios en el nivel gratuito, sin anuncios en el de pago)? Pasa enabled —
tu propia verificación de suscripción decide el valor, sin necesidad de despliegue separado ni
condicionales dispersos:
result["sponsored"] = await ads.sponsored_slot(
context={"tool": "search_flights"},
enabled=user.tier != "paid", # False resolves instantly, no network call
)
result.sponsored = await ads.sponsoredSlot({
context: { tool: "search_flights" },
enabled: user.tier !== "paid",
});
Integraciones de frameworks
| Stack | Línea única | Docs |
|---|---|---|
| FastMCP (Python), datos + widget | enable_lulu_ads(mcp, endpoint_url=...) | → |
| FastMCP (Python), solo datos | mcp.add_middleware(LuluAdsMiddleware()) | → |
| MCP TS SDK, datos + widget | await enableLuluAds(server, { endpointUrl }) | → |
| LangChain / LangGraph (Python) | middleware=[LuluAdsAgentMiddleware()] | → |
| CrewAI (Python) | lulu_crewai.install() | → |
| MCP TS SDK, solo datos | withLuluAds(server) | → |
| Skybridge (TypeScript) | withLuluAdsSkybridge(server) | → |
| Propietarios de runtime (bots de chat, agentes de WhatsApp/Telegram) | model_output + format_suffix(sponsored) | → |
| Cualquier otro runtime / lenguaje | sponsored_slot(context) sobre el contrato crudo | → |
Widgets de resultados — plantillas para la SALIDA de TU propia herramienta (0.8.5)
Un widget, todos los hosts. El marco habla tres puentes — MCP
Apps estable (ui/initialize, 2026-01-26), el respaldo de la era del borrador, y el
window.openai de ChatGPT — y el SDK registra ambas claves de plantilla
(_meta.ui.resourceUri + openai/outputTemplate) y ambos dialectos de CSP
automáticamente. Renderizado verificado en vivo en claude.ai y ChatGPT, incluido
el beacon de impresión renderizada (las impresiones cuentan lo que un humano realmente
vio, nunca mera salida de API). Después de actualizar, refresca tu conector en la
configuración de plugins de ChatGPT — almacena en caché los metadatos de herramientas.
El formato propio de la franja PATROCINADA es configurable por separado
mediante sponsor_template= (independiente de template=, el diseño del cuerpo
de este widget):
register_result_widget(
mcp, "search_flights",
template="table-card",
mapping={"rows": "flights", "columns": [...]},
endpoint_url="https://my-server.example.com/mcp",
sponsor_template="flip-card", # or "carousel", "scratch-reveal" -- "card" is the default
)
Hosts compatibles
El campo JSON simple sponsored es la línea base siempre activa: se envía en
cada resultado de herramienta, en cada host MCP, porque no es más que una
clave extra en un dict — no se requiere soporte específico del host para que funcione,
y el modelo decide por su cuenta si mostrarlo. El widget renderizado
de MCP Apps que está encima es aditivo, y solo se pinta donde un host ha
implementado realmente el handshake ui/initialize. Esta tabla dice exactamente
cuál es cuál por host, basado en nuestra propia verificación de producción donde la
tenemos y una encuesta reciente (2026-08-25) en el resto — un host solo
obtiene un estado de widget "En vivo" aquí cuando lo hemos confirmado nosotros mismos o el
proveedor ha publicado detalles de implementación concretos y verificables, nunca por una
suposición genérica de "debería funcionar".
Hosts que hemos examinado — los logos no son una afirmación de soporte por sí solos; lee la columna de Estado abajo para ver qué hace realmente cada uno. (Continue.dev está en la tabla pero no en la franja de arriba: es un producto descontinuado, mantenido aquí solo por completitud.)
| Host | Llamada de herramientas MCP | Widget renderizado | Estado |
|---|---|---|---|
| Claude (claude.ai) | Sí | Sí | En vivo, verificado en producción — beacons reales de impresión renderizada observados en tráfico en vivo. |
| ChatGPT | Sí | Sí | En vivo, verificado en producción. |
CopilotKit (@ag-ui/mcp-apps-middleware) | Sí | En progreso | Corrección en revisión, PR #8, sin verificar de extremo a extremo — se encontró y corrigió un bug de descubrimiento de herramientas, pero la corrección no se ha probado contra una UI de chat completa (sin LLM disponible en esa pasada) y aún no se ha publicado en npm/PyPI. No trates CopilotKit como compatible hasta que ese PR llegue y se verifique en vivo. El campo simple sponsored no se ve afectado por este bug y ya fluye hoy. |
| VS Code (MCP nativo + modo agente de GitHub Copilot Chat) | Sí | Reportado en vivo | La publicación de blog propia de Microsoft del 2026-01-26 y los docs actuales describen a VS Code como "el primer editor de código de IA importante con soporte completo de MCP Apps" y documentan detalles de implementación concretos y verificables (iframes en sandbox, configuración de dominio CSP, el handshake ui/initialize, el App SDK) — creíble, pero es una afirmación del proveedor que no hemos reproducido de forma independiente. La llamada de herramientas MCP simple (modo agente de Copilot Chat) está en GA desde v1.102. |
| Cursor | Sí | Reportado, sin verificar | Nombrado como implementador de MCP Apps en la Matriz de soporte de extensiones upstream de modelcontextprotocol.io — un listado de terceros, no los docs propios de Cursor, por lo que es evidencia más débil que el detalle publicado por el proveedor de VS Code/Goose arriba. Intentamos verificar esto nosotros mismos en vivo (2026-08-25) y nos bloqueamos antes de llegar a la prueba: el límite de uso del nivel gratuito de Cursor (2 prompts) se alcanzó antes de que pasara una llamada de herramienta real. Intento real, bloqueo real, aún sin confirmar — no es una afirmación que esquivemos. |
| Goose (Block / AAIF) | Sí | En vivo (experimental) | Los docs propios de Goose confirman el handshake ui/initialize y el renderizado en iframe con sandbox (Goose Desktop 1.19.1+), pero lo marcan explícitamente como "experimental y basado en una especificación de borrador; la implementación es mínima y puede cambiar." Trátalo como en vivo pero inestable, no como un objetivo de renderizado garantizado. |
| Grok (xAI) — conectores de grok.com, CLI de Grok Build, Herramientas MCP Remotas de API xAI | Sí | Sin evidencia encontrada | Capaz de MCP en las tres superficies de xAI (descubrimiento y llamada de herramientas simple), pero ningún doc oficial, changelog o matriz de soporte de hosts de terceros acredita a Grok con la extensión de UI de MCP Apps hasta esta encuesta. El campo de datos patrocinado sigue fluyendo y sigue renderizándose puramente por el criterio del modelo mediante el respaldo JSON siempre activo — el widget enriquecido simplemente no tiene dónde renderizarse. |
| Windsurf (Codeium) | Sí | Sin evidencia encontrada | Los docs propios de Windsurf afirman que soporta "las herramientas, recursos y prompts de un servidor MCP" solamente; toda lista de soporte de hosts de MCP Apps de terceros que encontramos lo omite. El campo de datos patrocinado sigue funcionando mediante el respaldo JSON siempre activo. |
| Cline (extensión de VS Code) | Sí | Sin evidencia encontrada | Cliente MCP maduro (herramientas, recursos, prompts, un marketplace MCP integrado); no se encontró código de ui/initialize, ui:// o renderizado en iframe en ningún lugar del repositorio. El campo de datos patrocinado sigue funcionando mediante el respaldo JSON siempre activo. |
| Editor Zed | Sí | Sin evidencia encontrada | Los docs propios de Zed afirman claramente que "actualmente soporta las características de Herramientas y Prompts de MCP" — sin renderizado de UI basado en Recursos. El campo de datos patrocinado sigue funcionando mediante el respaldo JSON siempre activo. |
| Continue.dev | Sí (históricamente) | Sin evidencia encontrada | Descontinuado: adquirido por Cursor en junio de 2026, y el repositorio continuedev/continue ahora es de solo lectura sin desarrollo adicional. Soportaba herramientas/recursos/prompts MCP simples mientras estuvo activo, sin evidencia de que alguna vez renderizara widgets de MCP Apps. No es un objetivo de integración viable de aquí en adelante — listado aquí solo por completitud. |
Por qué algunos hosts necesitan cero código extra y otros no
Diferentes hosts convergieron en diferentes convenciones sobre cómo una herramienta anuncia "tengo una UI renderizable" — y donde la convención de un host difiere de la que enviamos primero, el descubrimiento falla silenciosamente antes de que el renderizado tenga oportunidad de ejecutarse (esa fue la brecha de CopilotKit que PR #8 corrigió, 2026-08-25). Rastreamos cada convención que hemos confirmado y nos registramos contra todas ellas en cada herramienta con capacidad de widget — solo aditivo, nunca una reescritura, por lo que un host que no reconoce una señal simplemente la ignora. Esa es la razón práctica por la que Claude y VS Code renderizan con cero código extra (comparten una convención) mientras que CopilotKit necesitó una corrección específica, y es por lo que "sin evidencia encontrada" en la tabla de abajo significa exactamente eso — evidencia aún no encontrada, no evidencia de ausencia.
Cualquier otra cosa no listada arriba (LangGraph Studio, arneses de agentes internos personalizados,
y cada host que simplemente aún no hemos examinado): desconocido / aún no
investigado — el campo simple sponsored está diseñado para fallar abierto y
degradarse con gracia en cualquiera de ellos de todos modos, según las Garantías
de abajo. Si has verificado el renderizado en un host que no está en esta tabla, abre un
issue o PR — esta lista pretende mantenerse honesta, no exhaustiva.
Esta tabla trata específicamente sobre renderizado de widgets en hosts de chat.
Para el panorama más completo — SDKs/frameworks de agentes (la mayoría alcanza el campo
de datos mediante paso a través de MCP, sin necesidad de adaptador dedicado), runtimes de
sufijo de respuesta (bots de WhatsApp/Telegram/Slack/SMS, agentes en segundo plano), constructores
de apps de IA (aún no evaluados) y alojamiento/registros de MCP (irrelevantes para
este SDK por diseño) — consulta
Superficies compatibles.
No diseñes UI. Elige uno de cuatro widgets de resultado predefinidos con calidad nativa del host y mapea los campos structuredContent de tu herramienta en él — el marco, los tokens de diseño y la franja SPONSORED divulgada son fijados por el SDK. La franja se renderiza solo cuando existe un payload sponsored en vivo, siempre en la parte inferior, siempre etiquetada, con el logo del anunciante (respaldo de letra en mosaico cuando no carga ninguno). Tu cuerpo no puede eliminarla ni cambiarle el estilo.
Plantillas: stat-card (valor grande + chips + fondo atmosférico opcional según condición), table-card (filas con encabezado, numéricos monoespaciados, resaltado de mejor fila), notice-card (glifo de veredicto + filas de detalle), carousel-card (3–8 tarjetas de opciones deslizables).
from lulu_ads.widgets import register_result_widget
# after your @mcp.tool definitions:
register_result_widget(
mcp, "get_weather",
template="stat-card",
mapping={
"eyebrow": "location.name",
"value": {"path": "temperature_c", "suffix": "°"},
"condition": "conditions",
"chips": [{"path": "humidity_pct", "prefix": "💧 ", "suffix": "%"}],
"atmosphere": "weather_code", # WMO code or words -> sky gradient
},
endpoint_url="https://my-server.example.com/mcp",
)
TypeScript: import { registerResultWidget } from "lulu-ads/widgets" —
mismas plantillas y forma de mapeo; extiende el _meta devuelto en
server.registerTool(...). Las entradas de mapeo son rutas de puntos o
{path, prefix, suffix}; una vía de escape body_html= acepta marcado
personalizado compuesto a partir de los primitivos .lw-* para los casos que las plantillas
no cubren. Llamarlo para una herramienta reemplaza deliberadamente la tarjeta patrocinada genérica
de enable_lulu_ads en esa herramienta — los datos patrocinados
aún fluyen y se renderizan en la franja propia del widget.
Renderizado de widgets (UI de MCP Apps)
El campo simple sponsored siempre se envía y siempre funciona — algunos hosts
lo renderizan como tarjeta basándose únicamente en el criterio del modelo, sin instrucción
en ningún lugar. Para hosts que admiten la extensión MCP Apps
(io.modelcontextprotocol/ui), enable_lulu_ads / enableLuluAds
(ver Quickstart arriba) ya registran un widget renderizado real y lo
adjuntan automáticamente a cada herramienta — no necesitas nada debajo de esta
línea para eso. Existe como un paso distinto porque
register_sponsored_widget() requiere la URL exacta del endpoint público de tu servidor,
que LuluAdsMiddleware/withLuluAds por sí solos no tienen forma de conocer.
¿Prefieres control por herramienta (un widget diferente en herramientas distintas, o
solo algunas herramientas reciben uno)? Usa el bloque de construcción de nivel inferior directamente
en lugar de enable_lulu_ads:
from fastmcp import FastMCP
from lulu_ads.widget import register_sponsored_widget
mcp = FastMCP("my-server")
sponsored_app = register_sponsored_widget(
mcp,
endpoint_url="https://my-server.example.com/mcp", # your public MCP connector URL
text="Save 15% at checkout",
url="https://example.com/deal",
logo="https://example.com/logo.png", # optional, see "Logos" below
)
@mcp.tool(app=sponsored_app)
def search(...): ...
Mismo helper, SDK oficial de TS, para servidores MCP construidos en Node en lugar de Python
(el registro es async — puede obtener un logo antes de devolver):
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerSponsoredWidget } from "lulu-ads/widget";
const server = new McpServer({ name: "my-server", version: "1.0.0" });
const appMeta = await registerSponsoredWidget(server, {
endpointUrl: "https://my-server.example.com/mcp", // your public MCP connector URL
text: "Save 15% at checkout",
url: "https://example.com/deal",
logo: "https://example.com/logo.png", // optional, see "Logos" below
});
server.registerTool("search", { ...appMeta }, handler);
Formatos de tarjeta. template= elige el diseño visual del widget — una
elección en el momento del registro, con la misma forma que el propio
register_result_widget template= (widgets de Resultado, arriba). El valor predeterminado es "card" (el diseño
mostrado arriba); pasa un nombre diferente para un formato distinto:
sponsored_app = register_sponsored_widget(
mcp,
endpoint_url="https://my-server.example.com/mcp",
text="Save 15% at checkout",
url="https://example.com/deal",
template="banner", # full-width horizontal strip -- see Templates below
)
const appMeta = await registerSponsoredWidget(server, {
endpointUrl: "https://my-server.example.com/mcp",
text: "Save 15% at checkout",
url: "https://example.com/deal",
template: "banner",
});
Un template no reconocido se eleva inmediatamente (antes de cualquier llamada de red,
p. ej., la obtención del logo) en lugar de degradarse silenciosamente — el mismo contrato de fallo rápido
que la validación de plantillas de register_result_widget ya tiene.
"hero" acepta una opción más, background_image/backgroundImage — una
URL obtenida una vez en el registro e incrustada de la misma manera que logo:
sponsored_app = register_sponsored_widget(
mcp,
endpoint_url="https://my-server.example.com/mcp",
text="Save 15% at checkout",
url="https://example.com/deal",
template="hero",
background_image="https://example.com/hero-bg.jpg", # optional -- falls back to the gradient
)
Plantillas:
template= | Diseño | Notas |
|---|---|---|
"card" (predeterminado) | Apilado: línea de etiqueta, luego fila de logo + texto/CTA | El diseño original construido a mano — cada integrador existente lo recibe sin cambios. |
"banner" | Una sola fila horizontal: etiqueta, logo, texto/CTA todo en línea | Mejor ajuste para diseños anchos pero bajos (barra lateral de VS Code, paneles CLI anchos) donde una tarjeta apilada desperdicia espacio vertical. |
"hero" | Imagen de fondo a sangre completa (o el degradado compartido) con logo/texto/CTA anclados sobre un velo de legibilidad | background_image es opcional y proporcionado por el integrador — aún no es algo que la coincidencia automática de anuncios del servidor de anuncios pueda seleccionar por sí sola. |
"flip-card" | Tocar/hacer clic voltea una tarjeta 3D — el frente muestra la etiqueta, el reverso revela texto + CTA | La etiqueta divulgada siempre está al frente; solo los detalles de la oferta están detrás del volteo. |
"scratch-reveal" | Una capa de tarjeta raspable sobre la oferta | Se revela automáticamente después de 3 s independientemente de la interacción — la oferta es idéntica de cualquier manera, esto es decoración, nunca contenido restringido. |
"spin" | Un adorno decorativo de giro y asentamiento en la insignia del logo | Determinista — siempre la única oferta real, nunca un mecanismo de resultado variable. |
Más formatos llegan aquí a medida que se publican (carrusel, comparación, testimonio,
cuenta regresiva, cuestionario, video — ver la galería rastreada en Linear). Cada
plantilla comparte exactamente las mismas garantías: esqueleto al cargar, la
etiqueta Sponsored divulgada, el pie de página "Powered by Lulu Ads" y
apertura ante datos faltantes o malformados — nada de eso es renegociable por
plantilla.
Esto es también lo que enable_lulu_ads/enableLuluAds hacen internamente, en tu
nombre, para cada herramienta — se encontró en vivo (2026-07-26) que hacer bien este paso
por herramienta es fácil de olvidar: nuestro propio servidor dogfood lo tenía conectado a
exactamente una herramienta a mano, y cada herramienta agregada desde entonces silenciosamente nunca
lo recibió. Si quieres cobertura automática sin paso por herramienta, usa
enable_lulu_ads/enableLuluAds en lugar de esto directamente.
Envía una tarjeta flotante, redondeada y con degradado (mismo sistema visual que
getlulu.dev) con una etiqueta Sponsored divulgada —
sigue siendo solo marcado, nunca una directiva. Tres peculiaridades específicas del host que esto
maneja por ti: Claude requiere un valor _meta.ui.domain no documentado
derivado de la URL de tu endpoint (autocalculado aquí, no una credencial), el
widget debe enviar un protocolo de enlace ui/notifications/initialized al cargar o
Claude mantiene el iframe oculto, y los logos se incrustan en lugar de vincularse
(siguiente sección) para que la CSP del sandbox del widget no pueda eliminarlos silenciosamente.
Verificado en vivo contra producción
(dali.getlulu.dev/mcp, ext-apps#671),
actual al 2026-07-19 — el propio renderizado de widgets de MCP Apps de Claude estaba
roto en toda la plataforma antes de que ese arreglo llegara, así que trata cualquier afirmación de "debería renderizar"
(incluida esta, en otros lugares) como no verificada hasta que la hayas
comprobado en vivo en tu propio host.
El widget muestra un <Skeleton> de shadcn inmediatamente al cargar, luego cambia a
contenido real solo cuando llega una llamada de herramienta en vivo — text/url/logo
pasados a register_sponsored_widget() no se renderizan como contenido
inicial; solo label/cta/accent* de esas opciones se usan realmente
por la ruta en vivo (como valores predeterminados para campos que el payload de la red omite, y como
el tema de marca estático por integrador). En cada llamada de herramienta real, el
widget escucha el push ui/notifications/tool-result del propio host de MCP Apps
(se monta un iframe nuevo por llamada, no se reutiliza — "por llamada, no por
herramienta" es una garantía de protocolo, no se tuvo que construir nada en el servidor para
lograrlo) y renderiza con el structuredContent.sponsored de esa llamada — contenido
de anuncio en vivo, por llamada, no un payload fijo horneado una vez en el registro. Un
host que nunca envía la notificación sigue mostrando el esqueleto
indefinidamente (no un anuncio de respaldo — ver la brecha abierta señalada en
el docstring de InitialOptions de js/widget-src/src/mcpBridge.ts); una llamada
sin campo sponsored (el caso normal de apertura) renderiza una tarjeta
vacía solo con el pie de página. Tarjeta, esqueleto y el pie de página "Powered by Lulu Ads"
son un solo paquete compilado de React/shadcn compartido byte por byte entre
los SDK de Python y TypeScript (js/widget-src/, verificado, incrustado por
ambos lenguajes), y el pie de página siempre se renderiza dentro de ese mismo
caparazón de tarjeta persistente, en cada estado.
Logos
logo toma una URL para obtener una marca de, no una URL para incrustar
directamente — pásala y el SDK descarga la imagen justo ahí en el
registro y la incrusta en el widget como un URI data:. Esto
no es incidental: la especificación de MCP Apps hace que los hosts apliquen img-src 'self' data: <resourceDomains> dentro del iframe sandboxed del widget, y a menos que tú
declares por separado el dominio de tu logo en la configuración CSP de ese recurso, un
<img src="https://your-cdn.com/logo.png"> se elimina silenciosamente — sin
error en ningún lugar, la tarjeta simplemente se renderiza con un espacio en blanco para siempre, en cada
host. Los URI data: siempre se permiten bajo esa misma regla, así que obtener e
incrustar en el servidor evita todo el modo de fallo — no hay
configuración CSP que debas acertar u olvidar.
Un logo malo o inalcanzable nunca rompe el registro — se omite (con
un registro de advertencia) y la tarjeta se renderiza sin uno, igual que dejar logo
sin configurar. Solo se aceptan image/png, image/jpeg, image/svg+xml, image/webp y
image/gif, con un límite de 200 KB (el logo se renderiza a 28×28 en la
tarjeta — no hay razón para enviar más que eso por la red).
Renderizado CLI
Los terminales no tienen superficie de widget — el texto del propio modelo es la única
salida que existe, y es genuinamente el criterio del modelo si
mencionar la línea divulgada o no (nunca forzado, jamás — ver Garantías).
LuluAdsMiddleware / withLuluAds detectan clientes CLI conocidos a través del
clientInfo.name de MCP enviado en initialize (actualmente: claude-code, verificado
en vivo) y, cuando se conectan desde uno, agregan una tarjeta de texto plano con bordes a
content[] además del campo simple — sigue siendo solo datos, sigue siendo cero
instrucción para el modelo, solo formateado para que se lea como un bloque distinto
en lugar de una oración simple si el modelo decide transmitirlo:
╭─ Sponsored ────────────────────────────────────╮
│ Search 700+ airlines in one place — Kiwi.com │
│ finds routes other search engines miss. │
╰─ via Lulu Ads ─────────────────────────────────╯
→ https://ads.getlulu.dev/c/9f2a1c
Limitación conocida, divulgada aquí en lugar de pasarse por alto: algunos clientes
MCP no reenvían cada bloque content[] al modelo cuando
structuredContent también está presente en el mismo resultado — un
error de cliente abierto en Claude Code, reportado dos veces y cerrado dos veces sin
arreglo (#55677 →
consolidado en
#45575 →
cerrado automáticamente por obsoleto). Probado en vivo específicamente contra Claude Code
(2026-07-21): con structuredContent presente (el valor predeterminado enviado), la
tarjeta con bordes nunca llega al modelo, pero el campo simple sponsored sí
lo hace — el modelo la muestra de manera confiable como una línea honesta y etiquetada
"Sponsored: ..." en sus propias palabras, 3/3 ejecuciones, sin problemas.
cliTextMode — arreglo opcional para el error de cliente anterior
También probamos el arreglo de apariencia obvia — omitir structuredContent para que
content[] no tenga nada que compita con él — y el resultado dependió
enteramente de qué más había en content[]:
- Anuncio solo, sin datos reales de herramienta junto a él: la tarjeta llega cada vez, pero el modelo la marca como un posible intento de inyección de prompt y advierte al usuario que se aleje, 3/3 ejecuciones. Peor que no mostrarla.
- Anuncio junto a un renderizado real y completo del propio resultado de la herramienta: la tarjeta llega cada vez, el modelo la trata como un anuncio divulgado ordinario y lo menciona de manera neutral, 3/3 ejecuciones. Sin sospecha.
Entonces el arreglo es real, pero condicional al comportamiento de tu propia herramienta — que este SDK no puede verificar por ti, de ahí que sea opcional, desactivado por defecto:
mcp.add_middleware(LuluAdsMiddleware(cli_text_mode=True))
withLuluAds(server, ads, { cliTextMode: true });
Activa esto solo si el content[] de tu herramienta ya contiene un
renderizado completo y legible por humanos del resultado por sí solo — no un
marcador de posición como "ver structuredContent". Cuando está activado, los clientes CLI detectados
sin un outputSchema declarado reciben structuredContent eliminado para que
content[] (el texto propio de tu herramienta + nuestra tarjeta) llegue de manera confiable al
modelo. Las herramientas que declaran un outputSchema nunca se tocan con esto —
eliminar structuredContent allí rompería la validación de esquema del lado del cliente
por completo (confirmado: fastmcp.exceptions.ToolError
"outputSchema definido pero no se devolvió salida estructurada"), que es una
llamada de herramienta rota, un resultado estrictamente peor que una tarjeta eliminada. Este SDK
nunca elimina structuredContent.sponsored en herramientas con esquema para perseguir visibilidad de tarjeta,
cliTextMode o no.
Hasta que se arregle el error del cliente ascendente, trata la tarjeta CLI como "se renderiza de manera confiable una vez que optas y tu herramienta califica, además de una divulgación que ya funciona de cualquier manera" — la misma advertencia de verificar en tu propio host que la ruta del widget anterior.
Servidores stdio
Todo lo que está por encima de la sección "Widget rendering" funciona sin modificaciones en un
servidor de transporte stdio — el SDK es una biblioteca que tu código importa y llama;
no sabe ni le importa cómo tu propio servidor habla con sus clientes. El
campo de datos simple sponsored (LuluAdsMiddleware / mcp.add_middleware(),
withLuluAds(server)) no toma argumento de endpoint y hace una
llamada HTTPS saliente simple a ads.getlulu.dev/slot — la misma solicitud ya sea que tu
proceso sea un servidor remoto de larga duración o un local lanzado por npx/uvx.
La ruta de tarjeta de texto CLI (ver "CLI rendering" arriba) es el caso común
del mundo real aquí: Claude Code lanza la mayoría de sus servidores MCP sobre
stdio, y ese es exactamente el cliente que este SDK ya detecta y renderiza
una tarjeta de texto plano divulgada.
El widget de MCP Apps renderizado es la única pieza que no aplica —
enable_lulu_ads/enableLuluAds y los de nivel inferior
register_sponsored_widget/registerSponsoredWidget todos requieren un
endpoint_url real, con hash en el valor no documentado _meta.ui.domain de Claude
para el CSP del iframe del widget. Eso no es un límite de Lulu Ads; el
apretón de manos ui/initialize de MCP Apps es un protocolo de red entre el host y el
propio endpoint HTTP de tu servidor, y un servidor stdio no tiene ninguno. Si tu servidor es
solo stdio, llama a LuluAdsMiddleware/mcp.add_middleware() directamente (o
withLuluAds(server) en TypeScript) — nunca enable_lulu_ads — y obtienes
el campo de datos más la tarjeta de texto CLI, sin nada que configurar para
el endpoint que no tienes.
Nota del lado del editor: la insignia automática de "monetizado" del marketplace
actualmente coincide una lista con tu cuenta de editor registrada por
remote_url — una lista stdio no tiene ninguna, por lo que no se auto-insigniará incluso una vez
que hayas integrado el SDK y estés ganando. La ruta del SDK/ganancias en sí no se ve
afectada; esto es puramente una brecha de visualización de listado del marketplace, que se
rastrea por separado.
Garantías (aplicadas en código, no solo prometidas)
| Garantía | Cómo |
|---|---|
| Una llamada de herramienta nunca puede romperse por anuncios | cada ruta de fallo devuelve None/null; tiempo de espera duro de 800ms de pared (3000ms cuando la llamada implica clasificación del lado del servidor) |
| Siempre divulgado | label: "Sponsored" es establecido por el SDK, nunca obtenido del cuerpo de la respuesta |
| Sin inyección de prompts, nunca | enviamos un campo de datos; no hay instrucción de visualización en ningún lugar del contrato |
| Sin PII sale de tu servidor | context se filtra contra una lista blanca del lado del cliente, antes de que se construya cualquier solicitud |
| Controlado por calidad | cada creativo pasa la puntuación de Dali (≥70) antes de que pueda llenar un espacio |
| Intención, no identidad | la segmentación usa solo el contexto declarado de esta llamada — sin perfiles de usuario, sin ID entre sesiones |
| ¿Mal configurado? Aún seguro | credenciales faltantes → el cliente es inerte, devuelve None/null, cero llamadas de red |
Por qué no simplemente…
…decirle al modelo que mencione a un patrocinador en su respuesta?
Las instrucciones de visualización hacen que los servidores MCP sean eliminados de los registros que escanean
directivas inyectadas. Enviamos un objeto de datos simple — label, text, url —
sin campo, en ningún lugar del contrato, que le diga a un modelo cómo renderizar o
formular cualquier cosa.
…contar impresiones y cobrar por vista? Una "impresión" solo existe si un modelo realmente la renderizó, y eso es inverificable desde el lado del servidor — fácil de manipular, difícil de auditar. Cobramos solo CPA, en un clic que canjea un token firmado y verificado por el servidor. El pago se asigna a una acción real del usuario, no a una afirmación.
…escanear la conversación para segmentar mejor?
Leer transcripciones para segmentar anuncios es una trampa de privacidad: todo lo que un usuario dice
se convierte en datos de segmentación de anuncios. Aceptamos seis claves de contexto en lista blanca — tool,
category, query, route, locale, country — intención declarada para esta
llamada solamente. Sin transcripciones, sin perfiles, sin campos de PII en el esquema.
Cómo funciona
tool call
│
▼
your tool's own result
│
▼
POST /slot (1500ms cap — 3000ms when classifying a raw prompt — allowlisted context only)
│
▼
labeled data field { label: "Sponsored", text, url } ← attached, never injected
│
▼
host / model judgment → renders it, or doesn't — not our call
│ user clicks
▼
GET /c/{token} → signed redirect, click recorded
│
▼
advertiser's affiliate rails → POST /postback on conversion
│
▼
70% publisher / 30% Lulu, on the ledger. Earnings accrue to your balance from the first audited conversion — cash out from $100.
Detalle completo a nivel de cable: docs/contract.md.
Docs: https://getlulu.dev/docs · Quickstart · Contrato de API · Integraciones · Registro de editor · Puerta de calidad: Dali · MIT
Registro de cambios
-
0.9.19 — Corrige lo que el SDK informaba sobre sí mismo.
SDK_VERSIONen el cliente TypeScript era un"0.9.14"codificado mientras el paquete pasó por cuatro versiones — y se envía a/slotcomosdk_version, por lo que cada editor de TypeScript informó 0.9.14 y la telemetría de actualización no podía saber quién realmente había actualizado. Ahora es correcto, y fijado por una prueba contrapackage.jsonpara que una versión que lo olvide falle la suite en lugar de corromper silenciosamente los datos. El cliente Python nunca tuvo este error; importalulu_ads.__version__.Docs, sin cambio de comportamiento: el encabezado de
js/src/skybridge.tstodavía describía el adaptador como solo_meta, lo que dejó de ser cierto en 0.9.15 — ahora lleva la misma tabla de aterrizaje de cuatro casos quedocs/integrations.md. Ypython/README.md, que es lo que PyPI renderiza, no tenía sincronización con el README raíz y se había quedado cuatro versiones atrás, por lo que la página del paquete describía Skybridge usando esa misma afirmación retirada. Ambos READMEs de paquetes ahora se alimentan descripts/sync-readmes.sh. -
0.9.18 — Soporte de Skybridge 2.x, y una protección contra su único fallo silencioso.
skybridge 2.x movió el cableado de middleware de protocolo fuera de
McpServer.connect()hacia la ruta de solicitud de la aplicación, por lo que el punto de conexión difiere por versión principal:// 2.x — inside the app handler new Skybridge({ name, version, handler: (server) => { withLuluAdsSkybridge(server); return server.registerTool({ name: "t" }, handler); }}); // 1.x — on the server you connect yourself withLuluAdsSkybridge(server);Usar la forma 1.x en 2.x registra el middleware en una cadena que nada aplica: sin error, sin anuncio, nada que depurar. El adaptador ahora detecta eso (su middleware siempre se ejecuta antes de un manejador de herramienta, por lo que una herramienta que se ejecuta mientras nunca se ejecutó significa que no está conectado) y advierte una vez con el fragmento correcto. Advierte, nunca lanza.
Verificado contra ambas versiones principales — 106/106 en
skybridge@1.4.1y@2.0.0, más un ejemplo de extremo a extremo ejecutado contra el paquete publicado. Ten en cuenta que en 2.x una herramienta idiomática solo decontentlleva el anuncio en_metasolo, por lo que una vista debería leerstructuredContentprimero y recurrir a_meta; la vista de referencia enexamples/skybridge-views/hace exactamente eso. -
0.9.15–0.9.17 — Skybridge entrega en absoluto, y el SDK informa quién está llamando.
En 2.0.1: estos cambios se publicaron brevemente como
2.0.1antes de ser retirados — la versión principal dejó varado cada rango de dependencia^0.9.xexistente, lo que significaba que nadie los habría recibido sin editar manualmente su package.json. Está retirado en PyPI y reemplazado en npm; usa la línea 0.9.x. Nada se perdió, solo se renumeró.Skybridge.
withLuluAdsSkybridgesolía adjuntarsponsoredsolo a_meta. Nada en la ruta de Skybridge lee_meta—enableLuluAdsno puede registrar un widget allí (elregisterViewResourcede Skybridge es privado) y no hay tarjeta CLI — por lo que el espacio se obtenía, se registraba y no se mostraba a nadie. Sondear la forma del resultado en vivo mostró quecontent[]llega vacío en Skybridge constructuredContentpoblado (lo inverso del SDK oficial), por lo questructuredContentes lo que una vista renderiza. Ahora aterriza allí:forma de herramienta sponsoredva asin outputSchemastructuredContenty_metatiene outputSchemasolo _meta— un campo no listado fallaría la validación del clienteregistrado antes de withLuluAdsSkybridgesolo _meta— estado de esquema desconocido, valor predeterminado seguroLa bandera de esquema se captura en el momento del registro a través de un envoltorio
registerToolde 2 argumentos, ya quemcpMiddlewareno puede veroutputSchema.Identidad del cliente. Cada adaptador ahora reenvía el nombre del cliente MCP como
context.client, para que el servidor de anuncios pueda distinguir un agente real de un rastreador de directorio."client"se une a la lista blanca de contexto. Sin cambios de código del integrador.Dos rupturas silenciosas corregidas en fastmcp 4.x / SDK MCP v2.
clientInfofue renombrado aclient_info; la ortografía antigua lanzaba dentro de unexceptdesnudo, por lo que_connected_client_namedevolvíaNoneen cada host 4.x — lo que también deshabilitó silenciosamente la tarjeta patrocinada CLI (is_cli_client(None)es False). Y 4.x negociaserver/discoveren lugar deinitialize, por lo queon_initializenunca se disparó y el calentamiento asíncrono se detuvo, devolviendo la latencia de arranque en frío que el trabajo de tiempo de espera escalonado existe para evitar. Ambos accesores ahora intentan ortografías nuevas y luego antiguas, yon_discoverse ejecuta junto aon_initialize. Suite de Python: 144 aprobados / 0 fallidos en ambas 4.0.5 y 3.4.4 (eran 8 fallidos). JS: 104/104.Actualización: permanecer en la línea 0.9.x es deliberado — un rango
^0.9.xalcanza 0.9.18, por lo quenpm update lulu-ads/pip install -U lulu-adses suficiente y no se necesita editar ningún rango de dependencia. Esa propagación es exactamente lo que el retirado 2.0.1 habría costado.Ver
examples/skybridge_server.tsynpm run verify:skybridge, que afirma todo lo anterior contra un servidor real sobre un transporte en memoria. -
0.9.14 — El
templatepor llamada de un anuncio (LUL-64, cuando/slotinforma uno) ahora prevalece sobre eltemplate=/template:que registraste, para el widget independiente de tarjeta de patrocinador (register_sponsored_widget()/registerSponsoredWidget()). Si está ausente o no es una plantilla que esta compilación del paquete reconozca, vuelve a tu valor predeterminado de registro como antes, y luego a"card". Cambio de comportamiento al actualizar, sin cambio de código requerido: si registraste con untemplate=no predeterminado (p. ej.,"hero"), los anuncios que llevan su propia plantilla en vivo ahora se renderizarán en esa plantilla, sin opción de exclusión. Esto es intencional: un administrador que elige una plantilla para un anuncio específico está destinado a ser más específico que un valor predeterminado general del integrador, pero sí significa que la salida visual de tu widget puede cambiar después de actualizar, incluso si no cambiaste ningún código. También corrige un riesgo de búsqueda donde un nombre de cadena de prototipo (p. ej.,"constructor") en un valor detemplateen vivo podría malinterpretarse como "reconocido" y bloquear el renderizado después de que el beacon de impresión ya se hubiera disparado. -
0.9.13 — Corregida una brecha de facturación de impresión renderizada (LUL-71) en el widget independiente de tarjeta de patrocinador (
register_sponsored_widget()/registerSponsoredWidget()— la tarjeta construida con React, banner, tarjeta giratoria, raspado-revelado, giro y plantillas héroe): el campoimp_url/impUrldel payload de la red era analizado y expuesto por ambos clientes (Sponsored.impUrlen el SDK de Node,imp_urlen el cliente de Python) pero se descartaba silenciosamente por el análisis de mensajes del propio paquete del widget antes de que llegara a los componentes de React — por lo que el beacon de impresión renderizada (un<img>de 1px que confirma que un humano realmente vio el anuncio, contra el cual se factura el CPM) nunca se disparó para este widget, para ninguna plantilla, desde que se lanzó. Corregido centralmente en la transición de carga→"cargado" deApp.tsx— el mismo momento en que el contenido divulgado de cada plantilla se vuelve visible, incluyendo el teaser frontal de la tarjeta giratoria (se dispara antes de que el usuario la gire, coincidiendo con el comportamiento ya correcto de la franja de pie de página deregister_result_widget()) y el contenido cubierto de raspado-revelado (se dispara antes de que alguien raspe).register_sponsored_widget()/registerSponsoredWidget()ahora también declaran los dominios CSP de recurso/conexión deads.getlulu.devque el píxel del beacon necesita para no ser bloqueado silenciosamente por la política predeterminada deimg-src 'self' data:de un host — la misma declaración queregister_result_widget()ya tenía. Verificado en vivo: el beacon dispara una solicitud HTTP real en el instante en que llega untool-resultconimp_url, confirmado a través del registro de acceso de un servidor local, no solo pruebas unitarias. -
0.9.12 — El
sponsor_template=del pie de página del widget de resultados (LUL-69) rediseñado de una franja delgada de una sola línea a una tarjeta de portada: una banda de portada colorida — una imagen de patrocinador real cuando el payload en vivo proporcionacover_image_url/coverImageUrl, un gradiente animado en caso contrario — con el mosaico del logotipo superpuesto en la unión hacia el cuerpo, tipografía más grande y un botón de CTA tipo píldora real. El teaser de"flip-card"ahora lleva la misma identidad de portada en lugar de una etiqueta simple."spin"se elimina: una animación descaleXde lanzamiento de moneda en un logotipo de mosaico de letras pequeño y a menudo plano resultó visualmente ilegible en la práctica (a mitad de la animación se encoge a una astilla casi invisible contra un fondo de portada ocupado), detectado en vivo en lugar de inferido. Se reemplaza por"carousel"— cicla automáticamente a través de tres encuadres del mismo payload patrocinado único (un logotipo de marca más grande, el texto de la oferta, luego el CTA), nunca múltiples patrocinadores: este marco solo recibe un payload patrocinado por llamada, por lo que a diferencia de la plantilla de carrusel de la galería independiente, no hay rotación de múltiples anunciantes aquí (ver LUL-49 para esa pregunta de backend separada, aún bloqueada)."scratch-reveal"se rediseña como una tira de papel de aluminio de boleto de lotería — gradiente metálico dorado, rayado diagonal, una línea de perforación discontinua, un glifo de boleto — y su ventana de auto-revelado es ahora de 5s, no 3s, lo suficientemente larga para registrarse realmente como una interacción; sigue siendo una revelación estrictamente determinista sin estado de "ganar/perder", que es lo que mantiene el estilo de boleto fuera del territorio de mecánicas de juego reales. El razonamiento de "una sola fila, sin espacio para una imagen a sangre completa" que solía excluir también a"hero"ya no se sostiene ahora que la franja misma tiene altura de portada — señalado como un seguimiento abierto, no resuelto por esta versión. -
0.9.11 —
register_result_widget()/registerResultWidget()ganansponsor_template=/sponsorTemplate:(LUL-69): la franja PATROCINADO construida en el pie de página de un widget de resultados (stat-card/table-card/notice-card/carousel-card— la salida de herramienta propia de un editor, no el widget independiente de tarjeta de patrocinador) ahora también puede elegir un formato visual, independiente detemplate=, el diseño del cuerpo de este widget."card"(predeterminado) es la franja original siempre visible."flip-card"es un teaser compacto "Patrocinado" que se funde para revelar la oferta real al tocar — la cara frontal lleva un aviso real ("Toca para revelar →"), no solo una etiqueta de divulgación simple, por lo que hay una razón para tocarlo."spin"es un adorno decorativo de giro y asentamiento en la insignia del logotipo, contenido visible inmediatamente, nunca bloqueado (sin resultados variables — ver la salvaguarda de giro a continuación)."scratch-reveal"es una capa de raspado en lienzo, que se auto-revela después de 3s independientemente de la interacción — la oferta subyacente es idéntica si alguien raspa o no."banner"/"hero"se ofrecen deliberadamente no aquí: esta franja ya es una fila horizontal (el banner sería un no-op) y un fondo a sangre completa no cabe en una barra de pie de página delgada. Este es un sistema diferente de la galería de tarjetas de patrocinador independiente anterior (el propiotemplate=deregister_sponsored_widget()) — mismos nombres de plantilla donde se superponen, portados al renderizador vanilla-JS de este marco en lugar de compartir código, ya que los dos marcos son implementaciones independientes por diseño. -
0.9.10 — Cuatro plantillas más:
"flip-card"(LUL-53, un giro 3D CSS al tocar que revela la oferta en la cara posterior — la frontal siempre muestra primero la etiqueta divulgada, nunca oculta qué es esto, solo los detalles de la oferta),"scratch-reveal"(LUL-52, una capa de raspado en lienzo, se auto-revela después de 3s independientemente de la interacción — la oferta subyacente es idéntica si alguien raspa o no, esto es una animación de revelación, nunca contenido bloqueado),"spin"(LUL-54, un adorno decorativo de giro y asentamiento en la insignia del logotipo — determinista por diseño, sin resultados variables estilo rueda de la fortuna, ya que eso sería una mecánica de juego), y"hero"(LUL-48, una imagen de fondo a sangre completa o gradiente con logotipo/texto/CTA anclados sobre una cortina de legibilidad).register_sponsored_widget()/registerSponsoredWidget()también ganan un parámetro opcionalbackground_image/backgroundImagepara"hero"— el mismo contrato de buscar-una-vez-e-incrustar-como-URI-data:quelogoya tiene (un fallo de búsqueda solo vuelve al gradiente compartido, nunca un error de registro). Esto es marca proporcionada por el integrador, en el momento del registro, misma categoría quelogo/accent*— aún no conectado a la coincidencia automática de anuncios por campaña de ads-server, que no tiene un campo de imagen propio (seguido por separado). -
0.9.9 — Primera entrada real en la galería de plantillas:
template="banner"(LUL-47), una franja horizontal de ancho completo (etiqueta, logotipo, texto/CTA todo en una fila) en lugar del diseño apilado del"card"predeterminado — mejor ajuste para superficies anchas pero cortas (barra lateral de VS Code, paneles CLI anchos). Mismo contrato compartido que cada plantilla: se renderiza dentro del mismo shell de gradiente persistente, misma etiqueta divulgada + pie de página "Powered by Lulu Ads", mismo comportamiento de esqueleto-al-cargar y fallo-abierto — nada de esas garantías es específico de la plantilla. Sin soporte de imagen de fondo aún (la especificación de LUL-47 permite "imagen o gradiente"; un campo real de imagen de fondo necesita un nuevo esquema en el lado del anunciante, seguido por separado) — usa el gradiente de token de acento existente. Documentación añadida: El renderizado de widgets ahora documentatemplate=con un ejemplo en vivo y una tabla de formatos disponibles. -
0.9.8 —
register_sponsored_widget()/registerSponsoredWidget()ganan un parámetrotemplate=(LUL-46), misma forma que el deregister_result_widget: solo por palabra clave, predeterminado a"card"(el único formato de hoy, totalmente compatible con versiones anteriores — ningún sitio de llamada de integrador existente cambia su comportamiento), genera un error inmediatamente en un valor no reconocido (antes de cualquier llamada de red, p. ej., la búsqueda del logotipo). Esta es una elección del integrador en el momento del registro, no un valor por llamada — coincide con el comportamiento estático, "incorporado en el paquete compilado en el registro" que la propia plantilla deregister_result_widgetya tiene, no algo que varíe por anuncio servido. Solo fundamental: el mecanismo de registro/despacho enjs/widget-src/ahora admite buscar una plantilla por nombre, pero"card"(el diseño existente construido a mano) sigue siendo la única entrada — más plantillas llegan en sus propias versiones de seguimiento a medida que se construyen (banner, héroe, carrusel, comparación, tarjeta giratoria, raspado-revelado, giro, testimonio, cuenta regresiva, cuestionario, video). También corrige una deriva real y preexistente: la constante de telemetríaSDK_VERSIONdel propio SDK de TS había estado atascada en0.9.0desde una versión mucho anterior a pesar de quepackage.jsonavanzó a0.9.6— realineado aquí, y los números de versión de los dos paquetes ahora están de nuevo en sincronía (el0.9.7anterior era solo de Python; el paquete de TS salta directamente a0.9.8). -
0.9.7 (solo Python) — Los clientes CLI/texto ahora reciben un beacon de entrega confirmada: el píxel de impresión renderizada normal (
imp_url) es buscado por un cliente de renderizado en el instante en que muestra la franja patrocinada — los hosts de terminal/CLI (Claude Code y similares) no tienen motor de renderizado para hacer eso, por lo que cada tarjeta entregada por CLI era previamente invisible enad_events, sin más, incluso en entregas 100% exitosas. El middleware ahora dispara esa misma URL de beacon por sí mismo (dispara-y-olvida, no bloqueante, etiquetadosrc=cli_server) en el momento en que agrega una tarjeta a uncontent[]de cliente CLI, en ambas rutas de agregado de tarjeta is_cli (la ranura buscada por el propio middleware, y unsponsoredpreestablecido de una herramienta). Esto se registra como un nuevo eventocli_card_delivereddistinto — noimpression_rendered, y nunca se cuenta para la facturación/pago de CPM — una señal deliberadamente más débil que un golpe de píxel renderizado real: prueba que la tarjeta salió del servidor en la respuesta de la herramienta, nunca que un humano realmente la vio. No necesita configuración; las ranuras existentes que llevanimp_urllo recogen automáticamente. (ads-server:/i/{token}ahora acepta un parámetro de consulta opcionalsrc=cli_serverpara registrar el tipo de evento distinto.) -
0.9.6 — Solo documentación: nueva página Superficies compatibles que clasifica cada superficie de agente (hosts de chat, SDKs/marcos de agentes, runtimes de sufijo de respuesta, constructores de aplicaciones de IA, alojamiento/registros de MCP) por cómo realmente llega al SDK — un adaptador directo, paso a través del protocolo MCP (sin adaptador necesario una vez que un servidor tiene Lulu Ads conectado), el contrato genérico
format_suffix, o genuinamente aún no evaluado — mismo estándar de evidencia que el resto de este repositorio. También añade una sección de "servidores stdio" que aclara que la ruta del campo de datos no necesitaendpoint_urly funciona sin modificar en servidores de transporte stdio; solo el widget de aplicaciones MCP renderizado requiere uno y no puede aplicarse a stdio. También corrigelulu_ads.__version__, que se había desviado a 0.9.0 mientras el paquete se publicaba como 0.9.5 — misma clase de error que la entrada 0.7.0 a continuación, recurrió porque nada hace cumplir que los dos se mantengan sincronizados; considere eso como la próxima brecha real a cerrar aquí. -
0.9.2 (solo Python) — Corregido un error de middleware: una herramienta que establece su own
sponsoredfield (a documented pattern for e.g. a category-specific cross-sell) triggered the "never overwrite" early return inon_call_toolbefore the CLI-client check ran, so CLI hosts (Claude Code) got no visible ad at all on that tool — no widget surface, and no text-card safety net either, both skipped by the same early exit. The client check now runs first; a pre-setsponsoredvalue still gets the CLI text-card treatment, using the tool's own chosen ad. -
0.9.1 —
table-cardwidget gainsrowLink: an optional per-row dot-path resolving to a URL (e.g. a booking/checkout link), wired to the same host-agnosticopenLink()the sponsored strip already uses. Rows without a resolvable URL render exactly as before. -
0.9.0 — Soporte de Skybridge (https://skybridge.tech):
withLuluAdsSkybridge(server)(lulu-ads/skybridge). ElMcpServer.registerToolde Skybridge toma una forma de(config, handler)de 2 argumentos connameintegrado enconfig, no el(name, config, handler)de 3 argumentos del SDK oficial quewithLuluAdsenvuelve — reutilizarwithLuluAdstal cual malinterpretaría el objeto de configuración como el nombre de la herramienta. El nuevo adaptador en su lugar usa el hook de protocolomcpMiddleware("tools/call", ...)propio de Skybridge, verificado contra los tipos enviados del paquete realskybridge@1.4.0y un round-trip en vivo deInMemoryTransport. Deliberadamente solo_meta: el middleware ve el resultado de la llamada pero no eloutputSchemaregistrado de la herramienta, por lo questructuredContentnunca se toca. -
0.8.1 — Galería de plantillas del widget de resultados (reemplaza a 0.8.0, que se publicó brevemente en npm con un diseño de franja más llamativo):
lulu_ads.widgets/lulu-ads/widgetscon cuatro plantillas predefinidas (stat-card,table-card,notice-card,carousel-card), tokens de diseño + primitivas.lw-*, y la franja SPONSORED divulgada integrada en el marco (logotipo del anunciante a través del nuevologo_urlde la ranura, respaldo de letra-tile).register_result_widget()parchea una herramienta FastMCP ya registrada en su lugar (o devuelve el AppConfig paraapp=explícito). -
0.7.4 — Widget: el lienzo del iframe de la tarjeta patrocinada ya no pinta una caja blanca opaca en hosts oscuros.
background: transparentsolo no es suficiente para un iframe embebido: Chromium mantiene el lienzo transparente únicamente cuando el esquema de color utilizado del documento embebido coincide con el del embedder, y este documento no declaraba ninguno (por defectolight), por lo que los hosts con tema oscuro (p. ej., claude.ai en modo oscuro) forzaban un fondo blanco detrás de la tarjeta. El widget ahora declaracolor-scheme: light dark, que se resuelve al esquema preferido del usuario — coincidiendo con los hosts que lo siguen (claude.ai lo hace por defecto) en temas claros y oscuros. Verificado empíricamente contra páginas de embebido con esquema claro y oscuro. (También alinealulu_ads.__version__, que había derivado a 0.7.2 mientras los paquetes se publicaban como 0.7.3.) -
0.7.0 — Dos errores reales, encontrados en vivo contra un servidor MCP de terceros real detrás del conector remoto de Claude.ai, ambos corregidos:
- Entrega de anuncios al 0% en hosts que se reconectan por mensaje (confirmado: Claude.ai abre una sesión MCP nueva por cada mensaje de chat, no una vez por conversación). Causa raíz: la conexión HTTP persistente de este SDK se enfría en cualquier intervalo de inactividad real entre mensajes, pero solo una verificación única de "¿alguna vez he tenido éxito?" protegía la primera llamada de todas — cada llamada fría posterior aún recibía el tiempo de espera estricto de estado estable y fallaba. Corregido al volver a verificar el estado frío en cada llamada, basado en el tiempo desde el último éxito real, no en un pestillo permanente. Además: el tiempo de espera rápido de estado estable en sí se aumentó de 800 ms a 1500 ms (Python y TS) — la evidencia de producción mostró que incluso las llamadas "calientes" a veces medían 796-802 ms, justo en la línea anterior en lugar de cómodamente por debajo.
- Anuncio obtenido con éxito, nunca visto por el modelo. FastMCP/el
SDK TS de MCP construyen el
content[]de un resultado de herramienta una vez, a partir del valor de retorno original de la herramienta, antes de queLuluAdsMiddleware/withLuluAdsalguna vez se ejecuten — mutar solostructuredContent(lo único que el propio conjunto de pruebas de este SDK verificaba) dejabacontent[]permanentemente obsoleto. Confirmado en vivo: elstructuredContentde la respuesta en el cable demostrablemente teníasponsored, pero Claude.ai leía e informaba desdecontent[], que no lo tenía. Ambos SDK ahora mantienencontent[]sincronizado siempre que sea seguro (un único bloque de texto JSON autogenerado); se agregaron pruebas de regresión para la brecha exacta que permitió que esto se publicara sin ser notado la primera vez. - Nuevo:
enable_lulu_ads()(Python) /enableLuluAds()(TS) — una llamada que conecta tanto el campo de datos como el widget de MCP Apps renderizado a cada herramienta automáticamente, presente y futura. Los existentesregister_sponsored_widget()/registerSponsoredWidget()+app=/_meta.uipor herramienta aún funcionan y ahora están documentados como el bloque de construcción de nivel inferior para el control por herramienta; la brecha que dejaban (un paso manual fácil de olvidar por herramienta) es exactamente lo que esto cierra — encontrado en vivo en nuestro propio servidor dogfood, que había conectado el widget a exactamente una herramienta manualmente y silenciosamente nunca lo actualizó para las herramientas agregadas desde entonces.
-
0.6.2 — La tarjeta patrocinada ahora reproduce un barrido de luz diagonal único sobre sí misma cuando se asienta en el estado cargado (un anuncio real ganó) — CSS puro (
.card-shineenjs/widget-src/src/index.css), se dispara exactamente una vez por montaje (no un brillo en bucle, ya que esto se encuentra en línea en un hilo de chat real), y respetaprefers-reduced-motion. Los estados de esqueleto y sin relleno no se ven afectados. -
0.6.1 — Corrige un
0.6.0obsoleto publicado en npm antes de quedist/fuera reconstruido desde el código fuente fusionado (js/no tiene un paso de compilaciónprepublishOnly) — 0.6.0 está obsoleto en npm apuntando aquí. También corrige README.md y los docstrings dewidget.py/widget.tsen ambos idiomas, que afirmaban incorrectamente quetext/url/logopasados aregister_sponsored_widget()se renderizan como un "anuncio de casa" de respaldo hasta que llegue untool-resulten vivo; nunca lo hacen — el widget muestra el esqueleto indefinidamente si un host nunca lo empuja. -
0.6.0 — El widget patrocinado de MCP Apps ahora muestra contenido de anuncio en vivo, por llamada en lugar de un anuncio de casa fijo integrado en el momento del registro: reconstruido en React + shadcn/ui (
Card,Skeleton,Button), compilado a un único paquete autocontenido compartido byte por byte por ambos SDK (js/widget-src/). El widget muestra un esqueleto inmediatamente al cargar, luego escucha el propio push deui/notifications/tool-resultdel host de MCP Apps — que la especificación ya entrega una vez por llamada, a un iframe nuevo por llamada, sin necesidad de cambios en el servidor — y cambia a los datos reales destructuredContent.sponsoredde esa llamada — el widget muestra el esqueleto indefinidamente si un host nunca lo empuja, no un anuncio de respaldo; sololabel/cta/accent*de las opciones deregister_sponsored_widget()/registerSponsoredWidget()se usan realmente en la ruta en vivo. El pie de página "Powered by Lulu Ads" se renderiza una vez, inmediatamente, y nunca es parte del intercambio esqueleto→tarjeta. Verificado en vivo contra un host real (claude.ai) con un servidor de prueba desechable: el esqueleto se renderiza antes de que la llamada a la herramienta se resuelva, cambia a la tarjeta real por llamada una vez que lo hace, el pie de página nunca desaparece ni se refluye durante el intercambio, y dos llamadas a herramientas en el mismo turno renderizan dos instancias de widget completamente independientes, cada una mostrando solo los datos de su propia llamada — confirmando el comportamiento "por llamada, no por herramienta" en el que se basa esta característica. (La redirecciónui/open-linkdel CTA — vs. una navegación cruda — fue reconfirmada por inspección estática de código y las pruebas unitarias existentes de este repositorio durante este mismo pase; se intentó la captura de clics en vivo pero fue bloqueada por límites de las herramientas de automatización del navegador para llegar dentro del iframe doblemente sandboxed del host, no por ninguna falla de producto observada.) -
0.4.0 — Pre-conexión automática en la construcción para
LuluAdsAgentMiddlewarede LangChain,install()de CrewAI ywithLuluAdsde TypeScript (coincidiendo con elLuluAdsMiddlewarede FastMCP, que ya tenía esto). Además: elLuluAdsMiddlewarede FastMCP y elLuluAdsAgentMiddlewarede LangChain ahora también calientan el pool de conexiones asíncronas que su tráficoawaited desponsored_slot()realmente usa — el calentamiento en tiempo de construcción anterior solo tocaba el cliente síncrono, un pool separado que la ruta asíncrona nunca toca.LuluAds.async_warm_up()se dispara una vez por instancia desde un hook de ciclo de vida de framework real en el bucle de eventos de servicio en vivo (elon_initializede FastMCP, elabefore_agentde LangChain), ya que un hilo en segundo plano no puede pre-calentar de manera segura una conexión destinada a un bucle de eventos diferente. Controlado por el mismo flagauto_warm_upque el calentamiento síncrono (esta ruta asíncrona es solo de Python — elautoWarmUpde TypeScript solo tuvo un pool para controlar). Esta es la corrección que cierra la brecha de arranque en frío paradali-mcpen producción, que consume la ruta asíncrona. Caché de solo éxito con TTL corto (45 s por defecto) en ambos clientes base, basado en la categoría resuelta o un hash del texto del prompt. Documentación corregida: el tiempo de espera predeterminado real es de 800 ms (ruta rápida) / 3000 ms (ruta de clasificación) adaptativo, no un plano de 300 ms. -
0.3.7 —
cliTextMode(opt-in, desactivado por defecto): corrige el error de eliminación de content[] de Claude Code de verdad, pero solo para herramientas cuyocontent[]ya se sostiene por sí solo sinstructuredContent— probado en vivo tanto en casos que califican como en los que no, ver "Renderizado CLI". Nunca toca herramientas con unoutputSchemadeclarado (rompería la validación de esquema del lado del cliente, confirmado víafastmcp.exceptions.ToolError). -
0.3.6 — calentamiento automático de conexión en la construcción de
LuluAdsMiddleware(auto_warm_up, activado por defecto): una primera llamada a herramienta genuinamente fría midió 804 ms contra el predeterminado de ruta rápida de 800 ms — justo en el techo, no por debajo.LuluAdsen sí nunca se auto-calienta (una llamada de red como efecto secundario del constructor es sorprendente en un cliente de propósito general), pero el middleware es la promesa de "una línea, cero configuración", por lo que se calienta a sí mismo. -
0.3.5 — corrigió un
timeout_mspredeterminado de 300 ms codificado enLuluAdsMiddlewareque silenciosamente descartaba anuncios reales y llenables en latencia de red real — cada prueba en el conjunto usaba un transporte simulado instantáneo, que es exactamente por qué esto se publicó sin ser notado. El predeterminado ahora esNone, difiriendo al predeterminado condicional de 800 ms/3000 ms deLuluAds. -
0.3.4 — La tarjeta CLI obtiene esquinas redondeadas y un pie de página "via Lulu Ads" (solo dibujo de cajas Unicode — una prueba en vivo contra Claude Code confirmó que elimina los escapes de color ANSI crudos de la salida de la herramienta antes de que el modelo los vea, por lo que el color nunca estuvo sobre la mesa). También probado en vivo y explícitamente rechazado eliminar
structuredContentpara forzarcontent[]: sí hace que la tarjeta llegue, pero el modelo luego la marca como sospechosa de inyección de prompt y advierte al usuario que se aleje — peor que el status quo, donde el campo simple aún se muestra honestamente incluso sin la tarjeta. Ver "Renderizado CLI" para el informe completo. -
0.3.3 — Renderizado adaptativo a CLI:
LuluAdsMiddleware/withLuluAdsdetectan clientes CLI conocidos a través delclientInfo.namede MCP enviado eninitialize(actualmente:claude-code, verificado en vivo) y agregan una tarjeta de texto plano con bordes acontent[]para ellos, además del campo simple — las terminales no tienen superficie de widget, por lo que este es el equivalente seguro para CLI del widget de MCP Apps anterior. Sigue siendo solo datos; ver "Renderizado CLI" para la limitación conocida divulgada sobre el reenvío decontent[]en algunos clientes. -
0.3.0 —
register_sponsored_widget()/registerSponsoredWidget()ganan una opciónlogo: obtenida del lado del servidor en el momento del registro e incrustada en el widget como una URIdata:, por lo que se renderiza bajo la CSP del sandbox del widget (img-src 'self' data: <resourceDomains>) sin necesidad de configuraciónresourceDomainsde tu parte — una URL de logo remota cruda de otro modo se descartaría silenciosamente, sin error en ningún lugar. Un logo malo o inalcanzable nunca rompe el registro; la tarjeta simplemente se renderiza sin uno. ElregisterSponsoredWidget()de TypeScript ahora esasync(puede necesitar obtener el logo antes de devolver) — agregaawaiten los sitios de llamada existentes. -
0.2.0 —
register_sponsored_widget()(Python:lulu_ads.widget, ahora también TypeScript:lulu-ads/widget, SDK oficial de MCP): registra una tarjeta patrocinada de UI de MCP Apps realmente renderizada en tu servidor (no solo el campo JSON simple), manejando el requisito de dominio de iframe no documentado de Claude y el handshakeui/notifications/initializedpor ti. Generaliza la corrección verificada en vivo endali.getlulu.dev/mcpcontra ext-apps#671. Ambos SDK producen valores_meta.ui.domainbyte-idénticos para la misma URL de endpoint. -
0.1.1 — clientes HTTP persistentes en el SDK de Python (la construcción de clientes por llamada podía quemar todo el presupuesto de slots en contenedores con CPU limitada; los clientes ahora se crean una vez por instancia de
LuluAdsy se reutilizan con keep-alive). El comportamiento de fallo abierto no cambia. -
0.1.0 — lanzamiento inicial: clientes de Python + TypeScript, adaptadores FastMCP / LangChain / LangGraph / CrewAI / MCP-TS, helpers de sufijo, incorporación de conserje MCP.