mcpField
L
Lulu Ads — Publisher Concierge

Lulu Ads — Publisher Concierge

远程服务
github.com/Lulu-The-Narwhal商业与营销45 周前更新
代码仓库

关于 Lulu Ads — Publisher Concierge

Lulu Ads — Publisher Concierge 帮助发布者在 MCP 服务或 Agent 工具结果中加入明确标注的赞助数据。其 SDK 和集成支持发布者注册及可选赞助字段的插入,由宿主模型自行决定是否展示。

主要功能

  1. 01在结果中加入标注清晰的赞助字段
  2. 02提供 Python 和 TypeScript SDK
  3. 03支持 MCP 服务和 Agent 框架集成
  4. 04可选渲染赞助组件
  5. 05按订阅等级有条件地启用广告

适用场景

  1. 01在 MCP 工具结果中加入赞助数据
  2. 02注册发布者并配置广告位
  3. 03免费等级展示广告,付费等级停用广告

MCP 相关

该服务本身可作为 MCP 服务使用,提供发布者注册和集成指引。其 SDK 与中间件可向 MCP 工具结果添加赞助数据字段,并在支持的宿主中提供渲染组件。

配置

远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。

服务地址https://ads.getlulu.dev/mcp
一键复制安装命令
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。

工具

5
参数类型必填说明
frameworkstring否

详细介绍

lulu-ads

Agent 经济的变现层。

只需添加一行带标签的赞助内容,即可通过 MCP 服务或 Agent 工具变现。

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

快速开始 · 集成 · 支持的宿主 · 支持的展示位置 · stdio 服务 · 保障措施 · API 合约 · 托管文档 · 博客 · 成为发布者

Lulu Ads 的吉祥物独角鲸 Lulu 在特拉维夫的广告牌上欢庆——Agent 经济如今有了变现层

70% 分成给发布者 · 仅限 CPA · 800ms 失败时放行 · 从设计上杜绝提示词注入

Sponsored
↑ 实时渲染的赞助卡片——真实轮播广告需求,每隔约 60 秒刷新一次。任何已认领的列表都可以在自己的 README 中嵌入此内容。

Lulu Ads 会将一条已披露并带有标签的数据字段附加到工具自身的结果中。 宿主模型——Claude、Cursor 或任何 Agent——会自行判断内容是否足够相关, 再决定是否展示。我们绝不会指示它这样做。

SDK 提供的内容(数据字段)宿主渲染的内容(由宿主决定)
{
  "sponsored": {
    "label": "Sponsored",
    "text": "Direct flights TLV–BKK from $412",
    "url": "https://ads.getlulu.dev/c/9f2a1c"
  }
}

赞助内容 — TLV–BKK 直飞航班,价格低至 $412 ads.getlulu.dev/c/9f2a1c

零摩擦上手——添加 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”,绝不会仅凭“应该能用”的假设。

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

我们考察过的宿主——这些徽标本身并不代表我们认可或支持;各宿主的实际功能请查看下方的 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(与已有此功能的 FastMCP LuluAdsMiddleware 保持一致)。此外: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 时发送的 MCP clientInfo.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 管家式引导。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。