@aotter/mantle 0.2.0-alpha.7 → 0.2.0-alpha.9

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 (126) hide show
  1. package/README.md +27 -5
  2. package/dist/auth/adapter.js +9 -6
  3. package/dist/auth/caller.js +2 -1
  4. package/dist/auth/identity.d.ts +2 -2
  5. package/dist/auth/oauth.d.ts +33 -1
  6. package/dist/auth/oauth.js +121 -28
  7. package/dist/auth/routes.js +6 -4
  8. package/dist/build/compiler.d.ts +44 -122
  9. package/dist/build/compiler.js +202 -277
  10. package/dist/build/spec/domain/model/ManifestGrammar.d.ts +6 -5
  11. package/dist/build/spec/domain/model/SqlIr.d.ts +2 -0
  12. package/dist/build/spec/domain/service/PlanFingerprint.d.ts +2 -0
  13. package/dist/build/spec/kernel/diagnostic.d.ts +1 -1
  14. package/dist/domain/describe/{chunk-ZIQVLA7X.js → chunk-ZVXBGP7Y.js} +69 -8
  15. package/dist/domain/describe/index.js +1 -1
  16. package/dist/domain/describe/target-hash.js +1 -1
  17. package/dist/domain/describe/targets.d.ts +1 -1
  18. package/dist/domain/generated-tools.js +2 -0
  19. package/dist/domain/paging.js +41 -15
  20. package/dist/infrastructure/bundle.js +118 -15
  21. package/dist/infrastructure/cli.js +11 -15
  22. package/dist/infrastructure/generate.d.ts +1 -3
  23. package/dist/infrastructure/generate.js +11 -3
  24. package/dist/infrastructure/generateAuth.js +57 -0
  25. package/dist/infrastructure/inputs.js +8 -3
  26. package/dist/infrastructure/wrangler.js +50 -12
  27. package/dist/kernel/binds.d.ts +0 -3
  28. package/dist/kernel/contracts.d.ts +1 -1
  29. package/dist/kernel/contracts.js +4 -4
  30. package/dist/kernel/dialect/common.d.ts +0 -1
  31. package/dist/kernel/drain.d.ts +5 -0
  32. package/dist/kernel/drain.js +35 -0
  33. package/dist/kernel/driver/postgres.js +6 -2
  34. package/dist/kernel/host-caller.d.ts +1 -0
  35. package/dist/kernel/host-caller.js +1 -0
  36. package/dist/kernel/mcp-apps.d.ts +29 -9
  37. package/dist/kernel/mcp-apps.js +3 -3
  38. package/dist/kernel/mcp-handler.js +101 -18
  39. package/dist/kernel/mcp.d.ts +12 -3
  40. package/dist/kernel/mcp.js +31 -6
  41. package/dist/kernel/rest-router.js +3 -2
  42. package/dist/kernel/scalars.js +3 -2
  43. package/dist/kernel/site.d.ts +1 -3
  44. package/dist/kernel/site.js +2 -2
  45. package/dist/kernel/web-index.js +40 -0
  46. package/dist/kernel/web.d.ts +3 -1
  47. package/dist/kernel/web.js +6 -37
  48. package/dist/usecase/adminBindings.js +2 -3
  49. package/dist/usecase/adminClient.js +87 -0
  50. package/dist/usecase/adminDescriptors.js +19 -65
  51. package/dist/usecase/generate.js +5 -4
  52. package/dist/usecase/mcp-media.js +2 -2
  53. package/dist/usecase/mcp.js +16 -12
  54. package/dist/usecase/paging.js +2 -3
  55. package/dist/usecase/publishing.js +3 -3
  56. package/dist/usecase/site.js +0 -1
  57. package/dist/usecase/tool-version.js +9 -0
  58. package/dist/usecase/translations.js +1 -1
  59. package/dist/usecase/validation.js +128 -19
  60. package/dist/usecase/web.js +3 -2
  61. package/docs/examples/README.md +1 -1
  62. package/docs/examples/app-shell/README.md +45 -3
  63. package/docs/examples/app-shell/package.json +14 -6
  64. package/docs/examples/app-shell/smoke-kit.mjs +54 -0
  65. package/docs/examples/app-shell/src/auth.ts +6 -3
  66. package/docs/examples/app-shell/src/service.ts +3 -3
  67. package/docs/examples/app-shell/tsconfig.json +4 -1
  68. package/docs/examples/app-shell/worker-configuration.d.ts +2 -2
  69. package/docs/examples/app-shell/wrangler.jsonc +5 -4
  70. package/docs/examples/club-events/manifests/club-events.yaml +7 -0
  71. package/docs/examples/club-events/src/handlers.ts +1 -1
  72. package/docs/examples/generated-web/README.md +6 -2
  73. package/docs/examples/generated-web/wrangler.jsonc +1 -0
  74. package/docs/examples/purchasing/README.md +1 -1
  75. package/docs/examples/purchasing/manifests/purchasing.yaml +17 -1
  76. package/docs/examples/purchasing/smoke.mjs +10 -4
  77. package/docs/examples/purchasing/src/admin.tsx +13 -0
  78. package/docs/examples/purchasing/src/handlers.ts +1 -1
  79. package/docs/examples/purchasing/wrangler.jsonc +5 -4
  80. package/docs/examples/recipes/wrangler.jsonc +5 -4
  81. package/docs/handbook/cloudflare/authentication.md +43 -6
  82. package/docs/handbook/cloudflare/chatgpt-sites.md +4 -2
  83. package/docs/handbook/cloudflare/deploy-and-operate.md +45 -1
  84. package/docs/handbook/cloudflare/media-r2.md +2 -3
  85. package/docs/handbook/cloudflare/service-entry.md +68 -31
  86. package/docs/handbook/concepts/authorization.md +1 -1
  87. package/docs/handbook/concepts/mcp-and-agents.md +19 -0
  88. package/docs/handbook/concepts/runtime-and-adapters.md +1 -1
  89. package/docs/handbook/examples/hub.md +1 -1
  90. package/docs/handbook/guides/admin-ui.md +107 -1
  91. package/docs/handbook/guides/agent-setup.md +27 -9
  92. package/docs/handbook/guides/agent-workflow-acceptance.md +222 -0
  93. package/docs/handbook/guides/mcp-apps.md +522 -6
  94. package/docs/handbook/guides/native-admin.md +300 -51
  95. package/docs/handbook/guides/oauth.md +119 -5
  96. package/docs/handbook/guides/public-pages.md +2 -2
  97. package/docs/handbook/guides/size-and-startup.md +1 -1
  98. package/docs/handbook/guides/typed-queries.md +35 -0
  99. package/docs/handbook/navigation.json +0 -4
  100. package/docs/handbook/reference/admin-descriptors.md +0 -35
  101. package/docs/handbook/reference/procedure.md +6 -2
  102. package/docs/handbook/reference/schema.md +13 -3
  103. package/docs/handbook/reference/surface.md +129 -15
  104. package/docs/handbook/reference/trigger.md +43 -1
  105. package/docs/handbook/reference/view.md +1 -1
  106. package/docs/handbook/releases/index.md +74 -2
  107. package/docs/handbook/start/overview.md +1 -1
  108. package/docs/handbook/start/project-and-cli.md +16 -8
  109. package/docs/handbook/start/quickstart-worker.md +1 -1
  110. package/docs/skills/develop/SKILL.md +46 -18
  111. package/docs/skills/integrate/SKILL.md +4 -9
  112. package/docs/skills/media-gc/SKILL.md +7 -5
  113. package/docs/skills/provision/SKILL.md +18 -12
  114. package/docs/skills/update/SKILL.md +19 -18
  115. package/docs/upgrade-0.1-to-0.2.md +6 -6
  116. package/package.json +2 -1
  117. package/skills/README.md +11 -16
  118. package/skills/mantle/SKILL.md +39 -17
  119. package/skills/mantle/references/local-testing.md +9 -0
  120. package/skills/mantle/references/web.md +1 -1
  121. package/skills/mantle/references/website.md +26 -22
  122. package/docs/examples/app-shell/src/admin.ts +0 -10
  123. package/docs/examples/purchasing/src/admin.ts +0 -12
  124. package/docs/handbook/guides/admin-extensions.md +0 -20
  125. package/docs/skills/develop/mcp-app.md +0 -39
  126. package/skills/.keep +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  The SDK ships kernel, CLI, generator and private compiler together. Request modules do not import compiler code. Import only selected generated operations and bindings; direct operations can omit Store/Admin/auth entirely.
