@aotter/mantle 0.2.0-alpha.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +26 -3
  2. package/dist/auth/adapter.js +10 -34
  3. package/dist/auth/admin.js +2 -5
  4. package/dist/auth/caller.js +0 -27
  5. package/dist/auth/consent.js +100 -0
  6. package/dist/auth/hardening.js +0 -7
  7. package/dist/auth/identity.js +0 -11
  8. package/dist/auth/oauth.js +0 -32
  9. package/dist/auth/resource-seeds.js +0 -2
  10. package/dist/auth/route-http.js +33 -0
  11. package/dist/auth/routes.js +8 -153
  12. package/dist/build/compiler.d.ts +19 -609
  13. package/dist/build/compiler.js +3 -6896
  14. package/dist/build/spec/domain/model/SqlIr.d.ts +2 -0
  15. package/dist/build/spec/domain/model/UserFields.d.ts +1 -0
  16. package/dist/bun/assets.js +0 -2
  17. package/dist/domain/adminOrder.js +0 -2
  18. package/dist/domain/columns.js +7 -20
  19. package/dist/domain/describe/chunk-SGMRVDGE.js +1 -0
  20. package/dist/domain/describe/index.js +1 -115
  21. package/dist/domain/describe/target-hash.js +1 -12
  22. package/dist/domain/entry.js +0 -14
  23. package/dist/domain/generated-tools.js +0 -1
  24. package/dist/domain/input.js +0 -2
  25. package/dist/domain/paging.js +0 -17
  26. package/dist/domain/recipes.js +10 -7
  27. package/dist/domain/userReferences.js +0 -1
  28. package/dist/domain/wireScalars.js +0 -6
  29. package/dist/infrastructure/adminEntries.js +10 -7
  30. package/dist/infrastructure/adminPaging.js +55 -57
  31. package/dist/infrastructure/artifacts.js +0 -4
  32. package/dist/infrastructure/authSchema.js +0 -9
  33. package/dist/infrastructure/bundle.js +17 -32
  34. package/dist/infrastructure/cli.js +25 -11
  35. package/dist/infrastructure/generate.js +18 -17
  36. package/dist/infrastructure/generateAuth.js +48 -5
  37. package/dist/infrastructure/help.js +55 -0
  38. package/dist/infrastructure/init.js +50 -0
  39. package/dist/infrastructure/inputs.js +0 -9
  40. package/dist/infrastructure/mounts.js +22 -0
  41. package/dist/infrastructure/wrangler.js +18 -9
  42. package/dist/kernel/authorization.js +0 -1
  43. package/dist/kernel/binds.d.ts +16 -7
  44. package/dist/kernel/binds.js +4 -10
  45. package/dist/kernel/caller-identity.d.ts +0 -3
  46. package/dist/kernel/caller-identity.js +0 -15
  47. package/dist/kernel/caller-mode.d.ts +4 -0
  48. package/dist/kernel/caller-mode.js +14 -0
  49. package/dist/kernel/contracts.d.ts +7 -11
  50. package/dist/kernel/contracts.js +8 -44
  51. package/dist/kernel/cron.js +0 -4
  52. package/dist/kernel/dialect/common.d.ts +0 -1
  53. package/dist/kernel/dialect/common.js +0 -3
  54. package/dist/kernel/dialect/postgres.js +0 -2
  55. package/dist/kernel/dialect/sqlite.js +1 -6
  56. package/dist/kernel/drain.js +2 -8
  57. package/dist/kernel/driver/bun-sqlite.js +0 -4
  58. package/dist/kernel/driver/d1.js +0 -1
  59. package/dist/kernel/driver/postgres.js +3 -17
  60. package/dist/kernel/driver/transaction-context.js +0 -1
  61. package/dist/kernel/email.js +0 -4
  62. package/dist/kernel/host-access.js +2 -3
  63. package/dist/kernel/host-caller.d.ts +1 -1
  64. package/dist/kernel/host-caller.js +3 -6
  65. package/dist/kernel/json-body.js +0 -2
  66. package/dist/kernel/mcp-apps.js +1 -6
  67. package/dist/kernel/mcp-call.js +0 -1
  68. package/dist/kernel/mcp-handler.js +3 -27
  69. package/dist/kernel/mcp.d.ts +5 -1
  70. package/dist/kernel/mcp.js +2 -6
  71. package/dist/kernel/media.js +0 -18
  72. package/dist/kernel/paging.js +1 -9
  73. package/dist/kernel/publishing.js +3 -4
  74. package/dist/kernel/r2-media.js +0 -7
  75. package/dist/kernel/rest-dispatch.js +0 -1
  76. package/dist/kernel/rest-router.js +2 -3
  77. package/dist/kernel/rest.js +0 -3
  78. package/dist/kernel/scalars.js +4 -11
  79. package/dist/kernel/search.js +0 -1
  80. package/dist/kernel/site.js +0 -2
  81. package/dist/kernel/store.d.ts +11 -1
  82. package/dist/kernel/store.js +21 -34
  83. package/dist/kernel/surface-failure.d.ts +10 -0
  84. package/dist/kernel/surface-failure.js +29 -0
  85. package/dist/kernel/surface.js +0 -3
  86. package/dist/kernel/sweep.js +0 -1
  87. package/dist/kernel/translations.js +0 -1
  88. package/dist/kernel/user-caller.js +2 -8
  89. package/dist/kernel/user-fields.d.ts +3 -2
  90. package/dist/kernel/user-fields.js +9 -9
  91. package/dist/kernel/validation.js +0 -8
  92. package/dist/kernel/web-gateway.js +0 -2
  93. package/dist/kernel/web-head.js +0 -1
  94. package/dist/kernel/web-index.js +39 -0
  95. package/dist/kernel/web-response.js +0 -1
  96. package/dist/kernel/web-serve.js +97 -0
  97. package/dist/kernel/web.d.ts +1 -1
  98. package/dist/kernel/web.js +10 -125
  99. package/dist/kernel/where.js +0 -6
  100. package/dist/usecase/adminBindings.js +30 -21
  101. package/dist/usecase/adminClient.js +0 -21
  102. package/dist/usecase/adminDescriptors.js +2 -9
  103. package/dist/usecase/generate.js +7 -12
  104. package/dist/usecase/handlers.js +0 -3
  105. package/dist/usecase/mcp-media.js +0 -1
  106. package/dist/usecase/mcp.js +0 -4
  107. package/dist/usecase/paging.js +1 -3
  108. package/dist/usecase/publishing.js +1 -5
  109. package/dist/usecase/rest.js +0 -1
  110. package/dist/usecase/site.js +0 -2
  111. package/dist/usecase/stores.js +4 -14
  112. package/dist/usecase/tool-version.js +0 -3
  113. package/dist/usecase/translations.js +0 -2
  114. package/dist/usecase/validation.js +8 -20
  115. package/docs/examples/README.md +13 -5
  116. package/docs/examples/app-shell/README.md +43 -8
  117. package/docs/examples/app-shell/accept.mjs +91 -0
  118. package/docs/examples/app-shell/gitignore +7 -0
  119. package/docs/examples/app-shell/package.json +7 -6
  120. package/docs/examples/app-shell/smoke-kit.mjs +29 -15
  121. package/docs/examples/app-shell/src/auth.ts +3 -2
  122. package/docs/examples/app-shell/src/routes.ts +6 -3
  123. package/docs/examples/app-shell/src/service.ts +4 -3
  124. package/docs/examples/club-events/README.md +7 -2
  125. package/docs/examples/club-events/manifests/club-events.yaml +22 -0
  126. package/docs/examples/club-events/smoke.mjs +7 -1
  127. package/docs/examples/club-events/src/handlers.ts +1 -1
  128. package/docs/examples/purchasing/README.md +12 -3
  129. package/docs/examples/purchasing/manifests/purchasing.yaml +36 -3
  130. package/docs/examples/purchasing/smoke.mjs +14 -0
  131. package/docs/examples/purchasing/src/handlers.ts +1 -1
  132. package/docs/examples/recipes/README.md +5 -3
  133. package/docs/handbook/cloudflare/authentication.md +21 -4
  134. package/docs/handbook/cloudflare/chatgpt-sites.md +4 -2
  135. package/docs/handbook/cloudflare/deploy-and-operate.md +24 -1
  136. package/docs/handbook/cloudflare/media-r2.md +2 -3
  137. package/docs/handbook/cloudflare/service-entry.md +17 -4
  138. package/docs/handbook/concepts/runtime-and-adapters.md +1 -1
  139. package/docs/handbook/examples/hub.md +1 -1
  140. package/docs/handbook/guides/agent-setup.md +15 -10
  141. package/docs/handbook/guides/agent-workflow-acceptance.md +232 -0
  142. package/docs/handbook/guides/local-testing.md +40 -0
  143. package/docs/handbook/guides/mcp-apps.md +78 -1
  144. package/docs/handbook/guides/native-admin.md +29 -6
  145. package/docs/handbook/guides/native-execution.md +5 -4
  146. package/docs/handbook/guides/oauth.md +58 -0
  147. package/docs/handbook/guides/public-pages.md +44 -4
  148. package/docs/handbook/guides/sql-patterns.md +266 -0
  149. package/docs/handbook/guides/typed-queries.md +173 -0
  150. package/docs/handbook/navigation.json +16 -0
  151. package/docs/handbook/reference/admin-descriptors.md +4 -1
  152. package/docs/handbook/reference/authorization.md +1 -1
  153. package/docs/handbook/reference/diagnostics.md +6 -1
  154. package/docs/handbook/reference/procedure.md +5 -5
  155. package/docs/handbook/reference/schema.md +10 -4
  156. package/docs/handbook/reference/sql.md +64 -0
  157. package/docs/handbook/reference/surface.md +11 -4
  158. package/docs/handbook/reference/trigger.md +9 -0
  159. package/docs/handbook/reference/view.md +11 -3
  160. package/docs/handbook/releases/index.md +27 -6
  161. package/docs/handbook/start/agents.md +45 -0
  162. package/docs/handbook/start/project-and-cli.md +15 -1
  163. package/docs/handbook/start/quickstart-worker.md +1 -1
  164. package/docs/skills/develop/SKILL.md +44 -16
  165. package/docs/skills/integrate/SKILL.md +4 -9
  166. package/docs/skills/provision/SKILL.md +15 -12
  167. package/docs/skills/update/SKILL.md +21 -18
  168. package/package.json +24 -4
  169. package/skills/README.md +1 -1
  170. package/skills/mantle/SKILL.md +48 -107
  171. package/dist/domain/describe/chunk-ZVXBGP7Y.js +0 -424
  172. package/docs/skills/develop/mcp-app.md +0 -39
  173. package/skills/.keep +0 -0
  174. package/skills/mantle/references/local-testing.md +0 -15
  175. package/skills/mantle/references/web.md +0 -41
  176. package/skills/mantle/references/website.md +0 -49
