mcpField
G
gam-seller-mcp-node

gam-seller-mcp-node

本地服务
github.com/juan-sibbo商业与营销04 周前更新

关于 gam-seller-mcp-node

gam-seller-mcp-node 是一个受治理的 MCP 服务原型,让买方 Agent 发现广告库存、固定价格和可用量,同时限制数据暴露。它使用买方范围内的软意向而非广告服务器写入,并以哈希链审计账本记录决策。

主要功能

  1. 01公开获准查看的产品系列与价格
  2. 02返回分档的可用量预测
  3. 03创建和撤销买方范围内的意向
  4. 04验证工具请求身份并限流
  5. 05在审计账本中记录决策
  6. 06可选读取 GAM 预测数据

适用场景

  1. 01让买方 Agent 发现符合权限的广告库存
  2. 02查看可用量和固定标价
  3. 03记录买方软意向以供后续处理
  4. 04审计获准和拒绝的请求

MCP 相关

它通过 MCP 工具向买方 Agent 提供卖方能力发现、产品发现、预测以及买方范围内意向的创建和撤销。唯一写入面是软意向,不会创建或修改广告服务器订单。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"juan-sibbo-gam-seller-mcp-node": {
"command": "npx",
"args": [
"-y",
"gam-seller-mcp-node"
],
"env": {
"MCP_CONFIG_DIR": "<MCP_CONFIG_DIR>"
}
}
}
}
启动命令npx -y gam-seller-mcp-node
需要配置的环境变量
MCP_CONFIG_DIRDirectory holding your deployment.json/catalog.json/entitlements.json/pricing.json. Omit it to run the bundled demo.

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
npmgam-seller-mcp-node0.11.1

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

用于未来 GAM 集成的受治理 MCP 卖方控制平面原型。

npm 版本 npm 下载量 CI 许可证:MIT TypeScript MCP

一个 Model Context Protocol 服务,向买方 AI Agent 提供卖方广告库存信息:发现、确定价格和一种买方限定范围的软承诺机制。它不会向广告服务端写入任何数据。预测数据可以实时来自 Google Ad Manager(可选,只读的 ForecastService 快照);目录由运营方配置,未连接 GAM 时,预测数据为模拟数据。


这个项目解决什么问题?

卖方广告库存(可用量、定价、产品结构)存放在广告服务端中,其中包含商业敏感数据,有时也包含个人数据。让 AI 买方 Agent 直接访问 GAM 或类似系统的 API 会带来三种风险:

风险不使用本项目使用本项目
数据暴露过度Agent 可以读取原始可售库存、交易 ID 和确切底价只能访问粗略分档和预先声明的产品系列
意外写入Agent SDK 可以创建订单、修改广告订单项不会向广告服务端写入数据;唯一的写操作是买方自己的软承诺,永远不会转化为 GAM 订单或库存占用
缺乏问责API 调用会被记录,但无法审计哈希链审计账本;每项允许或拒绝的操作都会被记录

工作原理

买方 Agent 通过 MCP 连接后会获得五个工具——其中三个只读,另有一个买方限定范围的 承诺机制(创建/撤销),这是唯一的写入入口:

Buyer agent
    │
    ├── well_known_capabilities   ← Signed trust anchor. Check this first.
    │       Returns: RS256-signed capability document, node identity, privacy posture.
    │
    ├── discover_products         ← What can I buy here, and at what firm price?
    │       Returns: product families the buyer is entitled to see (e.g. "Pre-Roll Video"),
    │               each with its formats, channel and sites, and its firm list price
    │               when the publisher has configured them.
    │       Never returns: deal IDs, internal IDs, raw inventory, exact per-impression pricing.
    │
    ├── get_forecast              ← How available is this family next quarter?
    │       Returns: Low / Mid / High availability bucket.
    │       Never returns: exact impression counts, CPM curves, floor prices.
    │
    ├── check_availability        ← Can you deliver 2.8M impressions of this family in October?
    │       Returns: available / partial (up to ~2.7M, ~1.3M viewable) / unavailable, as of the
    │       last forecast — and, when it does not fit, where it would ("November: 3.1M").
    │       Volumes come from the forecast rounded down to 2 significant figures
    │       (configurable by the publisher).
    │       Never returns: other buyers' bookings, floor prices, audience data.
    │
    ├── create_intent             ← Commit to a product at its current firm price (with TTL).
    │       Records a firm, time-boxed buying intent — rejected if the price is stale or
    │       mismatched. NOT a GAM order and NOT an inventory hold; it is the handoff artifact
    │       the classic sales rails pick up. Buyer-scoped: you can only ever commit as yourself.
    │
    └── revoke_intent             ← Withdraw one of your own active intents by id.

