Repository navigation
fix(service-automation): a borrowed token refused at mcp tool discovery marks its connection, not the client secret - #28598
Conversation
…ry marks its connection, not the client secret An mcp instance with an authorization_code grant lists its tools with the token of its discovery organization's connection (ruled B). When the server refused that token, the refusal took the static-credential path: the client secret row was marked rejected and the instance held on a key a reconnect does not move. The refusal is now recorded on that connection (reconnect_required, the platform's sentence), the client secret is left alone, and the instance lists as the next connected organization or holds until a reconnect, which lists again. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ing keeps its grant Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 12 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 44c6db2d786aceef9459fae45f9487c7ae2d2b6b && git checkout 44c6db2d786aceef9459fae45f9487c7ae2d2b6b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cd065d3698db13969a28f46bf26f3e47e9ecc09c d21e1bac0bb5d9b4329962546ebb42d9db08e54f && git checkout -B drift-repro cd065d3698db13969a28f46bf26f3e47e9ecc09c && git merge --no-ff d21e1bac0bb5d9b4329962546ebb42d9db08e54f
node scripts/docs-audit/affected-docs.mjs --json cd065d3698db13969a28f46bf26f3e47e9ecc09c
|
…route a discovery 403 now takes Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH 席位核对 · ACCEPT · PR #28598 · 2026-11-01T08:01ZHead-sha: 维护者速读(席位终稿)
核对六项
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
Requested by Leehom · project thread
Part of #26702 (the card stays open for item 7, the Console, which is
domain:ui).Clause-②: no
Metadata-protocol: none
维护者速读
mcp连接器实例(oauth2授权码模式)列工具时用的是「发现组织」那条连接借来的令牌(裁决 B)。服务器拒绝这个令牌时,现在把这次拒绝记在那条连接上(状态reconnect_required,原因写平台自己的话),不再把实例的 client secret 标成rejected;实例改用下一个已连接组织列工具,没有就挂起为「需重新连接」,该组织重新连接后立即重新列工具。mcp+ 授权码 + 列工具时被拒」这一条路径。静态凭据(bearer / api-key / basic)、实例自己换取的令牌(client_credentials、app-credential)照旧标 client secret。另一副本恰在此刻重新连接时不覆盖新授权,实例按退避重试。回滚即 revert。packages/services/service-automation/src/plugin.ts的holdConnectorForRefusedConnection与物化 catch 里新加的那一支;同文件connectionStates读取处新增的两个字段;connector-mcp-authorization-door.test.ts新的 describe 块(connector-mcp/src/mcp-provider.ts只改了一段注释)。pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/connector-mcp-authorization-door.test.ts→ 18/18 通过(新增 4 个用例)。What changes
A discovery refusal of a connection-borrowed token is attributed to the connection, not to the instance's client secret.
Where.
packages/services/service-automation/src/plugin.ts, the declarative connector reconcile.mcpinstance with anoauth2authorization_codegrant materializes, its tool listing asks in the discovery organization's name (ruling B,6545215904). That organization and itsconnected_atare now kept for the materialization (discoveryConnection).holdConnectorForRefusedConnectionruns instead ofholdConnectorForRejectedCredential:connected_atunchanged, statusconnected), calls the store's existingmarkReconnectRequiredwith a platform sentence (discoveryTokenRefusedReason), never the upstream's text;credentialRefnames is not touched;connection) until a reconnect, which lists again.reconnect_requiredrow records (reconnectRequiredReasonon the connection state). The grant handle writes the same sentence it always produced, so its path reads unchanged; this path's row carries the discovery sentence.The comment this change made false.
packages/connectors/connector-mcp/src/mcp-provider.ts, the comment abovecredentialSent, said the credential path "neither marks nor re-attempts on a reconnect" a borrowed token. It now says what happens: a403the server answers a borrowed token with at discovery is routed by the materializer to the connection it was borrowed from (the discovery organization's connection is markedreconnect_required, a reconnect there lists again, the client secret is left alone), while theinclude/toolsSnapshotrefusals still keep grant-bearing instances on the plain fault (the accepted deviation of #28357). Comment only; no code in that package moves.Files.
packages/services/service-automation/src/plugin.ts,packages/services/service-automation/src/connector-mcp-authorization-door.test.ts,packages/connectors/connector-mcp/src/mcp-provider.ts(comment),.changeset/26702-mcp-discovery-refusal-marks-connection.md.Unchanged, by design. Static credentials (
bearer,api-key,basic) and tokens an instance mints from its own secret (client_credentials,app-credential) keep today's attribution: the client secret is what produced the refused token. The access-control semantics (who may connect, which grants exist) do not change. No status value or served field is added:reconnect_requiredandstatusReasonalready exist on the connection record andConnectorConnectionState.Tests
connector-mcp-authorization-door.test.ts(real ObjectQL on SQLite, themcpstand-in now able to answer a listing403the way the real provider throws it):reconnect_requiredwith the platform sentence, the held reason names it, and a reconnect by the same organization lists again and serves;org_a) leaves the secret rowset, marks onlyorg_a, the instance lists asorg_b,org_bdispatches andorg_ais refused503"needs to be reconnected";connected) and the instance retries;client_credentialsinstance (token minted by the instance itself) still marks its client secretrejected.Red first, on main's plugin.ts (310cb96)
The new door tests on main's
plugin.ts:Tests 2 failed | 15 passed (17)(expected … to match object { status: 'reconnect_required' };expected … to have a length of 3 but got 2).A probe (not committed) recording what main does, single organization, stored client secret, one refused listing, then a reconnect:
secretStatus: rejected,connStatus: connected, 1 listing; after the reconnectsecretStatus: rejected, still 1 listing, 1 materialization, still degraded.secretStatus: set,connStatus: reconnect_required; after the reconnect 2 listings, 2 materializations, served.So the reading
6553121554reproduces as written.Ablations (scripts/ablation-replace.mjs, each restored with `git diff HEAD` empty)
&& discoveryConnection === null): 3 red (the two attribution cases and the race case).record.connectedAt === connection.connectedAt→true): 1 red (the race case).service-automationimportsplugin.tsfromsrc/in these tests, so no rebuild sits between a mutation and the run.Suites
pnpm --filter @objectstack/service-automation test:Test Files 250 passed (250),Tests 3173 passed (3173).pnpm --filter @objectstack/service-automation run typecheck: exit 0 (check:test-typecheck: OK).eslint --no-inline-configon the two touched files: exit 0.Acceptance notes
ConnectorConnectionState.statusinpackages/spec/src/contracts/automation-service.tsreads "reconnect_required— the authorization server refused the grant". This path also sets it when the upstream refused the connection's token at discovery. Not edited here (the spec lane's file); noted in the report.include/toolsSnapshotrefusals of PR fix(connector-mcp): an include a credentialed MCP server does not list degrades the instance as a credential refusal, not the boot #28357 still keep grant-bearing instances on the plain fault (its accepted deviation); moving them onto this route is a follow-up the card owns, not this PR.connected) even when the listing during that same connect's reconcile is refused and marks it; the connection read right after answersreconnect_required.plugin.ts(the package file loader); different hunks.Verification
Gate sheet in the card report (derived families on the final head,
--ranreconciled); CI check-runs are the record for everything else.Generated by Claude Code