@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
@@ -25,7 +25,7 @@ spec:
25
25
  title: Purchase requests
26
26
  lifecycle: operational
27
27
  indexes: [[authorId, createdAt, id], [requestStatus, createdAt, id]]
28
- # Sidebar: in the Approvals group (src/admin.ts), after the queue of requests waiting for a decision.
28
+ # Sidebar: in the Approvals group (src/admin.tsx), after the queue of requests waiting for a decision.
29
29
  uiSchema: { list: { primaryField: title, columns: [department, amount, requestStatus], filterField: requestStatus }, nav: { group: approvals, order: 2 } }
30
30
  schema:
31
31
  type: object
@@ -117,6 +117,7 @@ kind: Procedure
117
117
  metadata: { name: submit-request }
118
118
  spec:
119
119
  title: Submit a purchase request
120
+ description: Submit a purchase request against an existing department budget.
120
121
  requires: { auth: { all: [ctx.user] } }
121
122
  input:
122
123
  type: object
@@ -140,6 +141,7 @@ kind: Procedure
140
141
  metadata: { name: change-amount }
141
142
  spec:
142
143
  title: Change the amount of my request
144
+ description: Change the amount of the signed-in member's submitted purchase request.
143
145
  requires: { auth: { all: [ctx.user] } }
144
146
  restrict: { requests: "author_id = auth.uid()" }
145
147
  input:
@@ -291,11 +293,25 @@ spec:
291
293
  ---
292
294
  apiVersion: cms.mantle.aotter.net/v2
293
295
  kind: Trigger
296
+ metadata: { name: submit-request-mcp }
297
+ spec:
298
+ source: { kind: mcp, surface: public }
299
+ target: { procedure: submit-request }
300
+ ---
301
+ apiVersion: cms.mantle.aotter.net/v2
302
+ kind: Trigger
294
303
  metadata: { name: change-amount-http }
295
304
  spec:
296
305
  source: { kind: http, method: POST, path: '/api/requests/{id}/amount' }
297
306
  target: { procedure: change-amount }
298
307
  ---
308
+ apiVersion: cms.mantle.aotter.net/v2
309
+ kind: Trigger
310
+ metadata: { name: change-amount-mcp }
311
+ spec:
312
+ source: { kind: mcp, surface: public }
313
+ target: { procedure: change-amount }
314
+ ---
299
315
  # Admin MCP tools: an AI agent connected to /mcp/admin can approve or reject.
300
316
  apiVersion: cms.mantle.aotter.net/v2
301
317
  kind: Trigger
@@ -3,7 +3,8 @@
3
3
  import assert from "node:assert/strict";
4
4
  import { createHmac } from "node:crypto";
5
5
  import { createServer } from "node:http";
6
- import { smoke } from "./smoke-kit.mjs";
6
+ import { assertMcpDiscovery, smoke } from "./smoke-kit.mjs";
7
+ import { developerConsole } from "./generated/admin-descriptors.ts";
7
8
 
8
9
  const SECRET = "local-supplier-secret", DAILY = "0 9 * * *", DAY_US = 86_400 * 1_000_000;
9
10
 