每次调用在运行任何领域逻辑之前,都会经过同一条处理流水线:

  Buyer request
       │
       ▼
  [SEC-GATE-3]  Replay detection — deduplicate client_request_id
       │
       ▼
  [Auth]        RS256 token validation → identity confirmed or AUTH_FAILED
       │
       ▼
  [Policy]      Surface denylist → entitlement check → scope check (Default-Deny)
       │
       ▼
  [Rate limit]  N=1 / T=30s per buyer_id
       │
       ▼
  [Domain]      Catalog / ForecastEngine — synthetic, seeded or live GAM forecast snapshot
       │
       ▼
  [Disclosure]  Response checked against the tool's strict schema — undeclared field → withheld
       │
       ▼
  [Audit]       Append-only hash-chained ledger, buyer pseudonymized (HMAC)
       │
       ▼
  Response to buyer

请求路径中的每个关卡都会在失败时拒绝请求。上图有一点需要如实说明:

  • client_request_id(重放防护的去重键)自 v0.9.0 起,默认要求在每个经过身份验证的接口上提供—— 不带此键的请求会被拒绝,因此无法通过省略它来绕过 SEC-GATE-3。运营方可以通过 MCP_REQUIRE_IDEMPOTENCY_KEY=0 显式选择退出,以兼容旧版客户端;这会重新开放该绕过途径。

限流阶段覆盖所有经过身份验证的工具——只读接口、create_intent 和 revoke_intent——因此没有任何经过身份验证的接口可以绕过限流。

磁盘上的账本若已损坏或遭篡改,系统会在启动时检测到,并拒绝提供服务 (加载时失败即关闭,并在首个请求前验证链完整性),而不是将账本重置为空链。

create_intent 会运行相同的关卡,并在记录任何内容前增加一道关卡:买方的 price_ref 必须与该产品系列当前的确定价格匹配,否则请求会被拒绝。

快速开始

用一条命令运行完整试点。 scripts/pilot.sh 会根据你的配置启动节点并启用生产防护措施, 为每位有权限的买方创建一个买方令牌,并说明如何让买方 Agent 完成整个流程(发现 → 预测 → 承诺 → 撤销)——详见 docs/PILOT-QUICKSTART.md。参考买方 Agent 位于 examples/buyer-client-ts/agent.ts;在 TLS 后托管的完整配置步骤见 deploy/。

在 MCP 客户端中安装(通过 npx)

将服务添加到你的 MCP 客户端(Claude Desktop、Claude Code、Cursor 等):

{
  "mcpServers": {
    "gam-seller": {
      "command": "npx",
      "args": ["-y", "gam-seller-mcp-node"]
    }
  }
}

或直接运行(stdio 传输——MCP 客户端的默认方式):

npx -y gam-seller-mcp-node

演示模式。 如果没有自己的配置,节点会基于捆绑的 pilot-publisher 示例启动(包含示意性的目录、价格和预测),并在 stderr 中说明这一点——它会启动而不是报错,因此你可以立即试用这些工具。 由于买家端始终需要令牌(即使在演示模式下也不支持匿名访问),节点会在启动时打印一个可直接使用的演示买家令牌:复制该令牌,并将其作为 token 参数传给 discover_products / get_forecast,即可查看示例产品系列、价格和预测。

对于实际部署,请将 MCP_CONFIG_DIR 指向包含你自己的 deployment.json、catalog.json、entitlements.json 和 pricing.json 的目录:

MCP_CONFIG_DIR=/etc/gam-seller/config npx -y gam-seller-mcp-node
从源代码构建
git clone https://github.com/juan-sibbo/gam-seller-mcp-node.git
cd gam-seller-mcp-node
npm install
npm run build
npm run start:http   # HTTP transport on 127.0.0.1:3900

