远程 Streamable HTTP MCP 的 OAuth 2.1 接入与批量任务编排实践
把一个远程 MCP 服务接入 AI 客户端,真正困难的通常不是“能否调用某个工具”,而是以下几个工程问题:
- 客户端如何在没有预置 Token 的情况下发现授权入口;
- 公共客户端如何完成授权码交换,同时避免泄露客户端密钥;
- 用户用自然语言提出一批任务时,系统如何避免误创建和重复提交;
- 一条任务失败后,后续任务应该继续、重试还是停止;
- 怎样区分“协议连接成功”和“业务结果符合预期”。
本文以飞影的远程 MCP 服务作为测试对象,记录一次 Streamable HTTP MCP 接入和小批量数字人口播任务编排实践。重点是协议链路、状态机和安全边界,不讨论产品购买或营销转化。
一、场景与测试边界
测试场景是:同一套人物、声音和视频模板,对应三段不同的口播文案。AI 客户端负责读取当前账号可用资源、整理批次计划,并在用户确认后按顺序调用创建工具。
这类任务有两个特点:
- 查询资源是只读操作,可以在授权后直接执行;
- 创建视频会消耗计算资源和账户积分,属于有副作用操作,不能因为模型“理解了需求”就立即执行。
因此,测试被拆成两条独立链路:
连接链路:发现 MCP → 浏览器 OAuth → 初始化 → 只读查询
业务链路:读取输入 → 生成计划 → 人工确认 → 顺序提交 → 查询结果
只有连接链路通过,并不代表业务链路可以跳过确认;反过来,任务状态显示成功,也不代表人物、声音、口型和字幕一定符合业务要求。
二、从 401 开始的 OAuth 发现
远程 MCP 客户端首次访问以下 Streamable HTTP 端点时,不携带静态认证 Header:
https://hifly.cc/mcp
服务端返回 HTTP 401,并通过 WWW-Authenticate 告知受保护资源元数据的位置:
HTTP/2 401
WWW-Authenticate: Bearer resource_metadata="https://hifly.cc/.well-known/oauth-protected-resource"
这里的 401 不是“服务不可用”,而是 OAuth 发现流程的起点。客户端不应该在此时猜测 Token 格式、尝试其他 Header,或者要求用户把凭证粘贴到对话中。
受保护资源元数据返回的关键字段如下:
{
"resource": "https://hifly.cc/mcp",
"authorization_servers": ["https://hifly.cc"],
"scopes_supported": ["hifly:mcp"],
"bearer_methods_supported": ["header"]
}
客户端由此得到三项信息:资源标识、授权服务器地址和需要申请的 scope。随后再读取授权服务器元数据,得到授权、换取 Token、动态客户端注册和撤销端点。
本次测试中与公共客户端相关的能力包括:
{
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"]
}
token_endpoint_auth_methods_supported 包含 none,说明客户端按公共客户端处理,不依赖内置 client secret;S256 表示授权码流程使用 PKCE,降低授权码被截获后直接兑换 Token 的风险。

三、客户端侧应该发生什么
支持通用远程 MCP OAuth 的客户端接入时,理想行为是:
- 保存 MCP URL,不写入静态
AuthorizationHeader; - 收到 401 后读取
resource_metadata; - 根据元数据发现授权服务器;
- 必要时完成动态客户端注册;
- 生成 PKCE verifier 和 S256 challenge;
- 在浏览器中打开授权页,由用户登录并确认授权;
- 使用授权码和 verifier 换取访问凭证;
- 重新初始化 MCP 会话并请求工具列表。
浏览器授权过程中,API Token、access token、refresh token 和授权码都不应该出现在 AI 对话里。凭证应由客户端安全保存、刷新和撤销。
本次测试在 WorkBuddy 新账号中完成了浏览器授权和只读账户能力查询。这个结果只能证明“该客户端与该服务的基础授权链路可用”,不能直接推导所有 MCP 客户端都兼容;其他客户端仍需分别验证远程传输、OAuth discovery、PKCE 和回调 URI。
四、为什么批量任务需要显式状态机
自然语言很适合表达“我要做三条口播”,但有副作用的工具调用不能只依赖语言上下文。更可靠的做法是把对话约束映射为显式状态:
READ_ONLY
↓ 读取资源与输入
PLAN_PENDING
↓ 用户确认完整计划
APPROVED
↓ 按顺序逐条提交
SUBMITTING
├─ 全部成功 → COMPLETED
└─ 任意失败 → STOPPED

