Lulu Ads

MCP de concierge para editores do SDK de monetização Lulu Ads — guia de integração autônoma, registro de editor e verificação para qualquer servidor MCP ou ferramenta de agente. 70% de participação na receita CPA, campo de dados patrocinados divulgado, nunca uma instrução de exibição.

Documentação

lulu-ads

A camada de monetização para a economia de agentes.

Monetize seu servidor MCP ou ferramenta de agente com uma linha patrocinada rotulada.

PyPI npm License: MIT Backend Publisher beta Rev share Lulu MCPs

Início rápido · Integrações · Hosts suportados · Superfícies suportadas · Servidores stdio · Garantias · Contrato de API · Docs hospedados · Blog · Torne-se um publisher

Lulu, the Lulu Ads narwhal mascot, celebrating on a Tel Aviv billboard — the agent economy has a monetization layer now

70% to publishers · CPA only · 800ms fail-open · 0 prompt injections, by design

Sponsored
↑ card de patrocínio renderizado ao vivo — demanda real de anúncios rotativos, atualiza a cada ~60s. Qualquer listagem reivindicada pode incorporar isso no próprio README.

Lulu Ads anexa um campo de dados divulgado e rotulado ao resultado da sua própria ferramenta. O modelo host — Claude, Cursor, qualquer agente — decide por conta própria se é relevante o suficiente para exibir. Nunca instruímos o modelo a fazer isso.

O que o SDK entrega (um campo de dados)O que o host renderiza (escolha dele)
{
  "sponsored": {
    "label": "Sponsored",
    "text": "Direct flights TLV–BKK from $412",
    "url": "https://ads.getlulu.dev/c/9f2a1c"
  }
}

Patrocinado — Voos diretos TLV–BKK a partir de $412 ads.getlulu.dev/c/9f2a1c

Início sem fricção — adicione o servidor MCP e deixe seu agente fazer o resto:

claude mcp add --transport http lulu-ads https://ads.getlulu.dev/mcp

monetize meu servidor

Ele buscará o guia de integração certo para sua stack, registrará um publisher (com seu consentimento), configurará a linha única e verificará que um slot foi ao ar.

Se renderizar e for clicado, você ganha 70% em CPA. Se não — ninguém paga, nada quebra.

Sem injeção de prompt — entregamos um campo de dados; o host decide.

Início 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

Servidores FastMCP conseguem isso em uma única chamada — as credenciais vêm do ambiente, e cada ferramenta (presente e futura) recebe tanto o campo de dados sponsored simples QUANTO, em hosts que suportam (ex.: Claude.ai), o widget de card patrocinado renderizado, automaticamente:

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")

Quer apenas o campo de dados, sem widget? O middleware simples ainda funciona sozinho:

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" } });

Servidores MCP construídos no SDK TS oficial recebem o mesmo tratamento de uma chamada — campo de dados E widget em cada ferramenta, automaticamente:

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" });

Ainda não tem ID de publisher? Veja docs/quickstart.md — três maneiras de obter um, nenhuma delas dependente das outras.

Preços em camadas (anúncios no nível gratuito, sem anúncios no pago)? Passe enabled — sua própria verificação de assinatura decide o valor, sem implantação separada ou condicionais espalhadas:

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",
});

Integrações de frameworks

StackLinha únicaDocs
FastMCP (Python), dados + widgetenable_lulu_ads(mcp, endpoint_url=...)→
FastMCP (Python), apenas dadosmcp.add_middleware(LuluAdsMiddleware())→
MCP TS SDK, dados + widgetawait enableLuluAds(server, { endpointUrl })→
LangChain / LangGraph (Python)middleware=[LuluAdsAgentMiddleware()]→
CrewAI (Python)lulu_crewai.install()→
MCP TS SDK, apenas dadoswithLuluAds(server)→
Skybridge (TypeScript)withLuluAdsSkybridge(server)→
Donos de runtime (chat bots, agentes WhatsApp/Telegram)model_output + format_suffix(sponsored)→
Qualquer outro runtime / linguagemsponsored_slot(context) sobre o contrato bruto→

Widgets de resultado — modelos para a SAÍDA da SUA própria ferramenta (0.8.5)

Um widget, todos os hosts. O frame fala três pontes — MCP Apps estável (ui/initialize, 2026-01-26), o fallback da era de rascunho e o window.openai do ChatGPT — e o SDK registra ambas as chaves de modelo (_meta.ui.resourceUri + openai/outputTemplate) e ambos os dialetos de CSP automaticamente. Renderização verificada ao vivo em claude.ai e ChatGPT, incluindo o beacon de impressão renderizada (impressões contam o que um humano realmente viu, nunca mera saída de API). Após atualizar, atualize seu conector nas configurações de plugin do ChatGPT — ele armazena em cache os metadados da ferramenta.