@@ -0,0 +1,232 @@
1
+ # Independently accept a user-facing workflow
2
+
3
+ Before reporting any user-facing deliverable as done, exercise it from the
4
+ user's position with a fresh end-user agent. This applies to Admin pages and
5
+ customisations, application pages and web routes, REST APIs used by clients,
6
+ MCP tools and MCP Apps. Generation, typechecking, scripted smoke tests and an
7
+ author's replay remain useful; they do not establish independent acceptance. A
8
+ fresh context is not enough if the author still chooses a workflow that mirrors
9
+ the implementation. Every acceptance round gives the evaluator only a
10
+ role and the entry points that role uses; it derives realistic tasks itself
11
+ and the author scores them against a private rubric.
12
+ If independent acceptance is not run, report `UNVERIFIED`; if a fresh evaluator
13
+ or authorized test transport is unavailable, report `BLOCKED`; neither outcome
14
+ is done.
15
+
16
+ Use the surface the intended user actually uses: a real browser session for
17
+ Admin and application pages; for REST, an HTTP client plus the public API
18
+ reference and base URL a real client developer receives; an MCP client for
19
+ tools; and the real MCP App host/AppBridge flow for Apps. This is an
20
+ application-authoring practice, not an SDK runtime service or a new test runner.
21
+ Use the project's existing local setup, normal authorized sign-in and isolated
22
+ fixtures. Setup hands over the role's established browser session or client
23
+ token; the evaluator never acquires or widens credentials. Give it only its
24
+ authorized surface. Do not enable extra routes, roles or generic writes to make
25
+ the exercise pass.
26
+
27
+ ## Audit MCP tool design before implementation
28
+
29
+ Start with the original user brief, business role and explicit requirements,
30
+ not a list of CRUD endpoints or a task derived from what you implemented.
31
+ Before writing manifests or handlers, record the intended jobs and assumptions
32
+ in the existing design note. The capability map feeds the private rubric
33
+ (below); an evaluator receives only its role and chooses its own tasks and
34
+ observable success criteria. Do not expand application scope or authority
35
+ merely to satisfy an evaluator's newly inferred product wish.
36
+
37
+ In the application's existing design note or issue, map each goal step to its
38
+ proposed public capability: discovery inputs, returned IDs and evidence, allowed
39
+ caller, write/transaction boundary and recovery. Mark missing capabilities or
40
+ unresolved contracts as gaps before writing manifests or handlers. Review the
41
+ following rubric against that map:
42
+
43
+ | Design question | Required contract or explicit gap |
44
+ | --- | --- |
45
+ | Can the agent locate the right record? | Bounded search/filter inputs, stable IDs, deterministic cursor paging and sensible limits. It must not scan every list or guess an ID from a display label. |
46
+ | Can it choose an eligible relation? | Finding an ID is not proof of business eligibility: verify the role, state and ownership required by the relationship. Return enough labels to distinguish same-name choices before writing. |
47
+ | Can it correct an ordinary mistake? | State how to amend, clear or replace optional fields and relations, and how duplicate records are detected or reconciled. Test omitted, null and empty inputs against that contract; existence and type checks alone do not establish business validity. Express conditional-required inputs with the supported [input rules](../reference/surface.md) before writing handler checks. |
48
+ | Can it read a complete business result? | ID-based detail/readback under the intended caller, including every relation the goal creates or changes. A parent list is not proof of its child/contact/state details. Distinguish absent/inaccessible from incomplete evidence. |
49
+ | Can it verify a create or update? | Return the backend entity ID, actual committed version where applicable, and a safe result summary or a documented authorized readback. State the exact result nesting. Echoed input or the submitted `expectedVersion` is not proof of committed state. Specify PATCH versus full replacement and omitted/null-field semantics so an update cannot erase unseen fields. |
50
+ | What does a version protect? | Name its entity/snapshot scope; a parent version does not imply child rows or aggregates are unchanged. Use the reviewed observed version for OCC, define conflict behavior and preserve the user's proposed changes without silently rebinding/retrying. |
51
+ | Can it complete a business transition? | Name permitted source/destination states, preconditions, related effects and final readback. Prefer an outcome-oriented operation when arbitrary field updates would leave the workflow unfinished. |
52
+ | What commits together? | Define the native Procedure/Store atomic boundary. Separate tool calls are independent; describe partial success and deliberate compensation. Do not imply that an agent can roll back earlier calls or that compensation is always possible. |
53
+ | Can it interpret refusal and failure? | Document success and structured diagnostic/error envelopes, including MCP `isError`; HTTP 200 or a wrapper's transport success is insufficient. Inspect raw MCP diagnostics separately from host-visible wrapper errors; a wrapper label alone does not locate a runtime defect or require global error normalization. Missing relations, stale versions and denied callers need meaningful refusals without leaking another caller's records or native internals. |
54
+ | What happens after a duplicate or uncertain write? | State whether idempotency exists, its key/scope/lifetime when supported, or that repeated successful calls may repeat effects. Provide an authorized reconciliation route for timeout/unknown outcomes; never automatically retry an uncertain write. |
55
+ | Can it explain amounts correctly? | Declare currency, stored/wire units, exact-value representation, rounding and whether null means unknown. Label every ratio's numerator and denominator: cost/budget, sales/budget and gross/sales are different measures. |
56
+ | Does data remain usable without an App? | Keep ordinary reads data-only; attach a renderer deliberately for interactive inspection, with structured/text fallback. Rendering must not write or imply approval. Host catalog refresh, OAuth and real client/UI behavior have separate acceptance; descriptions cannot force generative UI layout. |
57
+ | Does every step respect the user's authority? | Verify least privilege, tenant/owner scope, allowed fields, relation access and negative cases on the server. Tool visibility, annotations, descriptions and UI confirmation never grant permission. Use authorized disposable fixtures and a cleanup owner without widening production writes/deletes. |
58
+
59
+ A design passes this audit only when each required goal step has a supported
60
+ capability and verifiable output/recovery; preserve unresolved gaps and owners.
61
+ The audit is a design review, not execution evidence. Record intentional
62
+ exclusions, but keep an unmet explicit user requirement unresolved until the
63
+ user changes its scope; do not redefine it as optional to make a round pass.
64
+ Reuse the installed version's Views,
65
+ Procedures, Store and surface contracts; a missing business tool is not by
66
+ itself an SDK runtime defect.
67
+
68
+ For simple creates/partial updates, prefer a declared Store-backed `ref`
69
+ Procedure: await the caller-bound Store write, return a safe allowlist with its
70
+ actual returned ID/version, and keep `requires` plus generated input validation
71
+ at invocation. A transaction-bound write result remains provisional until the
72
+ outer transaction commits; publish success only after that boundary completes. Store row policy alone is not Procedure authorization. Define
73
+ omitted versus null patch semantics; never fill unseen fields with defaults.
74
+ Keep SQL Procedures for atomic stock changes, conditional state transitions and
75
+ related writes. A TypeScript pre-read does not replace an atomic WHERE/CHECK;
76
+ D1 supports batch, not an interactive transaction or consuming an earlier batch
77
+ result. See the [Store write guide](typed-queries.md#prefer-store-backed-ref-procedures-for-simple-writes).
78
+
79
+ Then implement the minimum contract and run generated/type, authorization,
80
+ atomicity and other relevant regressions. Follow the independent acceptance
81
+ rounds below before claiming the user-facing work is done. Keep the author's
82
+ capability map and worked call sequence out of the fresh agent's prompt: an MCP
83
+ evaluator must discover the workflow from public tool descriptions and results.
84
+
85
+ ## Run role-only acceptance rounds until they converge
86
+
87
+ The evaluator gets a role, not a task list. It works out what that role needs
88
+ to get done from the interface itself; what it derives, and what it misses, is
89
+ the evidence.
90
+
91
+ 1. **Write a private rubric first.** From the original brief, the author lists
92
+ what each role must be able to do on each surface (Admin, application
93
+ pages, REST, MCP), with observable success criteria, explicit exclusions
94
+ and the authority each role must not have. The rubric stays with the author;
95
+ no evaluator ever sees it, its wording or its order.
96
+ 2. **Hand each evaluator only its role.** Supply the business role and context
97
+ in one or two sentences, the entry points that role really uses (Admin URL,
98
+ site URL, REST base URL and API reference, MCP endpoint), a signed-in browser
99
+ session or client token from setup, the interaction/time/write budget,
100
+ permitted fake-data actions and what is forbidden (source, SQL, internal
101
+ docs, the database, other roles' credentials, blind retries of uncertain
102
+ writes). Withhold the brief's requirement list, capability maps, tool names,
103
+ precomputed IDs and earlier reports. Seed realistic fixtures: other owners'
104
+ records, several states and more than one page of results. Run the evaluator
105
+ outside the project directory, without Mantle skills or project instruction
106
+ files, and record that isolation.
107
+ 3. **Let it derive the work, then do it.** Before any interaction the evaluator
108
+ writes down what this role would need to accomplish and how it would know
109
+ each one succeeded. It then refines that list as it discovers the interface
110
+ (at least once after a first look, again after its first attempts), executes
111
+ it, and reports PASS/FAIL/BLOCKED per goal with readbacks, rejected writes,
112
+ recovery attempts and interaction counts. Zero findings is a valid result.
113
+ 4. **One fresh agent per role, every surface.** For the default two roles, run
114
+ an `admin` evaluator and a `user` evaluator in the same round on shared live
115
+ data, in the brief's natural order. Between them they cover every surface
116
+ the app exposes to each role: Admin for `admin`, pages for visitors and
117
+ members, REST for client developers, MCP for both roles' AI clients. A
118
+ surface no evaluator reached stays UNVERIFIED.
119
+ 5. **Score against the rubric outside the evaluator.** The author compares the
120
+ evaluator's derived goals and results with the rubric. A rubric item the
121
+ evaluator never thought of is itself a finding: the interface did not make
122
+ that job discoverable. Classify every finding P0 (data loss, authority or
123
+ integrity defect, uncertain write), P1 (a rubric item FAIL, BLOCKED or not
124
+ discoverable), P2 (friction) or P3 (suggestion), with evidence and the
125
+ smallest repair. Separate a violated explicit requirement, a demonstrated
126
+ defect and a newly inferred product wish; a wish is not permission to grow
127
+ the product.
128
+ 6. **Repair outside the evaluator's context, then start over.** The author keeps
129
+ the findings ledger, makes the smallest in-scope repair, adds focused
130
+ regressions (authorization, OCC, atomicity), and freezes a new revision.
131
+ The next round uses new agents and new fixtures and passes them nothing
132
+ from earlier rounds.
133
+ 7. **Converge or stop.** A round converges when it has no P0 or P1 and the
134
+ evaluators' observed results cover every rubric item visible to their roles;
135
+ items outside any role's view (another caller's isolation, hidden state)
136
+ need author regressions, reported under that label. Run at most three
137
+ rounds. If the third still has a P0 or P1 or an uncovered rubric item, report
138
+ BLOCKED with the remaining ledger, not acceptance. Setup cleans fixtures in
139
+ `finally` after every round, including aborted ones.
140
+
141
+ If a fresh agent or authorized test transport is unavailable, record independent
142
+ acceptance as BLOCKED and report the scripted checks separately. Do not invent
143
+ an independent result. Do not deploy or publish merely to obtain acceptance.
144
+
145
+ ## Hand each role to its evaluator
146
+
147
+ Setup signs each role in; the evaluator receives the role's access, never a credential it could reuse or widen.
148
+ This is the authorized test transport the BLOCKED rule above refers to.
149
+
150
+ - **Local session URL (the app shell's default).** `pnpm accept` in an app written by `mantle init` starts the app
151
+ on a fresh local D1, signs the admin in, runs an optional fixtures module, signs the user in and prints one local
152
+ URL per role. Each URL forwards every request with that role's session and refuses `/api/auth/*`. Give the
153
+ evaluator only its URL: MCP at `<url>/mcp` (user) or `<url>/mcp/admin` (admin) for any streamable HTTP MCP client,
154
+ Admin at `<url>/admin` and member pages at `<url>/` in a browser, REST at `<url>/` with no credential. An evaluator
155
+ without an MCP client uses `node accept.mjs mcp <endpoint> [tool [json]]`, which prints the raw result including
156
+ `isError`. The app README's "Independent acceptance" section shows the fixtures module.
157
+ - **An external MCP host over OAuth.** To accept the connection a real client uses (Claude, ChatGPT or another host
158
+ that supports MCP OAuth), deploy or tunnel the app as you would for that host, add `<origin>/mcp` or
159
+ `<origin>/mcp/admin` to the host in setup, and complete sign-in and consent as the role's fictional identity. The
160
+ evaluator then works in that already-connected host. Its token stays in the host; the evaluator neither sees nor
161
+ requests it. Record this as OAuth evidence, separate from the local session URL (see
162
+ [OAuth](oauth.md)).
163
+
164
+ Either way, the evaluator's prompt names the URL or the connected host, never a cookie, token or sign-in step.
165
+
166
+ If you cannot start a fresh agent yourself (for example, you are a subagent without that tool), prepare everything
167
+ else: the rubric, the fixtures module, `pnpm accept` running, and one filled-in role prompt per role (below) naming
168
+ its URL. Hand the prompts to whoever can start agents (the person or the orchestrating agent), keep the app running,
169
+ and report acceptance as BLOCKED with the prompts attached until their reports come back. Score those reports as
170
+ usual.
171
+
172
+ ## Give the agent a role
173
+
174
+ Fill this in per role. It names the role and the doors, never the jobs:
175
+
176
+ > You are [role, for example "a purchasing manager"] at [business context in
177
+ > one sentence]. You use [entry points for this role from `pnpm accept`: Admin
178
+ > URL, site URL, REST base URL and reference, MCP endpoint]; setup has already
179
+ > signed you in as [fictional identity]. Budget: [interactions/time/writes]. You may create and
180
+ > change fictional records; [owner] cleans up. Do not read source, internal docs
181
+ > or a database, use anyone else's access, or retry a write whose outcome you
182
+ > do not know.
183
+ > First, write down what someone in this role needs to get done with this
184
+ > service and how you would tell each one worked. Refine that list after a
185
+ > first look at the interface and again after your first attempts. Then do the
186
+ > work and report each goal as PASS/FAIL/BLOCKED with what you read back,
187
+ > what was refused, what it cost you, and the smallest change that would have
188
+ > helped. Report what you covered and what you could not reach; finding nothing
189
+ > wrong is acceptable.
190
+
191
+ The author separately verifies isolation between callers, stale-version
192
+ refusal, unknown-outcome handling and effects hidden from the evaluator's role
193
+ through authorized test fixtures. Do not grant the evaluator extra authority
194
+ just to observe hidden state. Keep author regressions distinct from what the
195
+ fresh agent actually observed.
196
+
197
+ ## Record evidence in the existing issue or handoff
198
+
199
+ Use ordinary Markdown; no new receipt schema or framework is required:
200
+
201
+ - Original brief, the private rubric, roles, environment, per-round budgets
202
+ and round cap.
203
+ - Setup assistance and what each agent could access; identify distinct rounds.
204
+ - Per round: revision, each evaluator's derived goals (first list and
205
+ refinements) and criteria, the gaps between them and the rubric, per-goal
206
+ PASS/FAIL/BLOCKED, actual readbacks and interaction references,
207
+ request/page/tool counts
208
+ and negative/recovery attempts, with their differing scope stated.
209
+ - Findings ledger with P0–P3, owner, disposition, minimal repair and
210
+ regression commands/results.
211
+ - Coverage of each rubric item (evaluator-observed, author regression only, or
212
+ UNVERIFIED); the final CONVERGED / BLOCKED / UNVERIFIED verdict and rounds
213
+ used. Retain every failed report.
214
+ - Fixture cleanup result and every remaining limitation.
215
+
216
+ Keep credentials, raw auth logs, full resource HTML and private operational
217
+ details local/private; publish only sanitized contract evidence. Do not claim a
218
+ call-cost improvement when rounds exercise different negative/discovery cases.
219
+ Version fields usually describe an entity row, not all related aggregates;
220
+ state money units and the denominator of any budget or margin calculation.
221
+
222
+ ## Keep surface evidence distinct
223
+
224
+ Evidence from one surface does not prove another. Tool metadata and protocol
225
+ discovery do not prove that an MCP App rendered in its real host through
226
+ AppBridge; a REST response does not prove the Admin or application page; and a
227
+ browser rendering does not prove an external client can use the REST contract.
228
+ Record the actual browser, client or host version, identity, observed interface
229
+ and behavior for the deliverable under acceptance. Keep data results available
230
+ without Apps; descriptions cannot force a host's generative UI layout. Local
231
+ cookie-session acceptance, OAuth client integration and long-term renewal
232
+ likewise require their own evidence when they are part of the requested work.
@@ -0,0 +1,40 @@
1
+ ---
2
+ description: Scripted local checks before acceptance - handlers over real HTTP, public pages without JavaScript, MCP discovery, Admin's MCP App, and how to account for every surface.
3
+ ---
4
+ # Local HTTP checks
5
+
6
+ `pnpm check` proves that the manifests compile and the app typechecks; it runs no handler. Before you report an app
7
+ or a feature ready, run it on a fresh local D1 and drive it over HTTP. In an app written by `mantle init`, that is
8
+ `smoke.mjs` on the shell's `smoke-kit.mjs` (`pnpm smoke`): it applies `generated/schema.sql` and `seed.sql`, starts
9
+ `wrangler dev --local`, signs users in with the console email code and deletes everything afterwards.
10
+ These checks are the author's evidence. [Independent acceptance](agent-workflow-acceptance.md) comes after them and
11
+ does not replace them, nor they it.
12
+
13
+ ## What smoke covers
14
+
15
+ - **Every Procedure and HTTP Trigger the app declares:** valid input with the expected status and body (a status
16
+ below 500 is not success), invalid input, an anonymous call to a protected operation, the other role, and the
17
+ stored result read back. Use only the generated routes, inputs and permissions.
18
+ - **Business rules at their edges:** the oversell, the stale `expectedVersion`, the second approval, a refusal that
19
+ must leave nothing behind (check the rows with the kit's `sql`), and two concurrent calls where a race matters.
20
+ - **Each caller sees only its own rows:** a second member's records are absent from lists and refused by id.
21
+ - **Visitor pages:** the checks in [public pages](public-pages.md#check-before-you-report-done), requested without
22
+ JavaScript.
23
+ - **MCP:** `assertMcpDiscovery(endpoint, { tools, headers })` from `smoke-kit.mjs` for `/mcp` and `/mcp/admin`, with
24
+ the expected tools taken from generated code (the app-shell README shows it), plus a separate assertion that an
25
+ anonymous call and a member calling `/mcp/admin` are refused. Discovery needs no business `tools/call`.
26
+ - **Admin:** `pnpm build`, then `pnpm test` (`generated/admin-mcp-app.check.mjs`) loads the Admin MCP App in an MCP
27
+ host and fails on a network error, console error or exception. It needs `playwright-core` and Chrome
28
+ (`npx playwright-core install chromium`, or `MANTLE_TEST_CHROMIUM=<executable>`). With no browser it fails: report
29
+ it as BLOCKED, not skipped.
30
+
31
+ Better Auth allows 3 one-time-code requests per minute per client IP; the kit gives each user its own
32
+ `cf-connecting-ip`. Credentials come only from the kit's sign-in: a check never scrapes another session or builds a
33
+ Caller by hand.
34
+
35
+ ## Account for every surface
36
+
37
+ In the app's README or handoff, list each declared surface (Admin, each MCP endpoint, REST, each visitor page type)
38
+ as verified (command, identity and result), intentionally not exposed, or UNVERIFIED with a reason. Local cookie
39
+ sessions do not prove an external MCP host's OAuth connection or a production origin; record those separately.
40
+ Record the commands and their results for the exact commit, fix failures and rerun the affected checks.
@@ -107,6 +107,82 @@ An explicit `uri` keeps application-owned identity. A custom `html` function mus
107
107
  supply a build-specific `uri`: Core cannot infer its build or safely memoize its
108
108
  request-dependent output. A raw `html` string may use the automatic identity.
109
109
 
110
+ ### Keep old conversations working after an upgrade
111
+
112
+ A new URI does not invalidate a host's saved tool definitions or old conversation
113
+ cards. Keep previously deployed pages with `adminMcpApps({ catalogue, previous })`.
114
+ `previous` is an array of `AdminMcpSnapshot` from `@aotter/mantle-admin/mcp`:
115
+ `{ uri, html, csp?, domain?, title? }`. Each `html` includes the original `mantle-catalog`
116
+ script (the page returned by `resources/read`, not the raw bundle). The SDK serves
117
+ its original HTML and metadata at its original URI, filters its catalogue against
118
+ the current caller's tools on every read, and gives only the current build renderer
119
+ ownership. A removed/denied tool gains no authority through an old page.
120
+
121
+ Store these snapshots in the application's source or durable artifact storage;
122
+ an isolate cache or untracked `dist/` is insufficient for a clean CI deployment.
123
+ Keep an explicit retention window. Old pages outside that window and incompatible
124
+ tool renames/input/output changes need an application migration. Never answer an
125
+ old digest with the latest HTML. Host metadata refresh is a development/recovery
126
+ operation, not a normal compatible-upgrade requirement.
127
+
128
+ For the default built loader, share the exact declaration in
129
+ `src/mcp-app-definition.ts` (no `html` or `previous`):
130
+
131
+ ```ts
132
+ import { mcpAppCatalogues } from '../generated/admin-descriptors.ts';
133
+ export const appDefinition = { catalogue: mcpAppCatalogues.admin };
134
+ ```
135
+
136
+ Keep a committed `mcp-app-previous.json` at the project root (initially `[]`). Load
137
+ it only inside the service's MCP branch, before constructing the handler, so other
138
+ routes do not parse archived full-page HTML at startup:
139
+
140
+ ```ts
141
+ const { default: previous } = await import('../mcp-app-previous.json', { with: { type: 'json' } });
142
+ serveMcpHandler ??= serveMcp(tools, { ...options, apps: {
143
+ admin: adminMcpApps({ ...appDefinition, previous }),
144
+ } });
145
+ ```
146
+
147
+ Keep custom `renders`, catalogue extensions, `domain` and `title` in the shared
148
+ definition too. Node 22.18+ can read these generated `.ts` modules; a TypeScript
149
+ app using `.ts` import paths enables `allowImportingTsExtensions` with `noEmit`.
150
+ Keep snapshots outside the directory containing `admin.tsx`: Tailwind scans that
151
+ directory, and compiled HTML/JavaScript inside a snapshot can be mistaken for
152
+ utility class names and change subsequent CSS builds.
153
+
154
+ After a successful deployment, archive the **exact asset just deployed** with
155
+ this application-owned `scripts/archive-mcp-app.mjs`, then commit the snapshot
156
+ for the next build. Run it in the deployment script, not as a manual cache fix:
157
+
158
+ ```js
159
+ import { readFileSync, writeFileSync } from 'node:fs';
160
+ import { adminMcpApps } from '@aotter/mantle-admin/mcp';
161
+ import { appDefinition } from '../src/mcp-app-definition.ts';
162
+
163
+ const path = 'mcp-app-previous.json';
164
+ const previous = JSON.parse(readFileSync(path, 'utf8'));
165
+ const context = { env: {}, request: new Request('https://archive.invalid/mcp') };
166
+ const source = adminMcpApps({ ...appDefinition,
167
+ html: readFileSync('dist/client/admin/mcp-app.html', 'utf8') });
168
+ const { resources: [resource] } = typeof source === 'function' ? await source(context) : source;
169
+ const tools = new Set([...Object.keys(appDefinition.catalogue.views),
170
+ ...Object.keys(appDefinition.catalogue.actions)]);
171
+ const loaded = typeof resource.html === 'string' ? resource.html
172
+ : await resource.html({ ...context, tools });
173
+ const page = typeof loaded === 'string' ? { html: loaded } : loaded;
174
+ const snapshot = { uri: resource.uri, title: resource.title, ...page,
175
+ ...(resource.domain === undefined ? {} : { domain: resource.domain }) };
176
+ // This application's policy: retain the last two deployed pages for future builds.
177
+ writeFileSync(path, JSON.stringify([...previous.filter(p => p.uri !== snapshot.uri), snapshot].slice(-2)) + '\n');
178
+ ```
179
+
180
+ This archives static UI/catalogue metadata, never rows, cookies or OAuth tokens.
181
+ Custom loaders must archive their actual immutable resource, including CSP/domain,
182
+ instead of using this default-asset recipe. Compare the archived URI with deployed
183
+ `tools/list`/`resources/read`; a mismatching declaration or stale local build is
184
+ not deployment evidence.
185
+
110
186
  Core accepts static contributors or async `(context: { env, request }) => McpApps`
111
187
  sources, including mixed arrays. Static declarations are checked at construction;
112
188
  async sources resolve after the endpoint's origin and caller gates, then their
@@ -133,6 +209,8 @@ authorization, caller, origin and database behavior.
133
209
 
134
210
  ## The Admin App in a host
135
211
 
212
+ > **Verification status:** automated AppBridge coverage exists, but the compatibility claims in this section have not yet been verified manually in real ChatGPT or Claude hosts.
213
+
136
214
  The Admin App speaks MCP Apps `2026-01-26` to the host and asks only for what
137
215
  the host advertises in `hostCapabilities`:
138
216
 
@@ -175,7 +253,6 @@ full screen, a row action, and a link. Check too that the host advertises
175
253
  `hostCapabilities.serverTools`; without it the App is read-only. The automated interoperability test
176
254
  uses the official `AppBridge`; your app runs its generated version ([Run the compatibility test](#run-the-compatibility-test)).
177
255
 
178
-
179
256
  ## Customising Admin for MCP hosts
180
257
 
181
258
  `mantle bundle` builds the Admin MCP App from the same entry as the browser Admin
@@ -85,6 +85,9 @@ All paths below start with `/admin/api` and require an admin session; mutations
85
85
  also require the shared same-origin check. Mount `entries(...)` for collection
86
86
  routes. Send JSON with `Content-Type: application/json`.
87
87
 
88
+ These routes serve Admin's console with its session cookie; they are not a client API. A REST client reads back what it
89
+ wrote through the app's own Views with HTTP Triggers (`mantle help trigger`).
90
+
88
91
  | Method and path | Request / response |
89
92
  | --- | --- |
90
93
  | `POST /entries` | Body `{ collection, data }`; creates an entry. |
@@ -92,10 +95,15 @@ routes. Send JSON with `Content-Type: application/json`.
92
95
  | `PATCH /entries/{id}?collection=<schema>` | Body `{ data, expectedVersion }`; sends the observed version, not the next one. Publishing entries must be drafts. |
93
96
  | `POST /entries/{id}/publish?collection=<schema>` | Body `{ expectedVersion }`; validates required data before publishing. |
94
97
  | `POST /entries/{id}/unpublish?collection=<schema>` | Body `{ expectedVersion }`; returns the entry to draft. |
95
- | `POST /operations/{name}` | Body is the Procedure's input; runs an Admin operation (a Procedure with `requires: ctx.admin` or a `target`). |
98
+ | `POST /operations/{name}` | Body is the Procedure's input; runs an Admin operation (a Procedure with `requires: ctx.admin` or a `target`). Returns `{ ok: true, output }`, where `output` is what REST and MCP return (`{ results }` for a SQL Procedure). |
96
99
  | `GET /views/{name}` | Generated admin View query parameters plus `limit`/`cursor`; returns `{ rows, nextCursor? }`. |
97
100
  | `GET /view-batch?names=a,b` | One to 20 comma-separated admin View names, deduplicated; reads the first row of each as the caller, with the single route's checks (`requires`, scope, TTL, `authorize`), and returns `{ views: { <name>: { rows } \| { unavailable: true } \| { error: true } } }`. A refused View and a missing one are the same `{ unavailable: true }`; a View that cannot answer with no input is `{ error: true }`. Neither fails the batch. Empty or more than 20 names is a 400, before the database is acquired. |
98
101
  | `GET /me` | Returns `{ userId, role, admin, login, image }` for the signed-in caller. |
102
+ | `GET /admin` | Administrators and pending invitations, plus `adminRoles` (mounted with `identity`). |
103
+ | `GET /members?search=&limit=&cursor=&direction=` | Pages members (1–100), searching id, name and email. |
104
+ | `PATCH /admin/{id}/role` | Body `{ role }`: an admin role name grants Admin, `null` removes it. You cannot change your own role. |
105
+ | `POST /admin/invitations` | Body `{ email, role }`; invites an administrator by email. Returns `{ ok, userId, emailSent }`. |
106
+ | `DELETE /admin/invitations/{id}` | Revokes an invitation nobody has signed in to yet (otherwise 409). |
99
107
 
100
108
  Entry create/read/patch/transitions return
101
109
  `{ collection, entry, parentEntryId, parentEntryTitle, related }`; the entry has
@@ -296,7 +304,9 @@ Generated full/partial input checks validate creates, edits, operations and
296
304
  filters. Plain titled string `oneOf` choices use the same enum checks; general
297
305
  or constrained alternatives remain refused. Draft creates can be incomplete;
298
306
  publish checks required data. Publishing edits apply only to drafts. Read-only
299
- collections refuse generic edits while their declared Procedures can still run.
307
+ collections refuse generic creates, edits, deletes and transitions with 403 `AUTH_DENIED`
308
+ ("This record is read-only; use its operations.") before any read or write, while
309
+ their declared Procedures can still run.
300
310
 
301
311
  Writes use compiler-printed native Procedure frames, bind recipes and only the
302
312
  supplied field fragments. They do not update omitted columns or run a schema
@@ -344,6 +354,10 @@ The API advertises capabilities for actually mounted routes, including statistic
344
354
 
345
355
  ## Acceptance
346
356
 
357
+ Before reporting Admin pages or customisations as done, follow the [independent
358
+ acceptance guide](agent-workflow-acceptance.md) with a fresh evaluator in the
359
+ real Admin browser session.
360
+
347
361
  Use a disposable database for native tests, then exercise actual OTP sign-in,
348
362
  the Admin console and member frontend. Check invalid input, another tenant's
349
363
  rows, expired rows, publishing transitions, related/translated entries, paging,
@@ -367,7 +381,12 @@ field and stored value. The server resolves them for the caller with one page qu
367
381
  target. A missing, deleted or unreadable target is absent, so the three look the same: Admin shows the exact ID with copy.
368
382
  A resolved title links to the related record. Admin Views and the MCP App have no such step, since their rows come from the
369
383
  View itself; a View that wants a title selects it (join the referenced table, within its own `requires`). `/bootstrap?collection=...` can include
370
- the first requested list page. CSV exports use `/entries/export` and
384
+ the first requested list page.
385
+ In the entry editor and every operation form (including a `collectionAction` create), a string field with
386
+ `x-mantle-ref` to another Admin collection is a searchable picker: it lists the target through the same `/entries` route
387
+ (20 records per search, read as the caller), shows each record's title and stores the referenced value (`id`, or the
388
+ ref's unique field). A target the caller cannot list (403/404) or that is not an Admin collection keeps the plain text
389
+ input. The picker is a convenience; the server still validates the value. CSV exports use `/entries/export` and
371
390
  `/views/{name}/export`; native browser downloads stream paged server output.
372
391
  A failure after headers aborts the stream, so a download can fail after partial
373
392
  bytes have arrived. Existing databases need an app-owned migration for the
@@ -512,7 +531,7 @@ placement on a Schema without a sidebar entry of its own: a translation, or a fo
512
531
  [`standalone`](#collection-navigation-and-operation-placement).
513
532
  The purchasing example (`node_modules/@aotter/mantle/docs/examples/purchasing/`) puts its approval queue and requests in an Approvals group.
514
533
 
515
- Operational collections have no generic create or draft: officers add records through a Procedure placed with `collectionAction` (the list header shows its button and input form), so every write is a declared, validated program. For a user-field scoped Schema, SQL INSERT is system-only, so that Procedure is a ref handler that inserts through `ctx.store` ([procedure.md](../reference/procedure.md)).
534
+ Operational collections have no generic create or draft: officers add records through a Procedure placed with `collectionAction` (the list header shows its button and input form), so every write is a declared, validated program. For a user-field scoped Schema the Procedure can stay SQL; see [field scope](../reference/schema.md#field-scope) for the partition input.
516
535
 
517
536
  Admin operations are Procedures exposed by an admin MCP Trigger. Declare
518
537
  `Procedure.spec.uiSchema.collectionAction: <schema-name>` to show the operation
@@ -523,9 +542,13 @@ Names such as `approve` never determine placement. An operation without a row
523
542
  target or collection action appears on the Operations page, whose sidebar link
524
543
  appears whenever such operations exist.
525
544
 
545
+ A record's page shows its row operations as labeled buttons in the header (more than three: two buttons and a
546
+ "More actions" menu); list rows keep them in an "Actions for …" menu.
526
547
  Generated row metadata includes simple AND-ed equality/IN literal conditions
527
- from the target-row UPDATE/DELETE WHERE. The menu hides known mismatches; missing
528
- preview values and unsupported predicates leave actions available. These are
548
+ from the target-row UPDATE/DELETE WHERE, and an operation whose conditions the
549
+ row does not meet is hidden: with `WHERE … AND orderStatus = 'approved'`, "Mark
550
+ fulfilled" appears only once the order is approved. Missing preview values and
551
+ unsupported predicates leave actions available. These are
529
552
  presentation hints: the Procedure still enforces its WHERE and authorization.
530
553
  Conflicts show a localized refresh message with the technical diagnostic in
531
554
  Details. Successful actions show a short summary and changes, with raw output
@@ -25,8 +25,8 @@ query. Authored output names should be emitted into SQL where valid, avoiding
25
25
  an extra per-row projection merely to repair names already known at generation.
26
26
 
27
27
  Selected REST/MCP/Admin boundaries use static generated column lineage. Declared
28
- integers (including aliased system versions) are `number | string | null` on every
29
- target: safe integers become numbers; unsafe integers remain exact decimal text.
28
+ integers (including aliased system versions) are `number | string` on every
29
+ target, `| null` unless the View proves the column NOT NULL ([nullability](../reference/view.md#results-and-paging)): safe integers become numbers; unsafe integers remain exact decimal text.
30
30
  Selected SQL projections cast integers and hidden integer cursor keys to text
31
31
  before the driver reads them, preserving stored int64 values on SQLite/D1 and
32
32
  PostgreSQL. Values already rounded when written
@@ -34,7 +34,8 @@ PostgreSQL. Values already rounded when written
34
34
  `views/<name>.ts` exports both driver-native `Row` and `WireRow` (declared wire fields agree across targets);
35
35
  `paged/views/<name>.ts` keeps native `Row` rows; REST/MCP use separate
36
36
  `paged/wire/views/<name>.ts` SQL and convert at their boundary. SQLite/D1 declared JSON
37
- and boolean columns become JSON values and booleans, preserving aliases and nulls;
37
+ and boolean columns, and computed outputs that are JSON documents by construction
38
+ (JSON aggregates and builders), become JSON values and booleans, preserving aliases and nulls;
38
39
  PostgreSQL already returns those native values.
39
40
  Declared real/float outputs remain numbers. Computed outputs without proved lineage,
40
41
  SQL Procedures and app-owned ref results retain native values and types.
@@ -77,7 +78,7 @@ microseconds and days while PostgreSQL receives the canonical text. Computed val
77
78
  PostgreSQL through Bun.SQL that is a `Date`, which drops microseconds. Other
78
79
  declared scalars are driver-native.
79
80
 
80
- Driver-ready JSON parameters are text on every target (a JavaScript array would otherwise be bound as a PostgreSQL array), so the application serializes them. Bare PG SQL declares each JSON parameter as text
81
+ JSON parameters are text on every target (a JavaScript array would otherwise be bound as a PostgreSQL array): an input field read as JSON takes its authored array or object, which the generated query serializes, and a string is bound as it is. Bare PG SQL declares each JSON parameter as text
81
82
  (`CAST(CAST($n AS text) AS jsonb)`), so Bun.SQL sends the text verbatim instead of
82
83
  JSON-encoding it again; generation refuses a JSON parameter used any other way. SQLite bool/date/timestamp/numeric parameters use their declared
83
84
  storage representations. Bound optional fields become explicit required values,
@@ -58,6 +58,15 @@ unchanged. A POST registration whose redirect URIs are all HTTP localhost,
58
58
  127.0.0.1 or [::1] becomes `application_type: "native"` when omitted; explicit
59
59
  types and mixed/external callbacks stay under the SDK's validation.
60
60
 
61
+ With the optional Admin UI selected, use `consentPage:
62
+ "/admin/oauth/consent"`, as in `docs/examples/app-shell/src/auth.ts`, and keep
63
+ `adminShell` mounted in the service entry. That page uses mantle-ui's `AuthCard`,
64
+ cards and sign-in buttons; it loads the verified request from
65
+ `/oauth/consent/data` and posts the original signed payload to `/oauth/consent`.
66
+ The UI displays the client, callback origin, requested scopes and background
67
+ access explanation. Keep Core's script-free `/oauth/consent` HTML as the
68
+ adapter-neutral fallback, not as the reference application's styled page.
69
+
61
70
  `/oauth/consent` and `/oauth/consents` serve escaped, script-free English or zh-TW
62
71
  HTML. Their `/data` routes serve the 0.2 UI JSON shapes. Consent approval and
63
72
  `POST /oauth/consents/revoke` require a live cookie session and same-origin
@@ -84,6 +93,55 @@ Grant lifetime follows Better Auth. Better Auth issues a refresh token only when
84
93
  the grant includes `offline_access`; a client registered for `authorization_code`
85
94
  may use `refresh_token` too, so registering only `authorization_code` does not
86
95
  prevent long-lived grants.
96
+ CIMD is the primary client-discovery path; dynamic client registration (DCR)
97
+ remains available for older MCP clients. Both paths require the client to request
98
+ `offline_access` and the user to consent before a renewable grant is issued.
99
+ The DCR registration defaults below apply only to that compatibility path.
100
+
101
+ For renewable MCP connections, the examples set both `scopes` and
102
+ `clientRegistrationDefaultScopes` to `["mcp", "offline_access"]`, so a dynamically
103
+ registered client that omits `scope` is registered for `mcp offline_access`. The
104
+ client must request those registered scopes during authorization and the user must
105
+ consent to background access. If the client explicitly requests only `mcp`, OAuth
106
+ must not add a scope the user did not consent to: the resulting grant is bound to
107
+ the browser session and has no refresh token. The client must request
108
+ `offline_access`, or the user must reconnect and authorize it, to gain renewable
109
+ access. Declaring the `refresh_token` grant type alone is insufficient.
110
+ `offline_access` belongs in authorization-server metadata, not in the protected
111
+ resource's required scope floor; resource metadata continues to advertise `mcp`.
112
+
113
+ For renewable connections, the app-shell's MCP handler also supplies an explicit
114
+ OAuth linking hint:
115
+
116
+ ```ts
117
+ const handleMcp = mcp({
118
+ db: (env: Env) => env.DB,
119
+ caller: resolveRequestCaller,
120
+ authorizationScopes: ["offline_access"],
121
+ });
122
+ ```
123
+
124
+ `authorizationScopes` adds scopes to the standard `WWW-Authenticate` challenge
125
+ when the caller is unauthenticated. With the default `requiredScopes`, that
126
+ challenge requests `mcp offline_access`. It does **not** change the API scope
127
+ floor, add scopes to a valid token, issue tokens or modify an already-signed
128
+ authorization request. An existing valid `mcp`-only token can still call a tool
129
+ that only requires `mcp`. The application must independently permit
130
+ `offline_access` in its provider and the user must explicitly consent.
131
+ Verify the actual requested scopes, refresh-token issuance and post-expiry
132
+ rotation in the intended client. Do not put `offline_access` in the resource's
133
+ required scope floor or silently add it after consent.
134
+
135
+ For an existing connection granted only `mcp`, deploy the allowed-scope change,
136
+ refresh the client's discovery/registration metadata, then reconnect or reauthorize
137
+ once with `offline_access` and obtain updated consent. Allowing a scope does not
138
+ add it to a stored consent. Do not edit consent rows, prolong access-token TTLs or
139
+ disable grant checks to avoid that step. Verify a refresh token was issued and
140
+ that the client can rotate it and resume MCP after access expiry; a successful
141
+ initial `tools/list` does not prove renewable authorization. The reference service's
142
+ local smoke exercises issuance, refresh after website sign-out, rotation retry
143
+ and revocation using real OAuth endpoints and a disposable D1.
144
+
87
145
  Without `offline_access` the grant is bound to the browser session: its tokens
88
146
  stop working when that session expires or the user signs out. A refresh that
89
147
  narrows the scope to drop `offline_access` after the session ends returns