运行完整的买家 Agent 演练(脚本化演示)——通过真正的进程内 MCP 传输驱动五个原生工具,最终展示受治理的拒绝(失败时关闭的身份验证、默认拒绝、失败时关闭的定价),并验证审计链:

npm run demo          # or: npx tsx demo/run-demo.ts
使用 Docker
docker compose up

节点会在 127.0.0.1:3900 上启动。众所周知的文档位于 /.well-known/seller-mcp-capabilities。密钥和审计数据的持久化卷已在 docker-compose.yml 中预先配置。

为发布商进行配置

四个 JSON 文件决定所有针对发布商的行为,无需更改代码。将它们放在 config/(从源代码运行时)或 MCP_CONFIG_DIR 指定的目录中(npx/容器化部署时):

deployment.json     # DSR contact, controller model, data retention window
catalog.json        # product families + per-buyer access grants
entitlements.json   # which buyers are entitled to which MCP surfaces
pricing.json        # firm list prices per family (fail-closed on expiry)
forecast.json       # OPTIONAL — seed availability buckets from real numbers (still synthetic-labeled)
gam.json            # OPTIONAL — live GAM forecast (network, service-account key path, family → targeting)

配置无效时始终会安全失败:文件格式错误会导致节点停止,而不会在访问策略悄然改变的情况下继续运行。配置缺失(没有 config/,也没有 MCP_CONFIG_DIR)时,会回退到随附的 config/examples/pilot-publisher/ 示例——进入演示模式,并在 stderr 上提示。因此节点不会处于损坏的安装状态,只会运行真实部署或明确标注的演示。

将试点部署切换为使用真实库存(无需连接线上 GAM)只需配置即可——请参阅 docs/PUBLISHER-DEPLOYMENT.md:

  • 预置预测数据:将发布商自己的可售情况从 GAM 报告导出一次,通过可选的 forecast.json 提供(模板:config/examples/pilot-publisher/forecast.sample.json)。 这样可使分桶结果更贴近实际,同时所有结果仍保持 synthetic: true——预加载数据不是实时读取,因此不会声称数据来自线上 GAM。
  • 连接线上 GAM:使用可选的 gam.json(模板: config/examples/pilot-publisher/gam.sample.json),并在 GAM 网络中添加具有可运行预测的读取权限的服务账号。节点会在启动时以及每 30 分钟,针对每个已配置的广告系列类型 × 周期,调用 ForecastService.getAvailabilityForecast,使用从不保存的预览订单项——不会在 GAM 中创建、修改或预留任何内容。系统会根据该快照,以 Low/Mid/High 分桶的形式回答买方请求,并设置 synthetic: false;原始可售情况不会离开节点。优先级:gam.json > forecast.json > synthetic。
  • 闭合交接流程:通过 MCP_INTENT_HANDOFF=file,将已提交的意向传递到发布商的销售流程中(本地 JSONL 文件,由操作员的转发程序监听)。交接过程不会进行出站调用——转发由操作员负责,并且系统会拒绝 URL 值。交接记录只是通知,不是 GAM 订单,也不会占用库存。
  • 加强生产环境安全性:设置 MCP_REQUIRE_OPERATOR_CONFIG=1(拒绝在演示配置下启动),设置 MCP_ANCHOR_SINK=tsa(将审计轨迹锚定到第三方)。

网络出口——已声明并受限,而非一律禁止。 处理买方请求时不会进行任何出站网络调用。线上 GAM 预测(gam.json)会在启动时以及每 30 分钟的刷新周期内,针对已配置的网络调用 Google OAuth 令牌端点和 Ad Manager SOAP API——这些调用绝不会发生在买方请求期间,因此买方无法给发布商的 GAM 造成负载。可选的审计锚定可在买方请求路径之外(启动时及定期锚定周期内)产生由操作员配置的网络出口:TSA 后端(MCP_ANCHOR_SINK=tsa)会将账本头部哈希提交给已配置的 RFC 3161 权威机构;S3 后端(MCP_ANCHOR_SINK=s3)会将锚定记录写入已配置的 Object Lock 存储桶;自定义接收器模块(MCP_ANCHOR_SINK=<module>)会运行操作员提供的代码。默认的本地锚定后端不会进行外部网络调用。目标地址仅来自操作员配置,买方输入无法选择目标。该网络出口范围由 tests/egress-surface.test.ts 固定:新增出站功能必须经过明确声明,否则 CI 将失败。

