feat(openviking-controlplane): enterprise tier, billing config, user management, stateless MCP 2026-07-28, and 0.2.0 release - #425
Conversation
…st headers - config: add VERSION_CHOICES (developer|enterprise), extra_headers with VIKING_EXTRA_HEADERS env parsing; protect Authorization/Content-Type - client: merge extra headers onto requests; validate version on create - cli: repeatable --header/-H; --version as an enumerated choice; drop the stale 'currently only developer' text - server: document the two tiers + enterprise billing in the tool docstring - docs: README / README_zh / SKILL cover the new header + enterprise usage
Add the remaining 5 data-plane control-plane Actions so MCP/CLI cover all 11 documented actions: - UpdateOpenVikingCollection (update_collection / ov-cp update) - ListOpenVikingCollectionUser (list_collection_users / ov-cp user list) - RegisterOpenVikingUser (register_collection_user / ov-cp user register) - UpdateOpenVikingUser (update_collection_user / ov-cp user update) - DeleteOpenVikingUser (delete_collection_user / ov-cp user delete) User actions require the AgentPlan key to be associated with the target library; user list returns a masked ApiKey (plaintext via api-key). UpdateOpenVikingCollection re-validates model credentials, so VLM/Embedding blocks are always sent (built like create, defaulting to the AgentPlan key). Contract verified E2E on the lujiakun swim-lane against an enterprise-tier library: user register/list/update/delete round-trip and collection update.
Expose PaymentConfig as first-class billing arguments on create and update, in both the CLI (ov-cp) and the MCP tools: - One flat user-facing enum --pay-type: agentplan_personal | agentplan_enterprise | volc_pay (split into wire PayType + AgentPlanConfig.BusinessScenarios by build_payment_config). The personal/enterprise choice is always explicit, never inferred from the key or the seat. - --seat-id: required with agentplan_enterprise, forbidden otherwise. Entered manually (no lookup API); the server does not verify the seat exists. - Omitting --pay-type on create leaves PaymentConfig unset and the server defaults to volc_pay (real-money Volcano pay-as-you-go); the CLI prints a warning to stderr. - update with pay flags is the billing-switch path (volc_pay <-> AgentPlan deduction, seat re-bind); omitting them leaves billing untouched. empty_pay is deliberately not offered. - All valid/invalid flag combinations fail fast locally before any request; verified live on stg (create with PaymentConfig, personal with empty SeatId, three-way billing switches).
…rsonal When create is called with no pay_type/seat_id, bind agentplan_personal client-side instead of falling through to the server default volc_pay. The two failure modes are asymmetric: a wrong personal binding is immediately visible and recoverable via update, while the volc_pay default silently bills real money. volc_pay now requires an explicit choice; the CLI note warns enterprise-seat-key accounts (no personal plan) that the default binding would fail deduction and disable the library. Verified live: stg bare create lands agent_plan_personal; prod (older backend without PaymentConfig support) silently ignores the injected field, so bare create keeps working there.
Replace the informal "real money" phrasing in help text, docstrings and docs with neutral wording: volc_pay charges are billed directly to the Volcano account (vs deducted from AgentPlan AFP). No behavior change.
Reject placeholder API keys and invalid extra headers before requests reaches its low-level Latin-1 encoder. Co-authored-by: TRAE CLI <noreply@bytedance.com>
Use Typer-native string enums so invalid tier and billing choices stay in the normal usage-error path instead of leaking tracebacks. Co-authored-by: TRAE CLI <noreply@bytedance.com>
Preserve EstimatedCosts while adding structured CNY, payment-source, and AgentPlan AFP-per-hour details to usage responses. Co-authored-by: TRAE CLI <noreply@bytedance.com>
Render structured Rich tables and panels on TTYs while preserving standard JSON for pipes, redirects, and explicit machine-output modes. Co-authored-by: TRAE CLI <noreply@bytedance.com>
Support per-user API key access and user-list filters while matching the backend register and API-key rotation request fields. Co-authored-by: TRAE CLI <noreply@bytedance.com>
Send VLM and Embedding blocks only when explicitly supplied so description and billing updates cannot overwrite existing multi-credential configuration. Co-authored-by: TRAE CLI <noreply@bytedance.com>
Drop the update-only OpenViking image version parameter because the current backend silently ignores it; creation-time version selection remains supported. Co-authored-by: TRAE CLI <noreply@bytedance.com>
Co-authored-by: TRAE CLI <traecli@bytedance.com>
A control plane that rebuilds VLM and Embedding on every update rejects a metadata-only request with "apikey is empty", because it rebuilds from the legacy flat ApiKey, which is blank once a collection stores an N-credential list. Read the collection's own credentials back and replay them once, then retry: credentials with an ApiKeyID are resolved server-side, and the AgentPlan credential reuses the control-plane key the client authenticates with, exactly as create_collection builds it. Anything else is refused instead of guessed at, and the retry is reported in the response Note.
`update --model-api-key` (MCP: model_api_key) writes the supplied AgentPlan key as the model credential of both VLM and Embedding, which always share one. The collection's remaining credentials are read back and replayed unchanged, so overwriting the AgentPlan key no longer costs the volcengine failover entry. A collection with no AgentPlan model credential is refused rather than reshaped, and the flag cannot be combined with an explicit vlm / embedding block.
mcp 2.0.0 drops FastMCP for MCPServer, so an unbounded requirement makes `uvx --from git+... mcp-server-openviking-controlplane` fail at import. Cap the requirement at <2 and import FastMCP from its canonical mcp.server.fastmcp path, which is stable across the 1.x line.
There was a problem hiding this comment.
need to support stateless protocol.
There was a problem hiding this comment.
Done — in two commits, since it turned out to be two separate things.
3529103 adds streamable-http to --transport and enables stateless HTTP by default (STATLESS_HTTP, matching the env var name the other servers here use, with STATELESS_HTTP accepted as an alias). It also sets host explicitly: FastMCP defaults to 127.0.0.1, which auto-enables DNS-rebinding protection that only allows localhost Host headers, so a server behind a gateway would reject every request.
That alone only makes the HTTP session layer stateless — the wire protocol stays a handshake-era revision. 970d1f1 goes the rest of the way to protocol revision 2026-07-28, which mcp-types documents as "protocol revisions that use the stateless per-request envelope" (no initialize; server/discover instead, protocol version carried per request in params._meta). That revision exists only in the mcp 2.x line, so this also lifts the mcp[cli]<2 cap and moves FastMCP to mcp.server.mcpserver.MCPServer.
MCPServer has no get_context(), so the request is reached through a Context parameter injected per tool. Context-typed parameters are excluded from the generated JSON schema, so all 11 tools keep their exact input and output schemas.
Verified with the SDK's own client: mode="auto" and mode="2026-07-28" both negotiate 2026-07-28, mode="legacy" still negotiates 2025-11-25, 2024-11-05 handshakes still work, and stdio (including the documented uvx --from ... config) is unaffected.
There was a problem hiding this comment.
To increase version for major.minor.patch
There was a problem hiding this comment.
Done in 107e577: 0.1.0 → 0.2.0.
Minor rather than patch because the unreleased delta is the enterprise library tier, billing configuration, collection user management, custom request headers, the credential-replay fixes, and now a new transport — all backward-compatible additions.
Worth flagging for the publish workflow added in #415: mcp-server-openviking-controlplane 0.1.0 is already on PyPI, hand-uploaded on 2026-07-10 before that workflow existed. Could you confirm the account behind secrets.PYPI_API_TOKEN is an owner/maintainer of that PyPI project? If it isn't, twine upload would only fail after merge — the PR-time job runs build + twine check and wouldn't catch it.
…sport A horizontally scaled gateway cannot pin an MCP session to one backend instance, so the server has to accept requests that carry no Mcp-Session-Id. Enable stateless HTTP and offer streamable-http alongside stdio and sse. host now has to be set explicitly: FastMCP defaults to 127.0.0.1, which auto-enables DNS-rebinding protection allowing only localhost Host headers, so every gateway-forwarded request would be rejected. MCP_SERVER_HOST / MCP_SERVER_PORT match the names the other servers here already use; PORT stays honoured. STATLESS_HTTP is this repo's (misspelled) standard name, with STATELESS_HTTP accepted as an alias so a correct spelling is not silently ignored. stateless_http only feeds StreamableHTTPSessionManager, so stdio and sse are unaffected.
…PI install The publish workflow added in volcengine#415 keys off project.version, so a bump is what actually releases the package. 0.1.0 is already on PyPI and PyPI releases are immutable; that published artifact is stale (no output.py, no rich dependency, no mcp[cli]<2 upper pin). 0.2.0 rather than 0.1.1: the unreleased delta is the enterprise library tier, billing configuration, collection user management, custom request headers, the credential-replay fixes and now a new transport -- all backward compatible additions. Also document the pinned uvx invocation and the streamable HTTP run mode in both READMEs.
Under stateless HTTP the module-level client singleton would transact every request with whatever credential the process started with. That is not merely untidy here: get_collection_api_key returns a plaintext data-plane key, and create/update_collection store the configured key as the collection's model credential, so a shared deployment would hand one caller's credential to another. Resolve the key from the request instead, falling back to the environment: X-AgentPlan-Api-Key, then Authorization, then AGENTPLAN_API_KEY. Only the Bearer scheme is read from Authorization -- a gateway terminating its own auth there must not have that value used as an Ark key. get_client keeps its name and signature, so the tools and their tests are unchanged. config.get_config() is removed rather than left in place: a cached environment config would silently defeat per-request resolution, and client.py was its only importer. stdio is unaffected -- there is no request context, and both lookups fall through to the environment.
…p SDK 2.x) `stateless_http` on the 1.x FastMCP only makes the HTTP session layer stateless; the wire protocol stays a handshake-era revision (2025-11-25 at best). The 2026-07-28 revision is the one mcp-types documents as "protocol revisions that use the stateless per-request envelope" -- no `initialize`, `server/discover` instead, protocol version carried in `params._meta` -- and it exists only in the mcp 2.x line. Move to it: `mcp.server.fastmcp` is a raising shim in 2.x, so FastMCP becomes `mcp.server.mcpserver.MCPServer`, and host / port / streamable_http_path / stateless_http move from constructor settings to run() arguments under the same names. Reverts the `mcp[cli]<2` cap. MCPServer has no get_context(), so the request is reached through a Context parameter injected per tool. Context-typed parameters are excluded from the generated JSON schema, so all 11 tools keep their exact input and output schemas; Context.headers replaces the request_context walk. Verified with the SDK's own client: mode="2026-07-28" and mode="auto" both negotiate 2026-07-28, mode="legacy" still negotiates 2025-11-25, and stdio is unaffected.
概述
在已合入的 OpenViking 控制面 MCP Server + CLI(#408)基础上,补齐剩余的控制面 Action、企业版库能力、计费配置与终端展示,并修复更新接口覆盖模型凭证的问题。
MCP tool 与
ov-cp命令共用同一套核心(client.py),因此下述能力在两个前端上完全一致。覆盖范围从 6 个 Action 扩展到 11 个(库生命周期 + 用量 + 数据面 Key + 用户管理)。本 PR 已合入
main(e96002a,无冲突),并按 review 意见补上了 stateless 协议支持与版本递增,见下方第 8–11 节。3529103(stateless streamable HTTP)、970d1f1(MCP 协议 2026-07-28)107e577(0.1.0 → 0.2.0)主要变更
1. 补齐用户管理与库更新(
b382ab9)新增 5 个 Action:
UpdateOpenVikingCollection、ListOpenVikingCollectionUser、RegisterOpenVikingUser、UpdateOpenVikingUser、DeleteOpenVikingUser。ov-cp update <rid>/ov-cp user list|register|update|deleteuser *用于管理企业版库的多用户,要求 AgentPlan key 与目标库已关联user list返回掩码 ApiKey;明文数据面 key 通过api-key <rid> --user-id <uid>获取60d3f8b对齐了后端的注册字段与 Key 轮转请求契约,并支持按用户取 Key、用户列表过滤2. 企业版库 + 自定义请求头(
6bad682)--version developer|enterprise,非法值在本地即被拒绝-H/--header与VIKING_EXTRA_HEADERS环境变量(逗号分隔的Key: Value),用于联调路由;Authorization/Content-Type为受保护头,不可覆盖3. 计费配置(
5a22816、c97bb25、1c63876)把
PaymentConfig提升为一等参数,create与update都支持:--pay-type:agentplan_personal|agentplan_enterprise|volc_pay(内部再拆成 wire 上的PayType+AgentPlanConfig.BusinessScenarios)--seat-id:仅agentplan_enterprise需要且必填,其它场景传入即报错。席位需从方舟控制台手动复制,服务端不校验席位是否存在--version与计费方式正交:前者决定费率,后者决定由谁付费create不传--pay-type时,客户端显式绑定agentplan_personal,而不是落到服务端默认的volc_pay。两种错误的代价不对称:个人版绑错可以立即发现并用update改回,而默认落到volc_pay会静默产生火山账号账单update带计费参数即为切换计费方式 / 换绑席位的路径;不传则完全不触碰计费。empty_pay有意不开放4. 用量计费明细(
537697d)usage保留后端原有的EstimatedCosts字段,同时新增EstimatedBilling:明确标注为每小时 CNY 估值;AgentPlan 付费的库额外返回 AFP 抵扣量与支付场景。5. 终端自适应输出(
dd602bc)stdout 连接终端时渲染 Rich 表格 / 分区详情卡片 / 明文 Key 的敏感信息警告;管道、重定向与显式的机器输出模式(
--json、--output json-compact)仍保持标准 JSON,因此ov-cp list | jq ...与命令替换不受影响。6. 更新接口修复(
1bba701、94f50ed、1829be7、e8434f4、58c0fc3)这是本 PR 中最值得关注的一组修复。
create不再开放模型来源、模型参数、模型鉴权与镜像版本,固定走 AgentPlan 模型路径与已配置的 AgentPlan key(底层client.create_collection仍保留这些参数作为低层接口)ApiKey字段重建的——而库一旦存了 N 条凭证列表,该字段就是空的,于是纯元数据更新会被拒为apikey is empty。现在客户端会读回该库自身的凭证并原样重放一次后重试:带ApiKeyID的凭证由服务端解析,AgentPlan 凭证复用客户端鉴权所用的那把 key(与create的构造方式一致)。无法重建的凭证会被拒绝而不是猜测,重试行为在响应Note中说明update --model-api-key <ark-key>(MCP 侧model_api_key)把传入的 AgentPlan key 写为 VLM 与 Embedding 的模型凭证(两者始终共用一把)。库上其余凭证按原样重放,因此覆盖 AgentPlan key 不会牺牲其它 failover 条目。没有 AgentPlan 模型凭证的库会被拒绝而非被重塑,该参数也不能与显式的 vlm / embedding 块同时使用7. 健壮性(
a0a9b2d、672f49e)requests的底层 Latin-1 编码器之前就被拒绝8. Stateless streamable HTTP 传输(
3529103)--transport增加streamable-http,默认开启 stateless:每个请求自带完整上下文,不依赖上一次返回的
Mcp-Session-Id,进程可在网关后水平扩缩。host显式设为0.0.0.0。不设的话 SDK 默认127.0.0.1并自动挂上只放行 localhostHost头的 DNS-rebinding 保护,网关转发来的请求会全部被拒MCP_SERVER_HOST/MCP_SERVER_PORT(PORT仍兼容)/STREAMABLE_HTTP_PATH/STATLESS_HTTP(同时接受正确拼写的STATELESS_HTTP作为别名)--transport sse,已发布版本的 README 记载过它9. 升级到 MCP 协议 2026-07-28(
970d1f1)1.x 上的
stateless_http只让 HTTP 会话层无状态,wire protocol 仍停在握手时代(上限
2025-11-25)。2026-07-28才是mcp-types中标注为"protocol revisions that use the stateless per-request envelope" 的修订版——不走
initialize握手,改用server/discover,协议版本随params._meta逐请求携带——且只存在于 mcp SDK 2.x。
mcp.server.fastmcp在 2.x 是主动抛错的 shim,FastMCP→mcp.server.mcpserver.MCPServerhost/port/streamable_http_path/stateless_http由构造参数变为run()参数,名称不变MCPServer无get_context(),请求上下文改由每个 tool 注入的Context参数获取。Context类型的参数不进 JSON schema,11 个 tool 的 input / output schema 逐字节不变mcp[cli]<2相应放开为>=2.1.1,<3向后兼容已实测:SDK 官方客户端
mode="auto"与mode="2026-07-28"均协商到2026-07-28,mode="legacy"协商到2025-11-25;stdio 下2024-11-05/2025-06-18的老式握手照常工作;uvx --from ... mcp-server-openviking-controlplane的 stdio 用法与--transport sse均无变化。10. 按请求解析 AgentPlan 凭证(
88ebc88)开启共享 HTTP 端点后,进程级的 client 单例会让所有请求都使用启动时的那把凭证。这个
Server 上后果是实际的:
get_collection_api_key返回明文数据面 key,create/update会把配置的 key 写为库的模型凭证。
凭证改为逐请求解析,优先级
X-AgentPlan-Api-Key>Authorization: Bearer>AGENTPLAN_API_KEY环境变量。Authorization上只接受 Bearer scheme——网关若在该头终结自己的鉴权,其凭证不得被当作 Ark key 使用。stdio 无请求上下文,行为不变。
11. 版本递增与 PyPI 发布(
107e577)0.1.0→0.2.0。PyPI 上的0.1.0是 workflow 建立之前手工上传的,已经陈旧(wheel 内无
output.py,metadata 无rich、无 SDK 上限)。README(中英)补充了指定版本的
uvx --from "mcp-server-openviking-controlplane>=0.2.0"安装块。文档
README.md/README_zh.md/skills/openviking-controlplane/SKILL.md同步更新,覆盖新增命令、计费语义、输出模式与更新接口的行为。测试
新增
test_collection_update.py、test_collection_create.py、test_user_contract.py、test_usage_billing.py、test_output.py、test_headers.py、test_cli_errors.py、test_server_transport.py。计费与用户管理的请求契约此前已在测试环境做过端到端验证;本 PR 的单测全部以 mock 校验请求体,不会触达线上写接口。