1. READ_ONLY:只读查询
在这个阶段可以执行账户能力、人物、声音和模板列表等查询,但不能创建任务。查询结果必须来自当前已授权账号,不能从互联网、历史聊天或其他用户数据中猜测资源 ID。
2. PLAN_PENDING:形成完整计划
客户端读取三条文案后,应先输出结构化计划:
| 字段 | 示例 |
|---|---|
| 作品数量 | 3 |
| 人物 | 当前账号中选定的一名人物 |
| 声音 | 当前账号中选定的一种声音 |
| 模板 | 当前账号中选定的一个竖屏模板 |
| 文案 | 按 001、002、003 原顺序列出全文 |
| 提交策略 | 顺序提交;失败即停;不自动重试 |
计划的作用不是美化输出,而是让用户在产生费用之前检查每一个会影响结果的参数。
3. APPROVED:确认必须指向具体计划
“开始吧”只有在上一步计划没有发生变化时才有意义。如果人物、声音、模板、数量或文案被修改,应回到 PLAN_PENDING,重新展示差异并确认。
确认范围还应明确:本次只允许创建 001—003,共三条;不能因为某条失败而擅自生成替代任务。
4. SUBMITTING:顺序调用而非并发轰炸
对于首次小批量验证,顺序提交更容易定位问题:
submit(001)
→ 成功后 submit(002)
→ 成功后 submit(003)
如果 002 失败,则状态进入 STOPPED,003 不再提交。系统保留 001 的结果和 002 的原始错误,等待人工判断。
五、重复提交与自动重试风险
创建类工具与查询类工具最大的差别是:网络超时不等于服务端没有收到请求。
假设客户端提交 001 后连接超时,立即重试可能产生两个相同任务,并造成重复扣费。因此需要遵守以下原则:
- 服务端若支持幂等键,应由客户端为每个批次项生成稳定的幂等键;
- 服务端若没有声明幂等能力,客户端必须把创建调用视为非幂等操作;
- 超时后先查询已有任务或让用户决定,不自动重放创建请求;
- request ID 用于日志诊断,除非服务端明确约定,否则不能当作幂等键;
- 批次项应保存“未提交、已提交、成功、失败、未知”五种状态,不能只用一个布尔值。
这也是为什么“失败即停”适合首次试跑:它牺牲一点吞吐量,换取更小的费用风险和更清晰的故障边界。
六、最小验收用例
协议和业务逻辑需要分开验收。下面这组用例可以覆盖主要风险:
| 用例 | 操作 | 预期结果 |
|---|---|---|
| 未授权初始化 | 不带凭证请求 MCP | 返回 401 和有效 resource_metadata |
| 浏览器授权 | 完成授权码 + PKCE 流程 | 客户端重新连接,不在对话中暴露凭证 |
| 只读能力查询 | 查询套餐或可用资源 | 返回当前账号数据,不创建任务 |
| 未确认计划 | 提供三条文案但不确认 | 只输出计划,创建次数为 0 |
| 确认后提交 | 确认固定人物、声音、模板和三条文案 | 按 001—003 顺序各提交一次 |
| 中途失败 | 第二条返回错误 | 停止第三条,保留已成功任务和原始错误 |
| 授权失效 | 撤销或使凭证失效 | 回到浏览器 OAuth,不索取静态 Token |
测试时还应分别记录 OAuth 成功、MCP 初始化、创建提交、创建成功/失败和结果打开,避免把“已授权”误计为“已完成作品”。
七、业务结果的人工验收仍不可省略
任务状态 success 只表示服务端完成了渲染。数字人口播场景至少还需要人工检查:
- 人物与声音是否匹配;
- 文案有没有错字、断句和多音字问题;
- 口型与声音是否同步;
- 字幕是否完整且位于画面安全区域;
- 横竖屏比例、时长和模板是否符合投放场景。
因此更稳妥的扩量方式是:先验证一条,再用同一套配置试跑三条,最后才扩大批次。这个顺序不是协议要求,而是减少错误扩散和无效消耗的生产策略。
八、结论
远程 MCP 的价值不只是让 AI “能调用 API”。一条可用于生产的链路,至少要同时满足:
- 用标准的 401 challenge 和元数据完成授权发现;
- 公共客户端通过授权码和 PKCE 完成浏览器 OAuth;
- 查询与创建严格分离;
- 创建前展示完整计划并获得针对该计划的确认;
- 将创建调用视为有副作用、可能非幂等的操作;
- 发生未知状态或失败时停止,而不是自动重试;
- 协议成功、任务成功和内容验收分别记录。
当这七层边界都成立时,自然语言才真正成为批量生产入口,而不是把一个按钮换成一次不可控的对话调用。