为什么不直接使用 GAM API?

方式数据暴露可写性可审计性对 AI Agent 的友好程度
原始 GAM API账户中的所有数据完整的增删改查仅日志记录较差(SOAP/REST,不支持 MCP)
OpenRTB 出价请求用户级数据、底价仅出价无较差
此服务来自实时预测的家庭组 + 可用性检查仅买家自己的软性承诺(不写入 GAM)哈希链式账本原生支持 MCP

当前状态

工作原型。完整请求流水线(身份验证 → 策略 → 速率限制 → 领域逻辑 → 审计)、 买方范围的承诺原语(create_intent / revoke_intent,带 TTL 到期机制)、 审计账本、GDPR 数据主体权利工具包、Docker 打包、HTTP 传输, 以及实时互操作探测(Python 买方 Agent 模拟)均已实现并通过测试。 持久化层已加固,可应对重启(仅追加、原子写入、持久化轮换状态、加载失败时安全关闭), 且头哈希锚点采用仅追加方式,并支持可选的外部 WORM 后端(RFC 3161 时间戳 / S3 Object Lock)——有关剩余事项(启用实时一次写入目标属于运维基础设施操作),请参阅已知限制。

GAM 连接 — 仅用于预测:实时适配器 (src/forecast/gam-source.ts)会在存在 gam.json 时从 GAM 的 ForecastService 读取可用量。目录系列及其 GAM 定向配置(广告单元、 尺寸、环境)仍需在配置中手动映射,价格仍为静态标价。 没有 gam.json 时,预测数据为合成数据(或使用种子数据),并会标明这一点(synthetic: true)。请参阅 未解决问题了解路线图。

已知限制 — 带日期的状态。特意保留已关闭的条目:一份状态随时间变化的限制清单,既能证明坦诚,也能证明进展。

限制定位状态关闭依据
归因信息(buyer_id / request_id)按条目存储,但位于链式篡改证据哈希之外audit/event.ts设计决策,并非缺陷 — 可追溯性与可擦除性之间的权衡(ADR-4)—
头哈希锚点每次写入都会重写整个文件(writeFileSync)——不是仅追加,也没有外部 WORMaudit/anchor.ts✅ 已关闭 2026-08-23 — 采用仅追加 JSONL 和可注入的 AnchorSink;通过 MCP_ANCHOR_SINK 可选择 tsa(RFC 3161)和 s3(S3 Object Lock)后端#92 #94 #95
外部 WORM 锚定要求运维人员指定一个实时一次写入目标(TSA URL 或已锁定的存储桶)——本节点提供后端,不提供目标audit/anchor-tsa.ts、audit/anchor-s3.ts未解决 — 部署边界(基础设施操作)—
client_request_id(重放防护)为可选项;省略它会绕过 SEC-GATE-3src/server.ts在 v0.9.0 中已关闭 — 默认要求所有经过身份验证的接口都提供该参数(失败时安全关闭);MCP_REQUIRE_IDEMPOTENCY_KEY=0 是运维人员明确选择的退出选项#82
传输过程中不使用 TLS(预期由反向代理终止 TLS)—未解决 — 部署边界—
revoke_intent 未经过速率限制阶段src/server.ts✅ 已关闭 2026-08-18 — 现在与所有经过身份验证的接口一样,受速率限制门控#80
npm 包未包含 GDPR DSR CLI(scripts/dsr.ts,……)package.json files✅ 已关闭 2026-08 — 作为 gam-seller-dsr bin 发布#78
账本加载时采用失败开放策略——文件损坏会重置为空链audit/ledger.ts✅ 已关闭 2026-08-07#65
启动时未在提供服务前验证链完整性src/server.ts✅ 已关闭 2026-08-07#65

架构

模块图和数据流图请参阅 docs/ARCHITECTURE.md。