4
4
 
5
- Workers use `mantle bundle` to emit split ESM. Keep Better Auth behind `authLoader(() => import(...))`; the startup graph guard rejects eager auth imports. The official MCP SDK and selected Admin operations load on matching requests. Generated module imports perform no SQL or kernel initialization; Store metadata initializes on first use without SQL.
5
+ Workers use `mantle bundle` to emit split ESM. Keep Better Auth behind `authLoader(() => import(...))`; the startup graph guard rejects eager auth imports, and Admin's browser code (`@aotter/mantle-admin/app`, `/hooks`, and so `generated/admin-client.ts`): import those only from `admin.tsx`, which `mantle bundle` builds into the assets directory. The official MCP SDK and selected Admin operations load on matching requests. Generated module imports perform no SQL or kernel initialization; Store metadata initializes on first use without SQL.
6
6
 
7
7
  Contributor checks use tracked fixtures to compare runtime/startup graphs against `size-budgets.json` and inspect the published allowlist in `check:package`. Package packed/unpacked bytes are reported separately from request startup and query costs. Do not infer latency from bundle bytes.
8
8
 
@@ -90,3 +90,38 @@ Interactive `transaction(async tx => …)` exists on bun:sqlite and PostgreSQL,
90
90
  Ref handlers receive `{ caller, db, store }` bound to the same Caller and transaction. Implement the generated `Handlers` interface and pass it to `store(db, caller, { handlers })`. Direct Procedure modules expose SQL Procedures; ref Procedures are reached through the bound Store or selected surfaces.
91
91
 
92
92
  See [native execution](native-execution.md) for values and write outcomes and [migrations](../concepts/runtime-and-adapters.md#schema-installation-and-migrations) before changing a Schema.
93
+
94
+ ## Prefer Store-backed ref Procedures for simple writes
95
+
96
+ For a single-entity create or partial update, prefer a declared `ref` Procedure
97
+ whose handler awaits its caller-bound `ctx.store.collection(...).insert(...)`
98
+ or `.update(id, allowedPatch, { lock: expectedVersion })`. Keep the Procedure's
99
+ `requires` and generated validation at its invocation boundary: Store enforces
100
+ row policy, but does not substitute for operation authorization. Do not invoke
101
+ a handler directly from a route and accidentally bypass those checks.
102
+
103
+ Insert/update returns the actual written native row. Inside a transaction,
104
+ that result is provisional until the outer transaction commits; later failure
105
+ can still roll it back. Report committed success or emit external effects only
106
+ after that transaction succeeds. Return an explicit
107
+ allowlist of useful business fields, its backend `id` and returned `version`;
108
+ do not spread the entire row, echo the submitted version, calculate a new
109
+ version instead of reading the result, or add a generic receipt adapter. These
110
+ ref results are app-owned: declare their output schema and make any required
111
+ native-to-wire conversion explicit at the boundary.
112
+
113
+ Patch only permitted fields. Omitted fields remain unchanged; define whether
114
+ explicit null clears a nullable field or is invalid. Do not expand a partial
115
+ request into a full replacement with defaults for unseen fields. A stale lock
116
+ is a conflict; preserve intent and reconcile/review rather than silently
117
+ refreshing the lock and retrying.
118
+
119
+ Use SQL Procedures when conditions or related effects must commit together:
120
+ for example, creating an order while decrementing stock under a CHECK, a state
121
+ transition guarded by an atomic WHERE, or an additive restock upsert. A
122
+ pre-read in TypeScript followed by an unguarded write is not equivalent.
123
+ Store `batch` is atomic for known writes; a D1 batch step cannot consume a prior
124
+ step's returned ID. D1 has no interactive transaction callback, so retain the
125
+ supported SQL program or another existing native atomic boundary for that case.
126
+ The [MCP design audit](agent-workflow-acceptance.md) applies these choices before
127
+ implementation and separates them from independent end-user acceptance.
@@ -41,10 +41,6 @@
41
41
  "text": "Customize Admin",
42
42
  "link": "/guides/admin-ui"
43
43
  },
44
- {
45
- "text": "Extend Admin",
46
- "link": "/guides/admin-extensions"
47
- },
48
44
  {
49
45
  "text": "Another host or engine",
50
46
  "link": "/guides/adapter-guide"
@@ -45,44 +45,9 @@ stars, CTEs and subqueries). Translation
45
45
  metadata and selected execution share the generated translation policy. Child
46
46
  fields win; private scope fields cannot be overlaid by inherited parent fields.
47
47
 
48
- Pass declarations with `mantle generate --admin-extensions ./admin-extensions.mjs`.
49
- The module exports a named `adminExtensions` array or a default array; it should
50
- contain plain declarations, without constructing runtime services:
51
-
52
- ```js
53
- export const adminExtensions = [{ id: 'audit', contributes: {
54
- fields: [{ id: 'color', target: 'field.input/v1',
55
- optionsSchema: { type: 'object', properties: { palette: { type: 'string' } } }
56
- }]
57
- }}];
58
- ```
59
-
60
- The programmatic generation options accept the same declarations:
61
-
62
- ```ts
63
- await generateFromSources({ target, sources,
64
- adminExtensions: [{ id: 'audit', contributes: {
65
- fields: [{ id: 'color', target: 'field.input/v1',
66
- optionsSchema: { type: 'object', properties: { palette: { type: 'string' } } }
67
- }]
68
- }}]
69
- });
70
- ```
71
-
72
- Only `id` and `contributes` are needed at generation. `extensionBindings` records
73
- Schema/Procedure field widgets, Schema/admin View cells and sidebar panels.
74
- Unknown contributions, wrong targets and invalid options fail generation with
75
- `UI_EXTENSION_UNKNOWN`, `UI_EXTENSION_TARGET` or `UI_EXTENSION_OPTIONS`, retaining
76
- 0.2 paths. Serving extension modules and handling extension actions is separate.
77
-
78
48
  `adminBindings()` provides the selected adapter and checked entry/View/operation