O formato da faixa PATROCINADO é configurável separadamente via sponsor_template= (independente de template=, o layout do corpo deste 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 suportados

O campo JSON sponsored simples é a linha de base sempre ativa: ele chega em cada resultado de ferramenta, em cada host MCP, porque não é nada mais que uma chave extra em um dict — nenhum suporte específico de host é necessário para funcionar, e o modelo decide por conta própria se deve exibi-lo. O widget renderizado de MCP Apps acima é aditivo e só é pintado onde um host realmente implementou o handshake ui/initialize. Esta tabela diz exatamente qual é qual por host, com base em nossa própria verificação de produção onde a temos e uma pesquisa recente (2026-08-25) em todos os outros lugares — um host só recebe status de widget "Ao vivo" aqui quando confirmamos nós mesmos ou o fornecedor publicou detalhes concretos e verificáveis de implementação, nunca em uma suposição genérica de "deveria funcionar".

Claude   ChatGPT   CopilotKit   VS Code   Cursor   Goose   Grok (xAI)   Windsurf   Cline   Zed

Hosts que analisamos — logos não são uma afirmação de suporte por si só; leia a coluna Status abaixo para o que cada um realmente faz. (Continue.dev está na tabela, mas não na faixa acima: é um produto descontinuado, mantido aqui apenas para completude.)

HostChamada de ferramenta MCPWidget renderizadoStatus
Claude (claude.ai)SimSimAo vivo, verificado em produção — beacons reais de impressão renderizada observados em tráfego ao vivo.
ChatGPTSimSimAo vivo, verificado em produção.
CopilotKit (@ag-ui/mcp-apps-middleware)SimEm andamentoCorreção em revisão, PR #8, não verificado de ponta a ponta — um bug de descoberta de ferramenta foi encontrado e corrigido, mas a correção não foi testada contra uma UI de chat completa (sem LLM disponível nessa passagem) e ainda não foi lançada para npm/PyPI. Não trate CopilotKit como suportado até que esse PR seja publicado e verificado ao vivo. O campo sponsored simples não é afetado por esse bug e já flui hoje.
VS Code (MCP nativo + modo agente do GitHub Copilot Chat)SimReportado ao vivoO post de blog oficial da Microsoft de 2026-01-26 e os docs atuais descrevem o VS Code como "o primeiro grande editor de código com IA com suporte completo a MCP Apps" e documentam detalhes concretos e verificáveis de implementação (iframes em sandbox, configuração de domínio CSP, o handshake ui/initialize, o App SDK) — crível, mas é uma afirmação do fornecedor que não reproduzimos de forma independente. Chamada de ferramenta MCP simples (modo agente do Copilot Chat) está GA desde v1.102.
CursorSimReportado, não verificadoNomeado como implementador de MCP Apps na Matriz de Suporte de Extensões upstream do modelcontextprotocol.io — uma listagem de terceiros, não os docs do próprio Cursor, então evidência mais fraca que o detalhe publicado pelo fornecedor do VS Code/Goose acima. Na verdade, tentamos verificar isso ao vivo (2026-08-25) e fomos bloqueados antes de chegar ao teste: o limite de uso do Agent no nível gratuito do Cursor (2 prompts) foi atingido antes de uma chamada de ferramenta real passar. Tentativa real, bloqueio real, ainda não confirmado — não é uma afirmação que estamos evitando.
Goose (Block / AAIF)SimAo vivo (experimental)Os docs do próprio Goose confirmam o handshake ui/initialize e a renderização de iframe em sandbox (Goose Desktop 1.19.1+), mas explicitamente marcam como "experimental e baseado em uma especificação de rascunho; a implementação é mínima e pode mudar." Trate como ao-vivo-mas-instável, não um alvo de renderização garantido.
Grok (xAI) — conectores grok.com, CLI Grok Build, Ferramentas MCP Remotas da API xAISimNenhuma evidência encontradaCapaz de MCP em todas as três superfícies xAI (descoberta de ferramenta simples + chamada), mas nenhum doc oficial, changelog ou matriz de suporte de host de terceiros credita o Grok com a extensão de UI MCP Apps até esta pesquisa. O campo de dados patrocinado ainda flui e ainda renderiza puramente no julgamento do modelo via fallback JSON sempre ativo — o widget rico apenas não tem onde renderizar.
Windsurf (Codeium)SimNenhuma evidência encontradaOs docs do próprio Windsurf afirmam que ele suporta apenas "as ferramentas, recursos e prompts de um servidor MCP"; toda lista de suporte de host MCP Apps de terceiros que encontramos o omite. Campo de dados patrocinado ainda funciona via fallback JSON sempre ativo.
Cline (extensão VS Code)SimNenhuma evidência encontradaCliente MCP maduro (ferramentas, recursos, prompts, um marketplace MCP integrado); nenhum código ui/initialize, ui:// ou de renderização de iframe encontrado em qualquer lugar do repositório. Campo de dados patrocinado ainda funciona via fallback JSON sempre ativo.
Editor ZedSimNenhuma evidência encontradaOs docs do próprio Zed afirmam claramente que ele "atualmente suporta os recursos de Ferramentas e Prompts do MCP" — sem renderização de UI baseada em Recursos. Campo de dados patrocinado ainda funciona via fallback JSON sempre ativo.
Continue.devSim (historicamente)Nenhuma evidência encontradaDescontinuado: adquirido pela Cursor em junho de 2026, e o repositório continuedev/continue agora é somente leitura, sem desenvolvimento adicional. Ele suportava ferramentas/recursos/prompts MCP simples enquanto ativo, sem evidência de que já renderizou widgets MCP Apps. Não é um alvo de integração viável daqui para frente — listado aqui apenas para completude.

Por que alguns hosts precisam de zero código extra e outros não

Hosts diferentes convergiram para convenções diferentes sobre como uma ferramenta anuncia "tenho uma UI renderizável" — e onde a convenção de um host difere da que entregamos primeiro, a descoberta falha silenciosamente antes que a renderização tenha a chance de rodar (essa foi a lacuna do CopilotKit que o PR #8 corrigiu, 2026-08-25). Rastreamos cada convenção que confirmamos e registramos contra todas elas em cada ferramenta com capacidade de widget — apenas aditivo, nunca uma reescrita, então um host que não reconhece um sinal simplesmente o ignora. Essa é a razão prática pela qual Claude e VS Code renderizam com zero código extra (eles compartilham uma convenção) enquanto CopilotKit precisou de uma correção direcionada, e é por isso que "nenhuma evidência encontrada" na tabela abaixo significa exatamente isso — evidência ainda não encontrada, não evidência de ausência.

Qualquer outra coisa não listada acima (LangGraph Studio, harnesses de agentes internos personalizados, e todos os hosts que simplesmente ainda não analisamos): desconhecido / ainda não investigado — o campo sponsored simples é projetado para falhar abertamente e degradar graciosamente em qualquer um deles, de acordo com as Garantias abaixo. Se você verificou a renderização em um host que não está nesta tabela, abra uma issue ou PR — esta lista deve permanecer honesta, não exaustiva.

Esta tabela é especificamente sobre renderização de widgets em hosts de chat. Para o panorama completo — SDKs/frameworks de agentes (a maioria alcança o campo de dados via passagem MCP, sem adaptador dedicado necessário), runtimes de sufixo de resposta (bots WhatsApp/Telegram/Slack/SMS, agentes em segundo plano), construtores de apps de IA (ainda não avaliados) e hospedagem/registros MCP (irrelevantes para este SDK por design) — veja Superfícies suportadas. Não projete UI. Escolha um dos quatro widgets de resultado predefinidos, com qualidade nativa do host, e mapeie os campos structuredContent da sua ferramenta para ele — o frame, os design tokens e a faixa SPONSORED divulgada são fixados pelo SDK. A faixa é renderizada somente quando existe um payload sponsored ativo, sempre na parte inferior, sempre rotulada, com o logotipo do anunciante (fallback de tile de letra quando nenhum carrega). Seu corpo não pode removê-la ou reestilizá-la.

Modelos: stat-card (valor grande + chips + fundo atmosférico opcional condicionado por chave), table-card (linhas com cabeçalho, numéricos mono, destaque da melhor linha), notice-card (glifo de veredito + linhas de detalhes), carousel-card (3–8 cartões de opções deslizáveis).

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" — mesmos modelos e formato de mapeamento; espalhe o _meta retornado em server.registerTool(...). As entradas de mapeamento são dot-paths ou {path, prefix, suffix}; uma saída de escape body_html= aceita marcação personalizada composta a partir dos primitivos .lw-* para os casos que os modelos não cobrem. Chamá-lo para uma ferramenta substitui deliberadamente o cartão patrocinado genérico de enable_lulu_ads nessa ferramenta — os dados patrocinados ainda fluem e são renderizados na própria faixa do widget.

Renderização de widget (UI de MCP Apps)

O campo simples sponsored sempre é enviado e sempre funciona — alguns hosts o renderizam como um cartão apenas pelo julgamento do próprio modelo, sem instrução em lugar nenhum. Para hosts que suportam a extensão MCP Apps (io.modelcontextprotocol/ui), enable_lulu_ads / enableLuluAds (veja o Quickstart acima) já registram um widget renderizado de verdade e o anexam automaticamente a cada ferramenta — você não precisa de nada abaixo desta linha para isso. Ele existe como uma etapa distinta porque register_sponsored_widget() exige o URL exato do endpoint público do seu servidor, que LuluAdsMiddleware/withLuluAds sozinhos não têm como saber.

Prefere controle por ferramenta (um widget diferente em ferramentas diferentes, ou apenas algumas ferramentas recebem um)? Use o bloco de construção de nível inferior diretamente em vez 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(...): ...

Mesmo helper, SDK TS oficial, para servidores MCP construídos em Node em vez de Python (o registro é async — ele pode buscar um logotipo antes de retornar):

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 cartão. template= escolhe o layout visual do widget — uma escolha no momento do registro, com o mesmo formato do próprio template= de register_result_widget (Widgets de resultado, acima). O padrão é "card" (o layout mostrado acima); passe um nome diferente para um formato diferente:

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",
});

Um template não reconhecido gera erro imediatamente (antes de qualquer chamada de rede, ex. a busca de logotipo) em vez de cair silenciosamente em fallback — o mesmo contrato fail-fast que a validação de modelo de register_result_widget já tem.

"hero" aceita mais uma opção, background_image/backgroundImage — um URL buscado uma vez no momento do registro e embutido da mesma forma 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
)

Modelos:

template=LayoutNotas
"card" (padrão)Empilhado: linha de rótulo, depois logotipo + linha de texto/CTAO layout original construído à mão — todo integrador existente recebe isso inalterado.
"banner"Linha horizontal única: rótulo, logotipo, texto/CTA tudo inlineMelhor ajuste para layouts largos mas baixos (barra lateral do VS Code, painéis CLI largos) onde um cartão empilhado desperdiça espaço vertical.
"hero"Imagem de fundo full-bleed (ou o gradiente compartilhado) com logotipo/texto/CTA ancorados sobre um scrim de legibilidadebackground_image é opcional e fornecido pelo integrador — ainda não é algo que a correspondência automática de anúncios do ads-server possa selecionar sozinha.
"flip-card"Toque/clique vira um cartão 3D — a frente mostra o rótulo, o verso revela texto + CTAO rótulo divulgado está sempre na frente; apenas os detalhes da oferta estão atrás da virada.
"scratch-reveal"Uma camada de raspadinha sobre a ofertaAuto-revela após 3s independentemente da interação — a oferta é idêntica de qualquer forma, isso é decoração, nunca conteúdo bloqueado.
"spin"Um floreio decorativo de girar e assentar no distintivo do logotipoDeterminístico — sempre a única oferta real, nunca uma mecânica de resultado variável.

Mais formatos chegam aqui conforme são lançados (carrossel, comparação, depoimento, contagem regressiva, quiz, vídeo — veja a galeria rastreada no Linear). Cada modelo compartilha exatamente as mesmas garantias: skeleton no carregamento, o rótulo divulgado Sponsored, o rodapé "Powered by Lulu Ads" e fail-open em dados ausentes/malformados — nada disso é renegociável por modelo.

Isso também é o que enable_lulu_ads/enableLuluAds fazem internamente, em seu nome, para cada ferramenta — descoberto ao vivo (2026-07-26) que acertar esta etapa por ferramenta é fácil de esquecer: nosso próprio servidor dogfood tinha isso conectado em exatamente uma ferramenta manualmente, e cada ferramenta adicionada desde então silenciosamente nunca recebeu. Se você quer cobertura automática sem etapa por ferramenta, use enable_lulu_ads/enableLuluAds em vez disso diretamente.