主要模块:

模块作用
src/server.tsMCP 工具定义 + 请求管道
src/policy/默认拒绝引擎、授权存储、界面白名单/黑名单、逐工具披露模式
src/identity/RS256 密钥管理、令牌签发/验证、撤销黑名单
src/audit/哈希链账本、HMAC 假名化、仅追加的头哈希锚定,支持可选的 WORM 后端(anchor-tsa.ts、anchor-s3.ts)
src/pricing/固定目录价存储,考虑有效期(价格过期时默认拒绝)
src/forecast/分桶引擎 + 数据源:合成、种子数据、实时 GAM 快照
src/gam/服务账号 OAuth + 精简的 Ad Manager SOAP 客户端(只读)
src/dsr/GDPR 第 15/17/18/20 条数据主体权利工具包(也以 gam-seller-dsr 二进制文件形式发布)
src/catalog/产品系列存储、逐买家访问授权

安全模型

默认拒绝。 除非有明确的授权,否则每个请求都会被拒绝——代码中不存在“默认允许”路径。

两道关卡:谁可以调用工具,以及工具可以返回什么。 策略层依据界面标签工作:每个经过身份验证的工具在注册时都会声明一个允许的界面;具体价格、交易 ID、原始可用量、跨买家状态、真实库存预留(软锁定)以及任何广告服务器写入操作都永久列入黑名单。标签检查不会查看响应,因此单靠它无法阻止使用允许标签的工具返回不应返回的内容。披露关卡则可以做到这一点:每个经过身份验证的工具都必须声明严格的响应模式(src/policy/disclosure.ts);任何包含未声明字段的响应都会被扣留,并替换为通用的 INTERNAL_ERROR(计入 mcp_disclosure_rejected_total)。注册新工具时必须提供该模式,因此添加买家可见字段意味着要在一个便于审查的文件中进行修改。唯一允许的写入是买家自己的承诺(create_intent / revoke_intent),这需要对界面白名单作出明确修改,并且仍限定于该买家。

不透明错误。 请求被拒绝、身份验证失败和令牌被撤销时,都会返回相同的通用 AUTH_FAILED 代码。内部原因绝不会传达给买家。

审计优先。 每项允许/拒绝决定都会在发送响应前写入账本。买家的 buyer_id 值会先经过假名化(HMAC-SHA256),再写入链中。请注意,buyer_id 和 request_id 虽然存储在每条审计记录中,但不会纳入哈希链的规范输入(audit/event.ts:50);防篡改哈希链保证不涵盖这些字段。

通过设计保护隐私。 响应仅包含库存级数据(产品系列、粗粒度分桶)。任何响应路径中都不存在用户级属性。

完整说明请参阅 docs/DESIGN-PRINCIPLES.md。

监管立场

AEPD(西班牙数据保护机构)于 2026 年 2 月发布了关于 Agentic AI 系统的指南。其中与广告库存节点最相关的四项建议,均可直接对应到现有设计决策:

AEPD 建议本节点
通过设计和默认设置实现保护默认拒绝:除非明确授权允许访问,否则所有接口均拒绝访问
记录并存档 Agent 的操作仅追加的哈希链式审计账本;每次允许或拒绝操作都会在发送响应前记录
控制向第三方传输的内容,并确保其可追溯面向买方的披露允许列表(SEC-GATE-*):响应中永久禁止包含精确价格、交易 ID 和原始可用量。网络出口允许列表:唯一的出站连接是运营方选择启用的审计锚定,以及运营方选择启用的 GAM 预测刷新(见上文 网络出口),并由 CI 固定
按照目的和保留规则管理 Agent 记忆DSR 工具包(第 15/17/18/20 条);对审计账本强制执行可配置的保留期限

这一对齐情况以机器可读方式声明在经过签名的 well-known 文档 (/.well-known/seller-mcp-capabilities)中的 privacy_posture.regulatory_alignment_declared 字段里: ["GDPR", "AEPD-orientaciones-IA-agentica-2026"]。买方 Agent 或审计人员无需信任此 README,即可通过密码学方式验证这一声明。