@@ -22,7 +23,7 @@ const supplier = createServer((request, response) => {
22
23
  await new Promise(resolve => supplier.listen(0, "127.0.0.1", resolve));
23
24
 
24
25
  try {
25
- await smoke(async ({ owner, call, mcp, signIn, sql, cron, waitForLog, step, logs }) => {
26
+ await smoke(async ({ origin, owner, call, mcp, signIn, sql, cron, waitForLog, step, logs }) => {
26
27
  const query = sql;
27
28
  const row = async id => (await query(`SELECT requeststatus AS status, version, supplierorderid AS "supplierOrderId" FROM requests WHERE id = '${id}'`))[0];
28
29
  const statusAndOrder = async id => { const { status, supplierOrderId } = await row(id); return { status, supplierOrderId }; };
@@ -77,9 +78,14 @@ try {
77
78
 
78
79
  const version = async id => (await row(id)).version;
79
80
  assert.equal((await operation(alice, "approve-request", { id: laptop, expectedVersion: await version(laptop) })).status, 403);
80
- const publicTools = (await mcp("/mcp", alice)).body.result.tools.map(t => t.name);
81
+ // Generated static metadata uses the MCP tool names; the app chooses the surface and test identity.
82
+ const declared = developerConsole.interfaces.callable;
83
+ const publicTools = await assertMcpDiscovery(`${origin}/mcp`, { headers: { cookie: alice, origin }, tools: declared.filter(tool => tool.surface === "public") });
81
84
  assert.ok(!publicTools.includes("approve_request") && !publicTools.includes("reject_request"), publicTools.join());
82
- const adminTools = (await mcp("/mcp/admin", admin)).body.result.tools.map(t => t.name);
85
+ const adminTools = await assertMcpDiscovery(`${origin}/mcp/admin`, { headers: { cookie: admin, origin }, tools: declared.filter(tool => tool.surface === "admin") });
86
+ // Denial is separate from successful initialized discovery.
87
+ assert.equal((await mcp("/mcp/admin")).status, 401);
88
+ assert.equal((await mcp("/mcp/admin", alice)).status, 403);
83
89
  assert.ok(adminTools.includes("approve_request") && adminTools.includes("reject_request"), adminTools.join());
84
90
  for (const hidden of ["confirm_order", "mark_ordered", "approve_request_write", "reject_request_write"])
85
91
  assert.ok(!adminTools.includes(hidden) && !publicTools.includes(hidden), hidden);
@@ -0,0 +1,13 @@
1
+ import { defineAdmin } from "@aotter/mantle-admin/app";
2
+ import { admin } from "../generated/admin-client.js";
3
+
4
+ /**
5
+ * The Admin entry, built by `mantle bundle`. The approver's daily work gets its own sidebar group above Admin's
6
+ * built-in ones (content 10, operations 20, reports 30; More stays last); manifests place `pending-requests` and
7
+ * `requests` in it with `uiSchema.nav`. `tsc` refuses a manifest group left undeclared here.
8
+ * AGENT: add groups for how the staff work; titles take one string or a { locale: text } map.
9
+ */
10
+ export default defineAdmin(admin, {
11
+ nav: { groups: { approvals: { title: { en: "Approvals", "zh-TW": "簽核" }, order: 5 } } },
12
+ pages: [{ path: "guide", title: "Approver guide", element: <p>Approve a request when the amount fits the budget; reject it with a reason otherwise.</p>, nav: { group: "approvals", order: 9 } }],
13
+ });
@@ -14,7 +14,7 @@ export const handlers: Handlers = {
14
14
  } catch (error) {
15
15
  // The declared budget check is a business refusal, not a server error. Nothing was written.
16
16
  // The approval's only Schema check is the budget one.
17
- if (error instanceof ContractError && error.code === "CONFLICT" && error.message.startsWith("A check of the Schema failed"))
17
+ if (error instanceof ContractError && error.code === "CONFLICT" && error.fields?.kind === "check" && error.fields.schema === "budgets")
18
18
  throw new ContractError("CONFLICT", "The department budget cannot cover this request.", "procedures/approve-request");
19
19
  throw error;
20
20
  }
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "$schema": "node_modules/wrangler/config-schema.json",
3
3
  "name": "mantle-app",
4
- "main": "dist/index.js",
4
+ "main": "dist/worker/index.js",
5
5
  "compatibility_date": "2026-09-01",
6
6
  "compatibility_flags": [
7
- "nodejs_compat"
7
+ "nodejs_compat",
8
+ "global_fetch_strictly_public"
8
9
  ],
9
10
  "vars": {
10
11
  "EMPLOYEE_DOMAIN": "example.com"
@@ -36,7 +37,7 @@
36
37
  }
37
38
  ],
38
39
  "assets": {
39
- "directory": "node_modules/@aotter/mantle-admin/dist/admin",
40
+ "directory": "dist/client",
40
41
  "binding": "ASSETS",
41
42
  "run_worker_first": true,
42
43
  "html_handling": "none"
@@ -48,7 +49,7 @@
48
49
  },
49
50
  "no_bundle": true,
50
51
  "find_additional_modules": true,
51
- "base_dir": "dist",
52
+ "base_dir": "dist/worker",
52
53
  "build": {
53
54
  "command": "mantle bundle src/index.ts dist",
54
55
  "watch_dir": [
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "$schema": "node_modules/wrangler/config-schema.json",
3
3
  "name": "mantle-app",
4
- "main": "dist/index.js",
4
+ "main": "dist/worker/index.js",
5
5
  "compatibility_date": "2026-09-01",
6
6
  "compatibility_flags": [
7
- "nodejs_compat"
7
+ "nodejs_compat",
8
+ "global_fetch_strictly_public"
8
9
  ],
9
10
  "secrets": {
10
11
  "required": [
@@ -43,7 +44,7 @@
43
44
  "MEDIA_S3_ENDPOINT": "https://mantle-app-media.ACCOUNT_ID.r2.cloudflarestorage.com"
44
45
  },
45
46
  "assets": {
46
- "directory": "node_modules/@aotter/mantle-admin/dist/admin",
47
+ "directory": "dist/client",
47
48
  "binding": "ASSETS",
48
49
  "run_worker_first": true,
49
50
  "html_handling": "none"
@@ -53,7 +54,7 @@
53
54
  },
54
55
  "no_bundle": true,
55
56
  "find_additional_modules": true,
56
- "base_dir": "dist",
57
+ "base_dir": "dist/worker",
57
58
  "build": {
58
59
  "command": "mantle bundle src/index.ts dist",
59
60
  "watch_dir": [
@@ -12,7 +12,24 @@ Mantle supplies the database adapter, schema generation and caller resolution:
12
12
 
13
13
  ## `auth.ts`
14
14
 
15
- Copy `src/auth.ts` from the [app shell](../../examples/app-shell/README.md), which CI runs from the packed packages.
15
+ > **Verification status:** the GitHub social-provider and magic-link recipes have not yet been exercised end to end against real deployed provider flows.
16
+
17
+ Copy `src/auth.ts` from the [app shell](../../examples/app-shell/README.md), which CI runs from the packed packages;
18
+ installed, it is `node_modules/@aotter/mantle/docs/examples/app-shell/src/auth.ts`. Its essentials:
19
+
20
+ - Imports: `betterAuth` from `better-auth/minimal`; `admin`, `emailOTP` (and `jwt` for MCP) from `better-auth/plugins`;
21
+ `createAccessControl` from `better-auth/plugins/access`; `defaultStatements` from `better-auth/plugins/admin/access`;
22
+ `d1Adapter` from `@aotter/mantle/auth/adapter`; the hardening and bootstrap helpers below. For MCP it also imports `jwt`,
23
+ `mcp` from `@better-auth/mcp`, `cimd` from `@better-auth/cimd` and `oauthGrantExtension`/`oauthGrantRevocation` from
24
+ `@aotter/mantle/auth/oauth`; the file marks that block as deletable.
25
+ - `authOptions(env)` returns plain options and is the module's export for `--auth src/auth.ts`. Generation calls it with an
26
+ empty `env`, so it must not throw: fall back to placeholders for `secret` and origin there.
27
+ - The bootstrap admin: `d1BootstrapAdmin(env.DB, { match: "email", value: env.ADMIN_EMAIL }, () => ({ options: { plugins } }))`
28
+ is assigned to `databaseHooks.user.create.after` and `.update.after` ([Identity helpers](#identity-helpers)).
29
+ - `createAuth(env)` is the same module's second export: it checks its three variables are set and returns
30
+ `betterAuth(authOptions(env))`. The Worker's `authLoader(() => import("./auth.js").then(m => m.createAuth(env)))`
31
+ ([service composition](service-entry.md)) is what calls it.
32
+
16
33
  The application owns Better Auth and its optional plugins. Schema generation reads plain options without constructing
17
34
  Better Auth; apply `schema.sql` during deployment, never at boot. OAuth resource
18
35
  rows are created on first OAuth use from runtime configuration; they require no
@@ -63,6 +80,7 @@ after success; invitations still work when email is absent.
63
80
  `databaseHooks.user.create.after` (and user.update.after for a later email verification). Only the first matching signup wins (case-insensitive); email matching requires emailVerified=true (required for bootstrap).
64
81
  D1 uses one conditional UPDATE; PostgreSQL uses a transaction advisory lock before UPDATE, avoiding concurrent winners.
65
82
  A blocked promotion logs a warning. A GitHub rule requires a configured trusted GitHub provider/profile mapper.
83
+ The promotion runs after Better Auth creates the user, so the first sign-in response (and a cached session payload) can still show the previous role, while authorization re-reads the role on every request and `/admin/api/me` and later sessions show the admin role.
66
84
  Helpers run no boot queries or DDL.
67
85
 
68
86
  ## Worker entry
@@ -154,7 +172,12 @@ Better Auth adapter code stays outside the Worker startup graph.
154
172
 
155
173
  `@aotter/mantle/auth/admin` exports `guardAuthAdminRequest(request, resolveFreshCaller, basePath?)` for selected auth transports. It resolves authority only for raw/encoded admin paths, requires an admin cookie session and checks mutation origin. Both the app-owned template and OAuth routes use this same guard.
156
174
 
157
- `ctx.admin` role names are open and must match `^[a-z][a-zA-Z0-9_]{0,62}$`. Bare `ctx.admin` admits any admin; a list admits admins holding one of its roles. Better Auth’s `adminRoles` decides who is an admin. Member role names alone grant no privilege.
175
+ Bare `ctx.admin` admits any verified admin layer classified by Better Auth's
176
+ `adminRoles` / `adminUserIds`. Role lists in `requires` are refused; decide
177
+ per-target permissions in `authorize(target, caller)`, using `hasAdminRole`
178
+ when appropriate. Verified role names match `^[a-z][a-zA-Z0-9_-]{0,62}$`.
179
+ Member role names alone grant no privilege. See
180
+ [authorization requirements](../reference/authorization.md).
158
181
 
159
182
  The generated auth DDL adds a native `(createdAt, id)` member-paging index (respecting schema field/table names). Unfiltered forward/backward paging orders native timestamps through that index; PostgreSQL cursors retain all six fractional digits and bind as text before timestamp casting. Member search (`%term%` over id/name/email, casting UUID ids to text only in the search expression) is a full scan; the index does not accelerate it.
160
183
 
@@ -164,10 +187,24 @@ The default is one instance with `admin` and `user` roles. An app that keeps two
164
187
  staff and customers) can run one Better Auth instance per population in the same Worker; every Mantle primitive
165
188
  takes the instance it works on:
166
189
 
167
- - Give each instance its own `basePath`, `advanced.cookiePrefix`, `secret` and table names (`user.modelName`,
168
- `session.modelName`, ... and plugin `schema` options). Keep each instance's options in its own module and pass
169
- one `--auth` per module to `mantle generate`; the tables are merged into one migration, and two instances that
170
- define the same table differently are refused. The native adapter follows each instance's mapping.
190
+ - Give each instance its own `basePath`, `advanced.cookiePrefix`, `secret` and core table names (`user.modelName`,
191
+ `session.modelName`, `account.modelName`, `verification.modelName`). Keep each instance's options in its own
192
+ module and pass one `--auth` per module to `mantle generate`; the tables are merged into one migration, and two
193
+ instances that define the same table differently are refused. The native adapter follows each instance's mapping.
194
+ - Better Auth plugin factories share their schema object, so differing plugin field/model mappings across instances
195
+ in one Worker override each other. Use one of two supported ways:
196
+ 1. Separate the instances by core table names only and leave every plugin mapping at its default.
197
+ 2. When plugin tables must differ (`jwt`'s `jwks`, `@better-auth/oauth-provider`'s `oauth*` tables), map them
198
+ explicitly in every instance, the default one included, and give each instance its own copy of the plugin's
199
+ schema right after constructing it:
200
+ `const keys = jwt({ schema: { jwks: { modelName: "staff_jwks" } } }); const plugin = { ...keys, schema: structuredClone(keys.schema) };`
201
+
202
+ `mantle generate` refuses anything else when two or more `--auth` instances use the same plugin (`jwt`, `admin`,
203
+ `oauth-provider`, `two-factor`, `phone-number`, `anonymous`, `siwe`, `device-authorization`) and a mapping exists
204
+ while an instance keeps the shared schema object, or two instances resolve a mapping to the same name (generate
205
+ cannot tell an identical mapping from an inherited one, so mapped names must differ between instances). Build
206
+ every instance's plugins in the same order in the Worker as in `--auth`; an instance built earlier than a mapped
207
+ one can still inherit its mapping, which generate cannot see.
171
208
  - One `authLoader(load, cookiePrefix)` per instance; call `resolveCaller(request, loader)` with the instance the
172
209
  route belongs to, and mount `authRoutes(loader, ...)` per instance (it guards that instance's `/admin/*`). The
173
210
  loader takes `cookiePrefix` separately on purpose: it checks the cookie before loading Better Auth, so anonymous
@@ -1,9 +1,11 @@
1
1
  # Mantle on ChatGPT Sites
2
2
 
3
+ > **Verification status:** this guide is experimental and has not yet been exercised end to end on a real ChatGPT Site.
4
+
3
5
  Treat the Site as an existing TypeScript host. Import selected generated Views/Procedures, pass the host's native handle and verified Caller, and compose only the surfaces it supports. Use [partial integration](../../skills/integrate/SKILL.md); Mantle supplies no Sites-specific identity or deployment adapter.
4
6
 
5
7
  Choose postgres or sqlite according to the host's actual SQL executor. Custom handles are app-owned; a structural match alone does not guarantee driver semantics. [Database boundaries](../concepts/runtime-and-adapters.md) defines the supported drivers.
6
8
 
7
- The runtime never creates, alters or checks tables. Apply initial `schema.sql` and selected auth/site DDL only to an empty database. For later changes the agent diffs regenerated SQL, writes and rehearses a host-native migration on a copy, and applies it before publishing new code. Cloud executes migrations at deploy time. Destructive/type changes require explicit data migration; Mantle drops nothing.
9
+ The runtime never creates, alters or checks tables. Apply initial `schema.sql` and selected auth/site DDL only to an empty database. For later changes the agent diffs regenerated SQL, writes and rehearses a host-native migration on a copy, and applies it before publishing new code. Destructive/type changes require explicit data migration; Mantle drops nothing.
8
10
 
9
- Use the Site's native publication workflow. Configure trusted origin and verified identity there; never construct a system/admin Caller from browser input. Serving Admin also requires its separate assets and shell security contract. Scheduling and storage provisioning belong to the host.
11
+ Use the Site's native publication workflow. Configure trusted origin and verified identity there; never construct a system/admin Caller from browser input. Serving Admin also requires its app-built assets and shell security contract. Scheduling and storage provisioning belong to the host.
@@ -5,13 +5,34 @@ description: Install schema explicitly, migrate before deploying, and operate a
5
5
 
6
6
  Run the same generation command with `--check`, typecheck the application and test its selected routes before deploying. Keep generated TypeScript, SQL and Wrangler edits committed; keep bundle `dist/` untracked. The app owns the database, auth secrets, trusted origin and assets.
7
7
 
8
+ ## Public fetch routing
9
+
10
+ Workers use `compatibility_flags: ["nodejs_compat", "global_fetch_strictly_public"]`.
11
+ The public-fetch flag routes native `fetch` through Cloudflare's public Internet
12
+ boundary. OAuth verification needs it to fetch JWKS on the service's own public
13
+ hostname; CIMD also requires this boundary for client metadata. `nodejs_compat`
14
+ alone does not establish it.
15
+
16
+ `mantle generate cloudflare-*` adds these flags without replacing other flags,
17
+ including in environments with their own `compatibility_flags` array. The same
18
+ command with `--check` refuses missing flags without writing. JSON/JSONC configs
19
+ are updated; TOML receives manual-edit instructions. Keep both flags in every
20
+ TOML environment override. An explicit `global_fetch_private_origin` is preserved
21
+ for applications without OAuth/CIMD, but conflicts with selected OAuth/CIMD auth
22
+ and with the public-fetch flag: generation refuses that config without rewriting
23
+ it. Such applications must use public routing for their OAuth/CIMD transport.
24
+
25
+ After deployment, complete a real OAuth sign-in and use the resulting bearer
26
+ for MCP `initialize` and `tools/list`. Local development and cookie-only Admin
27
+ checks do not verify production public-hostname routing.
28
+
8
29
  ## Install and change schemas
9
30
 
10
31
  The runtime never creates, alters or checks tables. Boot and requests run no DDL, introspection or schema checks.
11
32
 
12
33
  For the first install, apply generated `schema.sql`, including selected auth/site DDL, to an empty database. Apply selected seed SQL explicitly.
13
34
 
14
- For every later schema change, the application's coding agent diffs the regenerated `schema.sql`, writes a migration with the host's native tool (`wrangler d1 migrations` or the app's PostgreSQL migration tool), rehearses it on a copy, and applies it before deploying the new code. Cloud executes migrations at deploy time.
35
+ For every later schema change, the application's coding agent diffs the regenerated `schema.sql`, writes a migration with the host's native tool (`wrangler d1 migrations` or the app's PostgreSQL migration tool), rehearses it on a copy, and applies it before deploying the new code.
15
36
 
16
37
  Destructive changes and type changes need explicit data migration. Mantle drops nothing. Initial `schema.sql` is not an upgrade script.
17
38
 
@@ -19,6 +40,29 @@ For D1, create a migration with `pnpm exec wrangler d1 migrations create <databa
19
40
 
20
41
  For PostgreSQL, use the app's migration tool and deployment credentials. Plan lock waits and long index builds there; request credentials need no DDL privileges. Configure session settings on the role/database as described in [native execution](../guides/native-execution.md#postgresql-settings). Hyperdrive query caching must be disabled for authority and mutation visibility.
21
42
 
43
+ ## Latency placement and D1 read replication
44
+
45
+ Measure these options against the application's real traffic; neither is a
46
+ default latency guarantee. [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/)
47
+ can move fetch execution closer to observed back-end services:
48
+
49
+ ```jsonc
50
+ "placement": { "mode": "smart" }
51
+ ```
52
+
53
+ For globally distributed, read-heavy D1 traffic, enable
54
+ [read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/)
55
+ on the database and pass one request-scoped `env.DB.withSession(...)` handle to
56
+ the selected generated Views, Procedures and Store. Those D1 entry points accept
57
+ `D1DatabaseSession`. Without the Sessions API, queries continue to use the primary.
58
+
59
+ Use `first-unconstrained` only when a replica snapshot is acceptable,
60
+ `first-primary` when the flow must start from the latest primary state, or carry
61
+ a bookmark between logical sessions for sequential consistency. Reuse the same
62
+ session within that logical flow; all writes still go to the primary. Keep fresh
63
+ authentication/authorization checks and mutation/read-after-write paths on their
64
+ required current-authority path rather than weakening them for replica latency.
65
+
22
66
  ## Operate
23
67
 
24
68
  Enable Workers Logs and traces in production. The top-level `observability.enabled` alone does not enable traces:
@@ -20,9 +20,8 @@ Each media purpose in `siteDefaults.media.purposes` lists mime **slots** and a b
20
20
  `required` entries use `<input accept>` grammar (full mime, comma list, or a subtype shorthand such as `webp`);
21
21
  `maxBytes` must cover every mime the slots allow, keyed by the full mime type.
22
22
 
23
- OSS applications own their R2 binding, public bucket URL and S3 credentials.
24
- Mantle Cloud supplies that wiring by default; the OSS package has no Cloud API
25
- or credential management. Import `r2MediaStorage` from `@aotter/mantle/media/r2`:
23
+ Applications own their R2 binding, public bucket URL and S3 credentials; the SDK
24
+ package does not manage them. Import `r2MediaStorage` from `@aotter/mantle/media/r2`:
26
25
 
27
26
  ```ts
28
27
  const storage = r2MediaStorage({
@@ -31,16 +31,18 @@ import { authLoader, resolveCaller } from '@aotter/mantle/auth';
31
31
  import { authRoutes } from '@aotter/mantle/auth/routes';
32
32
  import { withAuthTasks } from '@aotter/mantle/auth/hardening';
33
33
  import { checkOrigin, surfaceFailure, type Caller } from '@aotter/mantle/contracts';
34
- import { admin } from '@aotter/mantle-admin/server';
34
+ import { admin, adminShell } from '@aotter/mantle-admin/server';
35
+ import { adminMcpApps } from '@aotter/mantle-admin/mcp';
35
36
  import { entries } from '@aotter/mantle-admin/server/entries';
36
37
  import { adminBindings } from '../generated/admin-bindings.js';
37
- import { adminAssets } from './admin-assets.js'; // app-owned SPA shell, assets and CSP
38
+ import { mcpAppCatalogues } from '../generated/admin-descriptors.js';
38
39
  import { mcp } from '../generated/mcp.js';
39
40
  import { rest } from '../generated/rest.js';
40
41
  import { handlers } from './handlers.js'; // omit when there are no ref Procedures
41
42
 
42
43
  // Env comes from worker-configuration.d.ts: declare DB, ASSETS, the email provider binding
43
- // and the ORIGIN, AUTH_SECRET and ADMIN_EMAIL secrets in Wrangler.
44
+ // and the PUBLIC_ORIGIN, BETTER_AUTH_SECRET and ADMIN_EMAIL secrets in Wrangler.
45
+ // createAuth(env) is exported by the app's src/auth.ts (see the auth template).
44
46
  let auth: import('@aotter/mantle/auth').AuthLoader<ReturnType<typeof import('./auth.js').createAuth>> | undefined;
45
47
  export default {
46
48
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
@@ -50,7 +52,7 @@ export default {
50
52
  let authority: Promise<Caller> | undefined, readAuthority: Promise<Caller> | undefined;
51
53
  const resolve = (readOnly: boolean) => resolveCaller(request, auth!, {
52
54
  readOnly,
53
- audience: [`${env.ORIGIN}/mcp`, `${env.ORIGIN}/mcp/admin`],
55
+ audience: [`${env.PUBLIC_ORIGIN}/mcp`, `${env.PUBLIC_ORIGIN}/mcp/admin`],
54
56
  responseHeaders: headers,
55
57
  }).then(value => { checkOrigin(request, value); return value; });
56
58
  const caller = (_request?: Request, _env?: Env, classification = { readOnly: false }) =>
@@ -60,9 +62,9 @@ export default {
60
62
  const routes = [
61
63
  authRoutes(auth, { methods: [{ kind: 'email-otp' }], resolver: caller }),
62
64
  admin(bindings.metadata, bindings.views, bindings.operations, {
63
- ...options, routes: entries(bindings.schemas, bindings.adapter),
65
+ ...options, developerViews: bindings.developerViews, routes: entries(bindings.schemas, bindings.adapter),
64
66
  }),
65
- mcp(options),
67
+ mcp({ ...options, apps: { admin: adminMcpApps({ catalogue: mcpAppCatalogues.admin }) } }),
66
68
  rest(options),
67
69
  ];
68
70
  try {
@@ -74,8 +76,9 @@ export default {
74
76
  for (const cookie of headers.getSetCookie()) result.headers.append('set-cookie', cookie);
75
77
  return result;
76
78
  }
77
- const asset = await adminAssets(request, env);
78
- if (asset) return asset;
79
+ // Serves the built Admin shell for /admin and its client routes; undefined for any other path.
80
+ const shell = await adminShell(request, path => env.ASSETS.fetch(new Request(new URL('/admin/' + path, request.url), request)));
81
+ if (shell) return shell;
79
82
  // Your public frontend/web.ts routes can use an anonymous Caller without
80
83
  // loading auth; their generated SQL still controls publication visibility.
81
84
  return new Response('Not found', { status: 404, headers: { 'cache-control': 'no-store' } });
@@ -85,31 +88,45 @@ export default {
85
88
  };
86
89
  ```
87
90
 
88
- Remove imports and array entries for omitted surfaces. `mcp.ts` exists only when
89
- the manifest has an MCP surface, and requires the documented optional MCP peer.
91
+ Keep the MCP and REST entries together for the normal symmetric path. For a deliberate
92
+ asymmetric exception, remove the omitted surface's imports and array entry. `mcp.ts`
93
+ exists only when the manifest has an MCP surface and requires the documented optional
94
+ MCP peer. An application that deliberately does not mount MCP need not import generated
95
+ `mcp.ts`; outside the entry import graph, it contributes nothing to the bundle.
90
96
  Pass the same handlers to each selected surface that exposes ref Procedures.
91
- Bind site/media/identity and extension capabilities through the selected Admin/MCP
97
+ Bind site/media/identity capabilities through the selected Admin/MCP
92
98
  options; do not expose native auth administrative routes without `authRoutes` or
93
99
  `guardAuthAdminRequest`. `withAuthTasks` retains background callbacks on the
94
100
  current request's context; `authHardening` configures the background handler.
95
101
  Select `--admin` to emit `admin-bindings.ts`. Admin's API is a separate
96
102
  `@aotter/mantle-admin` package; the SDK never imports it. The API handles only
97
- `/admin/api`. The app-owned asset route serves the built shell for `/admin` and
98
- client routes, including `/admin/sign-in`, and preserves missing asset 404s.
99
- Allow its built import map through the exact CSP hash documented by the installed
100
- Admin package. Collection statistics use the generated shared `statisticsView`:
103
+ `/admin/api`. `adminShell` (in the block above) serves the built shell for `/admin` and
104
+ client routes, including `/admin/sign-in`; it sets `frame-ancestors 'none'`, `X-Frame-Options: DENY`, `no-store` and `nosniff`
105
+ on the shell, answers a missing file under `/admin` with a 404 `Response`, and returns `undefined` for other paths and
106
+ non-GET requests. The app owns the rest of the CSP and the asset cache headers.
107
+ Collection statistics use the generated shared `statisticsView`:
101
108
  its collection parameter selects only a generated table and visibility policy.
102
109
 
103
110
  ## Workers assets and secrets
104
111
 
112
+ `mantle generate --admin --wrangler wrangler.jsonc` sets the selected assets
113
+ directory. A new assets block defaults to `binding: "ASSETS"`, `run_worker_first: true`
114
+ and `html_handling: "none"`. Existing blocks keep their binding and routing; when
115
+ `run_worker_first` is already true, a missing `html_handling` defaults to `none`.
116
+ A different binding needs a custom Admin HTML loader. Deploy the built assets and
117
+ Worker together.
118
+
105
119
  `mantle generate cloudflare-d1 generated manifests/*.yaml --auth src/auth.ts --admin`
106
- updates the split-build Wrangler keys. Configure the rest explicitly; JSONC
107
- comments and unrelated settings survive regeneration:
120
+ updates the split-build Wrangler keys (`main` and `base_dir` under `dist/worker`),
121
+ ensures `nodejs_compat` and defaults to `global_fetch_strictly_public` ([public fetch routing](deploy-and-operate.md#public-fetch-routing))
122
+ and, with `--admin`, `assets.directory`: `mantle bundle` builds Admin into
123
+ `dist/client/admin`, served at `/admin`. Configure existing assets blocks explicitly;
124
+ JSONC comments and unrelated settings survive regeneration:
108
125
 
109
126
  ```jsonc
110
127
  {
111
128
  "assets": {
112
- "directory": "node_modules/@aotter/mantle-admin/dist/admin",
129
+ "directory": "dist/client",
113
130
  "binding": "ASSETS",
114
131
  "run_worker_first": true,
115
132
  "html_handling": "none"
@@ -119,16 +136,18 @@ comments and unrelated settings survive regeneration:
119
136
  ```
120
137
 
121
138
  `run_worker_first` lets the application supply shell/security/cache headers;
122
- `none` keeps `index.html` available by name. Admin assets are not a public database
139
+ `none` keeps `index.html` available by name. `adminShell(request, assets)` passes
140
+ paths relative to the Admin directory, so the app fetches them under `/admin/`:
141
+ `path => env.ASSETS.fetch(new Request(new URL("/admin/" + path, request.url), request))`. Admin assets are not a public database
123
142
  API. PostgreSQL uses application-owned Hyperdrive/postgres.js wiring,
124
143
  with Hyperdrive query caching disabled for authority and mutation visibility.
125
144
 
126
145
  Commit `.dev.vars.example` containing variable names and placeholders only:
127
146
 
128
147
  ```dotenv
129
- ORIGIN=http://localhost:8787
148
+ PUBLIC_ORIGIN=http://localhost:8787
130
149
  ADMIN_EMAIL=you@example.test
131
- AUTH_SECRET=replace-with-a-random-secret
150
+ BETTER_AUTH_SECRET=replace-with-a-random-secret
132
151
  ```
133
152
 
134
153
  Copy it to ignored `.dev.vars`; use deployment secrets for production. Configure
@@ -137,8 +156,7 @@ an app-owned [`EmailSender`](../guides/email.md#application-wiring) and call its
137
156
  callbacks. Adapt the auth template's `MAIL.otp`/`MAIL.link` callbacks to this
138
157
  sender; it is an object, not a `.dev.vars` value. The auth
139
158
  module wires `ADMIN_EMAIL` to the bootstrap helper, which requires verified email.
140
- `PUBLIC_ORIGIN` can be your application variable instead of `ORIGIN`; map it
141
- explicitly when constructing auth and keep the trusted origin consistent.
159
+ Name the variables as your `auth.ts` reads them (the app shell uses these three) and keep the trusted origin consistent.
142
160
 
143
161
  ## Scheduling and Bun
144
162
 
@@ -150,22 +168,42 @@ invocation; see the [TTL contract](../guides/native-execution.md#ttl-maintenance
150
168
  larger sweeps' cursors for a later invocation instead of scanning without a bound.
151
169
 
152
170
  On Bun, reuse a process-owned `Database` or `SQL` pool and call the same generated
153
- surfaces from `Bun.serve({fetch})`. The optional host reader is independent of
154
- the query target:
171
+ surfaces from `Bun.serve({fetch})`. Bun runs the server as TypeScript, so
172
+ `mantle bundle` builds only Admin for it: the `bun-*` target that
173
+ `generated/.mantle-inputs.json` records skips the Worker bundle, and Admin goes
174
+ to `dist/client/admin` (`--assets` as on Workers) when the server imports
175
+ `@aotter/mantle-admin/server`. Add it as the build script and run it before
176
+ starting the server:
177
+
178
+ ```json
179
+ { "scripts": { "build": "mantle bundle src/server.ts dist" } }
180
+ ```
181
+
182
+ The optional host reader is independent of the query target:
155
183
 
156
184
  ```ts
157
185
  import { bunAdminAssets } from '@aotter/mantle/bun/assets';
158
- const assets = bunAdminAssets('node_modules/@aotter/mantle-admin/dist/admin');
186
+ const assets = bunAdminAssets('dist/client/admin');
159
187
  const bindings = adminBindings({ handlers });
160
188
  const serveAdmin = admin(bindings.metadata, bindings.views, bindings.operations, {
161
189
  db: () => db, caller, handlers, routes: entries(bindings.schemas, bindings.adapter),
162
190
  });
163
- // Call assets(path) from your separate shell/static route.
191
+ // Call assets(path) from your separate shell/static route. adminShell(request, reader) wants
192
+ // paths without a leading slash and a Response (404 when missing), so adapt the reader:
193
+ const reader = async (path: string) => (await assets('/' + path)) ?? new Response(null, { status: 404 });
164
194
  ```
165
195
 
166
196
  The reader returns a native `Response` or `null`, rejects traversal/control
167
197
  characters and escaping symlinks; the app owns shell, CSP and cache headers.
168
- Use ignored `.env` on Bun. Run cron work with your native scheduler; neither
198
+ Use ignored `.env` on Bun.
199
+
200
+ Admin's build tools and UI packages are dependencies of `@aotter/mantle-admin`,
201
+ so a production install carries them (about 65 MB). For a container image, use a
202
+ multi-stage build: the build stage installs everything and runs `mantle bundle`
203
+ and `bun build src/server.ts --target bun --outdir dist/server`; the runtime stage
204
+ copies only `dist/` (plus any files the server reads at runtime) and starts
205
+ `bun dist/server/server.js`. Neither the Admin assets nor the bundled server read
206
+ `node_modules` once built. Run cron work with your native scheduler; neither
169
207
  imports nor ordinary requests execute DDL, migrations or cron work. OAuth resource
170
208
  rows are inserted on first OAuth use from runtime configuration, with no deploy
171
209
  step; auth initialization and cookie/bearer/MCP/REST requests do no resource work.
@@ -190,13 +228,12 @@ untracked. Deployment uses committed generated source; it does not regenerate.
190
228
  # CI, with the project's exact installed SDK and peer versions:
191
229
  pnpm exec mantle generate cloudflare-d1 generated manifests/*.yaml --auth src/auth.ts --admin --check
192
230
  pnpm exec tsc --noEmit
193
- pnpm exec mantle bundle src/worker.ts dist
231
+ pnpm exec mantle bundle src/worker.ts dist # dist/worker, and dist/client/admin with --admin
194
232
  ```
195
233
 
196
234
  `--check` regenerates in memory, lists missing/changed/obsolete SDK-owned files
197
235
  and stale Wrangler build settings, exits nonzero, and writes nothing. Unmarked
198
236
  application files are preserved. TOML generation prints manual edits, so an
199
- exact Wrangler `--check` requires JSON/JSONC. Use all the same `--site`, `--auth`,
200
- `--admin` and `--admin-extensions` options as normal generation. The check needs no database,
237
+ exact Wrangler `--check` requires JSON/JSONC. Use all the same `--site`, `--auth` and `--admin` options as normal generation. The check needs no database,
201
238
  network access, credentials or deployment. Imported configuration modules must
202
239
  remain pure: exporting `authOptions(env)` avoids constructing Better Auth at build time.
@@ -16,4 +16,4 @@ The compiler adds row policy to every Schema relation, including joins and neste
16
16
 
17
17
  Field `scope` hides rows even from admin callers; use View/Procedure [`restrict`](../reference/view.md#restrict) to limit member access while Admin collection reads see all rows allowed by the Schema's scope.
18
18
 
19
- The app owns custom identity and roles. Better Auth’s admin plugin `adminRoles` (default `["admin"]`) classifies comma-separated `user.role` values. `resolveCaller` carries this layer as `admin`; `isAdmin(caller)` and `hasAdminRole(caller, name)` consult it. Application-owned resolvers mint the original Caller with `userCaller` from `@aotter/mantle/user-fields`, using only a verified user and the identity provider’s adminRoles. Setting `admin: true` on a plain object grants no privilege. Member role names alone do not grant privilege. Optional [Better Auth](../cloudflare/authentication.md) helpers use fresh primary authority by default. Admin additionally requires an admin cookie session. UI visibility never replaces a server authorization check.
19
+ The app owns custom identity and roles. Better Auth’s admin plugin `adminRoles` (default `["admin"]`) classifies comma-separated `user.role` values. `resolveCaller` carries this layer as `admin`; `isAdmin(caller)` and `hasAdminRole(caller, name)` consult it. Callers come from `resolveCaller` over Better Auth; an existing identity provider connects through Better Auth's OAuth or SSO plugins, since no public API builds a Caller from another provider's session (`authenticatedAs` in `@aotter/mantle/testing` is for tests). Setting `admin: true` on a plain object grants no privilege. Member role names alone do not grant privilege. Optional [Better Auth](../cloudflare/authentication.md) helpers use fresh primary authority by default. Admin additionally requires an admin cookie session. UI visibility never replaces a server authorization check.
@@ -16,3 +16,22 @@ The app owns OAuth configuration, discovery and consent; follow [OAuth](../guide
16
16
  [MCP Apps](../guides/mcp-apps.md) add static UI resources through the public contributor hook. Admin uses the same hook from its separate package. Credentials never go in App HTML. WebMCP catalogues are generated display/tool metadata for the mounted Admin product; server checks remain authoritative.
17
17
 
18
18
  See [surface contracts](../reference/surface.md) for tool schemas, challenges and scalar conversion.
19
+
20
+ ## When a host shows old tools
21
+
22
+ A fresh `tools/list` always returns the deployed tools, but many hosts keep their own copy and do not ask again after a deploy. Mantle sends no list-changed notifications: tools change only at deploy, and the hosts that keep copies do not act on them. Instead each generated tool's description ends with its own version, and the results of a call that reaches the tool carry the deployed version, so a model holding an older definition can see it, correct its call or ask the user to refresh. Failures also state the input schema the tool is listed with now, except authorization refusals. A call naming a tool the catalogue no longer has gets the protocol's unknown-tool error, whose message gives `serverInfo.version`: unless the application sets it, a hash of every tool the endpoint serves, before per-caller filtering. Refusals before a call reaches its tool, such as authentication challenges, carry neither.
23
+
24
+ In Admin, WebMCP results from the tool route carry the version, and refusals add it with the input schema, except authorization refusals; a tool removed since the page loaded is refused as not found, without a catalogue version. Admin's two navigation tools, `admin_get_context` and `admin_navigate`, carry no version: the page defines and runs them without a server call, so it cannot list one definition and run another, and a reload after a deploy replaces both. The exact shapes are in [surface contracts](../reference/surface.md#tool-versions).
25
+
26
+ How each client refreshes its copy (as of 2026-10):
27
+
28
+ | Client | Refresh |
29
+ | --- | --- |
30
+ | claude.ai connectors | A server-side copy keyed by URL, kept about an hour (users report up to 24 hours). Only adding the connector again under a new URL, such as `?v=2`, forces a refresh, and that needs authorization again. |
31
+ | Claude Code | `/mcp` reconnect, or a new session |
32
+ | ChatGPT developer mode | Refresh the connection, then start a new chat |
33
+ | Published ChatGPT apps | A daily scan, or Rescan, followed by review; keep tools compatible meanwhile |
34
+ | Cursor | Quit fully and reopen |
35
+ | VS Code | Run "MCP: Reset Cached Tools", or restart the server |
36
+ | Gemini CLI | `/mcp reload` |
37
+ | MCP Inspector | The refresh button |
@@ -11,7 +11,7 @@ The runtime never creates, alters or checks tables. Boot and requests run no DDL
11
11
 
12
12
  For the first install, apply generated `schema.sql`, including selected auth/site DDL, to an empty database. Apply selected seed SQL explicitly.
13
13
 
14
- For every later schema change, the application's coding agent diffs the regenerated `schema.sql`, writes a migration with the host's native tool (`wrangler d1 migrations` or the app's PostgreSQL migration tool), rehearses it on a copy, and applies it before deploying the new code. Cloud executes migrations at deploy time.
14
+ For every later schema change, the application's coding agent diffs the regenerated `schema.sql`, writes a migration with the host's native tool (`wrangler d1 migrations` or the app's PostgreSQL migration tool), rehearses it on a copy, and applies it before deploying the new code.
15
15
 
16
16
  Destructive changes and type changes need explicit data migration. Mantle drops nothing. Initial `schema.sql` is not an upgrade script.
17
17
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  Use the [Worker quickstart](../start/quickstart-worker.md) for a complete native manifest and entry, [typed queries](../guides/typed-queries.md) for embedding, and [service composition](../cloudflare/service-entry.md) for optional surfaces.
4
4
 
5
- The runnable [reference service](https://github.com/aotter/mantle/blob/develop/docs/examples/reference-service/README.md) exercises the native package integration. See the [generated public website](../../examples/generated-web/README.md) for a native Worker example.
5
+ The runnable [reference service](https://github.com/aotter/mantle/tree/v0.2.0-alpha.9/docs/examples/reference-service) exercises the native package integration. See the [generated public website](../../examples/generated-web/README.md) for a native Worker example.