Skip to content

feat(spec)!: 声明 routes.mcp,并把 discovery 一致性闸门下沉到 routes 一层 (#5679) - #5743

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-5679-routes-mcp-declare
Aug 6, 2026
Merged

feat(spec)!: 声明 routes.mcp,并把 discovery 一致性闸门下沉到 routes 一层 (#5679)#5743
os-zhuang merged 3 commits into
mainfrom
claude/issue-5679-routes-mcp-declare

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5679

结论先说

前提基本成立,但有一处需要更正:routes.mcp 确实被发出、被 objectui 真实消费、却从未在 ApiRoutesSchema 里声明。但 issue 正文那句「dispatcher 侧的 getDiscoveryInfo() 不发 routes.mcp,只有 REST 这条路径发」是错的 —— 两个生产者都发。详见下面「premise 更正」一节。

这是 #4828 的同族缺陷低一层,而处置方向与 endpoints 相反:endpoints 因零消费者被删,mcp 有两个真实消费者(而且它是 objectui 里唯一被读的 routes.* 键),所以是补声明,不是删。

为什么是缺陷,不是洁癖

ApiRoutesSchema 是普通 z.object,默认 strip 未知键。所以任何按 spec 严格解析 /discovery 的消费者都会把 routes.mcp 悄悄丢掉 —— Integrations 的 MCP 连接卡直接空掉,且无任何报错。今天没炸,只是因为 objectui 那两处读的是原始 JSON,没走 schema。

同一次测试运行里能直接看到这个盲区:DiscoverySchema.safeParse(body)mcp 未声明时依然是绿的(被 strip 掉了),只有键集检查能看见它。

测得的形状(不是猜的)

先测后声明。两个生产者的实测结果:

场景 routes.mcp
REST 未分环境挂载 "/api/v1/mcp"
REST 分环境挂载 (env_alpha) "/api/v1/mcp" —— 仍是无环境段的路径,而同一响应里 routes.data/api/v1/environments/env_alpha/data
REST,OS_MCP_SERVER_ENABLED=false 不存在(delete 掉)
REST,serveability 探测为 false (#4024) 不存在
dispatcher,注册了合形状的 mcp 服务 "/api/v1/mcp"
dispatcher,无 mcp 服务 键存在、值 undefined(JSON.stringify 在传输时丢弃)

据此声明为 mcp: z.string().optional() —— optional 而非 nullable:两个生产者都不会发 null/mcp 是裸挂载的,所以永远是 unscoped base。

改动

  1. packages/spec:ApiRoutesSchema 声明 mcp,注释里写清测得形状、为何 optional、以及为何与 endpoints 处置相反。
  2. packages/rest:去掉发出点的两处 as any纯类型改动,发出的响应体逐字节不变 —— 但那个 cast 的消失本身就是结构性证明(见下)。
  3. 闸门:两个 discovery 生产者都在线上返回 schema 未声明的顶层字段(scoping / features / endpoints),且 REST 形状永远无法通过 DiscoverySchema #4828/feat(spec,runtime,metadata-protocol)!: discovery 两个生产者统一到一个 schema —— capabilities 正名、features/endpoints 退役、scoping 声明 (#4828) #5682 建立的三个 discovery-schema-conformance.test.ts 现在除顶层键集外,也检查 routes 一层的键集,allowance 同样从 ApiRoutesSchema 推导(而不是手写数组),与顶层从协议 schema 推导的做法一致。

先证红

两个方向各一条,方向都是事先定好的:

红证 1 —— 类型(结构性证明)。 @objectstack/rest 的 typecheck ratchet 把该包钉在恰好 2 个错误。基线确认为 2(两条 package-routes.ts 的 TS2345)。在未声明 mcp 的前提下去掉 cast:

src/rest-server.ts(3077,46): error TS2339: Property 'mcp' does not exist on type '{ data: string; metadata: string; discovery?: string | undefined; ... }'
src/rest-server.ts(3079,53): error TS2339: Property 'mcp' does not exist on type '{ ... }'

2 → 4,check:type-check-coverage 会红。补上声明后回到恰好 2,cast 已删除且通过类型检查。

红证 2 —— 闸门。 在未声明的 schema 上,新的 routes 键集断言在两个生产者上都红:

REST:       undeclared keys inside `routes` on the REST /discovery body: expected [ 'mcp' ] to deeply equal []
dispatcher: undeclared keys inside `routes` on the getDiscoveryInfo() shape: expected [ 'mcp' ] to deeply equal []

同一次运行里 satisfies the canonical DiscoverySchema 保持绿 —— 这正是 strip 盲区本身。

premise 更正

issue 正文的括号注写着 dispatcher 不发 routes.mcp。实测不成立:packages/runtime/src/http-dispatcher.ts 的 routes 字面量里始终带着这个键

mcp: isMcpServerEnabled() && hasMcp ? `${prefix}/mcp` : undefined,

键恒在,值是路径或 undefined。之所以看起来「不发」,是因为 JSON.stringify 会丢弃 undefined 值 —— 但 Object.keys() 看得见,新闸门也就在这个生产者上照样红了。结论:受影响的是两个生产者,不是一个,补一次声明同时覆盖两者。

metadata-protocolgetDiscovery() 则确实不发:它的 routes 标注了 const routes: ApiRoutes,编译器把它按住在已声明键集内。那一侧的闸门改动前后都是绿的(诚实说明:它不是红证,是回归闸门)。

验证

命令 结果
packages/rest 全量 test 51 files / 755 passed
packages/spec 全量 test 317 files / 8084 passed
packages/metadata-protocol 全量 test 44 files / 409 passed
packages/runtime conformance 10 passed
packages/rest tsc --noEmit 2 errors = ratchet 基线
packages/spec typecheck OK
check:nul-bytes + 控制字符自查 clean

中途 packages/rest 曾出现 8 条 OpenAPI 用例红 —— 定位为我本地验证顺序的副作用:check:authorable-surface 会重跑 build-schemas.ts 但不跑 gen:openapi,把 packages/spec/json-schema/openapi.json 清掉了,而该路由在运行时从磁盘读它,于是返回 503。重跑 gen:openapi 后全绿。该目录是 gitignore 的,不进提交,CI 的 build 本身就是 gen:schema && gen:openapi && tsup,顺序正确。

生成物

Changeset

@objectstack/spec minor(新声明键)+ @objectstack/rest patch。rest 侧是纯类型改动、响应体不变,按仓库惯例仍给 patch 以记录 cast 的移除。

已合入最新 origin/main(#5721/#5722/#5731 等),无冲突。


Generated by Claude Code

claude added 2 commits August 6, 2026 02:55
…gate to routes keys (#5679)

`/discovery` advertises `routes.mcp`, objectui reads it, and `ApiRoutesSchema`
never declared it — #4828's defect one level down, with the opposite
disposition: `endpoints` was retired for having no reader, `mcp` has two real
ones and is in fact the only `routes.*` key anything in objectui reads.

`ApiRoutesSchema` is a plain z.object, which strips unknown keys, so any
consumer parsing /discovery through the spec dropped `routes.mcp` silently.

- spec: declare `mcp: z.string().optional()` as MEASURED off both producers —
  a path string, always the unscoped base (/mcp is mounted bare), optional
  rather than nullable (the key is absent, never null, when MCP is off or
  unserveable).
- rest: drop the two `as any` casts at the emit site. Type-only; the emitted
  body is byte-identical. With the key undeclared, removing them produced two
  TS2339; with it declared, tsc returns to its ratcheted baseline of 2.
- gates: the #4828 conformance tests now check `routes` keys as well as
  top-level ones in all three producer packages, deriving the allowance from
  ApiRoutesSchema. Extended one level, not recursed.

Corrects the issue's premise on one point: the runtime dispatcher's
getDiscoveryInfo() also emits `routes.mcp` (its routes literal always carries
the key, holding the path or undefined), so both producers were affected and
the new gate went red on both before the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 3:59am

Request Review

@github-actions github-actions Bot added the size/m label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/client, @objectstack/rest, @objectstack/spec.

114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/ai/skills-reference.mdx (via packages/client, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/client)
  • content/docs/api/environment-routing.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/client)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/client, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/client, @objectstack/rest)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/client, @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/client)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/client, @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/client, @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…5679)

CI 上两条不同签名的红,都是合并 origin/main 之后才出现的:

1. `@objectstack/client` DTS build — TS2741: `getRoute()` 的约定回退表
   `Record<ApiRouteType, string>` 按设计对 `keyof ApiRoutes` **全键必填**,
   所以给 ApiRoutesSchema 新增一个已声明键就必须给出它的约定路径。这是我
   消费半径漏掉的包。补 `mcp: '/api/v1/mcp'` —— 不是猜的,正是两个 discovery
   生产者实测发出的值,所以回退值与被发现值一致而非互相打架;而且该表本就
   是 unscoped 约定表(每行都是 /api/v1/...),恰好符合 /mcp 裸挂载的事实。
   全仓复扫 `keyof ApiRoutes` / ApiRoutes 型字面量,确认没有第三处:其余站点
   都是 `Partial<ApiRoutes>` 或只填两个必填键,不受影响。

2. `check:authorable-surface` — 合并带进了 #5721 的 `ui/ActionSession` 源,
   但 authorable-surface.json 未在合并树上重生成。跑 gen:schema 重出,该文件
   现同时含本单的 `api/ApiRoutes:mcp` 与 main 的 ActionSession 三键。

`authorable-surface.base.json` 的 baseRev 重锚照 #5358 再次剔除 —— 那是 main
前移导致的机械重锚,不属本单。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D

Copy link
Copy Markdown
Contributor Author

修复提交 c0cc833f —— 合并 origin/main 后的两条红,均已本地复验绿

两条红都是合并之后才出现的,签名不同,根因也不同:

1. @objectstack/client DTS build(TS2741)—— 我消费半径漏掉的包

getRoute() 的约定回退表是 Record<ApiRouteType, string>,而 ApiRouteType = keyof ApiRoutes,按设计对全键必填。所以给 ApiRoutesSchema 新增一个已声明键,就必须给出它的约定路径 —— 这不是类型把 optional 键错误物化成必填,而是这张表本来的语义:一个已声明的路由欠一条约定

mcp: '/api/v1/mcp'。这不是为了让类型闭合而编的值:它正是两个 discovery 生产者实测发出的路径,所以回退值与被发现值一致,而不是互相打架。解析行为也没变 —— getRoute() 仍优先用发现到的路由,而且原有的兜底 || \/api/v1/${type}`` 本来就会产出同一个字符串。没有用 cast 糊。

全仓复扫确认没有第三处:keyof ApiRoutes / ApiRoutes 型字面量的其余站点都是 Partial<ApiRoutes>(protocol.ts:2753)、Record<string, keyof ApiRoutes>(protocol.ts:2741,值是键不是全表)、或只填两个必填键的 const routes: ApiRoutes(protocol.ts:2767discovery.test.ts:24),都不受影响。objectui 两处读原始 JSON,不走 schema;cloud 全仓零消费者。

2. check:authorable-surface —— 合并树未重生成

合并带进了 #5721ui/ActionSession 源,但 authorable-surface.json 没在合并树上重跑。gen:schema 重出后,该文件现同时含本单的 api/ApiRoutes:mcp 与 main 的 ui/ActionSession:organizationId/roles/userId 三键。

authorable-surface.base.json 的 baseRev 重锚照 #5358 再次剔除 —— main 前移导致的机械重锚,不属本单。commit hook 的 os-regen 闸门已确认 authorable-surface.json — current

复验(合并树上,均绿)

检查 结果
check:generated 10/10 ✓(含 check:docscheck:authorable-surfacecheck:api-surface)
packages/spec typecheck OK
packages/client build(原 TS2741) ESM/CJS/DTS Build success
packages/rest tsc --noEmit 2 = ratchet 基线
check:nul-bytes OK(5642 files,无裸控制字节)

一条过程教训(记给后来者)

中途我一度把另一个并行 agent 的 check:generated 输出当成了自己的:本容器里多个 agent 共享同一个 scratchpad 目录,我用的 checkgen.log 与别人重名了 —— 那份日志里 0 处提到我的工作树、4 处指向 objectstack-5315-tenantfield。改用带 issue 号的唯一文件名后重跑才拿到自己的结果。并行跑 agent 时,日志文件名必须带任务标识,否则会拿别人的绿/红当自己的证据。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

routes.mcp 是 REST /discovery 发出、objectui 真实消费、但 ApiRoutesSchema 从未声明的键(#4828 同族,低一层)

2 participants