79
49
  bindings. The server shares native read templates across collections; descriptors
80
50
  carry compiler-verified policy/field fragments and generated validators, not a
81
51
  copy of full SQL for every order/direction. Metadata endpoints perform no storage
82
52
  read of their own; caller resolution and optional site/user enrichment may query.
83
53
  Statistics capability is advertised only when its native route is mounted.
84
-
85
- Full extension declarations also emit `extensionMetadata` and, with Admin selected,
86
- `admin-extensions.ts` with a runtime `extensionBindings` function and compiled
87
- settings checks. Native modules are host-served; install integrity in the host
88
- shell import map. `source` modules are refused.
@@ -4,7 +4,7 @@ A Procedure requires `input`, `output` JSON Schemas and a `handler` containing S
4
4
 
5
5
  SQL uses declared Schemas, `input.<name>`, `auth.uid()` and native expressions accepted by the selected compiler dialect. Compiler policy supplies scope and system fields. Do not assign scope, version, status, published_at, timestamps or author columns yourself. Publishing writes use generated transitions. `restrict` narrows the rows the statements read and write, as on a [View](view.md#restrict); it is refused on ref handlers.
6
6
 
7
- A row-classified INSERT/UPDATE/DELETE (one that targets a single row, e.g. `WHERE id = input.id`) must affect one row. Use `WHERE id = input.id AND version = input.expectedVersion` for explicit OCC. Set operations, including UPDATE with a subquery and `INSERT ... SELECT`, may affect zero rows without an error: check the returned rows when zero means failure. A unique clash, a failed Schema `checks` constraint (the message starts with `A check of the Schema failed`; SQLite and D1 add the check expression, PostgreSQL the constraint name) or a failed row assertion is `CONFLICT` (409); statements run atomically. Results are raw `{ results }`, not runtime-validated authored outputs. RETURNING uses SQL output names; quote an explicit alias to preserve declared case (`RETURNING reqStatus AS "reqStatus"`). Generation warns about unaliased direct fields whose declared name differs from storage.
7
+ A row-classified INSERT/UPDATE/DELETE (one that targets a single row, e.g. `WHERE id = input.id`) must affect one row. Use `WHERE id = input.id AND version = input.expectedVersion` for explicit OCC. Set operations, including UPDATE with a subquery and `INSERT ... SELECT`, may affect zero rows without an error: check the returned rows when zero means failure. A unique clash, a failed Schema `checks` constraint or a failed row assertion is `CONFLICT` (409); statements run atomically. A check failure keeps that code and its existing `A check of the Schema failed` message, and sets `ContractError.fields` to `{ kind: "check", schema, check }`; `check` is the expression on SQLite/D1 and the generated constraint name on PostgreSQL. Results are raw `{ results }`, not runtime-validated authored outputs. RETURNING uses SQL output names; quote an explicit alias to preserve declared case (`RETURNING reqStatus AS "reqStatus"`). Generation warns about unaliased direct fields whose declared name differs from storage.
8
8
 
9
9
  A row-classified UPDATE/DELETE on an excluded or missing id affects zero rows and returns HTTP 409 (`CONFLICT`), just like a stale version; reads return not found. Native Admin's visible-row pre-read returns 404 for excluded/missing ids.
10
10
 
@@ -12,8 +12,12 @@ A ref handler is application code called as `(ctx, input)`. Generation prints re
12
12
 
13
13
  `target: { schema, id, version? }` binds an Admin operation to a row. SQL targets can be inferred for a single locked row operation; declare a target for a ref operation when needed. Input fields must have the required shapes.
14
14
 
15
+ ## MCP tool hints
16
+
17
+ Every generated AI-facing tool (MCP, WebMCP, publishing and media tools) carries a `title`, `readOnlyHint` and `destructiveHint`; clients such as Claude use them to decide between auto-approval and confirmation. The title is the declared `title`, else the name made readable (`add-item` reads `Add item`). A View is `readOnlyHint: true, destructiveHint: false`. A Procedure is `readOnlyHint: false`, and `destructiveHint` is `false` only when its SQL inserts without overwriting: any DELETE or UPDATE, an `ON CONFLICT DO UPDATE` or a ref handler makes it `true`. Of the generated publishing transitions only `archive` is destructive. The media tools always carry `openWorldHint: true`; the upload policy, list and get tools are read-only and only `delete_media_asset` is destructive. Admin's WebMCP navigation tools (`admin_get_context`, `admin_navigate`) are read-only. `spec.mcp` accepts `readOnlyHint`, `destructiveHint` and `openWorldHint`, and a declaration always wins; a declared `readOnlyHint: true` defaults `destructiveHint` to `false`. Outside the media tools `openWorldHint` appears only when declared. `idempotentHint` is never generated, and `spec.mcp` refuses it. Hints never change authorization.
18
+
15
19
  Generated requires checks gate calls. Boundary input validators are generated separately; direct queries accept driver-ready, app-validated input. Dynamic guards and lifecycle hooks are rejected. See [native execution](../guides/native-execution.md) and [surface contracts](surface.md).
16
20
 
17
21
  SQL INSERT/upsert into a user-field scoped Schema is system-only. For a verified user, insert through `ctx.store` so membership and single-partition autofill run before the native write. `ctx.store` speaks storage names, not the manifest's: the handler's `input` uses authored names (`input.parentId`), while Store writes, filters and rows use the folded column (`insert({ parentid: input.parentId })`, `where: { parentid }`, `row.parentid`); generated Store types show the exact names ([typed queries](../guides/typed-queries.md)). SET cannot change a field-scope column, including ON CONFLICT DO UPDATE. Scoped unique conflict targets gain the owner/field prefix automatically.
18
22
 
19
- Triggers are the only way request data reaches a Procedure: REST and MCP validate the input against the Procedure's schema, then call it. App code may also call a generated Procedure directly (`import run from "../generated/procedures/<name>.js"`, `await run(db, caller, input)`); `requires`, row policy and checks still apply, but input validation does not, so pass only data the app produced or has verified (a cron job, a callback after its signature check). A page that takes user input submits it to the Procedure's REST Trigger; see `docs/examples/purchasing/src/requests-page.ts`.
23
+ Triggers are the only way request data reaches a Procedure: REST and MCP validate the input against the Procedure's schema, then call it. App code may also call a generated Procedure directly (`import run from "../generated/procedures/<name>.js"`, `await run(db, caller, input)`); `requires`, row policy and checks still apply, but input validation does not, so pass only data the app produced or has verified (a cron job, a callback after its signature check). A page that takes user input submits it to the Procedure's REST Trigger; see `node_modules/@aotter/mantle/docs/examples/purchasing/src/requests-page.ts`.
@@ -54,7 +54,17 @@ Owner and field columns prefix every unique index in that order. User reads/writ
54
54
 
55
55
  ## Indexes, checks and search
56
56
 
57
- Indexes are ordered composite scalar/native columns. A paged View's `ORDER BY` must match an index on the base Schema after any fixed equality prefix, ending with `id` as the tiebreak: `ORDER BY createdAt DESC, id DESC` needs `indexes: [[createdAt, id]]` (generation reports `Missing index for ORDER BY ...` otherwise). Required operational fields are NOT NULL; publishing drafts may be incomplete. Store sortable types use indexed/non-null columns, with explicit nullable published_at support.
57
+ Indexes are ordered composite scalar/native columns. A paged View's `ORDER BY` must match an index on the base Schema after any fixed equality prefix ([index prefixes](#index-prefixes-for-paged-views)), ending with `id` as the tiebreak: `ORDER BY created_at DESC, id DESC` needs `indexes: [[createdAt, id]]` (generation reports `Missing index for ORDER BY ...` otherwise). Required operational fields are NOT NULL; publishing drafts may be incomplete. Store sortable types use indexed/non-null columns, with explicit nullable published_at support.
58
+
59
+ ### Index prefixes for paged Views
60
+
61
+ Only equality fixes an index prefix: `col = input.x`, `col = 'literal'` and an `= auth.uid()` scope or restrict. A user-field term (`region IN auth.user('region')`, as a `scope` or `restrict`) binds a set of values, so it never fixes a prefix, even when the caller holds one value or `{ all: true }`: the statement is prepared for a set, and an `ORDER BY` after that column would read and sort all of the caller's rows on every page. With `scope: "region IN auth.user('region')"`, `indexes: [[region, orderNo, id]]` and `ORDER BY orderNo`, generation refuses the View, names the bounded order and links here. The bounded ways:
62
+
63
+ - `ORDER BY region, orderNo`, served by the same index: rows come back grouped by region. `region DESC, orderNo DESC` reads it backwards; on PostgreSQL confirm with `EXPLAIN` that no Sort remains. Every column added to the `ORDER BY` must be non-null and nameable in SQL; a `publishing` Schema field other than a user-field scope column can be null, and the `scope` owner column cannot be named, so the refusal then says this order is unavailable.
64
+ - A View input bound with equality, `WHERE region = input.region ORDER BY orderNo`: the input fixes the prefix, and the scope still applies. Every set-bound column before the ordered ones needs its own equality.
65
+ - For `{ all: true }` callers, an index on the equality-fixed columns, then the ordered columns, then `id`: `[orderNo, id]` here, `[tier, orderNo, id]` with `WHERE tier = input.tier`. It is not bounded for scoped callers, who scan the index and filter, and it does not help a View with any `restrict`, which [`{ all: true }` never lifts](view.md#restrict).
66
+
67
+ Declaring both indexes does not make one View bounded for every caller. System (trusted) reads drop scope terms, so a scope-first index such as `[owner, at]` is not a proved range scan for them.
58
68
 
59
69
  `uniqueIndexes` accept declared fields only, never native entry columns such as `authorId`. For per-member uniqueness, declare a required string `member` field, write `member: caller.subject` in a handler, and use `uniqueIndexes: [[member, orderNumber]]`; do not take `member` from client input.
60
70
 
@@ -68,9 +78,9 @@ The runtime never creates, alters or checks tables. Initial DDL is for an empty
68
78
 
69
79
  A property marked `readOnly: true` in the JSON Schema is server-maintained (a counter, a total): Admin's generic create stores its `default` whatever the input says, Admin edits ignore it, and only Procedures (or server code through the Store) change it afterwards. JSON Schema defaults are applied by Admin and the Store, not by the database: a SQL Procedure that inserts the row sets the field itself. A root `readOnly: true` makes the whole Schema read-only in Admin.
70
80
 
71
- `fields.<name>.widget/options` select supported controls or extension contributions; `list.primaryField`, columns/cells and enum `filterField` select list presentation; `nav.standalone/parentField` control child navigation; `nav.group/order/hidden` place it in the sidebar ([sidebar](../guides/native-admin.md#sidebar)); panels select record sidebars. Generation validates these against declared fields/indexes and selected extension declarations. JSON Schema root `schema.readOnly: true` disables generic Admin writes, not declared Procedures; `uiSchema.readOnly` is rejected with `SCHEMA_UI_INVALID`. `x-mantle-ref` relates a field to another Schema id or single-field unique field.
81
+ `fields.<name>.widget` selects a built-in control (`textarea`); `list.primaryField`, `list.columns` and enum `filterField` select list presentation; `nav.parentField` chooses the parent a child folds under (required when two or more required `id` references exist) and `nav.standalone` lists the child in the sidebar; `nav.group/order/hidden` place it in the sidebar ([sidebar](../guides/native-admin.md#sidebar)). `list.columns` must not repeat `list.primaryField` or itself (`SCHEMA_UI_INVALID`). Generation validates these against declared fields/indexes. Components that render a field, cell or record aside are not manifest references: they live in `defineAdmin` ([customising Admin](../guides/native-admin.md#customising-admin)), and a manifest that still names one (`widget: <extension>/<contribution>`, `widget` `options`, `list.cells`, `panels`) is refused. JSON Schema root `schema.readOnly: true` disables generic Admin writes, not declared Procedures; `uiSchema.readOnly` is rejected with `SCHEMA_UI_INVALID`. `x-mantle-ref` relates a field to another Schema id or single-field unique field.
72
82
 
73
- See [Admin](../guides/admin-ui.md), [extensions](../guides/admin-extensions.md) and [diagnostics](diagnostics.md).
83
+ See [Admin](../guides/admin-ui.md) and [diagnostics](diagnostics.md).
74
84
 
75
85
  ### Field scope
76
86
 
@@ -13,7 +13,7 @@
13
13
  snake-cased View names on each declared surface, and snake-cased Procedure
14
14
  names per MCP Trigger. Its SDK loads only through a request-time dynamic import.
15
15
  - HTTP/MCP Triggers declare exposure only. `mantle generate` refuses schedule
16
- and lifecycle Triggers; Cloud scheduling is a separate capability.
16
+ and lifecycle Triggers; host scheduling is a separate capability.
17
17
  - Native Admin retains selected 0.2 paths and the console contracts; see
18
18
  [Native Admin](../guides/native-admin.md) for app-mounted
19
19
  `adminBindings()` and `entries(bindings.schemas, bindings.adapter)`.
@@ -25,21 +25,43 @@ challenges, JSON-RPC, web rendering, keyset paging) lives in the package's
25
25
  `rest`, `mcp`, `web`, `paging` and `validation` kernel modules, so a runtime fix
26
26
  ships as a package update instead of a regeneration.
27
27
 
28
+ Procedure surface selection follows the [HTTP/MCP pairing default and deliberate asymmetry exception](trigger.md); no surface is implied.
29
+
28
30
  The generated `validation/views/<name>.ts` and
29
31
  `validation/procedures/<name>.ts` export `validate(unknown)`. View validators
30
32
  also export `query(URLSearchParams)` returning `{ input, limit?, cursor? }`.
31
33
  REST and MCP use the same generated checks, with no runtime schema interpreter.
32
34
  Supported checks are types/unions and nullable, required/properties/additionalProperties,
33
- arrays/items and length bounds, enum, local `$defs`/`$ref`, string length/pattern,
34
- numeric bounds/`multipleOf`, and date/date-time/email. The deliberately permissive
35
+ arrays/items and length bounds, enum, `const`, local `$defs`/`$ref`, string length/pattern,
36
+ numeric bounds/`multipleOf`, and the formats `date`, `date-time`, `email` and `uri`. The deliberately permissive
35
37
  email check requires exactly one `@`, non-empty local and domain parts, and no
36
38
  whitespace; it does not require a dot, verify deliverability, or implement full
37
- RFC mailbox syntax. Integers must be safe; fractional
38
- `multipleOf` uses a 1e-9 quotient tolerance. `const`, `oneOf`, `uniqueItems` and
39
- object property-count bounds are refused, alongside other unsupported keywords
40
- and formats. Defaults remain
39
+ RFC mailbox syntax. `uri` means an absolute `http:` or `https:` URL, stricter than
40
+ JSON Schema's any-scheme URI, so `javascript:` and `data:` links fail. Integers must be safe; fractional
41
+ `multipleOf` uses a 1e-9 quotient tolerance. Defaults remain
41
42
  annotations and are not inserted into request input.
42
43
 
44
+ View and Procedure input objects (not Schemas or outputs) also take three rules
45
+ between their declared properties, each failing with one `INPUT_VALIDATION_FAILED`
46
+ error whose message names the rule:
47
+
48
+ - `dependentRequired: { weight: [unit] }`: "'unit' is required when 'weight' is
49
+ present.", at the missing field.
50
+ - `anyOf` whose every branch is exactly `{ required: [...] }`: "At least one of
51
+ 'weight', 'heightCm' is required.", at the object.
52
+ - `if: { properties: { undo: { const: true } }, required: [undo] }` with `then`
53
+ and/or `else` each exactly `{ required: [...] }`: "'item' is required unless
54
+ 'undo' is true.", at the missing field. `if` must list its field in `required`:
55
+ otherwise an absent field would satisfy it.
56
+
57
+ Any other `anyOf`/`if`/`then`/`else`, `oneOf`, `uniqueItems`, object property-count
58
+ bounds and other keywords and formats are refused at generation. The rules only
59
+ validate: types, SQL binds and Admin forms keep the declared fields, and Admin
60
+ shows the server's error at its field. `describe` keeps them as written; MCP and
61
+ WebMCP tools advertise the schema without them and append each rule's sentence to
62
+ the tool description, since some model APIs refuse these keywords in a tool's
63
+ input schema. [Trigger](trigger.md#input-rules) has an example.
64
+
43
65
  Native `paged/views/<name>.ts` has a default export, callable as
44
66
  `page(db, caller, input, { limit?, cursor? })` after importing it as `page`,
45
67
  answering `{ rows, nextCursor? }`.
@@ -56,10 +78,18 @@ their contracts. Native integer bigint cursor keys are serialized separately.
56
78
  REST/MCP use separate `paged/wire/views/<name>.ts` SQL when declared integers
57
79
  require exact transport projections. Web pages and internal paged Views keep
58
80
  driver-native rows. Internal paging creates no public route/tool.
59
- Authored SQL `LIMIT` caps each page, up to 500, rather than the whole cursor walk.
81
+ Authored SQL `LIMIT` bounds the complete ordered row set, including across cursor pages.
82
+ Use `limit` in the paging options to choose each page size (up to 500); it does not expand the View's declared result set.
83
+ An MCP View page keeps the `limit` of 1 to 500 but holds only as many of those rows
84
+ as fit 60,000 characters of JSON, so a result stays under host truncation limits. It
85
+ is cut before the first row that overflows and its next cursor resumes at that row.
86
+ A single row over the budget is returned alone with the cursor, and a View without an
87
+ order stays one whole page; a page still over the budget carries a notice that hosts
88
+ may truncate it.
60
89
  A pageable Schema View has a complete NOT NULL scalar tuple ending in the
61
90
  compiler's `id` tiebreak. Its ordered columns must match an index after fixed
62
- equality prefixes (including caller scope); multiple columns and mixed directions
91
+ equality prefixes; a user-field `IN` scope or restrict is not one (see
92
+ [index prefixes](schema.md#index-prefixes-for-paged-views)); multiple columns and mixed directions
63
93
  retain the authored order. CTE projections preserve the proof; joins must have a
64
94
  proved singleton side, or equality on the joined Schema's `id` or single-field
65
95
  unique key, so the base id cannot fan out. LEFT JOIN accepts nullable or missing
@@ -132,7 +162,10 @@ const tags = await site.head(request, env); // HTML string, empty for unknown/mi
132
162
  ```
133
163
 
134
164
  Page parameter names must exactly match the View's declared input properties.
135
- Page and list Views must return the page parameters; list Views must accept empty input. Markdown
165
+ Page and list Views must return the page parameters; list Views must accept empty input: no
166
+ `required`, no root `anyOf`, `enum` or `const`, no `else` rule, and a root `type`, when given, that includes `object`.
167
+ These conditions also apply to each schema a root `$ref` chain reaches, and an unresolved or cyclic chain fails them.
168
+ `web()` throws a `TypeError` for any other list View. Markdown
136
169
  must name a textual output column. HTML remains application-owned.
137
170
 
138
171
  `/llms.txt` and `/sitemap.xml` traverse each list's `nextCursor`; `.md` and head
@@ -169,14 +202,30 @@ transport loads on the first matching request. Pass `handlers` when ref
169
202
  Procedures are declared, as for REST. Admin tools are listed only on the admin
170
203
  endpoint, and publishing visibility belongs to the generated SQL.
171
204
 
205
+ `apps.public` and `apps.admin` accept a `McpAppsSource` or an array mixing static
206
+ contributors with async `({ env, request }) => McpApps` factories. Static resources
207
+ are checked once at construction; factories resolve only after the endpoint's
208
+ origin and caller gates, and their resource identities and ownership are checked
209
+ on each request even when the tool names are unchanged.
210
+
211
+ Discovery is caller-independent by default (ADR-0014); listing a tool grants no
212
+ execution authority. Configure `authorize` to filter `tools/list` and the tool
213
+ names supplied to resource HTML loaders: that mode also excludes tools refused
214
+ by declared identity, scope and `check` requirements. Without `authorize`, the
215
+ complete endpoint catalogue is not a permission-filtered catalogue. Operation
216
+ validation, guards and credential checks still apply to calls.
217
+
172
218
  The app resolves bearer tokens with `resolveCaller(request, auth, { audience:
173
219
  new URL('/mcp', request.url).href })` (both surfaces use `<origin>/mcp`). The app
174
220
  must also mount Better Auth's `/.well-known/oauth-protected-resource/mcp`
175
221
  metadata route. Auth challenges point to that route. Presented bearer credentials
176
222
  need the `mcp` scope; cookie sessions use the same-origin mutation checks.
177
223
 
178
- Generation emits `mcp.ts` only for a non-internal View or MCP Trigger. These
179
- surfaces typecheck without the `@modelcontextprotocol/server` 2.1.0 peer.
224
+ Generation emits `mcp.ts` only for a non-internal View or MCP Trigger. Omitting
225
+ MCP remains a deliberate asymmetric choice; an HTTP-only Procedure still generates
226
+ successfully with the warning described in [Trigger](trigger.md). These
227
+ surfaces typecheck without the optional `@modelcontextprotocol/server` peer;
228
+ install the exact version declared in the installed SDK's `peerDependencies`.
180
229
  The peer loads by dynamic import on the first matching `/mcp` request; if
181
230
  missing, that request fails at runtime. Publishing alone does not select MCP;
182
231
  on a selected
@@ -207,15 +256,42 @@ as an explicit option.
207
256
  ### MCP catalogue and callbacks
208
257
 
209
258
  `mcp({ db, caller, handlers?, locale?, instructions?, serverInfo?, requiredScopes?,
210
- resourceMetadata?, onObservation?, onRefusal? })` keeps the SDK behind its first
259
+ resourceMetadata?, allowedOrigins?, onObservation?, onRefusal? })` keeps the SDK behind its first
211
260
  request. Locale defaults to `en`; localized titles/descriptions fall back to
212
- English, then their first entry. Views advertise `readOnlyHint`; Procedures use
213
- their declared MCP annotations. `instructions` is returned by initialize.
261
+ English, then their first entry. Tools carry the [tool hints](procedure.md#mcp-tool-hints). `instructions` is returned by initialize.
214
262
  The scope floor defaults to `["mcp"]` for every credential except cookie sessions;
215
263
  `requiredScopes: []` disables that floor. An anonymous closed surface challenges
216
264
  from initialize, and every call in a batch is checked before any call runs.
217
265
  `resourceMetadata` overrides the challenge URL for reverse proxies.
218
266
 
267
+ Both endpoints answer a request without `Origin`, which is what server-side MCP
268
+ clients send; a bearer credential needs none. A cookie session is ambient authority, so
269
+ `checkOrigin` also guards it: on a request that is not GET, HEAD or OPTIONS, at least one of
270
+ `Origin` and `Sec-Fetch-Site` must be present and every present one must agree (`Origin` equal
271
+ to the request's own origin; `Sec-Fetch-Site` `same-origin` or `none`), else `AUTH_DENIED` at
272
+ `request:origin`. On MCP it runs only for a tool call that executes (not a read-only call from an
273
+ App host), not for initialize or `tools/list`; `allowedOrigins` does not lift it. A present `Origin` marks a browser: it must name a loopback host
274
+ (`localhost`, `127.0.0.1`, `[::1]`, any port) or an `allowedOrigins` entry, else
275
+ 403 before authentication on every credential; `Origin: null` is refused. The
276
+ site's own origin is not implied, because a DNS-rebinding page sends an `Origin`
277
+ equal to the `Host` it rebound, so a browser client served from your deployed
278
+ site lists that origin: `allowedOrigins: ["https://app.example"]`. Entries are
279
+ compared by hostname as the official SDK does (not scheme or port); each is a
280
+ full origin (`https://console.example`) or the SDK's bare host
281
+ (`console.example`, `localhost:8787`), and anything else, such as a path, throws
282
+ when `mcp(...)` is configured; so does a wildcard such as `*` or
283
+ `https://*.example.com`, since entries are matched by exact hostname. `Host` is
284
+ not checked. Cookie writes also need the full same-origin check.
285
+
286
+ `/mcp` sends no CORS headers. `allowedOrigins` therefore serves pages on the
287
+ same origin as the endpoint, or clients behind a proxy that forwards their
288
+ `Origin`; a browser client on another origin also needs the app to answer
289
+ preflight and add CORS headers itself. Under `wrangler dev` with `routes` or
290
+ `dev.host`, Miniflare rewrites an `Origin` equal to the request's own origin to
291
+ the route or `dev.host` origin, and a DNS-rebinding request always has that
292
+ shape. Listing that host would admit rebinding against the local server, so keep
293
+ the route and `dev.host` hosts out of `allowedOrigins` in local development.
294
+
219
295
  `onObservation` receives request refusals and invocation attempt/completion
220
296
  metadata, including duration and outcome. Its Caller and scopes are frozen copies.
221
297
  `onRefusal` receives credential/origin refusals. Neither callback receives request
@@ -232,6 +308,44 @@ with an output schema they omit `structuredContent` to avoid validating an error
232
308
  against a success schema. Without one they also expose diagnostic structured
233
309
  content. MCP adds no prompts or non-UI resources.
234
310
 
311
+ ### Tool versions
312
+
313
+ Each generated tool (MCP, media, publishing and WebMCP) carries `version`, a
314
+ short hash computed by `mantle generate` of the definition as listed: name,
315
+ title, description without the marker, input schema (a View tool's with its
316
+ `limit` and `cursor` paging properties), output schema and annotations. A
317
+ localized title or description is hashed with all its locales. The description
318
+ ends with `(tool version <version>)`, so an unchanged tool keeps a
319
+ byte-identical description across deploys.
320
+
321
+ MCP results of a call that reaches the tool add a second text block,
322
+ `tool version <version>`; failures other than authorization refusals append
323
+ `; input schema <JSON>`, the input schema the tool is listed with.
324
+ `structuredContent` is unchanged. A `tools/call` naming a tool the endpoint
325
+ serves but the caller may not use is answered by the authentication or
326
+ authorization challenge (HTTP 401 or 403) before the SDK's transport and
327
+ request checks, even though that caller's `tools/list` omits the tool. A
328
+ `tools/call` naming a tool not in the endpoint's catalogue, in a batch or not
329
+ and from a host caller too, is answered with JSON-RPC error `-32602` and the
330
+ message
331
+ `No tool named "<name>" exists in the current catalogue (catalogue version <serverInfo.version>).`
332
+ For such a name the SDK's transport and request checks (headers, protocol
333
+ version, JSON-RPC shape) answer first, so a malformed request gets the same
334
+ refusal as one naming a served tool the caller may use.
335
+ Unless `serverInfo.version` is given, the kernel's `mcp()` sets it to
336
+ a hash of the `surface:name:version` of every tool the endpoint serves, before
337
+ per-caller filtering, so `/mcp` and `/mcp/admin` each report their own catalogue
338
+ version, and it changes when a generated tool's definition changes or a tool is
339
+ added or removed, however the tools are wired.
340
+
341
+ `POST /admin/api/webmcp/{tool}` returns `{ output, version }`. A refusal's
342
+ diagnostic adds `toolVersion`, and `inputSchema` unless it is an authorization
343
+ refusal; internal errors and refusals before the call carry neither. Admin's
344
+ WebMCP results repeat these as the same text block MCP adds. The navigation
345
+ tools `admin_get_context` and `admin_navigate` have no version. Tools an
346
+ application adds itself carry a version only if it sets one. Why: [MCP and
347
+ agents](../concepts/mcp-and-agents.md#when-a-host-shows-old-tools).
348
+
235
349
  ### Declared View scalars on the wire
236
350
 
237
351
  REST and MCP View pages expose declared JSON columns as JSON values and declared
@@ -24,12 +24,54 @@ metadata: { name: close-ticket-http }
24
24
  spec:
25
25
  source: { kind: http, method: POST, path: '/api/tickets/{id}/close' }
26
26
  target: { procedure: close-ticket }
27
+ ---
28
+ apiVersion: cms.mantle.aotter.net/v2
29
+ kind: Trigger
30
+ metadata: { name: close-ticket-mcp }
31
+ spec:
32
+ source: { kind: mcp, surface: admin }
33
+ target: { procedure: close-ticket }
27
34
  ```
28
35
 
29
36
  The target names a declared Procedure. `http` sources use POST/PUT/PATCH/DELETE and an `/api/` path. Path parameters bind to corresponding declared input fields and override JSON-body values. The generated REST module retains the Procedure's authorization and validates boundary input.
30
37
 
31
- An MCP source is `{ kind: mcp, surface: public }` or admin. The selected MCP module exposes the Procedure as a snake-case tool with its declared description/annotations and requires checks.
38
+ An MCP source is `{ kind: mcp, surface: public }` or admin. The selected MCP module exposes the Procedure as a snake-case tool with its description, [tool hints](procedure.md#mcp-tool-hints) and requires checks.
39
+
40
+ By default, pair every HTTP Trigger with an MCP Trigger so the same operation is reachable by the user's AI. `mantle generate` warns when a Procedure has HTTP exposure but no MCP exposure, suggesting `surface: admin` for an admin-only Procedure and `public` otherwise. Triggers still decide every Procedure surface: intentional asymmetric exposure is allowed and generation succeeds.
32
41
 
33
42
  Lifecycle and schedule sources are rejected at generation with `GENERATE_UNSUPPORTED`, including disabled schedules. Use an app-owned handler or scheduler. For signed webhooks, verify the original raw Request in the app and call a generated Procedure with validated input; do not route it through a JSON boundary before signature verification.
34
43
 
44
+ ## Input rules
45
+
46
+ The admin MCP tool below lists `reason` and `closed` without the rules, and its
47
+ description ends "'reason' is required when 'closed' is false."; a call with
48
+ `closed: false` and no `reason` fails with `INPUT_VALIDATION_FAILED` at
49
+ `procedures/set-ticket/input/reason` (the error path is
50
+ `procedures/<name>/input/<field>`). See [surface contracts](surface.md) for the
51
+ supported rules.
52
+
53
+ ```yaml
54
+ apiVersion: cms.mantle.aotter.net/v2
55
+ kind: Procedure
56
+ metadata: { name: set-ticket }
57
+ spec:
58
+ description: Open or close a ticket.
59
+ input:
60
+ type: object
61
+ required: [id, closed]
62
+ properties: { id: { type: string }, closed: { type: boolean }, reason: { type: string, maxLength: 200 } }
63
+ additionalProperties: false
64
+ if: { properties: { closed: { const: false } }, required: [closed] }
65
+ then: { required: [reason] }
66
+ output: { type: object }
67
+ handler: { sql: 'UPDATE tickets SET closed=input.closed WHERE id=input.id RETURNING id,closed' }
68
+ ---
69
+ apiVersion: cms.mantle.aotter.net/v2
70
+ kind: Trigger
71
+ metadata: { name: set-ticket-mcp }
72
+ spec:
73
+ source: { kind: mcp, surface: admin }
74
+ target: { procedure: set-ticket }
75
+ ```
76
+
35
77
  See [surfaces](surface.md) for mounted paths, error mapping and origin checks.
@@ -29,7 +29,7 @@ Store keeps `find`/`first` search and `count`. `count` scans every matching row;
29
29
 
30
30
  Each SELECT output needs a unique static name. Unquoted SQL identifiers fold to lower case; use an explicit quoted alias to preserve case (`r.reqStatus AS "reqStatus"`). Generation warns when an unaliased direct declared field would use its lower-case storage name. Declared-column lineage supplies generated types; computed outputs remain native/unknown. SQL formats declared date-time/date outputs, while computed timestamps retain driver values. See [native execution](../guides/native-execution.md).
31
31
 
32
- Bare `views/<encoded-name>.ts` returns row arrays. Proved pageable exposed/internal Views also receive native `paged/views/*`, returning `{ rows, nextCursor? }`. Selected REST/MCP boundaries use separate `paged/wire/views/*` SQL when declared integers require exact transport projections; bare modules also export `WireRow`. Indexed ordering after fixed equality prefixes, non-null typed keys, complete groups and singleton joins/lookups are checked at generation. Unsupported internal paging retains its bare View; exposure requires a supported page contract. Aggregates still scan/group their sources: inspect EXPLAIN.
32
+ Bare `views/<encoded-name>.ts` returns row arrays. Proved pageable exposed/internal Views also receive native `paged/views/*`, returning `{ rows, nextCursor? }`. Selected REST/MCP boundaries use separate `paged/wire/views/*` SQL when declared integers require exact transport projections; bare modules also export `WireRow`. Indexed ordering after fixed equality prefixes ([index prefixes](schema.md#index-prefixes-for-paged-views)), non-null typed keys, complete groups and singleton joins/lookups are checked at generation. Unsupported internal paging retains its bare View; exposure requires a supported page contract. Aggregates still scan/group their sources: inspect EXPLAIN.
33
33
 
34
34
  Publishing Schema fields may be NULL in drafts even when required, so public View paging cannot order by them. `published_at` is accepted by Store ordering but rejected as a View paging key. Use `ORDER BY id`, or keep the business date in an operational Schema as an indexed, required non-null field.
35
35
 
@@ -17,6 +17,78 @@ Pin every package you use to the same exact version and upgrade them together.
17
17
 
18
18
  ## 0.2.0 — in preparation
19
19
 
20
+ ### Upgrading to 0.2.0-alpha.9
21
+
22
+ Pin every selected `@aotter/mantle`, `@aotter/mantle-ui` and
23
+ `@aotter/mantle-admin` dependency to `0.2.0-alpha.9`, refresh the lockfile,
24
+ then regenerate and check the application's owned artifacts. Keep existing
25
+ production migrations application-owned; generated initial DDL is for an empty
26
+ database. This alpha includes breaking Admin authoring and paging changes.
27
+
28
+ 1. **Build output and host configuration.** `mantle bundle` writes server
29
+ modules to `dist/worker` and static files to `dist/client`, including
30
+ `dist/client/admin`. Rerun `mantle generate` with the application's `--entry`
31
+ and check the resulting Wrangler `main`, `base_dir`, asset directory and
32
+ build command. See [CLI and generated ownership](../start/project-and-cli.md).
33
+ 2. **Application-owned React.** Install React and React DOM in the application
34
+ at the same exact version (the examples use `19.3.0`). Admin takes them as
35
+ peers; a mismatched pair fails its build.
36
+ 3. **Replace extensions with application composition.** Move to
37
+ `defineAdmin(meta, { nav, pages, home, components, plugins, mcp })` in
38
+ `src/admin.tsx`. Remove `--admin-extensions`, `adminExtensions`,
39
+ `defineAdminExtension`, extension handlers and removed manifest widget,
40
+ `list.cells` and panel references. `navGroups` becomes `nav.groups`.
41
+ Use the [Admin migration table](../guides/native-admin.md#migrating-from-admin-extensions)
42
+ rather than carrying the former registration layer forward.
43
+ 4. **Build the MCP App with the application.** The prebuilt
44
+ `@aotter/mantle-admin/mcp-app` / `mantleAppHtml` export is gone. Use
45
+ `adminMcpApps({ catalogue: mcpAppCatalogues.admin })` with the generated
46
+ catalogue; it reads the built asset. Declare CSP once in `defineAdmin`.
47
+ The default resource URI hashes the built HTML, catalogue and metadata;
48
+ custom HTML functions need an explicit immutable URI. Load interactive
49
+ host HTML through authenticated `resources/read`, which embeds its
50
+ catalogue. See [MCP Apps](../guides/mcp-apps.md).
51
+ 5. **Run the generated compatibility check.** Run
52
+ `node --test generated/admin-mcp-app.check.mjs` after building. Install
53
+ the optional `playwright-core` peer and a browser, or set
54
+ `MANTLE_TEST_CHROMIUM`; a missing browser fails rather than skips. This
55
+ checks rendering with a synthetic catalogue, not application authorization
56
+ or completion of a Procedure action.
57
+ 6. **Keep the host graph safe.** The MCP build rejects `/admin/api` access,
58
+ cookies and undeclared remote loads. Its entry must be readable in Node.
59
+ An optional `src/admin.mcp.tsx` (or `buildAdmin({ mcpEntry })`) isolates
60
+ browser-only integrations; both roots can reuse one host-safe config,
61
+ components and CSS. Without it the whole shared entry graph is checked.
62
+ 7. **Distinguish total bounds from page sizes.** Authored SQL `LIMIT` now
63
+ bounds the complete ordered result set across cursor pages, including web
64
+ indexes. Use paging `options.limit` for page size; remove an authored limit
65
+ only if it was incorrectly being used as a page-size setting. MCP accepts
66
+ sizes of 1–500, but a response budget can return fewer rows: append pages
67
+ until `nextCursor` is absent, not until a short page. See
68
+ [View paging](../reference/surface.md).
69
+ 8. **Install build dependencies where the build runs.** Wrangler dev/deploy
70
+ runs its configured build command, so that environment needs
71
+ `node_modules`. The resulting built server/assets do not read
72
+ `node_modules` when served. See the
73
+ [application shell](../../examples/app-shell/README.md).
74
+
75
+ Record/collection cells, inputs, record actions, asides and `RecordView` are
76
+ now truthfully browser-only (`surface: "browser"`); remove unreachable MCP
77
+ branches. View cells remain `both`, and pages opt in through `pages[].mcp`.
78
+ A Procedure action's input is distinct from a collection record input: use a
79
+ Procedure target and MCP Trigger with the View's unchanged `id` and observed
80
+ `version` for native row actions. [Shared custom-page headers](../guides/mcp-apps.md#customising-admin-for-mcp-hosts)
81
+ can compose localized title, description and actions on both surfaces; omit the
82
+ option to keep prior behavior, or use `header: false` for an element-owned heading.
83
+
84
+ OAuth issuer verification now follows Better Auth's normalized URL while
85
+ preserving explicit issuer/JWKS settings. Cloudflare OAuth configurations need
86
+ `global_fetch_strictly_public` for public JWKS fetches; generated settings and
87
+ the official examples retain it. Native MCP Apps use the official ext-apps
88
+ client. Browser Admin now displays a localized explicit reload action when a
89
+ lazy page fails, rather than going blank; it does not reload automatically or
90
+ replay writes.
91
+
20
92
  ### Native SDK migration (alpha.7)
21
93
 
22
94
  Alpha.7 replaces the previous Core runtime and generated service contract.
@@ -32,7 +104,7 @@ Existing alpha.6 applications require a source migration:
32
104
  runs DDL at boot. Apply generated `schema.sql` only to an empty database;
33
105
  migrate existing data explicitly. PostgreSQL uses Bun.SQL on Bun and
34
106
  postgres.js on Workers. See [native execution](../guides/native-execution.md).
35
- - Install the separate `@aotter/mantle-admin` package when selecting Admin.
107
+ - Install the separate `@aotter/mantle-admin` package when selecting Admin, with `react` and `react-dom` in the application pinned to one identical exact version (Admin no longer installs them).
36
108
  Compose its [native bindings](../guides/native-admin.md) in the application.
37
109
  Bare queries retain driver-native values; selected REST/MCP/Admin boundaries
38
110
  own validation and declared-column wire conversion.
@@ -40,7 +112,7 @@ Existing alpha.6 applications require a source migration:
40
112
  application code. Review [authorization and Caller wiring](../concepts/runtime-and-adapters.md)
41
113
  and regenerate all owned artifacts before running `generate --check`.
42
114
 
43
- The [reference Worker](https://github.com/aotter/mantle/tree/v0.2.0-alpha.7/docs/examples/reference-service) and
115
+ The [reference Worker](https://github.com/aotter/mantle/tree/v0.2.0-alpha.9/docs/examples/reference-service) and
44
116
  [app shell](../../examples/app-shell/) demonstrate the current composition.
45
117
  The following earlier-alpha notes are historical; the current handbook and
46
118
  the native migration above supersede their runtime and CLI instructions.
@@ -9,7 +9,7 @@ Mantle compiles a closed v2 manifest into native SQL and typed TypeScript. The a
9
9
  | --- | --- |
10
10
  | `@aotter/mantle` | SDK, private compiler, generator and `mantle` CLI |
11
11
  | `@aotter/mantle-ui` | Pure UI kit and generic interaction controller; no SDK dependency |
12
- | `@aotter/mantle-admin` | Admin server, SPA, extensions, preview and MCP App; SDK peer |
12
+ | `@aotter/mantle-admin` | Admin server, app-built SPA library, preview and MCP App; SDK peer |
13
13
 
14
14
  Start with [a Worker](quickstart-worker.md) or [CLI and targets](project-and-cli.md). Existing TypeScript/Node apps can import selected generated operations using their own handle and verified Caller; no router or identity replacement is required. See the packaged [integrate skill](../../skills/integrate/SKILL.md).
15
15