本节点不作出法律认定——例如某项处理是否具有合法依据、同意是否有效,或特定处理是否被允许。这些判断应由控制者(广播商)作出。本节点提供机制;控制者应用判断标准。正是这一边界,使节点设计不受欧盟《数据法案》谈判结果的影响,保持稳定。

机器可读的信任锚点

/.well-known/seller-mcp-capabilities 端点会返回一个 RS256 签名的 JWT。买方 Agent 会在首次进行身份验证请求前读取并验证此文档。文档中的 privacy_posture 区块为机器可读,并通过密码学方式与节点密钥对绑定:

属性当前值含义
end_user_personal_data"none"所有响应路径均不包含终端用户个人数据
audience_segmentation"not_offered_v1"不提供任何受众定向接口
tc_string_consumption"none"节点不使用 TC 字符串(服务器到服务器,PATH A)
device_storage_access"none"不访问设备存储(不适用 ePrivacy)
jurisdiction["ES", "EU"]声明的运营司法管辖区
regulatory_alignment_declared["GDPR", "AEPD-orientaciones-IA-agentica-2026"]声明的合规对齐情况
dsr_contact来自 deployment.json数据主体请求的联系方式
controller_model来自 deployment.json发布商声明的控制者角色
audit_retention来自 deployment.json热存储/归档保留期限,以天/月为单位

尚未包含在 well-known 文档中的内容(仍为隐式的属性):

  • 目录和预测数据是合成数据还是实时数据(data_source)
  • 头哈希锚定使用本地文件还是云 Object Lock(anchor_store)
  • 节点处于演示模式,还是正在使用真实发布商配置提供服务(deployment_mode)

这些属性可帮助买方 Agent 以编程方式区分演示部署和生产部署,以及本地锚定节点和具有外部防篡改证据的节点。当前版本中尚未提供这些属性。

测试

npm test                              # full suite (vitest)
python3 sandbox/buyer-agent-probe.py  # external Python interop probe (no shared code with server)

测试套件包括:

  • 单元测试,覆盖每个模块(policy、pricing、identity、audit、catalog、forecast、DSR)
  • 集成测试,使用真实的内存 MCP 传输(tests/server.test.ts)
  • HTTP 传输测试,使用真实的临时端口 HTTP 服务(tests/http.test.ts)
  • 端到端会话测试,模拟完整的买方 Agent 会话(tests/buyer-agent-session.test.ts)
  • 外部 Python 探测程序,在不使用任何共享 Node.js 代码的情况下测试 HTTP 传输

CI 会通过 GitHub Actions 在每次推送时运行。

数据保护

原始 buyer_id 值绝不会进入审计账本,账本中只保存 HMAC 假名。 src/dsr/toolkit.ts 实现了买方审计数据的导出、限制处理和擦除(GDPR 第 15/17/18/20 条)。节点不会存储任何关于终端用户的信息;DSR 的范围恰好就是它记录的内容——B2B 买方组织的假名及其请求事件。

分发说明。 运维命令随 npm 包和容器镜像一同发布,提供 gam-seller-admin 可执行程序(issue-token、revoke-token、dsr …;gam-seller-dsr 仍可作为 DSR 命令的别名),因此无需检出代码。这些命令会写入节点状态,因此只能在节点停止时运行:运行中的节点会独占其状态目录(所有者租约),此时命令会以退出码 3 被拒绝。scripts/*.ts 条目是对同一代码的开发包装程序。

路线图

完整路线图请参阅未解决的问题。重点包括:

  • GAM 库存映射——从 GAM 广告单元/版位推导系列定向,而不是手动编写 gam.json
  • 买方 Agent SDK——用于 MCP 买方流程的 Python 和 TypeScript 客户端库
  • OpenRTB 3.0 分类体系——使 family_id 方案与 IAB 标准保持一致
  • 通用可观测性属性——公开 data_source / anchor_store / deployment_mode,让买方 Agent 能以编程方式区分演示环境和生产环境

(Prometheus /metrics 端点已发布——仅限环回地址,且需选择启用。)

贡献

请参阅 CONTRIBUTING.md。标记为 good first issue 的问题是不错的入门选择。

许可证

MIT——请参阅 LICENSE。

评论

支持 Markdown

第一次接触 MCP?

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

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