Envia um cartão flutuante, arredondado, com gradiente (mesmo sistema visual do getlulu.dev) com um rótulo divulgado Sponsored — ainda apenas marcação, nunca uma diretiva. Três peculiaridades específicas de host que isso trata para você: Claude exige um valor _meta.ui.domain não documentado derivado do URL do seu endpoint (autocomputado aqui, não uma credencial), o widget deve enviar um handshake ui/notifications/initialized no carregamento ou Claude mantém o iframe oculto, e os logotipos são embutidos em vez de vinculados (próxima seção) para que o CSP do sandbox do widget não possa descartá-los silenciosamente. Verificado ao vivo contra produção (dali.getlulu.dev/mcp, ext-apps#671), atual em 2026-07-19 — a própria renderização de widgets MCP Apps do Claude estava quebrada em toda a plataforma antes desse fix chegar, então trate qualquer alegação de "deve renderizar" (incluindo esta, em outros lugares) como não verificada até que você a tenha verificado ao vivo no seu próprio host.

O widget mostra um <Skeleton> shadcn imediatamente no carregamento, depois troca para conteúdo real somente quando uma chamada de ferramenta ao vivo chega — text/url/logo passados para register_sponsored_widget() não são renderizados como conteúdo inicial; apenas label/cta/accent* dessas opções são realmente usados pelo caminho ao vivo (como padrões para campos que o payload da rede omite, e como o tema de marca estático por integrador). Em cada chamada de ferramenta real, o widget escuta o push ui/notifications/tool-result do próprio host MCP Apps (um iframe novo é montado por chamada, não reutilizado — "por chamada, não por ferramenta" é uma garantia de protocolo, nada teve que ser construído no servidor para obter isso) e renderiza com o structuredContent.sponsored daquela chamada — conteúdo de anúncio ao vivo, por chamada, não um payload fixo assado uma vez no registro. Um host que nunca envia a notificação continua mostrando o skeleton indefinidamente (não um anúncio de fallback — veja a lacuna aberta anotada no docstring de InitialOptions de js/widget-src/src/mcpBridge.ts); uma chamada sem campo sponsored (o caso normal de fail-open) renderiza um cartão vazio com apenas o rodapé. Cartão, skeleton e o rodapé "Powered by Lulu Ads" são um único bundle React/shadcn compilado compartilhado byte por byte entre os SDKs Python e TypeScript (js/widget-src/, verificado, embutido por ambas as linguagens), e o rodapé sempre renderiza dentro desse mesmo invólucro de cartão persistente, em todos os estados.

Logotipos

logo recebe um URL para buscar uma marca, não um URL para embutir diretamente — passe-o e o SDK baixa a imagem ali mesmo no momento do registro e a embute no widget como um URI data:. Isso não é incidental: a especificação MCP Apps faz os hosts aplicarem img-src 'self' data: <resourceDomains> dentro do iframe com sandbox do widget, e a menos que você declare separadamente o domínio do seu logotipo na configuração CSP desse recurso, um <img src="https://your-cdn.com/logo.png"> é descartado silenciosamente — sem erro em lugar nenhum, o cartão apenas renderiza com um slot vazio para sempre, em todos os hosts. URIs data: são sempre permitidos sob essa mesma regra, então buscar e embutir no lado do servidor contorna todo o modo de falha — não há configuração de CSP para você acertar ou esquecer.

Um logo ruim ou inacessível nunca quebra o registro — é ignorado (com um log de aviso) e o cartão renderiza sem um, igual a deixar logo não definido. Apenas image/png, image/jpeg, image/svg+xml, image/webp e image/gif são aceitos, limitados a 200KB (o logotipo renderiza em 28×28 no cartão — não há razão para enviar mais do que isso pela rede).

Renderização CLI

Terminais não têm superfície de widget — o texto do próprio modelo é a única saída que existe, e é genuinamente o julgamento do modelo se mencionar a linha divulgada ou não (nunca forçado, jamais — veja Garantias). LuluAdsMiddleware / withLuluAds detectam clientes CLI conhecidos via o clientInfo.name MCP enviado no initialize (atualmente: claude-code, verificado ao vivo) e, quando conectados de um, anexam um cartão de texto simples com borda ao content[] além do campo simples — ainda apenas dados, ainda zero instrução para o modelo, apenas formatado para que leia como um bloco distinto em vez de uma frase simples se o modelo escolher retransmiti-lo:

╭─ Sponsored ────────────────────────────────────╮
│ Search 700+ airlines in one place — Kiwi.com   │
│ finds routes other search engines miss.        │
╰─ via Lulu Ads ─────────────────────────────────╯
→ https://ads.getlulu.dev/c/9f2a1c

Limitação conhecida, divulgada aqui em vez de encoberta: alguns clientes MCP não encaminham todo bloco content[] para o modelo quando structuredContent também está presente no mesmo resultado — um bug aberto no lado do cliente no Claude Code, relatado duas vezes e fechado duas vezes sem fix (#55677 → consolidado em #45575 → auto-fechado como obsoleto). Testado ao vivo especificamente contra o Claude Code (2026-07-21): com structuredContent presente (o padrão enviado), o cartão emoldurado nunca chega ao modelo, mas o campo simples sponsored ainda chega — o modelo o superfície de forma confiável como uma linha honesta, rotulada "Sponsored: ..." em suas próprias palavras, 3/3 execuções, sem problemas.

cliTextMode — fix opt-in para o bug do cliente acima

Também testamos o fix de aparência óbvia — omitir structuredContent para que content[] não tenha nada competindo com ele — e o resultado dependeu inteiramente do que mais estava em content[]:

  • Anúncio sozinho, sem dados reais de ferramenta ao lado: o cartão chega toda vez, mas o modelo o sinaliza como uma suspeita de tentativa de injeção de prompt e avisa o usuário para evitá-lo, 3/3 execuções. Pior do que não mostrá-lo.
  • Anúncio ao lado de uma renderização real e completa do próprio resultado da ferramenta: o cartão chega toda vez, o modelo o trata como um anúncio divulgado comum e o menciona de forma neutra, 3/3 execuções. Sem suspeita.

Então o fix é real, mas condicional ao comportamento da sua própria ferramenta — que este SDK não pode verificar por você, daí opt-in, desligado por padrão:

mcp.add_middleware(LuluAdsMiddleware(cli_text_mode=True))
withLuluAds(server, ads, { cliTextMode: true });

Ative isso somente se o content[] da sua ferramenta já contiver uma renderização completa e legível por humanos do resultado por conta própria — não um placeholder como "veja structuredContent". Quando ativado, clientes CLI detectados sem outputSchema declarado recebem structuredContent removido para que content[] (o próprio texto da sua ferramenta + nosso cartão) alcance de forma confiável o modelo. Ferramentas que declaram um outputSchema nunca são tocadas por isso — remover structuredContent lá quebraria a validação de schema no lado do cliente completamente (confirmado: fastmcp.exceptions.ToolError "outputSchema definido mas nenhuma saída estruturada retornada"), que é uma chamada de ferramenta quebrada, um resultado estritamente pior do que um cartão descartado. Este SDK nunca remove structuredContent.sponsored em ferramentas com schema para buscar visibilidade de cartão, cliTextMode ou não.

Até que o bug do cliente upstream seja corrigido, trate o cartão CLI como "renderiza de forma confiável uma vez que você opta e sua ferramenta se qualifica, além de uma divulgação que já funciona de qualquer forma" — mesma ressalva de verificar no seu próprio host que o caminho do widget acima.

Servidores stdio

Tudo acima da seção "Renderização do widget" funciona sem modificações em um servidor de transporte stdio — o SDK é uma biblioteca que seu código importa e chama; ele não sabe nem se importa como seu próprio servidor fala com seus clientes. O campo de dados simples sponsored (LuluAdsMiddleware / mcp.add_middleware(), withLuluAds(server)) não recebe argumento de endpoint e faz uma chamada HTTPS de saída simples para ads.getlulu.dev/slot — a mesma solicitação, seja seu processo um servidor remoto de longa duração ou um local iniciado por npx/uvx. O caminho do cartão de texto da CLI (veja "Renderização da CLI" acima) é o caso comum no mundo real aqui: o Claude Code inicia a maioria de seus servidores MCP via stdio, e é exatamente esse o cliente que este SDK já detecta e renderiza um cartão de texto simples divulgado.

O widget de MCP Apps renderizado é a única parte que não se aplica — enable_lulu_ads/enableLuluAds e os de nível inferior register_sponsored_widget/registerSponsoredWidget todos exigem um endpoint_url real, com hash no valor _meta.ui.domain não documentado do Claude para o CSP do iframe do widget. Isso não é um limite do Lulu Ads; o handshake ui/initialize do MCP Apps é um protocolo de rede entre o host e o próprio endpoint HTTP do seu servidor, e um servidor stdio não tem nenhum. Se o seu servidor for somente stdio, chame LuluAdsMiddleware/mcp.add_middleware() diretamente (ou withLuluAds(server) em TypeScript) — nunca enable_lulu_ads — e você obtém o campo de dados mais o cartão de texto da CLI, sem nada para configurar para o endpoint que você não tem.

Nota do lado do editor: o selo automático de "monetizado" do marketplace atualmente corresponde uma listagem à sua conta de editor registrada por remote_url — uma listagem stdio não tem nenhuma, então não receberá o selo automático mesmo depois de você integrar o SDK e estar ganhando. O caminho do SDK/ganhos em si é não afetado; isso é puramente uma lacuna de exibição da listagem do marketplace, sendo rastreada separadamente.

Garantias (aplicadas no código, não apenas prometidas)

GarantiaComo
Uma chamada de ferramenta nunca pode quebrar por causa de anúnciostodo caminho de falha retorna None/null; tempo limite rígido de 800ms de parede (3000ms quando a chamada implica classificação no lado do servidor)
Sempre divulgadolabel: "Sponsored" é definido pelo SDK, nunca originado do corpo da resposta
Sem injeção de prompt, nuncaenviamos um campo de dados; não há instrução de exibição em nenhum lugar do contrato
Nenhum PII sai do seu servidorcontext é filtrado contra uma lista de permissões no lado do cliente, antes de qualquer solicitação ser construída
Controlado por qualidadecada criativo passa na pontuação Dali (≥70) antes de poder preencher um slot
Intenção, não identidadea segmentação usa apenas o contexto declarado desta chamada — sem perfis de usuário, sem ID entre sessões
Mal configurado? Ainda segurocredenciais ausentes → cliente fica inerte, retorna None/null, zero chamadas de rede

Por que não apenas…

…dizer ao modelo para mencionar um patrocinador na resposta? Instruções de exibição fazem os servidores MCP serem removidos dos registros que verificam diretivas injetadas. Enviamos um objeto de dados simples — label, text, url — sem nenhum campo, em qualquer lugar do contrato, que diga a um modelo como renderizar ou formular qualquer coisa.

…contar impressões e cobrar por visualização? Uma "impressão" só existe se um modelo realmente a renderizou, e isso é inverificável do lado do servidor — fácil de manipular, difícil de auditar. Cobramos somente CPA, em um clique que resgata um token assinado e verificado pelo servidor. O pagamento mapeia para uma ação real do usuário, não uma alegação.

…examinar a conversa para segmentar melhor? Ler transcrições para segmentar anúncios é uma armadilha de privacidade: tudo o que um usuário diz se torna dados de segmentação de anúncios. Aceitamos seis chaves de contexto na lista de permissões — tool, category, query, route, locale, country — intenção declarada para esta chamada apenas. Sem transcrições, sem perfis, sem campos de PII existem no esquema.

Como 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.

Detalhe completo no nível de fio: docs/contract.md.


Docs: https://getlulu.dev/docs · Início rápido · Contrato da API · Integrações · Inscrição do editor · Portão de qualidade: Dali · MIT

Changelog

  • 0.9.19 — Corrige o que o SDK relatava sobre si mesmo.

    SDK_VERSION no cliente TypeScript era um "0.9.14" codificado enquanto o pacote passava por quatro versões — e é enviado para /slot como sdk_version, então todo editor TypeScript relatava 0.9.14 e a telemetria de atualização não conseguia dizer quem realmente havia atualizado. Agora está correto, e fixado por um teste contra package.json para que uma versão que o esqueça falhe na suíte em vez de corromper silenciosamente os dados. O cliente Python nunca teve esse bug; ele importa lulu_ads.__version__.

    Docs, sem mudança de comportamento: o cabeçalho de js/src/skybridge.ts ainda descrevia o adaptador como somente _meta, o que deixou de ser verdade em 0.9.15 — agora ele carrega a mesma tabela de aterrissagem de quatro casos que docs/integrations.md. E python/README.md, que é o que o PyPI renderiza, não tinha sincronia com o README raiz e havia ficado quatro versões para trás, então a página do pacote descrevia a Skybridge usando essa mesma alegação aposentada. Ambos os READMEs do pacote agora são alimentados por scripts/sync-readmes.sh.

  • 0.9.18 — Suporte ao Skybridge 2.x, e uma proteção contra sua única falha silenciosa.

    O skybridge 2.x moveu a fiação do middleware de protocolo para fora de McpServer.connect() para o caminho de solicitação do aplicativo, então o ponto de anexação difere por major:

    // 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 a forma 1.x em 2.x registra o middleware em uma cadeia que nada aplica: sem erro, sem anúncio, nada para depurar. O adaptador agora detecta isso (seu middleware sempre roda antes de um manipulador de ferramenta, então uma ferramenta executando enquanto ele nunca rodou significa que não está conectado) e avisa uma vez com o trecho correto. Avisado, nunca lançado.

    Verificado contra ambos os majors — 106/106 em skybridge@1.4.1 e @2.0.0, além de um exemplo de execução ponta a ponta contra o pacote publicado. Observe que em 2.x uma ferramenta idiomática somente content carrega o anúncio em _meta sozinho, então uma visualização deve ler structuredContent primeiro e recorrer a _meta; a visualização de referência em examples/skybridge-views/ faz exatamente isso.

  • 0.9.15–0.9.17 — Skybridge entrega de fato, e o SDK relata quem está chamando.

    Na 2.0.1: essas mudanças foram brevemente publicadas como 2.0.1 antes de serem aposentadas — o major deixou órfã toda faixa de dependência ^0.9.x existente, o que significava que ninguém as teria recebido sem editar manualmente seu package.json. Está removido no PyPI e substituído no npm; use a linha 0.9.x. Nada foi perdido, apenas renumerado.

    Skybridge. withLuluAdsSkybridge costumava anexar sponsored a _meta apenas. Nada no caminho do Skybridge lê _meta — enableLuluAds não pode registrar um widget lá (o registerViewResource do Skybridge é privado) e não há cartão de CLI — então o slot era buscado, registrado e exibido para ninguém. Sondar a forma do resultado ao vivo mostrou que content[] chega vazio no Skybridge com structuredContent preenchido (o inverso do SDK oficial), então structuredContent é o que uma visualização renderiza. Agora ele chega lá:

    forma da ferramentasponsored vai para
    sem outputSchemastructuredContent e _meta
    tem outputSchema_meta apenas — um campo não listado falharia na validação do cliente
    registrado antes de withLuluAdsSkybridge_meta apenas — status do esquema desconhecido, padrão seguro

    O sinalizador de esquema é capturado no momento do registro via um wrapper registerTool de 2 argumentos, já que mcpMiddleware não pode ver outputSchema.

    Identidade do cliente. Todo adaptador agora encaminha o nome do cliente MCP como context.client, para que o servidor de anúncios possa distinguir um agente real de um rastreador de diretório. "client" junta-se à lista de permissões de contexto. Nenhuma mudança de código do integrador.

    Duas quebras silenciosas corrigidas no fastmcp 4.x / SDK MCP v2. clientInfo foi renomeado para client_info; a grafia antiga lançava dentro de um except simples, então _connected_client_name retornava None em todo host 4.x — o que também desativava silenciosamente o cartão patrocinado da CLI (is_cli_client(None) é False). E o 4.x negocia server/discover em vez de initialize, então on_initialize nunca disparava e o aquecimento assíncrono parava, devolvendo a latência de início a frio que o trabalho de tempo limite em camadas existe para evitar. Ambos os acessores agora tentam grafias novas-depois-antigas, e on_discover roda ao lado de on_initialize. Suíte Python: 144 aprovados / 0 falhas em ambos 4.0.5 e 3.4.4 (era 8 falhas). JS: 104/104.

    Atualizando: permanecer na linha 0.9.x é deliberado — uma faixa ^0.9.x alcança 0.9.18, então npm update lulu-ads / pip install -U lulu-ads é suficiente e nenhuma faixa de dependência precisa ser editada. Essa propagação é exatamente o que a aposentada 2.0.1 teria custado.

    Veja examples/skybridge_server.ts e npm run verify:skybridge, que afirma tudo o acima contra um servidor real sobre um transporte em memória.

  • 0.9.14 — O template por chamada de um anúncio (LUL-64, quando o /slot reporta um) agora prevalece sobre o template=/template: que você registrou, para o widget de cartão de patrocinador autônomo (register_sponsored_widget()/ registerSponsoredWidget()). Se estiver ausente ou não for um modelo que esta compilação do pacote reconheça, ele volta ao seu padrão de registro como antes, e então para "card". Mudança de comportamento na atualização, sem necessidade de mudança de código: se você registrou com um template= não padrão (ex.: "hero"), anúncios que carregam seu próprio modelo ao vivo agora serão renderizados nesse modelo, sem opção de desativação. Isso é intencional — um administrador escolhendo um modelo para um anúncio específico deve ser mais específico do que um padrão genérico de integrador — mas isso significa que a saída visual do seu widget pode mudar após a atualização, mesmo que você não tenha alterado nenhum código. Também corrige um risco de busca onde um nome na cadeia de protótipos (ex.: "constructor") em um valor de template ao vivo poderia ser interpretado erroneamente como "reconhecido" e travar a renderização após o beacon de impressão já ter sido disparado.

  • 0.9.13 — Corrigida uma lacuna de cobrança de impressão renderizada (LUL-71) no widget de cartão de patrocinador autônomo (register_sponsored_widget()/ registerSponsoredWidget() — o cartão construído em React, banner, flip-card, scratch-reveal, spin e modelos de herói): o campo imp_url/ impUrl do payload da rede era analisado e exposto por ambos os clientes (Sponsored.impUrl no SDK Node, imp_url no cliente Python), mas silenciosamente descartado pelo analisador de mensagens do próprio pacote do widget antes de chegar aos componentes React — então o beacon de impressão renderizada (um <img> de 1px que confirma que um humano realmente viu o anúncio, pelo qual o CPM é cobrado) nunca disparou para este widget, para nenhum modelo, desde que foi lançado. Corrigido centralmente na transição de carregamento→"carregado" do App.tsx — o mesmo momento em que o conteúdo divulgado de cada modelo se torna visível, incluindo o teaser frontal do flip-card (dispara antes que o usuário o vire, correspondendo ao comportamento já correto da faixa de rodapé do register_result_widget()) e o conteúdo coberto do scratch-reveal (dispara antes que alguém risque). register_sponsored_widget()/registerSponsoredWidget() agora também declaram os domínios CSP de recurso/conexão ads.getlulu.dev que o pixel do beacon precisa para não ser silenciosamente bloqueado pela política img-src 'self' data: padrão de um host — a mesma declaração que register_result_widget() já tinha. Verificado ao vivo: o beacon dispara uma requisição HTTP real no instante em que um tool-result com imp_url chega, confirmado via log de acesso de um servidor local, não apenas testes de unidade.

  • 0.9.12 — O sponsor_template= do rodapé do widget de resultado (LUL-69) foi redesenhado de uma faixa fina de linha única para um cartão de capa: uma faixa de capa colorida — uma imagem real do patrocinador quando o payload ao vivo fornece cover_image_url/ coverImageUrl, um gradiente animado caso contrário — com o bloco de logotipo sobreposto na costura para o corpo, tipografia maior e um botão de CTA em pílula real. O teaser do "flip-card" agora carrega a mesma identidade de capa em vez de um rótulo simples. "spin" é removido: uma animação de scaleX de cara ou coroa em um logotipo de letra pequeno e muitas vezes plano mostrou-se visualmente ilegível na prática (no meio da animação, ele encolhe para uma lasca quase invisível contra um fundo de capa ocupado), capturado ao vivo em vez de inferido. É substituído por "carousel" — cicla automaticamente por três enquadramentos do mesmo payload patrocinado único (um logotipo de marca maior, o texto da oferta, e então o CTA), nunca múltiplos patrocinadores: este quadro só recebe um payload patrocinado por chamada, então, ao contrário do modelo de carrossel da galeria autônoma, não há rotação multi-anunciante aqui (veja LUL-49 para essa questão separada, ainda bloqueada, de backend). "scratch-reveal" é reestilizado como um stub de papel alumínio de bilhete de loteria — gradiente metálico dourado, hachura diagonal, uma linha de perfuração tracejada, um glifo de bilhete — e sua janela de revelação automática agora é de 5s, não 3s, tempo suficiente para realmente ser registrado como uma interação; permanece uma revelação estritamente determinística sem estado de "ganhar/perder", que é o que mantém o estilo de bilhete fora do território de mecânica de jogo real. O raciocínio de "linha única, sem espaço para uma imagem de sangria total" que costumava também excluir "hero" não se aplica mais agora que a faixa em si tem altura de capa — anotado como um acompanhamento em aberto, não resolvido por esta versão.

  • 0.9.11 — register_result_widget()/registerResultWidget() ganham sponsor_template=/sponsorTemplate: (LUL-69): a faixa SPONSORED embutida no rodapé de um widget de resultado (stat-card/table-card/notice-card/ carousel-card — a saída de ferramenta do próprio editor, não o widget de cartão de patrocinador autônomo) agora também pode escolher um formato visual, independente de template=, o layout do corpo deste próprio widget. "card" (padrão) é a faixa original sempre visível. "flip-card" é um teaser compacto "Patrocinado" que faz crossfade para revelar a oferta real ao toque — a face frontal carrega um prompt real ("Toque para revelar →"), não apenas um rótulo de divulgação simples, então há um motivo para tocá-lo. "spin" é um floreio decorativo de giro e assentamento no distintivo do logotipo, conteúdo visível imediatamente, nunca bloqueado (sem resultados variáveis — veja a salvaguarda de giro abaixo). "scratch-reveal" é uma camada de raspadinha em canvas, com revelação automática após 3s independentemente da interação — a oferta subjacente é idêntica, quer alguém risque ou não. "banner"/ "hero" são deliberadamente não oferecidos aqui: esta faixa já é uma linha horizontal (banner seria um no-op) e um fundo de sangria total não cabe em uma barra de rodapé fina. Este é um sistema diferente da galeria de cartão de patrocinador autônomo acima (o próprio register_sponsored_widget()'s template=) — mesmos nomes de modelo onde se sobrepõem, portados para o renderizador vanilla-JS deste quadro em vez de compartilhar código, já que os dois quadros são implementações independentes por design.

  • 0.9.10 — Quatro modelos adicionais: "flip-card" (LUL-53, um flip 3D em CSS ao toque revelando a oferta na face traseira — a frente sempre mostra o rótulo divulgado primeiro, nunca esconde o que é, apenas os detalhes da oferta), "scratch-reveal" (LUL-52, uma camada de raspadinha em canvas, com revelação automática após 3s independentemente da interação — a oferta subjacente é idêntica, quer alguém risque ou não, isso é uma animação de revelação, nunca conteúdo bloqueado), "spin" (LUL-54, um floreio decorativo de giro e assentamento no distintivo do logotipo — determinístico por design, sem resultados variáveis estilo roda da fortuna, já que isso seria uma mecânica de jogo), e "hero" (LUL-48, uma imagem de fundo de sangria total ou gradiente com logotipo/texto/CTA ancorados sobre um véu de legibilidade). register_sponsored_widget()/registerSponsoredWidget() também ganham um parâmetro opcional background_image/backgroundImage para "hero" — o mesmo contrato de buscar-uma-vez-e-embutir-como-URI-data: que logo já tem (uma falha de busca apenas volta ao gradiente compartilhado, nunca um erro de registro). Isso é branding fornecido pelo integrador, no momento do registro, mesma categoria que logo/accent* — ainda não conectado à correspondência automática de anúncios por campanha do ads-server, que não tem um campo de imagem próprio (rastreado separadamente).

  • 0.9.9 — Primeira entrada real na galeria de modelos: template="banner" (LUL-47), uma faixa horizontal de largura total (rótulo, logotipo, texto/CTA tudo em uma linha) em vez do layout empilhado do "card" padrão — melhor ajuste para superfícies largas mas curtas (barra lateral do VS Code, painéis CLI largos). Mesmo contrato compartilhado de todos os modelos: renderiza dentro do mesmo shell de gradiente persistente, mesmo rótulo divulgado + rodapé "Powered by Lulu Ads", mesmo comportamento de esqueleto-no-carregamento e fail-open — nada dessas garantias é específico do modelo. Sem suporte a imagem de fundo ainda (a especificação do LUL-47 permite "imagem ou gradiente"; um campo real de imagem de fundo precisa de novo esquema no lado do anunciante, rastreado separadamente) — usa o gradiente de token de destaque existente. Documentação adicionada: A renderização de widgets agora documenta template= com um exemplo ao vivo e uma tabela de formatos disponíveis.

  • 0.9.8 — register_sponsored_widget() / registerSponsoredWidget() ganham um parâmetro template= (LUL-46), mesma forma que o do register_result_widget's: somente por palavra-chave, padrão para "card" (o único formato de hoje, totalmente compatível com versões anteriores — nenhum site de chamada de integrador existente muda de comportamento), gera imediatamente um erro em um valor não reconhecido (antes de qualquer chamada de rede, ex.: a busca de logotipo). Esta é uma escolha do integrador no momento do registro, não um valor por chamada — corresponde ao comportamento estático, "embutido no pacote compilado no registro" que o modelo do próprio register_result_widget já tem, não algo que varia por anúncio servido. Apenas fundamental: o mecanismo de registro/despacho em js/widget-src/ agora suporta buscar um modelo por nome, mas "card" (o layout existente construído à mão) ainda é a única entrada — mais modelos chegam em suas próprias versões de acompanhamento conforme são construídos (banner, herói, carrossel, comparação, flip-card, scratch-reveal, spin, depoimento, contagem regressiva, quiz, vídeo). Também corrige uma deriva real e pré-existente: a constante de telemetria SDK_VERSION do próprio SDK TS estava presa em 0.9.0 desde uma versão muito anterior, apesar de package.json ter avançado para 0.9.6 — realinhado aqui, e os números de versão dos dois pacotes agora estão novamente em sincronia (o 0.9.7 anterior era apenas Python; o pacote TS pula direto para 0.9.8).

  • 0.9.7 (apenas Python) — Clientes CLI/texto agora recebem um beacon de entrega confirmada: o pixel de impressão renderizada normal (imp_url) é buscado pelo próprio cliente de renderização no instante em que exibe a faixa patrocinada — hosts de terminal/CLI (Claude Code e afins) não têm mecanismo de renderização para fazer isso, então cada cartão entregue via CLI era anteriormente invisível em ad_events, ponto final, mesmo em entregas 100% bem-sucedidas. O middleware agora dispara essa mesma URL de beacon por conta própria (fire-and-forget, não bloqueante, marcado como src=cli_server) no momento em que anexa um cartão ao content[] de um cliente CLI, em ambos os caminhos de anexação de cartão is_cli (o slot buscado pelo próprio middleware e um sponsored pré-definido de uma ferramenta). Isso registra como um novo evento cli_card_delivered distinto — não impression_rendered, e nunca contado para cobrança/pagamento de CPM — um sinal deliberadamente mais fraco do que um hit de pixel renderizado real: prova que o cartão saiu do servidor na resposta da ferramenta, nunca que um humano realmente o viu. Não requer configuração; slots existentes que carregam imp_url captam isso automaticamente. (ads-server: /i/{token} agora aceita um parâmetro de consulta opcional src=cli_server para registrar o tipo de evento distinto.)

  • 0.9.6 — Apenas documentação: nova página Superfícies suportadas classificando cada superfície de agente (hosts de chat, SDKs/frameworks agênticos, runtimes de sufixo de resposta, construtores de aplicativos de IA, hospedagem/registros MCP) por como ela realmente alcança o SDK — um adaptador direto, passagem de protocolo MCP (sem adaptador necessário uma vez que um servidor tenha Lulu Ads conectado), o contrato genérico format_suffix, ou genuinamente ainda não avaliado — mesmo padrão de evidência que o resto deste repositório. Também adiciona uma seção "servidores stdio" esclarecendo que o caminho do campo de dados não precisa de endpoint_url e funciona sem modificações em servidores de transporte stdio; apenas o widget MCP Apps renderizado requer um e não pode ser aplicado a stdio. Também corrige lulu_ads.__version__, que tinha derivado para 0.9.0 enquanto o pacote foi publicado como 0.9.5 — mesma classe de bug que a entrada 0.7.0 abaixo, recorreu porque nada impõe que os dois permaneçam em sincronia; considere isso como a próxima lacuna real a fechar aqui.

  • 0.9.2 (apenas Python) — Corrigido um bug de middleware: uma ferramenta que define seu own sponsored field (um padrão documentado para, por exemplo, um cross-sell específico de categoria) acionou o retorno antecipado de "nunca sobrescrever" em on_call_tool antes da verificação do cliente CLI ser executada, então hosts CLI (Claude Code) não receberam nenhum anúncio visível naquela ferramenta — sem superfície de widget, e sem rede de segurança de cartão de texto também, ambos ignorados pela mesma saída antecipada. A verificação do cliente agora é executada primeiro; um valor sponsored pré-definido ainda recebe o tratamento de cartão de texto CLI, usando o anúncio escolhido pela própria ferramenta.

  • 0.9.1 — O widget table-card ganha rowLink: um caminho de ponto opcional por linha que resolve para uma URL (por exemplo, um link de reserva/checkout), conectado ao mesmo openLink() agnóstico de host que a faixa patrocinada já usa. Linhas sem uma URL resolvível são renderizadas exatamente como antes.

  • 0.9.0 — Suporte a Skybridge (https://skybridge.tech): withLuluAdsSkybridge(server) (lulu-ads/skybridge). O McpServer.registerTool do Skybridge usa uma forma (config, handler) de 2 argumentos com name incorporado em config, não o (name, config, handler) de 3 argumentos do SDK oficial que withLuluAds encapsula — reutilizar withLuluAds como está interpretaria mal o objeto de configuração como o nome da ferramenta. O novo adaptador em vez disso usa o hook de protocolo mcpMiddleware("tools/call", ...) do próprio Skybridge, verificado contra os tipos fornecidos pelo pacote real skybridge@1.4.0 e um round-trip ao vivo InMemoryTransport. Deliberadamente apenas _meta: o middleware vê o resultado da chamada, mas não o outputSchema registrado da ferramenta, então structuredContent nunca é tocado.

  • 0.8.1 — Galeria de modelos de widget de resultado (substitui 0.8.0, que foi brevemente publicado no npm com um design de faixa mais chamativo): lulu_ads.widgets / lulu-ads/widgets com quatro modelos predefinidos (stat-card, table-card, notice-card, carousel-card), tokens de design + primitivas .lw-*, e a faixa SPONSORED divulgada embutida no quadro (logotipo do anunciante via novo logo_url do slot, fallback de tile de letra). register_result_widget() corrige uma ferramenta FastMCP já registrada no lugar (ou retorna o AppConfig para app= explícito).

  • 0.7.4 — Widget: o canvas do iframe do card patrocinado não pinta mais uma caixa branca opaca em hosts com tema escuro. background: transparent sozinho não é suficiente para um iframe embutido: o Chromium mantém o canvas transparente apenas quando o esquema de cores usado pelo documento embutido corresponde ao do incorporador, e este documento não declarava nenhum (padrão para light), então hosts com tema escuro (ex.: claude.ai no modo escuro) forçavam um fundo branco atrás do card. O widget agora declara color-scheme: light dark, que resolve para o esquema preferido do usuário — correspondendo a hosts que o seguem (claude.ai faz isso por padrão) em temas claros e escuros. Verificado empiricamente contra páginas de incorporação com esquemas claro e escuro. (Também alinha lulu_ads.__version__, que havia derivado para 0.7.2 enquanto os pacotes foram publicados como 0.7.3.)

  • 0.7.0 — Dois bugs reais, encontrados ao vivo contra um servidor MCP de terceiros real por trás do conector remoto do Claude.ai, ambos corrigidos:

    • 0% de entrega de anúncios em hosts que reconectam por mensagem (confirmado: Claude.ai abre uma sessão MCP nova por mensagem de chat, não uma vez por conversa). Causa raiz: a conexão HTTP persistente deste SDK esfria em qualquer intervalo ocioso real entre mensagens, mas apenas uma verificação única de "já tive sucesso" protegia a primeira chamada de todas — cada chamada fria posterior ainda recebia o timeout apertado de estado estável e falhava. Corrigido ao re-verificar o estado frio em cada chamada, baseado no tempo desde o último sucesso real, não em um latch permanente. Também: o timeout rápido de estado estável foi aumentado de 800ms → 1500ms (Python e TS) — evidência de produção mostrou que até chamadas "quentes" às vezes mediam 796-802ms, bem na linha antiga em vez de confortavelmente abaixo dela.
    • Anúncio buscado com sucesso, nunca visto pelo modelo. FastMCP/o SDK MCP TS constroem o content[] de um resultado de ferramenta uma vez, a partir do valor de retorno original da ferramenta, antes de LuluAdsMiddleware/withLuluAds ever executarem — mutar structuredContent sozinho (a única coisa que o próprio conjunto de testes deste SDK verificava) deixava content[] permanentemente desatualizado. Confirmado ao vivo: o structuredContent da resposta na rede demonstravelmente tinha sponsored, mas Claude.ai lia e reportava de content[], que não tinha. Ambos os SDKs agora mantêm content[] em sincronia sempre que for seguro (um único bloco de texto JSON gerado automaticamente); testes de regressão adicionados para a lacuna exata que permitiu que isso fosse publicado despercebido na primeira vez.
    • Novo: enable_lulu_ads() (Python) / enableLuluAds() (TS) — uma chamada que conecta tanto o campo de dados QUANTO o widget renderizado de MCP Apps em cada ferramenta automaticamente, presente e futuro. Os existentes register_sponsored_widget()/registerSponsoredWidget() + app=/_meta.ui por ferramenta ainda funcionam e agora são documentados como o bloco de construção de nível mais baixo para controle por ferramenta; a lacuna que deixavam (um passo manual fácil de esquecer por ferramenta) é exatamente o que isso fecha — encontrado ao vivo em nosso próprio servidor de dogfood, que tinha conectado o widget a exatamente uma ferramenta manualmente e silenciosamente nunca o atualizou para ferramentas adicionadas desde então.
  • 0.6.2 — O card patrocinado agora reproduz uma varredura de luz diagonal única sobre si mesmo quando se estabiliza no estado carregado (um anúncio real venceu) — CSS puro (.card-shine em js/widget-src/src/index.css), dispara exatamente uma vez por montagem (não um brilho em loop, já que isso fica inline em um thread de chat real), e respeita prefers-reduced-motion. Os estados de esqueleto e sem preenchimento não são afetados.

  • 0.6.1 — Corrige um 0.6.0 desatualizado publicado no npm antes de dist/ ser reconstruído a partir do código-fonte mesclado (js/ não tem etapa de build prepublishOnly) — 0.6.0 está deprecado no npm apontando para aqui. Também corrige README.md e os docstrings de widget.py/widget.ts em ambos os idiomas, que incorretamente afirmavam que text/url/logo passados para register_sponsored_widget() renderizam como um "anúncio de casa" de fallback até que um tool-result ao vivo chegue; eles nunca fazem isso — o widget mostra o esqueleto indefinidamente se um host nunca o enviar.

  • 0.6.0 — O widget patrocinado de MCP Apps agora mostra conteúdo de anúncio ao vivo, por chamada em vez de um anúncio de casa fixo embutido no momento do registro: reconstruído em React + shadcn/ui (Card, Skeleton, Button), compilado para um único bundle autocontido compartilhado byte por byte por ambos os SDKs (js/widget-src/). O widget mostra um esqueleto imediatamente ao carregar, então escuta o push de ui/notifications/tool-result do próprio host de MCP Apps — que a especificação já entrega uma vez por chamada, para um iframe novo por chamada, sem necessidade de mudança no servidor — e troca para os dados reais de structuredContent.sponsored daquela chamada — o widget mostra o esqueleto indefinidamente se um host nunca o enviar, não um anúncio de fallback; apenas label/cta/accent* das opções de register_sponsored_widget()/ registerSponsoredWidget() são realmente usados pelo caminho ao vivo. O rodapé "Powered by Lulu Ads" renderiza uma vez, imediatamente, e nunca é parte da troca esqueleto→card. Verificado ao vivo contra um host real (claude.ai) com um servidor de teste descartável: o esqueleto renderiza antes da chamada de ferramenta resolver, troca para o card real por chamada assim que resolve, o rodapé nunca desaparece ou reflui durante a troca, e duas chamadas de ferramenta na mesma rodada renderizam duas instâncias de widget totalmente independentes, cada uma mostrando apenas os dados de sua própria chamada — confirmando o comportamento "por chamada, não por ferramenta" no qual este recurso é construído. (O redirecionamento ui/open-link do CTA — vs. uma navegação bruta — foi reconfirmado por inspeção estática de código e pelos testes de unidade existentes deste repositório durante esta mesma passada; a captura de clique ao vivo foi tentada, mas bloqueada por limites das ferramentas de automação de navegador ao alcançar dentro do iframe duplamente isolado do host, não por qualquer falha de produto observada.)

  • 0.4.0 — Pré-conexão automática na construção para o LuluAdsAgentMiddleware do LangChain, o install() do CrewAI e o withLuluAds do TypeScript (correspondendo ao LuluAdsMiddleware do FastMCP, que já tinha isso). Também: o LuluAdsMiddleware do FastMCP e o LuluAdsAgentMiddleware do LangChain agora também aquecem o pool de conexões assíncronas que seu tráfego awaited sponsored_slot() realmente usa — o aquecimento no momento da construção acima só tocava o cliente síncrono, um pool separado que o caminho assíncrono nunca toca. LuluAds.async_warm_up() é disparado uma vez por instância a partir de um hook de ciclo de vida real do framework no loop de eventos de serviço ao vivo (o on_initialize do FastMCP, o abefore_agent do LangChain), já que uma thread em segundo plano não pode pré-aquecer com segurança uma conexão destinada a um loop de eventos diferente. Controlado pela mesma flag auto_warm_up que o aquecimento síncrono (este caminho assíncrono é apenas Python — o autoWarmUp do TypeScript só teve um pool para controlar). Esta é a correção que fecha a lacuna de inicialização a frio para dali-mcp em produção, que consome o caminho assíncrono. Cache de sucesso apenas com TTL curto (padrão 45s) em ambos os clientes base, baseado na categoria resolvida ou em um hash do texto do prompt. Documentação corrigida: o timeout padrão real é 800ms (caminho rápido) / 3000ms (caminho de classificação) adaptativo, não um plano de 300ms.

  • 0.3.7 — cliTextMode (opt-in, desligado por padrão): corrige o bug de queda de content[] do Claude Code de verdade, mas apenas para ferramentas cujo content[] já se sustenta sozinho sem structuredContent — testado ao vivo em ambos os casos qualificados e não qualificados, veja "Renderização CLI". Nunca toca ferramentas com um outputSchema declarado (quebraria a validação de esquema no lado do cliente, confirmado via fastmcp.exceptions.ToolError).

  • 0.3.6 — aquecimento automático de conexão na construção de LuluAdsMiddleware (auto_warm_up, ligado por padrão): uma primeira chamada de ferramenta genuinamente fria mediu 804ms contra o padrão de caminho rápido de 800ms — bem no teto, não abaixo dele. O próprio LuluAds ainda nunca aquece automaticamente (uma chamada de rede como efeito colateral do construtor é surpreendente em um cliente de propósito geral), mas o middleware é a promessa de "uma linha, zero configuração", então ele se aquece.

  • 0.3.5 — corrigido um timeout_ms padrão fixo de 300ms em LuluAdsMiddleware que silenciosamente descartava anúncios reais e preenchíveis em latência de rede real — cada teste no conjunto usava um transporte mock instantâneo, que é exatamente por que isso foi publicado despercebido. O padrão agora é None, adiando para o padrão condicional de 800ms/3000ms do próprio LuluAds.

  • 0.3.4 — O card da CLI ganha cantos arredondados e um rodapé "via Lulu Ads" (apenas desenho de caixa Unicode — um teste ao vivo contra o Claude Code confirmou que ele remove escapes de cor ANSI brutos da saída da ferramenta antes que o modelo os veja, então cor nunca esteve em questão). Também testado ao vivo e explicitamente rejeitado descartar structuredContent para forçar content[]: isso faz o card chegar, mas o modelo então o sinaliza como suspeita de injeção de prompt e avisa o usuário para evitá-lo — pior que o status quo, onde o campo simples ainda é exibido honestamente mesmo sem o card. Veja "Renderização CLI" para o relatório completo.

  • 0.3.3 — Renderização adaptativa à CLI: LuluAdsMiddleware / withLuluAds detectam clientes CLI conhecidos via o clientInfo.name MCP enviado em initialize (atualmente: claude-code, verificado ao vivo) e anexam um card de texto simples com borda a content[] para eles, além do campo simples — terminais não têm superfície de widget, então este é o equivalente seguro para CLI do widget de MCP Apps acima. Ainda são apenas dados; veja "Renderização CLI" para a limitação conhecida divulgada sobre o encaminhamento de content[] em alguns clientes.

  • 0.3.0 — register_sponsored_widget() / registerSponsoredWidget() ganham uma opção logo: buscada no lado do servidor no momento do registro e embutida no widget como uma URI data:, para que renderize sob a CSP do sandbox do widget (img-src 'self' data: <resourceDomains>) sem configuração de resourceDomains necessária da sua parte — uma URL de logo remota bruta seria de outra forma silenciosamente descartada, sem erro em lugar nenhum. Um logo ruim/inalcançável nunca quebra o registro; o card apenas renderiza sem um. O registerSponsoredWidget() do TypeScript agora é async (pode precisar buscar o logo antes de retornar) — adicione await nos locais de chamada existentes.

  • 0.2.0 — register_sponsored_widget() (Python: lulu_ads.widget, agora também TypeScript: lulu-ads/widget, SDK MCP oficial): registra um card patrocinado de UI de MCP Apps realmente renderizado no seu servidor (não apenas o campo JSON simples), lidando com o requisito não documentado de domínio de iframe do Claude e o handshake ui/notifications/initialized para você. Generaliza a correção verificada ao vivo em dali.getlulu.dev/mcp contra ext-apps#671. Ambos os SDKs produzem valores _meta.ui.domain byte idênticos para a mesma URL de endpoint.

  • 0.1.1 — clientes HTTP persistentes no SDK Python (a construção de cliente por chamada podia queimar todo o orçamento de slot em contêineres com CPU limitada; os clientes agora são criados uma vez por instância de LuluAds e reutilizados com keep-alive). Comportamento de falha aberta inalterado.

  • 0.1.0 — lançamento inicial: clientes Python + TypeScript, adaptadores FastMCP / LangChain / LangGraph / CrewAI / MCP-TS, helpers de sufixo, onboarding de concierge MCP.