Skip to content

Commit 3f2b32c

Browse files
committed
docs(qa): the MCP surface clause names all four OAuth scopes; changeset for the consent screen
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jUZATdbPBDkvjTyX8D
1 parent 2b20ca5 commit 3f2b32c

2 files changed

Lines changed: 20 additions & 6 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/i18n': patch
3+
'@object-ui/console': patch
4+
---
5+
6+
fix(console,i18n): the OAuth consent screen explains the `metadata:write` scope and never shows an unknown scope as its bare spelling
7+
8+
Clause-②: yes
9+
Metadata-protocol: none
10+
11+
- **`@object-ui/console`.** The consent screen at `/oauth/consent` names every MCP scope in a sentence. The sentences come from `@objectstack/spec`'s `MCP_OAUTH_SCOPE_CONSENT_COPY`, the copy the spec declares beside each scope, through a switch that is exhaustive over `McpOauthScope`: a scope added to the spec without a case here fails to compile. `metadata:write` used to appear as that raw string beside three plain sentences. It now reads "Build and change this app's metadata as drafts, within your own permissions — a person publishes them". A scope the page cannot name renders as "Unrecognised permission: SCOPE" instead of the bare scope string, which could read as a description of the grant.
12+
- **`@object-ui/i18n`.** Every locale pack gains `oauth.consent.scope.metadataWrite` and `oauth.consent.scope.unrecognised` (the latter takes a `{{scope}}` placeholder). `TranslationKeys` widens by these two keys.

