Skip to content

feat(openviking-controlplane): enterprise tier, billing config, user management, stateless MCP 2026-07-28, and 0.2.0 release - #425

Merged
cuericlee merged 21 commits into
volcengine:mainfrom
t0saki:feat/openviking-controlplane-enterprise-tier
Aug 28, 2026
Merged

feat(openviking-controlplane): enterprise tier, billing config, user management, stateless MCP 2026-07-28, and 0.2.0 release#425
cuericlee merged 21 commits into
volcengine:mainfrom
t0saki:feat/openviking-controlplane-enterprise-tier

Conversation

@t0saki

@t0saki t0saki commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

概述

在已合入的 OpenViking 控制面 MCP Server + CLI(#408)基础上,补齐剩余的控制面 Action、企业版库能力、计费配置与终端展示,并修复更新接口覆盖模型凭证的问题。

MCP tool 与 ov-cp 命令共用同一套核心(client.py),因此下述能力在两个前端上完全一致。覆盖范围从 6 个 Action 扩展到 11 个(库生命周期 + 用量 + 数据面 Key + 用户管理)。

本 PR 已合入 maine96002a,无冲突),并按 review 意见补上了 stateless 协议支持与版本递增,见下方第 8–11 节。

review 意见 对应提交
need to support stateless protocol 3529103(stateless streamable HTTP)、970d1f1(MCP 协议 2026-07-28)
To increase version for major.minor.patch 107e577(0.1.0 → 0.2.0)

主要变更

1. 补齐用户管理与库更新(b382ab9

新增 5 个 Action:UpdateOpenVikingCollectionListOpenVikingCollectionUserRegisterOpenVikingUserUpdateOpenVikingUserDeleteOpenVikingUser

  • ov-cp update <rid> / ov-cp user list|register|update|delete
  • user * 用于管理企业版库的多用户,要求 AgentPlan key 与目标库已关联
  • user list 返回掩码 ApiKey;明文数据面 key 通过 api-key <rid> --user-id <uid> 获取
  • 后续 60d3f8b 对齐了后端的注册字段与 Key 轮转请求契约,并支持按用户取 Key、用户列表过滤

2. 企业版库 + 自定义请求头(6bad682

  • --version developer|enterprise,非法值在本地即被拒绝
  • 可重复的 -H/--headerVIKING_EXTRA_HEADERS 环境变量(逗号分隔的 Key: Value),用于联调路由;Authorization / Content-Type 为受保护头,不可覆盖

3. 计费配置(5a22816c97bb251c63876

PaymentConfig 提升为一等参数,createupdate 都支持:

  • 面向用户的单一扁平枚举 --pay-typeagentplan_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. 更新接口修复(1bba70194f50ed1829be7e8434f458c0fc3

这是本 PR 中最值得关注的一组修复。

  • 不再误伤模型凭证:只在调用方显式提供时才发送 VLM / Embedding 块,避免"改个描述"把库上已有的多凭证配置覆盖成单凭证
  • 移除不生效的参数:更新时的 OpenViking 镜像版本参数当前会被后端静默忽略,予以删除;建库时选择版本不受影响
  • 收窄建库入口create 不再开放模型来源、模型参数、模型鉴权与镜像版本,固定走 AgentPlan 模型路径与已配置的 AgentPlan key(底层 client.create_collection 仍保留这些参数作为低层接口)
  • 凭证重放兜底:部分控制面在每次更新时都会重建 VLM 与 Embedding,且是从旧的扁平 ApiKey 字段重建的——而库一旦存了 N 条凭证列表,该字段就是空的,于是纯元数据更新会被拒为 apikey is empty。现在客户端会读回该库自身的凭证并原样重放一次后重试:带 ApiKeyID 的凭证由服务端解析,AgentPlan 凭证复用客户端鉴权所用的那把 key(与 create 的构造方式一致)。无法重建的凭证会被拒绝而不是猜测,重试行为在响应 Note 中说明
  • 允许手动覆盖模型 keyupdate --model-api-key <ark-key>(MCP 侧 model_api_key)把传入的 AgentPlan key 写为 VLM 与 Embedding 的模型凭证(两者始终共用一把)。库上其余凭证按原样重放,因此覆盖 AgentPlan key 不会牺牲其它 failover 条目。没有 AgentPlan 模型凭证的库会被拒绝而非被重塑,该参数也不能与显式的 vlm / embedding 块同时使用

7. 健壮性(a0a9b2d672f49e

  • 占位符 API Key 与非法额外请求头在进入 requests 的底层 Latin-1 编码器之前就被拒绝
  • 用 Typer 原生的字符串枚举承载版本与计费选项,非法取值走正常的 usage error 路径,不再抛出 traceback

8. Stateless streamable HTTP 传输(3529103

--transport 增加 streamable-http,默认开启 stateless:每个请求自带完整上下文,不依赖
上一次返回的 Mcp-Session-Id,进程可在网关后水平扩缩。

  • host 显式设为 0.0.0.0。不设的话 SDK 默认 127.0.0.1 并自动挂上只放行 localhost
    Host 头的 DNS-rebinding 保护,网关转发来的请求会全部被拒
  • 环境变量沿用仓库既有命名:MCP_SERVER_HOST / MCP_SERVER_PORTPORT 仍兼容)/
    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,FastMCPmcp.server.mcpserver.MCPServer
  • host / port / streamable_http_path / stateless_http 由构造参数变为 run() 参数,名称不变
  • MCPServerget_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.00.2.0。PyPI 上的 0.1.0 是 workflow 建立之前手工上传的,已经陈旧
(wheel 内无 output.py,metadata 无 rich、无 SDK 上限)。README(中英)补充了
指定版本的 uvx --from "mcp-server-openviking-controlplane>=0.2.0" 安装块。

请确认 secrets.PYPI_API_TOKEN 对应的账号是 PyPI 上 mcp-server-openviking-controlplane
项目的 owner/maintainer。该项目在 workflow 建立前已由个人手工发布过 0.1.0;若 token
无权限,twine upload 会在合入后才失败(PR 期只跑 build + twine check,抓不到)。

文档

README.md / README_zh.md / skills/openviking-controlplane/SKILL.md 同步更新,覆盖新增命令、计费语义、输出模式与更新接口的行为。

测试

新增 test_collection_update.pytest_collection_create.pytest_user_contract.pytest_usage_billing.pytest_output.pytest_headers.pytest_cli_errors.pytest_server_transport.py

65 passed

计费与用户管理的请求契约此前已在测试环境做过端到端验证;本 PR 的单测全部以 mock 校验请求体,不会触达线上写接口。

t0saki and others added 15 commits July 24, 2026 14:58
…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.
Copilot AI lite review requested due to automatic review settings August 21, 2026 09:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to support stateless protocol.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To increase version for major.minor.patch

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 107e577: 0.1.00.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.

t0saki added 5 commits August 26, 2026 19:19
…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.
@t0saki t0saki changed the title feat(openviking-controlplane): enterprise tier, billing config, user management, and update fixes feat(openviking-controlplane): enterprise tier, billing config, user management, stateless MCP 2026-07-28, and 0.2.0 release Aug 26, 2026

@cuericlee cuericlee left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

@cuericlee
cuericlee merged commit 00ad809 into volcengine:main Aug 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants