关于 Lulu Ads — Publisher Concierge
Lulu Ads — Publisher Concierge 帮助发布者在 MCP 服务或 Agent 工具结果中加入明确标注的赞助数据。其 SDK 和集成支持发布者注册及可选赞助字段的插入,由宿主模型自行决定是否展示。
主要功能
- 01在结果中加入标注清晰的赞助字段
- 02提供 Python 和 TypeScript SDK
- 03支持 MCP 服务和 Agent 框架集成
- 04可选渲染赞助组件
- 05按订阅等级有条件地启用广告
适用场景
- 01在 MCP 工具结果中加入赞助数据
- 02注册发布者并配置广告位
- 03免费等级展示广告,付费等级停用广告
MCP 相关
该服务本身可作为 MCP 服务使用,提供发布者注册和集成指引。其 SDK 与中间件可向 MCP 工具结果添加赞助数据字段,并在支持的宿主中提供渲染组件。
配置
远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。
https://ads.getlulu.dev/mcpclaude mcp add --transport http lulu-the-narwhal-lulu-ads https://ads.getlulu.dev/mcp{"mcpServers":{"lulu-the-narwhal-lulu-ads":{"type":"http","url":"https://ads.getlulu.dev/mcp"}}}在 Cursor 中打开{"servers":{"lulu-the-narwhal-lulu-ads":{"type":"http","url":"https://ads.getlulu.dev/mcp"}}}在 VS Code 中打开{"mcpServers":{"lulu-the-narwhal-lulu-ads":{"type":"http","url":"https://ads.getlulu.dev/mcp"}}}{"mcpServers":{"lulu-the-narwhal-lulu-ads":{"serverUrl":"https://ads.getlulu.dev/mcp"}}}命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。
工具
5| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| framework | string | 否 |
详细介绍
lulu-ads
Agent 经济的变现层。
只需添加一行带标签的赞助内容,即可通过 MCP 服务或 Agent 工具变现。
快速开始 · 集成 · 支持的宿主 · 支持的展示位置 · stdio 服务 · 保障措施 · API 合约 · 托管文档 · 博客 · 成为发布者
70% 分成给发布者 · 仅限 CPA · 800ms 失败时放行 · 从设计上杜绝提示词注入
↑ 实时渲染的赞助卡片——真实轮播广告需求,每隔约 60 秒刷新一次。任何已认领的列表都可以在自己的 README 中嵌入此内容。
Lulu Ads 会将一条已披露并带有标签的数据字段附加到工具自身的结果中。 宿主模型——Claude、Cursor 或任何 Agent——会自行判断内容是否足够相关, 再决定是否展示。我们绝不会指示它这样做。
| SDK 提供的内容(数据字段) | 宿主渲染的内容(由宿主决定) |
|---|---|
|
|
零摩擦上手——添加 MCP 服务,剩下的交给你的 Agent:
claude mcp add --transport http lulu-ads https://ads.getlulu.dev/mcp
将我的服务变现
它会为你的技术栈获取合适的集成指南,在征得你同意后注册发布者, 接入这一行代码,并验证广告位已上线。
如果内容被渲染并获得点击,你就能赚取 70% 的 CPA 收入。如果没有—— 无人付费,一切照常运行。
无提示词注入——我们只提供数据字段,由宿主自行决定。
快速开始
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
FastMCP 服务只需调用一次即可完成接入——凭据从环境中读取,
所有工具(现有和未来新增的)都会自动获得纯 sponsored 数据
字段,以及在支持该功能的宿主(例如 Claude.ai)中渲染的
赞助卡片组件:
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")
只需要数据字段,不需要组件?纯中间件也可以独立使用:
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" } });
基于官方 TS SDK 构建的 MCP 服务也能通过一次调用完成同样的接入—— 所有工具都会自动获得数据字段和组件:
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" });
还没有发布者 ID?请参阅 docs/quickstart.md——
有三种获取方式,彼此互不设限。
分级定价(免费层展示广告,付费层无广告)? 传入 enabled——
由你自己的订阅检查决定其值,无需单独部署或在各处散布条件判断:
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",
});
框架集成
| 技术栈 | 一行代码 | 文档 |
|---|---|---|
| FastMCP(Python),数据 + 小组件 | enable_lulu_ads(mcp, endpoint_url=...) | → |
| FastMCP(Python),仅数据 | mcp.add_middleware(LuluAdsMiddleware()) | → |
| MCP TS SDK,数据 + 小组件 | await enableLuluAds(server, { endpointUrl }) | → |
| LangChain / LangGraph(Python) | middleware=[LuluAdsAgentMiddleware()] | → |
| CrewAI(Python) | `lulu_crewai.install()`` | → |
| MCP TS SDK,仅数据 | withLuluAds(server) | → |
| Skybridge(TypeScript) | withLuluAdsSkybridge(server) | → |
| 运行时所有者(聊天机器人、WhatsApp/Telegram Agent) | model_output + format_suffix(sponsored) | → |
| 其他任何运行时 / 语言 | 基于原始契约使用 sponsored_slot(context) | → |
结果小组件——为你自己的工具输出提供模板(0.8.5)
**一个小组件,适用于所有宿主。**该框架支持三种桥接方式——稳定版 MCP
Apps(ui/initialize,2026-01-26)、草案时代的回退方案,以及 ChatGPT 的
window.openai——SDK 会自动注册两种模板键
(_meta.ui.resourceUri + openai/outputTemplate)以及两种 CSP 方言。在 claude.ai 和 ChatGPT 上已验证实时渲染,包括
渲染曝光信标(曝光量统计的是人实际看到的内容,而不是单纯的 API 输出)。升级后,请在 ChatGPT 的插件设置中刷新你的连接器——它会缓存工具元数据。
SPONSORED 条的格式可单独配置,通过
sponsor_template=(与 template= 独立,后者用于配置此小组件自身的主体布局):
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
)
支持的宿主
普通的 sponsored JSON 字段是始终启用的基础功能:它会随每个工具结果发送到每个 MCP 宿主,因为它不过是字典中的一个额外键——不需要宿主提供特定支持即可工作,模型会自行决定是否展示。上述渲染后的 MCP Apps 小组件则是附加功能,只有宿主实际实现了 ui/initialize 握手流程时才会显示。本表根据我们自己的生产环境验证(如有)以及对其他所有宿主的最新调查(2026-08-25),明确列出各宿主的支持情况——只有在我们亲自确认,或供应商发布了具体且可核验的实现细节时,宿主的小组件状态才会标为“Live”,绝不会仅凭“应该能用”的假设。
我们考察过的宿主——这些徽标本身并不代表我们认可或支持;各宿主的实际功能请查看下方的 Status 列。(Continue.dev 列在表格中,但未出现在上方的徽标栏中:它已停止维护,仅为完整起见而保留。)
| 宿主 | MCP 工具调用 | 渲染小组件 | 状态 |
|---|---|---|---|
| Claude (claude.ai) | 是 | 是 | 已上线,并已在生产环境中验证——在实际流量中观察到真实的渲染展示信标。 |
| ChatGPT | 是 | 是 | 已上线,并已在生产环境中验证。 |
CopilotKit (@ag-ui/mcp-apps-middleware) | 是 | 进行中 | 修复正在审核中,见 PR #8,尚未完成端到端验证——我们发现并修复了一个工具发现错误,但尚未针对完整聊天 UI 测试此修复(该轮测试没有可用的大模型),且修复尚未发布到 npm/PyPI。在该 PR 合并并经线上验证之前,请勿将 CopilotKit 视为受支持。纯 sponsored 字段不受此错误影响,目前已可正常传递。 |
| VS Code(原生 MCP + GitHub Copilot Chat Agent 模式) | 是 | 据称已上线 | Microsoft 在 2026-01-26 发布的博客文章及当前文档中称,VS Code 是“首个全面支持 MCP Apps 的主流 AI 代码编辑器”,并介绍了具体且可核查的实现细节(沙盒 iframe、CSP 域配置、ui/initialize 握手、App SDK)——这些说法可信,但这是供应商的声明,我们尚未独立复现。纯 MCP 工具调用(Copilot Chat Agent 模式)自 v1.102 起已正式发布。 |
| Cursor | 是 | 据称支持,尚未验证 | 在上游 modelcontextprotocol.io 扩展支持矩阵中被列为 MCP Apps 实现方——这是第三方列表,而非 Cursor 自己的文档,因此证据不如上文 VS Code/Goose 供应商发布的细节充分。我们确实在实际环境中尝试自行验证(2026-08-25),但尚未开始测试就遇到了阻碍:Cursor 免费版 Agent 的使用上限(2 次提示词)在实际工具调用成功前就已触发。真实尝试,真实阻碍,仍未确认——我们并非刻意回避这一说法。 |
| Goose(Block / AAIF) | 是 | 已上线(实验性) | Goose 自己的文档确认支持 ui/initialize 握手和沙盒 iframe 渲染(Goose Desktop 1.19.1+),但明确指出该功能“处于实验阶段,基于规范草案;实现较为精简,且可能发生变化”。应将其视为已上线但不稳定,而非保证能渲染的目标环境。 |
| Grok(xAI)——grok.com 连接器、Grok Build CLI、xAI API Remote MCP Tools | 是 | 未找到证据 | xAI 的这三个产品界面均支持 MCP(纯工具发现与调用),但截至本次调查,没有官方文档、更新日志或第三方宿主支持矩阵表明 Grok 支持 MCP Apps UI 扩展。赞助数据字段仍会传递,并始终由模型根据自身判断通过 JSON 回退机制渲染;只是没有可供渲染丰富小组件的界面。 |
| Windsurf(Codeium) | 是 | 未找到证据 | Windsurf 自己的文档仅称其支持“MCP 服务的工具、资源和提示词”;我们找到的所有第三方 MCP Apps 宿主支持列表均未提及它。赞助数据字段仍可通过始终启用的 JSON 回退机制正常工作。 |
| Cline(VS Code 扩展) | 是 | 未找到证据 | 成熟的 MCP 客户端(支持工具、资源、提示词,并内置 MCP 市场);在整个代码仓库中均未找到 ui/initialize、ui:// 或 iframe 渲染代码。赞助数据字段仍可通过始终启用的 JSON 回退机制正常工作。 |
| Zed 编辑器 | 是 | 未找到证据 | Zed 自己的文档明确指出,其“目前支持 MCP 的 Tools 和 Prompts 功能”——不支持基于 Resources 的 UI 渲染。赞助数据字段仍可通过始终启用的 JSON 回退机制正常工作。 |
| Continue.dev | 是(曾支持) | 未找到证据 | 已停止开发:该项目于 2026 年 6 月被 Cursor 收购,continuedev/continue 仓库现已设为只读,不再继续开发。其仍在运营期间支持纯 MCP 工具、资源和提示词,但没有证据表明它曾渲染 MCP Apps 小组件。今后不再是可行的集成目标——此处仅为完整起见而列出。 |
为什么有些宿主无需额外代码,而有些则需要
不同宿主采用了不同约定来声明“我有可渲染的 UI”——如果宿主的约定与我们最初采用的约定不同,发现阶段就会悄无声息地失败,根本不会进入渲染阶段(这是 CopilotKit 的缺口,PR #8 已于 2026-08-25 修复)。我们会跟踪已确认的每种约定,并在每个支持 Widget 的工具上注册所有约定——只做增补,绝不重写,因此宿主无法识别某个信号时,只会忽略它。这就是 Claude 和 VS Code 无需额外代码即可渲染(它们采用相同的约定),而 CopilotKit 需要有针对性的修复的实际原因;也正因如此,下表中的“未找到证据”确切表示尚未找到证据,而不是不存在的证据。
上文未列出的其他所有宿主(LangGraph Studio、内部自建的 Agent 框架,以及我们还没查看过的所有宿主):情况未知/尚未调查——无论如何,按照下文的保障,纯 sponsored 字段旨在以开放方式失败,并优雅降级。如果你已验证某个未列于此表的宿主能够渲染,请提交 issue 或 PR——这份列表力求真实,而非穷尽所有情况。
本表专门讨论聊天宿主中的 Widget 渲染。 如需了解更全面的情况——Agent SDK/框架(大多数通过 MCP 透传即可获取数据字段,无需专用适配器)、响应后缀运行时(WhatsApp/Telegram/Slack/SMS 机器人、后台 Agent)、AI 应用构建器(尚未评估),以及 MCP 托管/注册表(按设计与此 SDK 无关)——请参阅 支持的平台。
无需设计 UI。请选择四种预定义的、符合宿主原生质量标准的结果 Widget 之一,并将工具的 structuredContent 字段映射到其中——框架、设计令牌和明示的 SPONSORED 横条均由 SDK 固定。只有存在实时 sponsored 负载时才会渲染横条;它始终位于底部,始终带有标签,并显示广告商的 Logo(未能加载时则使用字母方块作为后备)。你的主体内容无法移除或重新设置它的样式。
模板:stat-card(大号数值 + 标签 + 可选的按条件键控的氛围背景)、table-card(带表头的行、等宽数字、最佳行高亮)、notice-card(结果图标 + 详情行)、carousel-card(3–8 张可滑动的选项卡片)。
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"——采用相同的模板和映射格式;将返回的 _meta 展开传入 server.registerTool(...)。映射项可以是点路径,也可以是 {path, prefix, suffix};对于模板无法涵盖的情况,可使用 body_html= 逃生口,接受由 .lw-* 基本组件构成的自定义标记。对某个工具调用此函数,会有意替换该工具上 enable_lulu_ads 提供的通用赞助卡片——赞助数据仍会传递,并在 Widget 自带的横条中渲染。
Widget 渲染(MCP Apps UI)
纯 sponsored 字段始终会随消息发送,也始终有效——有些宿主完全依靠模型自身判断,将它渲染为卡片,不需要任何指令。对于支持 MCP Apps 扩展(io.modelcontextprotocol/ui)的宿主,enable_lulu_ads / enableLuluAds(见上文快速入门)已经会自动注册一个实际渲染的 Widget,并将其附加到每个工具上——因此你无需执行本行以下的任何操作。之所以还要单独提供这一步,是因为 register_sponsored_widget() 需要服务器的准确公共端点 URL,而 LuluAdsMiddleware/withLuluAds 本身无法得知这个 URL。
更希望按工具控制(不同工具使用不同 Widget,或只让部分工具启用 Widget)?请直接使用较底层的构建模块,而不是 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(...): ...
面向使用 Node 而非 Python 构建的 MCP 服务,使用相同的辅助函数和官方 TS SDK
(注册是 async 的——返回前可能会获取徽标):
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);
卡片格式。 template= 用于选择小组件的视觉布局——这是注册时的选项,形式与 register_result_widget 自身的 template= 相同(见上文“结果小组件”)。默认值为 "card"(上方展示的布局);传入其他名称即可选择不同格式:
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",
});
遇到无法识别的 template 时会立即报错(在任何网络调用之前,例如获取徽标),而不是静默回退——这与 register_result_widget 现有模板验证的快速失败约定一致。
"hero" 还接受一个选项:background_image/backgroundImage——注册时获取一次的 URL,并以与 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
)
模板:
template= | 布局 | 备注 |
|---|---|---|
"card"(默认) | 纵向排列:标签行,然后是徽标 + 文本/CTA 行 | 原有的手工构建布局——所有现有集成方都会原样获得此布局。 |
"banner" | 单行横向排列:标签、徽标、文本/CTA 均内联显示 | 更适合宽而矮的布局(VS Code 侧边栏、较宽的 CLI 窗格),因为纵向排列的卡片会浪费垂直空间。 |
"hero" | 满幅背景图片(或共享渐变),徽标/文本/CTA 覆盖在易读性遮罩层上 | background_image 为可选项,由集成方提供——ads-server 的自动广告匹配目前还无法自行选择此项。 |
"flip-card" | 点击可翻转 3D 卡片——正面显示标签,背面展示文本 + CTA | 已披露的标签始终显示在正面;只有广告本身的详情位于翻转后的背面。 |
"scratch-reveal" | 覆盖在广告上的刮刮层 | 无论是否交互,都会在 3 秒后自动揭示——广告内容始终相同,这只是装饰,绝不会用于门控内容。 |
"spin" | 徽标徽章上装饰性的旋转并停稳效果 | 确定性呈现——始终是唯一的真实广告,从不采用结果可变的机制。 |
更多格式将在推出后添加到这里(轮播、对比、用户评价、倒计时、测验、视频——参见 Linear 中跟踪的图库)。所有模板都共享完全相同的保障:加载时显示骨架屏、披露 Sponsored 标签、“Powered by Lulu Ads”页脚,以及缺少或格式错误的数据时快速放行——这些保障不会因模板而改变。
enable_lulu_ads/enableLuluAds 也会在内部为你对每个工具执行此操作——我们在实时环境中发现(2026-07-26),很容易忘记为每个工具正确完成此步骤:我们自己的 dogfood 服务曾只手动为一个工具接入此功能,此后新增的每个工具都悄无声息地漏掉了。如果想自动覆盖所有工具,无需逐个处理,请使用 enable_lulu_ads/enableLuluAds,而不要直接调用此函数。
它会提供一张悬浮、圆角、渐变的卡片(与 getlulu.dev 使用相同的视觉系统),并披露 Sponsored 标签——它仍然只是标记,从不构成指令。它会为你处理三个特定宿主的细节:Claude 要求提供一个未公开文档的 _meta.ui.domain 值,该值根据端点 URL 推导得出(在此自行计算,不是凭据);小组件必须在加载时发送 ui/notifications/initialized 握手,否则 Claude 会一直隐藏 iframe;徽标会被内联而非链接(见下一节),以免小组件沙盒自身的 CSP 静默拦截它们。
已在生产环境实时验证
(dali.getlulu.dev/mcp、ext-apps#671),截至 2026-07-19 为最新状态——在该修复上线之前,Claude 自身对 MCP Apps 小组件的渲染在整个平台范围内都无法正常工作,因此任何“应该能渲染”的说法(包括本文其他位置的说法)在你自己的宿主中实时验证之前,都应视为未经验证。
该小组件加载时会立即显示 shadcn <Skeleton>,只有收到实时工具调用后才会替换为实际内容——传递给 register_sponsored_widget() 的 text/url/logo 不会渲染为初始内容;实时路径实际只会使用这些选项中的 label/cta/accent*(作为线缆载荷省略字段时的默认值,以及每个集成方的静态品牌主题)。每次真实工具调用时,小组件都会监听 MCP Apps 主机自身推送的 ui/notifications/tool-result(每次调用都会挂载一个新的 iframe,不会复用——“按调用,而非按工具”是协议保证,无需在服务端构建任何东西即可实现),并使用该次调用的 structuredContent.sponsored 进行渲染——这是实时、按调用提供的广告内容,并非注册时一次性嵌入的固定载荷。若主机始终不发送通知,小组件就会一直显示骨架屏(不会显示备用广告——请参阅 js/widget-src/src/mcpBridge.ts 的 InitialOptions 文档字符串中记录的待解决问题);若某次调用没有 sponsored 字段(正常的故障开放情形),则会渲染一张只有页脚的空卡片。卡片、骨架屏和“Powered by Lulu Ads”页脚都来自同一个编译后的 React/shadcn 包,在 Python 和 TypeScript SDK 之间逐字节共享(位于 js/widget-src/,已纳入代码库,并由两种语言嵌入),且页脚始终渲染在同一个持久卡片外壳内,适用于所有状态。
标志
logo 接受的是用于获取品牌标志的 URL,而不是直接嵌入的 URL——传入后,SDK 会在注册时立即下载图片,并将其以内联 data: URI 的形式放入小组件。这并非偶然:MCP Apps 规范要求主机在小组件的沙盒 iframe 中强制实施 img-src 'self' data: <resourceDomains>,除非你另外在该资源的 CSP 配置中声明标志所在的域名,否则 <img src="https://your-cdn.com/logo.png"> 会被静默丢弃——任何地方都不会报错,卡片只会永远显示一个空白位置,在所有主机上都是如此。根据同一规则,data: URI 始终获准使用,因此在服务端获取并内联图片可以绕过整个故障情形——你无需配置 CSP,也不用担心忘记配置。
无效或无法访问的 logo 不会导致注册失败——它会被跳过(并记录警告日志),卡片也会像未设置 logo 一样正常渲染。只接受 image/png、image/jpeg、image/svg+xml、image/webp 和 image/gif,大小上限为 200KB(标志在卡片中以 28×28 显示——没有理由通过线缆传输更大的图片)。
CLI 渲染
终端没有小组件界面——模型自己的文本是唯一输出,而是否提及披露的内容,完全由模型自行判断(绝不强制——请参阅“保证”)。LuluAdsMiddleware / withLuluAds 会通过 initialize 时发送的 MCP clientInfo.name 检测已知 CLI 客户端(目前包括 claude-code,已通过实时验证);从这类客户端连接时,它们会在 content[] 中附加一张带边框的纯文本卡片,同时保留普通字段——它仍然只是数据,不会对模型发出任何指令,只是格式化成独立区块,以便模型选择转述时,不会像普通句子那样呈现:
╭─ Sponsored ────────────────────────────────────╮
│ Search 700+ airlines in one place — Kiwi.com │
│ finds routes other search engines miss. │
╰─ via Lulu Ads ─────────────────────────────────╯
→ https://ads.getlulu.dev/c/9f2a1c
已知限制,在此如实说明,不予掩饰:有些 MCP 客户端在同一结果中也存在 structuredContent 时,不会将每个 content[] 区块都转发给模型——这是 Claude Code 中尚未解决的客户端问题,已两次报告,又两次在未修复的情况下关闭(#55677 → 合并至 #45575 → 因过期自动关闭)。已针对 Claude Code 进行实时测试(2026-07-21):存在 structuredContent(已发布版本的默认设置)时,带框的卡片不会传达到模型,但普通的 sponsored 字段仍会传达——模型每次都会用自己的措辞,如实呈现一行标注清晰的“Sponsored: ...”内容,3/3 次运行均如此,没有问题。
cliTextMode — 针对上述客户端 Bug 的选择性修复
我们还测试了一个看起来显而易见的修复方案——省略 structuredContent,让 content[] 中没有其他内容与之竞争——但结果完全取决于 content[] 中还有什么:
- 只有广告,没有附带实际工具数据:每次都会显示卡片,但模型会将其标记为疑似提示词注入尝试,并提醒用户不要相信,3/3 次运行都是如此。比不显示还糟。
- 广告与工具自身结果的完整实际呈现一起出现:每次都会显示卡片,模型会将其视为已披露的普通广告,并中性地提及,3/3 次运行都是如此。不会引起怀疑。
因此,这项修复确实有效,但是否适用取决于工具自身的行为——SDK 无法替你验证这一点,所以此功能需要选择启用,且默认关闭:
mcp.add_middleware(LuluAdsMiddleware(cli_text_mode=True))
withLuluAds(server, ads, { cliTextMode: true });
只有当工具的 content[] 本身已经包含完整、易于阅读的结果呈现时,才启用此功能——不能只是“请参见 structuredContent”之类的占位内容。启用后,对于未声明 outputSchema 的已检测 CLI 客户端,SDK 会剥离 structuredContent,确保 content[](工具自身的文本加上我们的卡片)能够可靠地到达模型。对于声明了 outputSchema 的工具,SDK 绝不会进行此处理——剥离其中的 structuredContent 会直接破坏客户端的架构验证(已确认会出现 fastmcp.exceptions.ToolError:“outputSchema defined but no structured output returned”),导致工具调用失败,其结果远比卡片未显示更糟。无论是否启用 cliTextMode,SDK 都不会为了显示卡片而从声明了架构的工具中剥离 structuredContent.sponsored。
在上游客户端 Bug 修复之前,请将 CLI 卡片视为“启用此选项且工具符合条件后,卡片即可可靠呈现;无论如何,披露机制本身都已正常工作”——与上面的 widget 路径一样,仍需在自己的宿主环境中验证。
stdio 服务
“Widget rendering”一节之前的所有内容,在 stdio 传输的服务中都可以原样使用——SDK 是由你的代码导入并调用的库;它不知道也不关心你自己的服务如何与其客户端通信。普通的 sponsored 数据字段(LuluAdsMiddleware / mcp.add_middleware()、withLuluAds(server))不需要传入端点参数,会直接向 ads.getlulu.dev/slot 发出普通的出站 HTTPS 请求——无论你的进程是长期运行的远程服务,还是由 npx/uvx 启动的本地服务,请求都一样。CLI 文本卡片路径(参见上面的“CLI rendering”)是这里最常见的实际用例:Claude Code 会通过 stdio 启动大多数 MCP 服务,而这正是此 SDK 已能检测并显示已披露纯文本卡片的客户端。
MCP Apps widget 的呈现是唯一不适用的部分——enable_lulu_ads/enableLuluAds 和更底层的 register_sponsored_widget/registerSponsoredWidget 都要求提供真实的 endpoint_url,并将其哈希后写入 Claude 未公开的 _meta.ui.domain 值,供 widget 的 iframe CSP 使用。这并非 Lulu Ads 的限制;MCP Apps 的 ui/initialize 握手是宿主与服务自身 HTTP 端点之间的网络协议,而 stdio 服务没有这样的端点。如果你的服务只能使用 stdio,请直接调用 LuluAdsMiddleware/mcp.add_middleware()(或在 TypeScript 中调用 withLuluAds(server))——切勿调用 enable_lulu_ads。这样你就能获得数据字段和 CLI 文本卡片,而无需配置你并不具备的端点。
发布者须知:市场当前通过 remote_url 将商品列表与已注册的发布者账户匹配,并自动添加“已变现”徽章——stdio 商品没有 remote_url,因此即使你已经集成 SDK 并开始获得收益,也不会自动显示徽章。SDK 和收益流程本身不受影响;这纯粹是市场商品列表的展示缺失,目前正在单独跟进。
保证(由代码强制执行,而非仅作承诺)
| 保证 | 实现方式 |
|---|---|
| 工具调用绝不会因广告而失败 | 每条失败路径都会返回 None/null;硬性 800ms 墙钟超时(当调用意味着服务器端分类时为 3000ms) |
| 始终明确标示 | label: "Sponsored" 由 SDK 设置,绝不取自响应正文 |
| 绝不发生提示词注入 | 我们只提供一个数据字段;契约中没有任何显示指令 |
| 不会有 PII 离开你的服务器 | 在构建任何请求之前,客户端会根据允许列表过滤 context |
| 质量门槛把关 | 每条广告素材都必须通过 Dali 评分(≥70),才能填充广告位 |
| 基于意图,而非身份 | 定向仅使用本次调用中声明的上下文——没有用户档案,也没有跨会话 ID |
| 配置错误?依然安全 | 缺少凭据 → 客户端不执行操作,返回 None/null,不发起任何网络调用 |
为什么不……
……让模型在回复中提及赞助商?
显示指令会导致 MCP 服务被扫描注入指令的目录下架。我们提供的是一个普通数据对象——label、text、url——契约中的任何字段都不会告诉模型如何呈现或措辞。
……统计展示次数并按浏览收费? 只有模型实际呈现内容时,“展示”才算发生,而服务器端无法验证这一点——容易作弊,也难以审计。我们只按 CPA 收费,且仅当点击兑换了经服务器验证的签名令牌时才收费。付款对应真实的用户操作,而非空口声称。
……扫描对话以实现更精准的定向?
读取对话记录来定向广告会带来隐私风险:用户说的一切都会成为广告定向数据。我们只接受六个列入允许列表的上下文键——tool、category、query、route、locale、country——仅用于本次调用中声明的意图。架构中不存在对话记录、用户档案或 PII 字段。
工作原理
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.
完整的线级细节:docs/contract.md。
文档:https://getlulu.dev/docs · 快速入门 · API 契约 · 集成 · 发布者注册 · 质量门槛: Dali · MIT
更新日志
-
0.9.19 — 修复 SDK 对自身版本的错误报告。
TypeScript 客户端中的
SDK_VERSION曾硬编码为"0.9.14",而软件包已经历四次发布——并且它会以sdk_version的形式发送到/slot,因此每位 TypeScript 发布者报告的都是 0.9.14,升级遥测也无法识别实际已升级的用户。现在版本号已修正,并通过针对package.json的测试锁定,因此若某次发布遗漏更新,测试套件会失败,而不是悄悄污染数据。Python 客户端从未有过这个问题;它会导入lulu_ads.__version__。文档更新,不改变行为:
js/src/skybridge.ts的头部仍将适配器描述为仅支持_meta,但从 0.9.15 起情况已不再如此——现在它包含与docs/integrations.md相同的四种情况对照表。此外,PyPI 展示的python/README.md一直未与根目录 README 同步,落后了四个版本,因此软件包页面仍用同一条已废弃的说法描述 Skybridge。现在两个软件包的 README 都由scripts/sync-readmes.sh同步生成。 -
0.9.18 — 支持 Skybridge 2.x,并防范其一种静默故障。
skybridge 2.x 将协议中间件的接线从
McpServer.connect()移到了应用的请求路径中,因此不同主版本的挂载点也不同:// 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);在 2.x 上使用 1.x 的接线方式,会把中间件注册到一个没有任何代码执行的链中:没有错误、没有广告,也没有任何线索可供排查。现在适配器会检测这种情况(其中间件总是在工具处理程序之前运行,因此,如果工具正在执行而中间件从未运行,就说明接线有误),并且只发出一次警告,附上正确的代码片段。只警告,不抛出异常。 已在两个主版本上完成验证:
[email protected]和@2.0.0均为 106/106,通过已发布的包运行了端到端示例。请注意,在 2.x 中,惯用的仅含content的工具会将广告仅放在_meta中,因此视图应先读取structuredContent,再回退到_meta;examples/skybridge-views/中的参考视图正是这样处理的。 -
0.9.15–0.9.17 — Skybridge 终于能投放广告,SDK 也会报告调用方身份。
关于 2.0.1: 这些变更曾短暂以
2.0.1发布,随后被撤回——该主版本导致所有现有的^0.9.x依赖范围都无法获取这些变更,这意味着除非手动编辑package.json,否则无人能收到更新。该版本已从 PyPI 撤回,并在 npm 上被后续版本取代;请使用 0.9.x 系列。没有任何内容丢失,只是重新编号了。Skybridge。
withLuluAdsSkybridge以前只会将sponsored附加到_meta。Skybridge 路径上没有任何内容会读取_meta——enableLuluAds无法在那里注册小组件(Skybridge 的registerViewResource是私有的),也没有 CLI 卡片——因此广告位虽然被获取、记录,却没有展示给任何人。探测实时结果的形状后发现,Skybridge 上的content[]为空,而structuredContent已填充(与官方 SDK 相反),因此视图渲染时应读取structuredContent。现在广告会写入该字段:工具形状 sponsored写入位置没有 outputSchemastructuredContent和_meta有 outputSchema仅 _meta——未列出的字段会导致客户端验证失败在 withLuluAdsSkybridge之前注册仅 _meta——模式状态未知,采用安全默认值模式标记会在注册时通过一个双参数的
registerTool包装器捕获,因为mcpMiddleware无法看到outputSchema。客户端身份。 现在每个适配器都会将 MCP 客户端名称作为
context.client转发,这样 ads-server 就能区分真正的 Agent 和目录爬虫。"client"已加入上下文允许列表。集成方无需修改代码。修复 fastmcp 4.x / MCP SDK v2 中两个无提示故障。
clientInfo已更名为client_info;旧名称在裸except中触发异常,因此在所有 4.x 宿主上,_connected_client_name都会返回None——这也会无提示地禁用 CLI 赞助卡片 (is_cli_client(None)为 False)。此外,4.x 协商使用的是server/discover而非initialize,因此on_initialize从未触发,异步预热也会停止,从而重新引入分层超时工作原本要避免的冷启动延迟。现在两个访问器都会先尝试新名称,再尝试旧名称;on_discover也会与on_initialize一同运行。Python 测试套件:在 4.0.5 和 3.4.4 上均为 144 项通过 / 0 项失败(此前有 8 项失败)。JS:104/104。升级: 有意继续留在 0.9.x 系列——
^0.9.x范围可覆盖 0.9.18,因此只需运行npm update lulu-ads/pip install -U lulu-ads,无需编辑依赖范围。正是这种传播方式,让已撤回的 2.0.1 避免了造成影响。请参阅
examples/skybridge_server.ts和npm run verify:skybridge,该命令会通过内存传输,针对真实服务断言以上所有内容。 -
0.9.14 — 广告每次调用的
template(LUL-64,当/slot返回该字段时) 现在会优先于你注册时指定的template=/template:,用于 独立赞助卡片小组件(register_sponsored_widget()/registerSponsoredWidget())。如果该字段缺失,或不是此构建版本 能识别的模板,则像以前一样回退到注册时指定的默认值,再回退到"card"。 **升级后行为会改变,无需修改代码:**如果你注册时指定了非默认的template=(例如"hero"),现在带有自身实时模板的广告会改用该模板 渲染,且无法选择退出。这是有意为之——管理员为特定广告选择的模板, 应当比集成方的一刀切默认值更具体——但这确实意味着升级后小组件的 视觉效果可能改变,即使你没有修改任何代码。此版本还修复了一个查找 隐患:实时template值中的原型链名称(例如"constructor")可能被 误判为“已识别”,并在展示次数信标已经触发后导致渲染崩溃。 -
0.9.13 — 修复了独立赞助卡片小组件(
register_sponsored_widget()/registerSponsoredWidget()——由 React 构建的卡片、横幅、翻转卡片、 刮奖揭晓、旋转和主视觉模板)中一个已渲染展示次数的计费缺失问题 (LUL-71):线缆负载中的imp_url/impUrl字段已由两个客户端解析并 暴露(Node SDK 中的Sponsored.impUrl,Python 客户端中的imp_url), 但在到达 React 组件之前,却被小组件包自身的消息解析静默丢弃——因此 该小组件自发布以来,无论使用哪个模板,都从未触发过已渲染展示次数 信标(一个确认真人确实看到了广告的 1px<img>,CPM 计费依据就是 它)。此问题在App.tsx从加载状态切换到“loaded”的阶段统一修复—— 这是每个模板披露内容变为可见的时刻,包括翻转卡片的正面预告内容 (在用户翻转之前触发,与register_result_widget()页脚条已正确实现 的行为一致),以及刮奖揭晓的覆盖内容(在任何人刮开之前触发)。register_sponsored_widget()/registerSponsoredWidget()现在还声明了 信标像素所需的ads.getlulu.dev资源/连接 CSP 域,以免被主机默认的img-src 'self' data:策略静默拦截——register_result_widget()早已 声明了同样的域。已通过实时验证:收到带有imp_url的tool-result时,信标会立即触发真实 HTTP 请求,并通过本地服务器的访问日志确认, 而不只是通过单元测试。 -
0.9.12 — 结果小组件页脚的
sponsor_template=(LUL-69)从纤细的 单行条重新设计为封面卡片:一条彩色封面带——实时负载提供cover_image_url/coverImageUrl时显示真实赞助商图片,否则显示动画 渐变——Logo 磁贴与主体交界处重叠,字体更大,并配有真正的胶囊形 CTA 按钮。"flip-card"的预告现在也带有相同的封面标识,而非单独的标签。"spin"已移除:在小型、通常扁平的字母磁贴 Logo 上播放抛硬币式scaleX动画,实际效果在视觉上难以辨认(动画过程中会缩小为几乎 看不见的细条,背景又是繁杂的封面),这是实时发现的问题,而非推测。 现在由"carousel"取代——自动轮播同一份赞助负载的三种呈现方式 (更大的品牌 Logo、优惠文案,然后是 CTA),绝不轮播多个赞助商:每次 调用的这个区域始终只会收到一份赞助负载,因此与独立图库的 carousel 模板不同,这里不会轮换多个广告商(该独立问题仍受阻,见 LUL-49)。"scratch-reveal"重新设计为彩票刮奖券样式——金属渐变、斜线纹理、 虚线撕口线和票券图标——自动揭晓时间现在为 5s,而非 3s,足以让人 确实感受到交互;它仍然是严格确定性的揭晓,不设“赢/输”状态,因此 票券样式不会演变为真正的赌博机制。由于现在条本身已有封面高度, 以前也用于排除"hero"的“单行、没有空间显示全幅图片”这一理由不再 成立——此问题已记录为待跟进事项,本次发布并未解决。 -
0.9.11 —
register_result_widget()/registerResultWidget()新增sponsor_template=/sponsorTemplate:(LUL-69):结果小组件页脚中内置的 SPONSORED 条(stat-card/table-card/notice-card/carousel-card——发布者自己的工具输出,而非独立赞助卡片小组件) 现在也可以选择视觉格式,且独立于template=(即该小组件自身的主体 布局)。"card"(默认值)是原有的常驻显示条。"flip-card"是紧凑的 “Sponsored”预告,轻触后交叉淡化并显示真实优惠——正面带有实际的 提示词(“轻触以揭晓 →”),而非只有披露标签,因此用户有理由轻触。"spin"是 Logo 徽章上的装饰性旋转并停驻效果,内容会立即显示,绝不 设门槛(没有可变结果——见下方的旋转护栏)。"scratch-reveal"是 Canvas 刮奖图层,无论用户是否交互,都会在 3s 后自动揭晓——无论是否 有人刮开,底下的优惠都完全相同。这里有意不提供"banner"/"hero": 此区域本身已经是单行横条(banner 不会带来变化),而全幅背景也不适合 窄小的页脚栏。这与上文的独立赞助卡片图库(register_sponsored_widget()自身的template=)是不同的系统——名称相同的模板会沿用到这里,但 在本区域自有的原生 JS 渲染器中实现,而非共享代码,因为这两个区域 按设计就是独立实现。 -
0.9.10 — 新增四种模板:
"flip-card"(LUL-53,轻触时通过 CSS 3D 翻转显示背面的优惠——正面始终先显示披露标签,从不隐藏内容 是什么,只隐藏优惠详情)、"scratch-reveal"(LUL-52,Canvas 刮奖 图层,无论是否交互,都会在 3s 后自动揭晓——无论是否有人刮开,底下的 优惠都完全相同;这是揭晓动画,绝非设门槛的内容)、"spin"(LUL-54, Logo 徽章上的装饰性旋转并停驻效果——设计上具有确定性,不会像幸运 转盘那样产生可变结果,因为那会构成赌博机制),以及"hero"(LUL-48, 全幅背景图片或渐变,并在保证文字易读的遮罩层上叠加 Logo/文本/CTA)。register_sponsored_widget()/registerSponsoredWidget()还新增了"hero"可选参数background_image/backgroundImage——与logo相同,采用只抓取一次并内联为data:URI 的约定(抓取失败时只会回退 到共享渐变,不会导致注册错误)。这是集成方在注册时提供的品牌元素, 与logo/accent*属于同一类别——尚未接入 ads-server 的自动广告与 广告活动匹配,因为 ads-server 自身没有图片字段(另行跟踪)。 -
0.9.9 — 模板图库中首个实际条目:
template="banner"(LUL-47), 一条全宽横条(标签、Logo、文本/CTA 都在同一行),取代默认"card"的堆叠布局——更适合宽而矮的表面(VS Code 侧边栏、宽大的 CLI 窗格)。 与所有模板遵循相同的共享约定:在同一持久渐变外壳中渲染,具有相同的 披露标签和“Powered by Lulu Ads”页脚,以及相同的加载骨架屏和失败时 开放行为——这些保证都不因模板而异。暂不支持背景图片(LUL-47 的规范 允许“图片或渐变”;真正的背景图片字段需要在广告商侧新增架构,另行 跟踪)——使用现有的强调色令牌渐变。已新增文档:小组件渲染部分现在 通过实时示例和可用格式表格说明template=。 -
0.9.8 —
register_sponsored_widget()/registerSponsoredWidget()新增template=参数(LUL-46),形式与register_result_widget的 参数相同:仅限关键字参数,默认为"card"(当前唯一格式,完全向后 兼容——现有集成方的调用点行为均不改变),遇到无法识别的值时立即报错 (在任何网络请求之前,例如抓取 Logo 之前)。这是注册时由集成方选择的 参数,而非每次调用时指定的值——与register_result_widget自身模板 静态“编译进注册时构建的包”的行为一致,而不是随投放的广告变化。此次 仅奠定基础:js/widget-src/中的注册表/分派机制现在支持按名称查找 模板,但"card"(现有的手工构建布局)仍是唯一条目——更多模板会在 后续版本中分别构建并发布(banner、hero、carousel、comparison、 flip-card、scratch-reveal、spin、testimonial、countdown、quiz、video)。 此版本还修复了一个真实存在已久的偏差:TS SDK 自身的SDK_VERSION遥测常量自更早的某个版本起一直停留在0.9.0,尽管package.json已更新至0.9.6——此处已使其重新一致,两个包自身的版本号也恢复同步 (之前的0.9.7仅适用于 Python;TS 包直接跳至0.9.8)。 -
0.9.7(仅 Python)— CLI/文本客户端现在会发送已确认投递信标: 正常的已渲染展示次数像素(
imp_url)会由渲染客户端在显示赞助条的 瞬间抓取——终端/CLI 主机(Claude Code 等)没有渲染引擎来完成这一步, 因此之前所有通过 CLI 投放的卡片在ad_events中都不可见,即使投递 100% 成功也是如此。现在中间件会在将卡片追加到 CLI 客户端的content[]时自行触发同一信标 URL(发后即忘、非阻塞,并标记为src=cli_server),覆盖两条is_cli卡片追加路径(中间件自行获取的 广告位,以及工具预先设置的sponsored)。这会记录为一种新的独立事件cli_card_delivered——不是impression_rendered,绝不会计入 CPM 计费/付款——这是刻意设置的较弱信号,不等同于真实的渲染像素请求:它 只能证明卡片已随工具响应离开服务器,不能证明真人确实看到了卡片。 无需配置;现有携带imp_url的广告位会自动启用此功能。 (ads-server:/i/{token}现在接受可选的src=cli_server查询参数, 用于记录这种独立事件类型。) -
0.9.6 — 仅文档更新:新增支持的表面 页面,根据每种 Agent 表面实际接入 SDK 的方式整理分类(聊天宿主、 Agent SDK/框架、响应后缀运行时、AI 应用构建器、MCP 托管/注册目录): 直接适配器、MCP 协议透传(服务接入 Lulu Ads 后无需适配器)、通用
format_suffix约定,或确实尚未评估——证据标准与本仓库其他部分一致。 还新增了“stdio 服务”一节,说明数据字段路径无需endpoint_url,且在 stdio 传输服务上无需修改即可使用;只有渲染后的 MCP Apps 小组件需要endpoint_url,且无法用于 stdio。此版本还修复了lulu_ads.__version__: 软件包发布为 0.9.5 时,它却已偏离为 0.9.0——与下文 0.7.0 条目属于同类 问题;由于没有机制确保两者保持同步,这个问题再次出现。可以把它视为 此处下一个需要解决的实际缺口。 -
0.9.2(仅 Python)— 修复了一个中间件错误:工具自行设置
sponsored字段(例如用于特定类别交叉销售的文档所述模式)时,会在 CLI 客户端检查运行前触发on_call_tool中的“绝不覆盖”提前返回, 因此 CLI 主机(Claude Code)在使用该工具时完全看不到广告——既没有 小组件表面,也没有文本卡片兜底;两者都被同一个提前退出跳过。现在会 先检查客户端;如果已预先设置sponsored值,仍会应用 CLI 文本卡片 处理,并使用工具自行选择的广告。 -
0.9.1 —
table-card小组件新增rowLink:一个可选的逐行点路径, 用于解析出 URL(例如预订/结账链接),并接入赞助条已在使用的、与主机 无关的openLink()。无法解析出 URL 的行仍与以前完全相同。 -
0.9.0 — 支持 Skybridge(https://skybridge.tech):
withLuluAdsSkybridge(server)(lulu-ads/skybridge)。Skybridge 的McpServer.registerTool采用 2 个参数的(config, handler)形式,其中name包含在config中,而非withLuluAds所包装的官方 SDK 的 3 个 参数(name, config, handler)形式——直接复用withLuluAds会把config对象误读为工具名称。新适配器改用 Skybridge 自身的mcpMiddleware("tools/call", ...)协议钩子,并已通过真实的[email protected]软件包随附类型及InMemoryTransport实时往返测试 验证。该适配器刻意仅处理_meta:中间件能看到调用结果,却看不到工具 注册时的outputSchema,因此不会接触structuredContent。 -
0.8.1 — 结果小组件模板图库(取代曾以更醒目的条样式短暂发布到 npm 的 0.8.0):
lulu_ads.widgets/lulu-ads/widgets,包含四种预定义模板 (stat-card、table-card、notice-card、carousel-card)、设计令牌 和.lw-*基础组件,以及内置在框架中的 SPONSORED 条(通过广告位新增的logo_url显示广告商 Logo,缺失时回退到字母磁贴)。register_result_widget()会原地修改已注册的 FastMCP 工具(或返回 AppConfig,以供显式传入app=)。 -
0.7.4 — Widget:赞助卡片的 iframe 画布在深色主题的宿主页面中不再显示不透明的白色方块。对于嵌入式 iframe,仅设置
background: transparent并不足够:只有当嵌入文档的实际配色方案与嵌入方一致时,Chromium 才会让画布保持透明,而该文档未声明配色方案(默认使用light),因此深色主题的宿主页面(例如处于深色模式的 claude.ai)会在卡片后面强制显示白色背景。现在,Widget 声明了color-scheme: light dark,该设置会解析为用户偏好的配色方案,从而在浅色和深色主题下都能与遵循此设置的宿主页面匹配(claude.ai 默认会遵循)。已在浅色和深色配色方案的嵌入页面中进行实际验证。(同时使lulu_ads.__version__与之保持一致;该值此前已偏离,仍为 0.7.2,而发布的包版本为 0.7.3。) -
0.7.0 — 在 Claude.ai 的远程连接器后方,针对真实第三方 MCP 服务进行实时测试时发现了两个实际问题,均已修复:
- 对于每条消息都会重新连接的宿主,广告投放率为 0%(已确认:Claude.ai 每发送一条聊天消息都会开启全新的 MCP 会话,而不是每次对话开启一次)。根本原因:本 SDK 的持久 HTTP 连接在消息间出现任何实际空闲间隔时都会变冷,但只有一次性的“是否曾经成功过”检查保护了首次调用——之后每次冷调用仍会使用严格的稳态超时并失败。现已修复:每次调用都会重新检查连接是否变冷,并依据上次实际成功距今的时间判断,而不再使用永久锁存标记。此外:稳态快速超时也从 800ms 提高到 1500ms(Python 和 TS)——生产环境证据显示,即使是“热”调用,有时也会测得 796-802ms,正好卡在旧阈值附近,而不是明显低于阈值。
- 广告成功获取,但模型始终看不到。 FastMCP/MCP TS SDK 会在
LuluAdsMiddleware/withLuluAds执行之前,根据工具的原始返回值一次性构建工具结果的content[]——仅修改structuredContent(本 SDK 自有测试套件唯一检查的内容)会导致content[]永远过时。实时测试确认:网络响应中的structuredContent确实包含sponsored,但 Claude.ai 读取并根据content[]作答,而其中并没有该字段。现在两个 SDK 都会在安全的情况下保持content[]同步(即只有一个自动生成的 JSON 文本块时);同时新增了针对这一确切漏洞的回归测试,避免它再次悄然上线。 - 新增:
enable_lulu_ads()(Python)/enableLuluAds()(TS)——一键自动为所有现有和未来的工具接入数据字段和渲染后的 MCP Apps 小组件。现有的register_sponsored_widget()/registerSponsoredWidget()与每个工具的app=/_meta.ui仍可使用,现已说明这是用于逐工具控制的底层构建模块;它留下的缺口(容易忘记为每个工具单独手动接入)正是此功能要弥补的——在我们自己的内部测试服务上实时发现了这个问题:该服务只为一个工具手动接入了小组件,之后新增的工具从未更新。
-
0.6.2 — 赞助卡片进入已加载状态(赢得了真实广告)时,现在会在卡片上播放一次对角线扫光动画(纯 CSS,位于
js/widget-src/src/index.css的.card-shine),每次挂载恰好触发一次(而不是循环闪烁,因为它嵌在真实聊天对话中),并遵循prefers-reduced-motion设置。骨架屏和无填充状态不受影响。 -
0.6.1 — 修正了在从合并后的源代码重新构建
dist/之前就发布到 npm 的过时0.6.0(js/没有prepublishOnly构建步骤)——npm 上的 0.6.0 已被弃用,并指向此处。还修复了 README.md 以及两种语言中widget.py/widget.ts的文档字符串;它们错误地声称传给register_sponsored_widget()的text/url/logo会作为备用“自营广告”渲染,直到收到实时tool-result;实际并非如此——如果宿主从未推送数据,小组件会无限期显示骨架屏。 -
0.6.0 — MCP Apps 赞助小组件现在展示每次调用对应的实时广告内容,而不是在注册时固定写入的自营广告:使用 React + shadcn/ui(
Card、Skeleton、Button)重建,并编译为单个自包含程序包,两个 SDK 共用且字节完全相同(js/widget-src/)。小组件加载后立即显示骨架屏,随后监听 MCP Apps 宿主自身的ui/notifications/tool-result推送——规范本就规定每次调用都会推送一次,并为每次调用创建一个全新的 iframe,无需更改服务端——然后切换为该次调用的真实structuredContent.sponsored数据。如果宿主从未推送数据,小组件会无限期显示骨架屏,而不是备用广告;实时路径实际使用的只有register_sponsored_widget()/registerSponsoredWidget()选项中的label/cta/accent*。页脚“Powered by Lulu Ads”会立即渲染一次,且不会参与骨架屏→卡片的切换。使用临时测试服务在真实宿主(claude.ai)上完成了实时验证:工具调用返回前会显示骨架屏,返回后会切换为该次调用对应的真实卡片;切换期间页脚不会消失或重新布局;同一轮中的两次工具调用会渲染两个完全独立的小组件实例,各自只显示本次调用的数据——证实了此功能所基于的“按调用区分,而非按工具区分”行为。(在同一轮测试中,通过静态代码检查和本仓库现有的单元测试,再次确认了 CTA 使用ui/open-link重定向,而非原始导航;曾尝试实时捕获点击跳转,但受浏览器自动化工具限制,无法深入宿主双重沙盒隔离的 iframe,并非观察到产品故障。) -
0.4.0 — 构造时自动预连接,适用于 LangChain 的
LuluAdsAgentMiddleware、CrewAI 的install()和 TypeScript 的withLuluAds(与已有此功能的 FastMCPLuluAdsMiddleware保持一致)。此外:FastMCP 的LuluAdsMiddleware和 LangChain 的LuluAdsAgentMiddleware现在还会预热其await调用的sponsored_slot()流量实际使用的异步连接池——上述构造时预热只触及同步客户端,而异步路径永远不会用到该独立连接池。LuluAds.async_warm_up()会在真实框架生命周期钩子中,于实际服务所用的事件循环上每个实例触发一次(FastMCP 的on_initialize、LangChain 的abefore_agent),因为后台线程无法安全预热供另一个事件循环使用的连接。此功能受与同步预热相同的auto_warm_up标志控制(异步路径仅适用于 Python——TypeScript 的autoWarmUp只需控制一个连接池)。这一修复弥补了生产环境中dali-mcp的冷启动问题,它使用的是异步路径。两个基础客户端都新增了短 TTL(默认 45s)的仅缓存成功结果机制,按解析后的类别或提示词文本的哈希值作为键。文档已更正:实际默认超时为自适应的 800ms(快速路径)/3000ms(分类路径),而不是固定的 300ms。 -
0.3.7 —
cliTextMode(可选启用,默认关闭):彻底修复 Claude Code 丢弃content[]的问题,但仅适用于content[]本身无需structuredContent也能独立呈现的工具——已对符合和不符合条件的情况进行实时测试,参见“CLI 渲染”。绝不触碰声明了outputSchema的工具(会破坏客户端的架构验证,已通过fastmcp.exceptions.ToolError确认)。 -
0.3.6 — 构造
LuluAdsMiddleware时自动预热连接(auto_warm_up,默认开启):一次真正的冷启动首次工具调用测得 804ms,而快速路径默认值为 800ms——正好达到上限,并未低于上限。LuluAds本身仍不会自动预热(将网络调用作为构造函数的副作用,对于通用客户端而言出人意料),但中间件承诺“一行代码、无需配置”,因此会自行预热。 -
0.3.5 — 修复了
LuluAdsMiddleware中硬编码的 300ms 默认timeout_ms;该设置会在真实网络延迟下悄然丢弃真实且可填充的广告——测试套件中的所有测试都使用即时响应的模拟传输,正因如此,这个问题才悄然上线。默认值现为None,由LuluAds自身条件判断并使用 800ms/3000ms 默认值。 -
0.3.4 — CLI 卡片新增圆角和“via Lulu Ads”页脚(仅使用 Unicode 制表符绘图字符——针对 Claude Code 的实时测试确认,它会在模型看到工具输出之前剥除原始 ANSI 颜色转义序列,因此从未考虑过使用颜色)。此外,经过实时测试后,明确否决了丢弃
structuredContent以强制传递content[]的做法:这确实会让卡片显示出来,但模型随后会将其标记为疑似提示词注入,并警告用户不要使用——这比现状更糟;现状下,即使没有卡片,普通字段仍会被如实展示。完整说明请参见“CLI 渲染”。 -
0.3.3 — 适配 CLI 的渲染:
LuluAdsMiddleware/withLuluAds会根据initialize时发送的 MCPclientInfo.name检测已知 CLI 客户端(目前为已通过实时验证的claude-code),并为其向content[]追加带边框的纯文本卡片,同时保留普通字段——终端没有小组件显示界面,因此这是上述 MCP Apps 小组件的 CLI 安全替代方案。仍然只是数据;有关某些客户端转发content[]时已披露的局限,请参见“CLI 渲染”。 -
0.3.0 —
register_sponsored_widget()/registerSponsoredWidget()新增logo选项:在注册时由服务端获取,并以内联data:URI 的形式写入小组件,使其能在小组件沙盒的 CSP(img-src 'self' data: <resourceDomains>)下渲染,无需你配置resourceDomains——否则,原始远程徽标 URL 会被静默丢弃,且不会产生任何错误。无效或无法访问的徽标不会导致注册失败;卡片只是不显示徽标。TypeScript 的registerSponsoredWidget()现为async(可能需要先获取徽标再返回)——请在现有调用处添加await。 -
0.2.0 —
register_sponsored_widget()(Python:lulu_ads.widget,现也支持 TypeScript:lulu-ads/widget,官方 MCP SDK):在你的服务上注册真正渲染的 MCP Apps UI 赞助卡片(而不只是普通 JSON 字段),并代为处理 Claude 未公开说明的 iframe 域名要求和ui/notifications/initialized握手。推广了针对ext-apps#671验证过的修复,验证地址为dali.getlulu.dev/mcp:ext-apps#671。对于相同的端点 URL,两个 SDK 生成的_meta.ui.domain值字节完全一致。 -
0.1.1 — Python SDK 使用持久 HTTP 客户端(每次调用都构建客户端可能会在 CPU 受限的容器中耗尽整个广告位预算;现在每个
LuluAds实例只创建一次客户端,并通过 keep-alive 重用)。失败放行行为不变。 -
0.1.0 — 首次发布:Python + TypeScript 客户端、FastMCP / LangChain / LangGraph / CrewAI / MCP-TS 适配器、后缀辅助工具、MCP 管家式引导。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。