‎docs/qa/platform-checklist/areas/ai.json‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
"title": "MCP HTTP transport (/mcp) and the public /mcp/skill endpoint gate exactly as documented: 404 when opted out, 501 when unimplemented, 401 anonymous, 403 scopeless-OAuth, full tool list when keyed",
128128
"since": "v15",
129129
"status": "active",
130-
"revision": 5,
130+
"revision": 6,
131131
"priority": "P1",
132132
"surface": "api",
133133
"fixtures": {
@@ -140,15 +140,16 @@
140140
"variants": [
141141
"OAuth scope data:read → read-family tools: list_objects, describe_object, validate_expression, query_records, get_record, and aggregate_records where the bridge implements aggregate (packages/mcp/src/mcp-http-tools.ts canRead block; packages/spec/src/ai/mcp.zod.ts MCP_OAUTH_SCOPE_DATA_READ)",
142142
"OAuth scope data:write → write-family tools: create_record, update_record, delete_record",
143-
"OAuth scope actions:execute → list_actions, run_action"
143+
"OAuth scope actions:execute → list_actions, run_action",
144+
"OAuth scope metadata:write → the authoring tools the host injects (packages/mcp/src/mcp-server-runtime.ts registerAuthoringTools; packages/spec/src/ai/mcp.zod.ts MCP_OAUTH_SCOPE_METADATA_WRITE), served only to an OAuth grant naming metadata:write — a key or a session is served none"
144145
],
145146
"steps": [
146147
"boot with OS_MCP_SERVER_ENABLED=false; GET /api/v1/mcp/skill and POST /api/v1/mcp; record status + body of both",
147148
"boot with the default-on config; GET /api/v1/mcp/skill UNAUTHENTICATED; capture status, content-type, content-disposition and the markdown head",
148149
"POST /api/v1/mcp/skill (wrong method); capture the 405 and its Allow header",
149150
"POST /api/v1/mcp with NO credentials (an MCP initialize body); capture the 401 body and any WWW-Authenticate header",
150151
"open an MCP Streamable HTTP session against /api/v1/mcp with the osk_ key: initialize, then tools/list; record the full tool-name set",
151-
"if the OAuth track is live in the environment, request /mcp with an OAuth token (to mint one locally, register the client with application_type: 'native' — dynamic client registration refuses a web client whose redirect is http or a loopback host, 'web clients require https redirect URIs on non-loopback hosts', @better-auth/oauth-provider) carrying NONE of the three MCP scopes; capture the 403 + WWW-Authenticate error=\"insufficient_scope\"",
152+
"if the OAuth track is live in the environment, request /mcp with an OAuth token (to mint one locally, register the client with application_type: 'native' — dynamic client registration refuses a web client whose redirect is http or a loopback host, 'web clients require https redirect URIs on non-loopback hosts', @better-auth/oauth-provider) carrying NONE of the four MCP scopes; capture the 403 + WWW-Authenticate error=\"insufficient_scope\"",
152153
"capture the dispatcher-vs-hono distinction: the disabled boot's 404 comes from the gate (surface not advertised), never a hang or 500"
153154
],
154155
"acceptance": [
@@ -183,9 +184,9 @@
183184
"evidence": "the session trace + tool-name list"
184185
},
185186
{
186-
"clause": "an OAuth token granting none of the MCP scopes is refused 403 up front with insufficient_scope naming all three scopes (data:read, data:write, actions:execute); a partial grant narrows the tool list per the variants matrix (#2698, ADR-0090 D10)",
187+
"clause": "an OAuth token granting none of the MCP scopes is refused 403 up front with insufficient_scope naming all four scopes (data:read, data:write, actions:execute, metadata:write); a partial grant narrows the tool list per the variants matrix (#2698, ADR-0090 D10)",
187188
"oracle": "api",
188-
"verify": "403 body 'Forbidden: the access token grants none of the MCP scopes …' + WWW-Authenticate error=\"insufficient_scope\" scope=\"data:read data:write actions:execute\"; for a data:read-only token, tools/list carries the read family only",
189+
"verify": "403 body 'Forbidden: the access token grants none of the MCP scopes …' + WWW-Authenticate error=\"insufficient_scope\" scope=\"data:read data:write actions:execute metadata:write\"; for a data:read-only token, tools/list carries the read family only",
189190
"evidence": "the 403 + the narrowed tools/list"
190191
}
191192
],
@@ -209,7 +210,8 @@
209210
{ "revision": 2, "date": "2026-08-07", "change": "expanded to deep-test contract: concrete steps, multi-clause acceptance, negatives, variants", "ref": "claude/platform-test-checklist-ocwugl" },
210211
{ "revision": 3, "date": "2026-10-16", "change": "clause 5's tool list omitted resume_run (#23878 checklist-accuracy finding). Re-measured on main c91af8ed6 with an admin osk_ key: tools/list returns twelve tools, resume_run among them; mcp-http-tools.ts registers it only when the bridge exposes resumeRun, the same optional shape as aggregate_records, so the verify now names that degradation too", "ref": "#24036" },
211212
{ "revision": 4, "date": "2026-10-19", "change": "variants[0] and steps[5] corrected — checklist-accuracy findings of the 18.0 QA sweep (QA run #24540, ai.mcp-http-surface; wave anchor #24579 §3). The data:read family also serves aggregate_records and validate_expression (both read-only), and a local OAuth leg needs a native client. Re-derived at base 9a7838edf5 (mcp-http-tools.ts registers validate_expression and, when the bridge exposes aggregate, aggregate_records inside the canRead gate; the oauth-provider DCR rule in the installed @better-auth/oauth-provider 1.7.3). No clause changed", "ref": "#24649" },
212-
{ "revision": 5, "date": "2026-10-26", "change": "automated.ref extended and annotated per clause — checklist-accuracy findings of the 18.2.0 sweep, Tier 1 record #25815 at 6679644a (wave anchor #25807): the one-file ref left acceptance[0]–[2] and [5] with no named pin, and the pin asserts acceptance[3]'s status only and acceptance[4] with a session bearer, no initialize and two tool names. Re-read on main at b4199c96: the record's 'no pin at any layer' for acceptance[0]–[2] and [5] no longer holds, since dispatcher-unit and Hono-integration pins in packages/runtime and the scope-narrowing unit in packages/mcp assert parts of them; they join the ref with the layer each reaches and what each leaves unasserted. No clause changed, no status changed", "ref": "#25836" }
213+
{ "revision": 5, "date": "2026-10-26", "change": "automated.ref extended and annotated per clause — checklist-accuracy findings of the 18.2.0 sweep, Tier 1 record #25815 at 6679644a (wave anchor #25807): the one-file ref left acceptance[0]–[2] and [5] with no named pin, and the pin asserts acceptance[3]'s status only and acceptance[4] with a session bearer, no initialize and two tool names. Re-read on main at b4199c96: the record's 'no pin at any layer' for acceptance[0]–[2] and [5] no longer holds, since dispatcher-unit and Hono-integration pins in packages/runtime and the scope-narrowing unit in packages/mcp assert parts of them; they join the ref with the layer each reaches and what each leaves unasserted. No clause changed, no status changed", "ref": "#25836" },
214+
{ "revision": 6, "date": "2026-10-27", "change": "acceptance[5] clause and verify, steps[5] and variants follow the fourth MCP scope, metadata:write: the scopeless 403 names all four scopes in its body and in its WWW-Authenticate scope list (packages/runtime/src/domains/mcp.ts joins MCP_OAUTH_SCOPES), and the variants matrix gains the metadata:write row (the host-injected authoring tools, served only to an OAuth grant naming the scope). Re-read on main at c739de75. No status changed", "ref": "#26587" }
213215
]
214216
},
215217
{

0 commit comments

Comments
 (0)