@aotter/mantle 0.2.1-alpha.4 → 0.2.2-alpha.1
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.
- package/dist/auth/routes.d.ts +3 -1
- package/dist/auth/routes.js +15 -2
- package/dist/build/compiler.d.ts +1 -0
- package/dist/build/compiler.js +2 -2
- package/dist/build/spec/kernel/diagnostic.d.ts +1 -1
- package/dist/bun/assets.d.ts +3 -1
- package/dist/bun/assets.js +4 -1
- package/dist/domain/describe/chunk-GMUORMOX.js +1 -0
- package/dist/domain/describe/index.js +1 -1
- package/dist/domain/describe/public.js +1 -1
- package/dist/domain/generated-tools.js +24 -9
- package/dist/infrastructure/bundle.js +30 -1
- package/dist/infrastructure/inputs.js +3 -2
- package/dist/infrastructure/wrangler.js +4 -3
- package/dist/kernel/authorization.d.ts +1 -0
- package/dist/kernel/authorization.js +12 -5
- package/dist/kernel/mcp-call.d.ts +3 -3
- package/dist/kernel/mcp-call.js +4 -4
- package/dist/kernel/mcp-handler.js +4 -3
- package/dist/kernel/mcp.d.ts +12 -10
- package/dist/kernel/mcp.js +1 -1
- package/dist/kernel/media-fetch.js +109 -0
- package/dist/kernel/media.d.ts +15 -2
- package/dist/kernel/media.js +138 -16
- package/dist/kernel/r2-media.d.ts +3 -2
- package/dist/kernel/r2-media.js +12 -8
- package/dist/kernel/result-meta.d.ts +10 -0
- package/dist/kernel/result-meta.js +1 -0
- package/dist/kernel/site-types.d.ts +54 -6
- package/dist/kernel/site.d.ts +1 -1
- package/dist/usecase/generate.js +2 -2
- package/dist/usecase/mcp-media.js +17 -5
- package/dist/usecase/mcp.js +6 -5
- package/dist/usecase/site.js +4 -2
- package/docs/examples/README.md +5 -1
- package/docs/examples/app-shell/README.md +7 -3
- package/docs/examples/app-shell/package.json +9 -9
- package/docs/examples/app-shell/src/jwks.ts +22 -0
- package/docs/examples/app-shell/src/service.ts +12 -1
- package/docs/examples/app-shell/wrangler.jsonc +1 -1
- package/docs/examples/club-events/README.md +10 -3
- package/docs/examples/club-events/manifests/club-events.yaml +42 -11
- package/docs/examples/club-events/smoke.mjs +31 -1
- package/docs/examples/club-events/src/handlers.ts +9 -8
- package/docs/examples/frontend-mcp-app/README.md +23 -0
- package/docs/examples/frontend-mcp-app/app.tsx +50 -0
- package/docs/examples/frontend-mcp-app/manifest.yaml +30 -0
- package/docs/examples/purchasing/README.md +9 -0
- package/docs/examples/purchasing/manifests/purchasing.yaml +18 -16
- package/docs/examples/purchasing/smoke.mjs +5 -0
- package/docs/examples/purchasing/wrangler.jsonc +1 -1
- package/docs/examples/recipes/wrangler.jsonc +1 -1
- package/docs/handbook/cloudflare/authentication.md +51 -0
- package/docs/handbook/cloudflare/deploy-and-operate.md +5 -0
- package/docs/handbook/cloudflare/media-r2.md +5 -2
- package/docs/handbook/cloudflare/service-entry.md +28 -7
- package/docs/handbook/concepts/runtime-and-adapters.md +1 -1
- package/docs/handbook/examples/hub.md +1 -1
- package/docs/handbook/guides/adapter-guide.md +1 -1
- package/docs/handbook/guides/mcp-apps.md +94 -18
- package/docs/handbook/guides/native-admin.md +30 -1
- package/docs/handbook/guides/performance.md +5 -1
- package/docs/handbook/guides/revisions.md +1 -1
- package/docs/handbook/guides/size-and-startup.md +3 -3
- package/docs/handbook/guides/sql-patterns.md +34 -1
- package/docs/handbook/reference/authorization.md +1 -1
- package/docs/handbook/reference/describe.md +5 -0
- package/docs/handbook/reference/features.md +1 -1
- package/docs/handbook/reference/schema.md +19 -0
- package/docs/handbook/reference/site-config.md +2 -0
- package/docs/handbook/reference/sql.md +1 -1
- package/docs/handbook/reference/surface.md +66 -10
- package/docs/handbook/reference/view.md +1 -2
- package/docs/handbook/releases/index.md +131 -115
- package/docs/handbook/start/agents.md +2 -2
- package/docs/handbook/start/overview.md +1 -1
- package/docs/handbook/start/project-and-cli.md +1 -1
- package/package.json +10 -10
- package/dist/domain/describe/chunk-CW7X5Q5T.js +0 -1
|
@@ -215,6 +215,16 @@ by declared identity, scope and `check` requirements. Without `authorize`, the
|
|
|
215
215
|
complete endpoint catalogue is not a permission-filtered catalogue. Operation
|
|
216
216
|
validation, guards and credential checks still apply to calls.
|
|
217
217
|
|
|
218
|
+
With `authorize`, `discovery: "all"` keeps the caller-independent list: every
|
|
219
|
+
caller of the endpoint gets the same `tools/list` and the same tool names in
|
|
220
|
+
resource HTML, and `authorize` (with `requires`, scopes and `check`) decides at
|
|
221
|
+
call time. Prefer it when a caller's grant can change during a conversation, for
|
|
222
|
+
example a role switched in a directory: a host keeps the tool list it saved, and a
|
|
223
|
+
filtered list would leave it naming tools that are gone or missing ones the caller
|
|
224
|
+
now has. An `authorize` that rejects with `new ContractError("AUTH_DENIED", message)`
|
|
225
|
+
returns that message, so a refusal can name the role that may call the tool; a
|
|
226
|
+
plain `false` or any other rejection answers "Target is not authorized."
|
|
227
|
+
|
|
218
228
|
`toolsPageSize` pages `tools/list`: each page holds at most that many tools and
|
|
219
229
|
carries an opaque `nextCursor` while more remain; pass it back as `cursor`. A
|
|
220
230
|
cursor is valid only for the catalogue version that issued it; an unknown or
|
|
@@ -389,16 +399,62 @@ own entry; inherited property names never become titles or descriptions.
|
|
|
389
399
|
### Admin media MCP tools
|
|
390
400
|
|
|
391
401
|
With generated site configuration, `mcp({ db, caller, media: env => storage })`
|
|
392
|
-
loads the media module only on a configured admin MCP request and
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
+
loads the media module only on a configured admin MCP request and adds four
|
|
403
|
+
tools to `/mcp/admin` only. `storage` is the same `MediaStorage` used by the
|
|
404
|
+
Admin media library. Omitting `media` omits every media tool. Admins can invoke
|
|
405
|
+
them; cookie writes retain the origin check. Generated boundary code checks each
|
|
406
|
+
input and advertises read-only/destructive/open-world hints.
|
|
407
|
+
|
|
408
|
+
| Tool | Does |
|
|
409
|
+
|---|---|
|
|
410
|
+
| `upload_media` | Upload one image, in two calls (below). |
|
|
411
|
+
| `find_media` | Committed assets by `ids` (ids not found come back in `missing`), or newest first with `limit`, `cursor` and `search` (the exact id, or text in alt or caption). Read-only. |
|
|
412
|
+
| `update_media` | Set `alt` or `caption` of `id`; an omitted field is unchanged, an empty string clears it. |
|
|
413
|
+
| `delete_media` | Delete `id`'s objects and record (`destructiveHint`, so hosts confirm). A partial failure is retried with the same id. |
|
|
414
|
+
|
|
415
|
+
`upload_media` takes one mode per call:
|
|
416
|
+
|
|
417
|
+
1. `{ purpose, filename, variants: [{ mimeType, byteSize, role }], alt?, caption? }`
|
|
418
|
+
returns `{ uploadId, expiresAt, targets: [{ role, mimeType, method, url, headers }] }`.
|
|
419
|
+
PUT each variant's bytes to its `url` with its `headers`. Targets expire in
|
|
420
|
+
15 minutes and serve this one upload. When the request declares MCP Apps
|
|
421
|
+
support and a resource such as the upload card renders `upload_media`,
|
|
422
|
+
`targets` is left out of the result and sent only in the result
|
|
423
|
+
`_meta["net.aotter.mantle/upload"]`, which hosts give the App and not the
|
|
424
|
+
model; an agent without MCP Apps (a CLI agent such as Claude Code) reads them
|
|
425
|
+
in the result.
|
|
426
|
+
2. `{ uploadId, purpose?, alt?, caption? }` commits and returns the asset.
|
|
427
|
+
Store its id in fields that reference `media_assets.id`, never an upload URL.
|
|
428
|
+
An unknown or expired `uploadId` is `MEDIA_UPLOAD_EXPIRED` (make a new first
|
|
429
|
+
call); before the commit, a `purpose` other than the upload's is
|
|
430
|
+
`MEDIA_UPLOAD_MISMATCH`; a committed `uploadId` returns the same asset again
|
|
431
|
+
(the asset records no purpose).
|
|
432
|
+
|
|
433
|
+
`{ purpose? }` alone returns the purposes' policy, `{ purposes: [{ name, required, maxBytes }] }`
|
|
434
|
+
(each `required` slot lists its accepted MIME types, comma-separated); the
|
|
435
|
+
generated input schema also names the purposes and their caps, and policy
|
|
436
|
+
failures say what was expected. With the [upload card](../guides/mcp-apps.md#the-upload-card)
|
|
437
|
+
registered, an MCP Apps host shows it for this call.
|
|
438
|
+
|
|
439
|
+
`{ purpose, sourceUrl }` and `{ purpose, file: { download_url, file_id } }` have the
|
|
440
|
+
server fetch one image and run both steps in one call. `file` is the tool's
|
|
441
|
+
`openai/fileParams` argument, so a host that hands the tool a chat attachment
|
|
442
|
+
passes its download URL. Either takes a purpose with one variant slot. The fetch
|
|
443
|
+
accepts only `https` on the default port, without credentials, to a public host:
|
|
444
|
+
IPv6 literals, private, loopback, link-local, shared, reserved and multicast
|
|
445
|
+
IPv4 addresses, single-label names and names such as `localhost`, `*.local` and
|
|
446
|
+
`*.internal` are `MEDIA_SOURCE_REJECTED`, as is each redirect target (at most
|
|
447
|
+
three). The body is capped at the purpose's byte cap
|
|
448
|
+
(`MEDIA_VARIANT_SIZE_EXCEEDED`) and must sniff as PNG, JPEG, GIF, WebP or AVIF
|
|
449
|
+
(`MEDIA_MIME_REJECTED`); an unreachable source is `MEDIA_SOURCE_FAILED` and a
|
|
450
|
+
storage PUT failure `MEDIA_STORAGE_FAILED`. Names are checked, not resolved:
|
|
451
|
+
Workers cannot reach private addresses, and on another runtime restrict the
|
|
452
|
+
server's egress to the public internet. File content is never accepted as base64.
|
|
453
|
+
|
|
454
|
+
The 0.1 names `get_media_upload_policy`, `create_media_upload`,
|
|
455
|
+
`commit_media_upload`, `list_media_assets`, `get_media_asset`,
|
|
456
|
+
`update_media_asset` and `delete_media_asset` still work in 0.2, after the four,
|
|
457
|
+
with descriptions marked deprecated; they are removed in 0.3.
|
|
402
458
|
|
|
403
459
|
Generation always emits a `Handlers` type (empty when no Procedure uses `handler: { ref }`), so every app wires the
|
|
404
460
|
same way: `const handlers: Handlers = {...}`, then `store(db, caller, { handlers })`, `adminBindings({ handlers })`,
|
|
@@ -21,8 +21,7 @@ OFFSET, arbitrary functions/relations, DDL and transaction control are not a rea
|
|
|
21
21
|
`mantle.search(<alias>, <text>)` matches the Schema's declared `searchableFields`, and `mantle.search_rank(<alias>)`
|
|
22
22
|
orders by relevance; qualify both with `mantle.`. An optional search reads
|
|
23
23
|
`WHERE p.active AND (input.q IS NULL OR mantle.search(p, input.q))`, and a ranked one
|
|
24
|
-
`WHERE mantle.search(p, input.q) ORDER BY mantle.search_rank(p), p.id
|
|
25
|
-
for cursor paging over the rank).
|
|
24
|
+
`WHERE mantle.search(p, input.q) ORDER BY mantle.search_rank(p), p.id`; page it with a cursor over the rank and the id.
|
|
26
25
|
|
|
27
26
|
Views using `mantle.search`, Store search and Admin list search share the dialect search: SQLite/D1 use FTS for queries of 3 or more characters (shorter queries use LIKE); PostgreSQL uses ILIKE over the declared fields and scans by default. A View does not make PostgreSQL search faster. For large PostgreSQL tables, the application's agent adds trigram indexes in a migration, which ILIKE then uses with no code change:
|
|
28
27
|
|
|
@@ -32,123 +32,139 @@ Pin every package you use to the same exact version and upgrade them together.
|
|
|
32
32
|
From 0.1.x, follow [Upgrade 0.1 to 0.2](upgrade-0.1-to-0.2.md) instead: a new database, a reviewed and rehearsed data
|
|
33
33
|
move, and never booting 0.2 against a 0.1.x database.
|
|
34
34
|
|
|
35
|
-
## 0.2.
|
|
36
|
-
|
|
37
|
-
0.2.1
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- **
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
-
|
|
35
|
+
## 0.2.2 — in preparation
|
|
36
|
+
|
|
37
|
+
### Upgrading to 0.2.2-alpha.1
|
|
38
|
+
|
|
39
|
+
Pin the selected packages to `0.2.2-alpha.1` and regenerate. Nothing breaks, and existing configuration keeps working:
|
|
40
|
+
|
|
41
|
+
- **Media tools.** `/mcp/admin` has four media tools: `upload_media`, `find_media`, `update_media` and
|
|
42
|
+
`delete_media`, plus an upload card. The seven older names still answer, marked "Deprecated, removed in 0.3".
|
|
43
|
+
A media purpose may be `visibility: "private"`: its assets carry no public URL, and `signRead` returns a
|
|
44
|
+
short-lived one. New media ids carry a per-database tag; another server's id is `MEDIA_NOT_OWNED` (404).
|
|
45
|
+
- **MCP.** `discovery: "all"` gives every caller of an endpoint the same `tools/list`, and `authorize` still decides
|
|
46
|
+
each call. The Admin MCP App links at the fixed `ui://mantle-admin/admin` by default; the build's hash URI still
|
|
47
|
+
answers.
|
|
48
|
+
- **Admin sign-in seams.** A `{ kind: "passkey" }` method, a `SignIn` component slot and an `onSignedIn` step; Admin
|
|
49
|
+
ships no passkey experience of its own.
|
|
50
|
+
- **Static files at the edge.** A newly generated `run_worker_first` excludes `/admin/assets/*` and `/assets/*`, and
|
|
51
|
+
`mantle bundle` copies `public/` into the assets directory. Existing `run_worker_first` settings are not changed;
|
|
52
|
+
in a new config an app route under `/assets/` no longer reaches the Worker.
|
|
53
|
+
- **Fixes.** OTP failures are normalised at the HTTP boundary; a Schema check refusal is no longer reported as a stale
|
|
54
|
+
write; media search by id works on D1.
|
|
55
|
+
- **Docs.** [Bulk seeding](../reference/schema.md#seed-in-bulk), changing another member's row with an inline
|
|
56
|
+
`author_id = auth.uid()` ([SQL patterns](../guides/sql-patterns.md)), and a waiting list in the club-events example.
|
|
57
|
+
|
|
58
|
+
## 0.2.1 (2026-10-30)
|
|
59
|
+
|
|
60
|
+
0.2.1 adds **Schema locks**: a manifest rule that refuses a row's update or delete on every caller path (REST, MCP,
|
|
61
|
+
Admin and Procedures), and Admin shows the lock before an edit. `spec.publishing.liveEdits` lets an application edit a
|
|
62
|
+
published row in place. The [revisions guide](../guides/revisions.md) now freezes the live revision with a lock and
|
|
63
|
+
switches it atomically.
|
|
64
|
+
|
|
65
|
+
Procedures gain `handler.steps`, a program written one statement per step. With `onNoRows: conflict`, a set-op write
|
|
66
|
+
that writes nothing becomes a `CONFLICT` and rolls the whole program back ("create only if the parent exists"). Ref
|
|
67
|
+
handlers and interactive transactions can use `store.batch` for atomic multi-writes. Hand-built MCP tools can attach
|
|
68
|
+
listing and result metadata and page `tools/list`.
|
|
69
|
+
|
|
70
|
+
For agents and for the people working with them:
|
|
71
|
+
|
|
72
|
+
- `mantle help` is the single source of guidance, and the `mantle` skill no longer depends on the SDK version.
|
|
73
|
+
- An agent building from a brief first replies with one plan, written in business terms, and asks what the brief
|
|
74
|
+
leaves open. After one go-ahead, it builds, shows the Developer UI, runs acceptance and a performance pass, and
|
|
75
|
+
reports what each found and changed.
|
|
76
|
+
- The Developer UI draws a real ER diagram, a columned system wiring graph and one Procedure logic graph.
|
|
77
|
+
|
|
78
|
+
Generated Workers and the Admin start smaller.
|
|
79
|
+
|
|
80
|
+
[GitHub release](https://github.com/aotter/mantle/releases/tag/v0.2.1) is the complete change history. Known
|
|
81
|
+
follow-ups:
|
|
82
|
+
|
|
83
|
+
- mounting an adopter MCP server behind Mantle's caller gate ([#1843](https://github.com/aotter/mantle/issues/1843));
|
|
84
|
+
- tool-catalogue change notifications ([#1810](https://github.com/aotter/mantle/issues/1810));
|
|
85
|
+
- the Admin MCP App, which is still above its 480 KB size reference. About 40% of it is the official MCP SDK and zod,
|
|
86
|
+
which ADR-0029 requires.
|
|
87
|
+
|
|
88
|
+
### Upgrading from 0.2.0
|
|
89
|
+
|
|
90
|
+
Pin every selected `@aotter/mantle`, `@aotter/mantle-ui` and `@aotter/mantle-admin` dependency to `0.2.1`, refresh the
|
|
91
|
+
lockfile, regenerate, and review the owned output. No DDL changes are required.
|
|
92
|
+
|
|
93
|
+
Breaking changes:
|
|
94
|
+
|
|
95
|
+
- **`@aotter/mantle/describe`** keeps only `matchTarget` and the `AppDescription`, `DescribeTarget` and
|
|
96
|
+
`TargetRequest` types. `describeApp`, `diffApp` and the `@aotter/mantle/target-hash` subpath are removed.
|
|
97
|
+
- Get a description outside the CLI with `mantle describe <target> manifests/*.yaml > describe.json`, run in Node.js
|
|
98
|
+
or Bun (for example in a deploy step), and give the JSON to `matchTarget`. Do not import compiler files from
|
|
99
|
+
`dist`: a Worker cannot compile manifests that contain SQL ([describe](../reference/describe.md)).
|
|
100
|
+
- Use `mantle describe --diff` instead of `diffApp`, and `_meta.targetHash` instead of `targetHash()`.
|
|
101
|
+
- **PostgreSQL `version`:** Store results return `version` as a number, as on SQLite and D1. Procedure results stay
|
|
102
|
+
driver-native.
|
|
103
|
+
- **PostgreSQL serialization failures:** SQLSTATE `40001` and `40P01` from a definitive server refusal are now
|
|
104
|
+
`CONFLICT` with `kind: "serialization"` (409), instead of an unclassified driver error. Nothing is retried
|
|
105
|
+
automatically; read again before deciding to retry.
|
|
106
|
+
- **Admin record form:** clearing an optional field sends `null`, and generated form types for optional fields are
|
|
107
|
+
`T | null`.
|
|
108
|
+
- **REST route precedence** is fixed and most-specific-first. Only overlapping same-method http Triggers with the same
|
|
109
|
+
parameter count can change which one wins. `TRIGGER_PATH_INVALID` and `TRIGGER_PATH_COLLISION` refuse ambiguous
|
|
110
|
+
paths at generation; see [route precedence](../reference/trigger.md#route-precedence).
|
|
111
|
+
- **Agent guidance moved into the handbook.** The package no longer ships `docs/skills/`, so the `mantle help
|
|
112
|
+
skills/<name>` topics are gone. Their guidance now lives in these pages, and `mantle help` reaches all of them from
|
|
113
|
+
its entry page:
|
|
114
|
+
- [performance pass](../guides/performance.md);
|
|
115
|
+
- [deploy and operate](../cloudflare/deploy-and-operate.md);
|
|
116
|
+
- [cleaning up stale uploads](../cloudflare/media-r2.md#clean-up-stale-uploads);
|
|
117
|
+
- [upgrading an application](#upgrade-an-application);
|
|
118
|
+
- [adoption in an existing app](../guides/typed-queries.md#adopt-mantle-in-an-existing-application).
|
|
119
|
+
|
|
120
|
+
The 0.1 → 0.2 upgrade guide is the topic `mantle help upgrade-0.1-to-0.2`. A `mantle` skill installed from an
|
|
121
|
+
earlier tag still names `docs/skills/` paths; reinstall it with `npx skills add aotter/mantle@mantle`.
|
|
122
|
+
- **Revisions recipe:** an application that copied the guide's `switch-revision` handler should adopt the new version.
|
|
123
|
+
It requires `expectedRevisionVersion` from the preview, and seals and switches in one `ctx.store.batch`.
|
|
124
|
+
- **Tooling:** the templates move to Wrangler 4.153.0 and `@cloudflare/workers-types` 5.20261027.1.
|
|
125
|
+
|
|
126
|
+
New, opt-in:
|
|
127
|
+
|
|
128
|
+
- **Schema locks and live edits:** see [revisions](../guides/revisions.md).
|
|
129
|
+
- **`handler.steps` and `onNoRows: conflict`.** Ref handlers and interactive transactions can also use
|
|
130
|
+
`store.batch` for atomic multi-writes.
|
|
131
|
+
- **Hand-built MCP tools** ([MCP Apps](../guides/mcp-apps.md), [surface](../reference/surface.md)):
|
|
132
|
+
- `Tool.meta` merges into the tool's listed `_meta`. `ui.visibility: ["app"]` can hide a Procedure from clients
|
|
133
|
+
without MCP Apps; it is a listing hint, and calls are authorized as before.
|
|
134
|
+
- `withResultMeta(data, meta)` returns `meta` only in the result `_meta`, so a value such as a signed upload URL stays
|
|
135
|
+
out of `content`. `callTool` returns `{ data, _meta }`; drop `_meta` before forwarding a result to a model.
|
|
136
|
+
- `toolsPageSize` pages `tools/list` with an opaque `nextCursor`.
|
|
137
|
+
- **Admin media picker** for custom inputs and pages: `@aotter/mantle-admin/media`.
|
|
138
|
+
|
|
139
|
+
Picked up by regenerating, with no manifest or API change:
|
|
140
|
+
|
|
141
|
+
- **Agent workflow** ([Build from a product brief](../start/agents.md#build-from-a-product-brief)). The agent works
|
|
142
|
+
in four phases:
|
|
143
|
+
1. Before anything else, it replies with one plain-language plan: what each kind of user can do, the rules the app
|
|
144
|
+
enforces, who will try it, how much sample data, and what counts as fast enough. The plan ends with the open
|
|
145
|
+
questions, each with a default answer.
|
|
146
|
+
2. After one go-ahead, it builds and opens the Developer UI (`pnpm accept --open`) in the user's browser.
|
|
147
|
+
3. It runs role-only acceptance and the performance pass without stopping.
|
|
148
|
+
4. It ends with an acceptance summary and a before → after performance summary.
|
|
149
|
+
- **Developer UI diagrams:**
|
|
150
|
+
- The data model is an ER diagram with keys, types and crow's-foot cardinality, in three display modes, with a
|
|
151
|
+
minimap.
|
|
152
|
+
- System wiring is laid out in columns, also with a minimap.
|
|
87
153
|
- A Procedure's logic is one graph, the same in the wiring side panel and on the Procedure page: a transaction box,
|
|
88
154
|
CASE as ordered branch rows, and COMMIT and ROLLBACK ends. The SQL syntax trees stay available, folded, on the
|
|
89
155
|
Procedure page.
|
|
90
|
-
-
|
|
91
|
-
- **Smaller startup
|
|
92
|
-
- Generated Workers emit a repeated SQL statement once
|
|
93
|
-
|
|
94
|
-
- Admin loads sign-in, OAuth consent and the collection list on demand
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
`AppDescription`, `DescribeTarget` and `TargetRequest` types. `describeApp`,
|
|
103
|
-
`diffApp` and the `@aotter/mantle/target-hash` subpath are removed.
|
|
104
|
-
**`mantle describe` output is the supported way to get a description outside
|
|
105
|
-
the CLI:** run `mantle describe <target> manifests/*.yaml > describe.json` in
|
|
106
|
-
Node.js or Bun (for example in a deploy step) and give the JSON to
|
|
107
|
-
`matchTarget`. Do not import compiler files from `dist`; a Worker cannot
|
|
108
|
-
compile manifests that contain SQL ([describe](../reference/describe.md)).
|
|
109
|
-
Use `mantle describe --diff` instead of `diffApp`, and `_meta.targetHash`
|
|
110
|
-
instead of `targetHash()`.
|
|
111
|
-
|
|
112
|
-
Hand-built MCP tools gain three opt-in capabilities; without them the output
|
|
113
|
-
is unchanged ([MCP Apps](../guides/mcp-apps.md), [surface](../reference/surface.md)):
|
|
114
|
-
|
|
115
|
-
- `Tool.meta` merges into the tool's listed `_meta`. `ui.visibility: ["app"]`
|
|
116
|
-
can now hide a Procedure from clients without MCP Apps; it is a listing hint,
|
|
117
|
-
and calls are authorized as before.
|
|
118
|
-
- `withResultMeta(data, meta)` returns `meta` only in the result `_meta`, so a
|
|
119
|
-
value such as a signed upload URL stays out of `content`. `callTool` returns
|
|
120
|
-
`{ data, _meta }`; drop `_meta` before forwarding a result to a model.
|
|
121
|
-
- `toolsPageSize` pages `tools/list` with an opaque `nextCursor`.
|
|
122
|
-
|
|
123
|
-
Also: generation reports a SQL parser that cannot load in the current runtime
|
|
124
|
-
as such, instead of `SQL_SYNTAX`, and the handbook renders under Vue-based
|
|
125
|
-
site generators (bare `<placeholder>` text is now quoted, and `pnpm check`
|
|
126
|
-
fails on any that return).
|
|
127
|
-
|
|
128
|
-
### Upgrading to 0.2.1-alpha.1
|
|
129
|
-
|
|
130
|
-
Pin `@aotter/mantle`, and any selected `@aotter/mantle-ui` and
|
|
131
|
-
`@aotter/mantle-admin`, to `0.2.1-alpha.1`, refresh the lockfile, regenerate and
|
|
132
|
-
review owned output. No DDL changes are required. Breaking changes:
|
|
133
|
-
|
|
134
|
-
- **PostgreSQL `version`:** Store results return `version` as a number, as on
|
|
135
|
-
SQLite and D1. Procedure results stay driver-native.
|
|
136
|
-
- **Admin record form:** clearing an optional field sends `null`; generated
|
|
137
|
-
form types for optional fields are `T | null`.
|
|
138
|
-
- **REST route precedence** is fixed and most-specific-first. Only overlapping
|
|
139
|
-
same-method http Triggers with the same parameter count can change winner.
|
|
140
|
-
`TRIGGER_PATH_INVALID` and `TRIGGER_PATH_COLLISION` refuse ambiguous paths
|
|
141
|
-
at generation; see [route precedence](../reference/trigger.md#route-precedence).
|
|
142
|
-
- **Tooling:** the templates move to Wrangler 4.153.0 and
|
|
143
|
-
`@cloudflare/workers-types` 5.20261027.1.
|
|
144
|
-
- **PostgreSQL serialization failures:** SQLSTATE `40001` and `40P01` from a
|
|
145
|
-
definitive server refusal are now `CONFLICT` with `kind: "serialization"`
|
|
146
|
-
(409), instead of an unclassified driver error. Nothing is retried
|
|
147
|
-
automatically; read again before deciding to retry.
|
|
148
|
-
- **Revisions recipe:** an application that copied the guide's
|
|
149
|
-
`switch-revision` handler should adopt the new version. It requires
|
|
150
|
-
`expectedRevisionVersion` from the preview and seals and switches in one
|
|
151
|
-
`ctx.store.batch`.
|
|
156
|
+
- All of their wording is translated in every Admin language that has translations.
|
|
157
|
+
- **Smaller startup:**
|
|
158
|
+
- Generated Workers emit a repeated SQL statement once, and REST loads a Procedure on its first request. A generated
|
|
159
|
+
D1 Worker's startup is about 20% smaller.
|
|
160
|
+
- Admin loads sign-in, OAuth consent and the collection list on demand, and the Admin MCP App uses native select and
|
|
161
|
+
popover controls.
|
|
162
|
+
- **Fixes:**
|
|
163
|
+
- Admin forms and refresh;
|
|
164
|
+
- `mantle bundle` under `wrangler dev` reports stale generated output in one line, instead of a failed build;
|
|
165
|
+
- generation reports a SQL parser that cannot load in the current runtime as such, instead of `SQL_SYNTAX`;
|
|
166
|
+
- the handbook renders under Vue-based site generators;
|
|
167
|
+
- dependency updates for current advisories.
|
|
152
168
|
|
|
153
169
|
## 0.2.0 (2026-10-19)
|
|
154
170
|
|
|
@@ -501,7 +517,7 @@ See [HTTP, MCP, CLI and packages](../reference/surface.md) for the full list.
|
|
|
501
517
|
|
|
502
518
|
**Hosting.** `createMantleWorker` assembles the conventional Cloudflare Worker:
|
|
503
519
|
D1 and assets bindings, Better Auth 1.7 (social providers, email OTP, magic
|
|
504
|
-
link, passkey), Admin, MCP, Web and R2 media uploads. The Bun and Vercel
|
|
520
|
+
link, and app-enabled plugins such as passkey), Admin, MCP, Web and R2 media uploads. The Bun and Vercel
|
|
505
521
|
adapters are experimental, cover public Views and HTTP Triggers only, and leave
|
|
506
522
|
authentication and CSRF to the host. ChatGPT Sites is a
|
|
507
523
|
first-class integration with a runnable reference.
|
|
@@ -62,8 +62,8 @@ scope.
|
|
|
62
62
|
what they are looking at. Keep building while they look; the app rebuilds on every change. Without a browser on the
|
|
63
63
|
machine, give the user the admin URL it prints.
|
|
64
64
|
6. **Run [role-only acceptance](../guides/agent-workflow-acceptance.md)** (`mantle help agent-workflow-acceptance`) as
|
|
65
|
-
planned. Scripted smoke is not acceptance. `pnpm accept` starts the app and
|
|
66
|
-
|
|
65
|
+
planned. Scripted smoke is not acceptance. `pnpm accept` starts the app and prints, for each role, a URL that carries
|
|
66
|
+
that role's session; give each URL to a fresh evaluator. After a repair, the guide's
|
|
67
67
|
[re-verification rule](../guides/agent-workflow-acceptance.md#re-verify-what-a-repair-touches) decides which roles
|
|
68
68
|
and surfaces run again.
|
|
69
69
|
7. **Run the [performance pass](../guides/performance.md)** as planned, for every role and surface acceptance reports
|
|
@@ -40,7 +40,7 @@ module the entry never imports.
|
|
|
40
40
|
| `cloudflare-d1` | sqlite | D1 binding/session |
|
|
41
41
|
| `cloudflare-postgres` | postgres | postgres.js, usually with Hyperdrive |
|
|
42
42
|
|
|
43
|
-
Other handles may adapt to the same [structural driver contracts](../guides/adapter-guide.md). Dialects are exactly postgres and sqlite in 0.2.
|
|
43
|
+
Other handles may adapt to the same [structural driver contracts](../guides/adapter-guide.md). Dialects are exactly postgres and sqlite in 0.2.
|
|
44
44
|
|
|
45
45
|
| Option | Meaning |
|
|
46
46
|
| --- | --- |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aotter/mantle",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2-alpha.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"engines": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"jsonc-parser": "3.3.1",
|
|
21
21
|
"@pgsql/types": "18.0.0",
|
|
22
22
|
"libpg-query": "18.1.5",
|
|
23
|
-
"pgsql-deparser": "18.3.
|
|
23
|
+
"pgsql-deparser": "18.3.12",
|
|
24
24
|
"yaml": "^2.9.1",
|
|
25
25
|
"zod": "^4.6.7"
|
|
26
26
|
},
|
|
@@ -72,9 +72,9 @@
|
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"@modelcontextprotocol/server": "2.3.1",
|
|
75
|
-
"@better-auth/core": "1.7.
|
|
76
|
-
"@better-auth/oauth-provider": "1.7.
|
|
77
|
-
"better-auth": "1.7.
|
|
75
|
+
"@better-auth/core": "1.7.12",
|
|
76
|
+
"@better-auth/oauth-provider": "1.7.12",
|
|
77
|
+
"better-auth": "1.7.12"
|
|
78
78
|
},
|
|
79
79
|
"peerDependenciesMeta": {
|
|
80
80
|
"@modelcontextprotocol/server": {
|
|
@@ -93,13 +93,13 @@
|
|
|
93
93
|
"devDependencies": {
|
|
94
94
|
"@modelcontextprotocol/client": "2.3.1",
|
|
95
95
|
"@modelcontextprotocol/server": "2.3.1",
|
|
96
|
-
"@better-auth/core": "1.7.
|
|
97
|
-
"@better-auth/oauth-provider": "1.7.
|
|
98
|
-
"@better-auth/test-utils": "1.7.
|
|
96
|
+
"@better-auth/core": "1.7.12",
|
|
97
|
+
"@better-auth/oauth-provider": "1.7.12",
|
|
98
|
+
"@better-auth/test-utils": "1.7.12",
|
|
99
99
|
"@cloudflare/workers-types": "^5.20261027.1",
|
|
100
100
|
"@types/node": "^26",
|
|
101
|
-
"better-auth": "1.7.
|
|
102
|
-
"bun-types": "1.4.
|
|
101
|
+
"better-auth": "1.7.12",
|
|
102
|
+
"bun-types": "1.4.3",
|
|
103
103
|
"postgres": "3.4.9",
|
|
104
104
|
"typescript": "^6.0.3",
|
|
105
105
|
"wrangler": "4.153.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
function canonical(v){return Array.isArray(v)?`[${v.map(canonical).join(",")}]`:v&&typeof v=="object"?`{${Object.keys(v).sort().filter(k=>v[k]!==void 0).map(k=>`${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`:JSON.stringify(v)??"null"}function resolveLocalizedText(value,preferred,canonical2){if(value==null)return null;if(typeof value=="string")return value;if(Object.prototype.hasOwnProperty.call(value,preferred))return value[preferred];if(canonical2&&Object.prototype.hasOwnProperty.call(value,canonical2))return value[canonical2];let firstKey=Object.keys(value)[0];return firstKey!==void 0?value[firstKey]:null}var INPUT_RULE_KEYWORDS=["dependentRequired","anyOf","if","then","else"];function lookupLocalRef(root,ref){if(typeof ref!="string"||!ref.startsWith("#/"))return;let value=root;try{for(let key of decodeURIComponent(ref.slice(2)).split("/").map(key2=>key2.replace(/~1/g,"/").replace(/~0/g,"~")))value=value&&typeof value=="object"&&Object.hasOwn(value,key)?value[key]:void 0}catch{return}return value&&typeof value=="object"?value:void 0}function rootRefChain(input){let chain=[];for(let schema=input;schema!==void 0;schema=lookupLocalRef(input,schema.$ref)){if(chain.includes(schema))return;if(chain.push(schema),schema.$ref===void 0)return chain}}function mcpToolNameSegment(name){return name.toLowerCase().replace(/-/g,"_")}function toolTitle(name){let words=name.replace(/([a-z0-9])([A-Z])/g,"$1 $2").replace(/[-_.\s]+/g," ").trim().toLowerCase();return words.charAt(0).toUpperCase()+words.slice(1)}var viewAnnotations={readOnlyHint:!0,destructiveHint:!1};function procedureAnnotations(p){let readOnlyHint=p.mcp?.readOnlyHint??!1;return{readOnlyHint,destructiveHint:!readOnlyHint&&("ref"in p.handler||overwrites(p.handler.sql.stmts)),...p.mcp}}var overwrites=node=>!!node&&typeof node=="object"&&("DeleteStmt"in node||"UpdateStmt"in node||node.onConflictClause?.action==="ONCONFLICT_UPDATE"||Object.values(node).some(overwrites));function mcpTools(plan,surface,locale="en"){let text=t=>resolveLocalizedText(t,locale,"en")??void 0,tools=new Map,add=tool=>{let prior=tools.get(tool.name);if(prior&&(prior.kind!==tool.kind||prior.source!==tool.source))throw new TypeError(`MCP tool '${tool.name}' comes from both '${prior.source}' and '${tool.source}'.`);tools.set(tool.name,tool)};for(let t of Object.values(plan.triggers)){if(t.source.kind!=="mcp"||t.source.surface!==surface)continue;let p=plan.procedures[t.procedure],title=text(p.title);add({name:mcpToolNameSegment(t.procedure),kind:"procedure",source:t.procedure,title:title??toolTitle(t.procedure),description:text(p.description)??title??t.procedure,inputSchema:p.input,...p.output.type==="object"?{outputSchema:p.output}:{},annotations:procedureAnnotations(p),requires:p.requires})}for(let[name,v]of Object.entries(plan.views)){if(v.surface!==surface)continue;let chain=(v.input?rootRefChain(v.input)??[v.input]:[]).reverse(),props=Object.assign({},...chain.map(s=>s.properties??{})),required=[...new Set(chain.flatMap(s=>s.required??[]))];if("limit"in props||"cursor"in props)throw new TypeError(`View '${name}' declares an input named limit or cursor; the MCP tool reserves both for paging.`);let rules=chain.map(s=>Object.fromEntries(INPUT_RULE_KEYWORDS.flatMap(k=>s[k]===void 0?[]:[[k,s[k]]]))).filter(r=>Object.keys(r).length),defs=v.input?referencedDefs(v.input,props):{},title=text(v.title);add({name:mcpToolNameSegment(name),kind:"view",source:name,title:title??toolTitle(name),description:text(v.description)??title??name,inputSchema:{type:"object",properties:{...props,limit:{type:"integer",minimum:1,maximum:500},cursor:{type:"string"}},...required.length?{required}:{},...rules.length>1?{allOf:rules}:rules[0],...Object.keys(defs).length?{$defs:defs}:{}},annotations:viewAnnotations,requires:v.requires})}return[...tools.values()]}function referencedDefs(root,node,found={}){if(!node||typeof node!="object")return found;let ref=node.$ref,[head,segment]=typeof ref=="string"&&ref.startsWith("#/")?decoded(ref.slice(2)).split("/"):[],key=head==="$defs"?segment?.replace(/~1/g,"/").replace(/~0/g,"~"):void 0;key!==void 0&&!Object.hasOwn(found,key)&&root.$defs&&Object.hasOwn(root.$defs,key)&&(found[key]=root.$defs[key],referencedDefs(root,found[key],found));for(let child of Object.values(node))referencedDefs(root,child,found);return found}var decoded=pointer=>{try{return decodeURIComponent(pointer)}catch{return""}};var publishingTransitions=[["publish",["draft"],"published"],["unpublish",["published","archived"],"draft"],["archive",["draft","published"],"archived"]];var publishingInput={type:"object",properties:{id:{type:"string",minLength:1},expectedVersion:{type:"integer",minimum:1}},required:["id","expectedVersion"],additionalProperties:!1};function mediaDefinitions(){let define=(name,description,inputSchema,action,readOnlyHint=!1)=>({name,description,inputSchema,action,annotations:{readOnlyHint,destructiveHint:action==="delete",openWorldHint:!0}}),string={type:"string"},texts={alt:{type:"string",maxLength:1e3},caption:{type:"string",maxLength:1e3}},input=(properties,required=[])=>({type:"object",properties,required,additionalProperties:!1});return[define("get_media_upload_policy","Read declared image purpose MIME slots and byte caps before preparing variants or requesting an upload. Optimization runs locally, never in the host.",input({}),"policy",!0),define("create_media_upload","Create short-lived PUT capabilities for image variants. Read site media purpose policy first; optimize images locally. Obtain attachment bytes yourself, then PUT each variant with its requiredHeaders and call commit_media_upload. Never store upload URLs as published images or ask the user to run terminal commands.",input({filename:{type:"string",maxLength:255},purpose:string,variants:{type:"array",minItems:1,items:input({mimeType:string,byteSize:{type:"integer",minimum:1},role:{type:"string",enum:["primary","alternate","fallback"]}},["mimeType","byteSize","role"])},...texts},["filename","purpose","variants"]),"create"),define("commit_media_upload","Verify all uploaded variants and commit one permanent media asset. Store the returned asset id in fields referencing media_assets.id, never a temporary upload URL. Unknown or expired upload groups require a new create_media_upload.",input({uploadGroupId:string,...texts},["uploadGroupId"]),"commit"),define("list_media_assets","List committed image assets, newest first, with their permanent variants and alt/caption. Search matches id, alt or caption; reuse only a returned cursor.",input({limit:{type:"integer",minimum:1,maximum:500},cursor:string,search:string}),"list",!0),define("get_media_asset","Read one committed media asset and its permanent variant URLs.",input({id:string},["id"]),"get",!0),define("update_media_asset","Update committed asset alt or caption; omitted fields remain unchanged and empty strings clear text.",input({id:string,...texts},["id"]),"update"),define("delete_media_asset","Delete an asset's objects and library record. Check application references first. A partial failure can be retried using the same id.",input({id:string},["id"]),"delete")]}var ADMIN_API_BASE="/admin/api",pathTemplate=path=>path.replace(/\{([^}]+)\}/g,":$1"),routePath=template=>template.split("/").map(segment=>segment.startsWith(":")?`{${segment.slice(1)}}`:segment).join("/");function routeSource(template,base="",suffix=!1){let keys=[],path=template.split("/").map(segment=>{let rest=suffix?/^\{([a-zA-Z]\w*)\*\}$/.exec(segment):null,one=/^\{([^}]+)\}$/.exec(segment);return rest||one?(keys.push((rest??one)[1]),rest?"(.*)":"([^/]+)"):encodeURIComponent(segment).replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}).join("/");return{source:`^${base}${path}/?$`,keys}}function canonicalPath(pathname){try{return pathname.split("/").map(segment=>encodeURIComponent(decodeURIComponent(segment))).join("/")}catch{return}}var isParameter=segment=>/^\{[^}]+\}$/.test(segment);function routeOrder(a,b){let x=a.split("/"),y=b.split("/"),count=segments=>segments.filter(isParameter).length;if(count(x)!==count(y))return count(x)-count(y);for(let i=0;i<Math.min(x.length,y.length);i++)if(isParameter(x[i])!==isParameter(y[i]))return isParameter(x[i])?1:-1;return a<b?-1:a>b?1:0}function inspectSql(value){let schemas=new Set,readSchemas=new Set,writes=[],readOnly=!0,walk=v=>{if(!v||typeof v!="object")return;let n=v;n.RangeVar?.mantle==="table"&&(schemas.add(n.RangeVar.relname.toLowerCase()),readSchemas.add(n.RangeVar.relname.toLowerCase()));for(let kind of["InsertStmt","UpdateStmt","DeleteStmt"])n[kind]&&(readOnly=!1,n[kind].relation?.relname&&writes.push({schema:n[kind].relation.relname.toLowerCase(),operation:kind==="InsertStmt"?"create":kind==="UpdateStmt"?"update":"delete"}),n[kind].relation?.relname&&(schemas.add(n[kind].relation.relname.toLowerCase()),kind!=="InsertStmt"&&readSchemas.add(n[kind].relation.relname.toLowerCase())));Object.values(n).forEach(walk)};return walk(value),{schemas:[...schemas].sort(),readSchemas:[...readSchemas].sort(),readOnly,writes}}function mergeRestrict(policies,conjunctive){return Object.fromEntries([...new Set(policies.flatMap(p=>Object.keys(p)))].sort().map(schema=>{let rows=policies.map(p=>p[schema]??{terms:[]}),terms=[...new Map(rows.flatMap(p=>p.terms).map(t=>[canonical(t),t])).values()].filter(t=>conjunctive||rows.every(p=>p.terms.some(v=>canonical(v)===canonical(t)))),owner=rows.find(p=>p.owner)?.owner;return[schema,{...owner&&(conjunctive||rows.every(p=>p.owner===owner))?{owner}:{},terms}]}))}function inspectBehavior(plan,ir,opaque,requires,restrict,writes){let inspected=inspectSql(ir);writes&&(inspected.writes=[...writes],inspected.schemas.push(...writes.map(w=>w.schema)),inspected.readSchemas.push(...writes.filter(w=>w.operation!=="create").map(w=>w.schema)));let dependencies=[],policies=[restrict??{}],procedures=new Set,hooks=new Set,visit=name=>{if(procedures.has(name))return;procedures.add(name);let p=plan.procedures[name];if(!p)throw new TypeError(`Unknown dependency '${name}'.`);dependencies.push({procedure:name,declaration:p}),policies.push(p.restrict??{}),opaque||="ref"in p.handler;let g=inspectSql("sql"in p.handler?p.handler.sql.stmts:void 0);inspected.schemas.push(...g.schemas),inspected.readSchemas.push(...g.readSchemas),inspected.writes.push(...g.writes),inspected.readOnly&&=g.readOnly,p.requires?.guard&&visit(p.requires.guard.procedure)};for(requires?.guard&&visit(requires.guard.procedure);;){let before=hooks.size;for(let[id,t]of Object.entries(plan.triggers).sort(([a],[b])=>a<b?-1:a>b?1:0)){if(t.source.kind!=="lifecycle"||hooks.has(id))continue;let source=t.source;!opaque&&!inspected.writes.some(w=>w.schema===source.schema.toLowerCase()&&source.on.some(on=>on.endsWith(`_${w.operation}`)))||(hooks.add(id),dependencies.push({trigger:id,source}),visit(t.procedure))}if(hooks.size===before)break}let related=names=>{let all=new Set(opaque?Object.keys(plan.schemas):names);for(let name of all){let parent=plan.schemas[name]?.translates?.parent.toLowerCase();parent&&all.add(parent)}return[...all].sort()};return{...inspected,schemas:related(inspected.schemas),readSchemas:related(inspected.readSchemas),opaque,restrict:mergeRestrict(policies,!0),dependencies}}var objectInput={type:"object",properties:{}},adminRoutes=["GET /me","GET /site","GET /collections","GET /views-manifest","GET /operations","GET /webmcp","POST /webmcp/:tool","GET /bootstrap","GET /views/:name/export","GET /views/:name","GET /view-batch","GET /developer/views/:name","POST /operations/:name","GET /developer-console","GET /collections/:name/statistics","GET /entries","GET /entries/export","GET /entries/:id","POST /entries","PATCH /entries/:id","POST /entries/:id/publish","POST /entries/:id/unpublish","POST /entries/:id/archive","DELETE /entries/:id","GET /site-settings","PATCH /site-settings","POST /media/uploads","POST /media/uploads/:groupId/commit","GET /media","GET /media/:id","PATCH /media/:id","DELETE /media/:id","GET /admin","GET /members","PATCH /admin/:id/role","POST /admin/invitations","DELETE /admin/invitations/:id"];function targetDefinitions(plan){let out=[],tools=[...mcpTools(plan,"public"),...mcpTools(plan,"admin")];for(let[kind,entries]of[["view",plan.views],["procedure",plan.procedures]])for(let[name,p]of Object.entries(entries).sort(([a],[b])=>a<b?-1:a>b?1:0)){let view=kind==="view"?plan.views[name]:void 0,proc=kind==="procedure"?plan.procedures[name]:void 0,ir=view?.stmts??(proc&&"sql"in proc.handler?proc.handler.sql.stmts:void 0),inspected=inspectBehavior(plan,ir,!!proc&&"ref"in proc.handler,p.requires,p.restrict),tool=tools.find(t=>t.kind===kind&&t.source===name),title=resolveLocalizedText(p.title,"en","en")??name,triggers=Object.entries(plan.triggers).filter(([,t])=>kind==="procedure"&&t.procedure===name).sort(([a],[b])=>a<b?-1:a>b?1:0).map(([id,t])=>({id,source:t.source})),surfaces=[...new Set([...view?[view.surface,"admin",...view.surface==="public"?["rest"]:[]]:[],...triggers.map(t=>t.source.kind==="mcp"?t.source.surface:t.source.kind==="http"?"rest":t.source.kind)])].sort();surfaces.length||surfaces.push("internal"),out.push({id:`${kind}:${name}`,name:tool?.name??mcpToolNameSegment(name),kind,surface:view?.surface??(surfaces.includes("public")||surfaces.includes("rest")?"public":surfaces.includes("admin")?"admin":"internal"),surfaces,triggers,title,description:resolveLocalizedText(p.description,"en","en")??title,inputSchema:tool?.inputSchema??view?.input??proc?.input??objectInput,annotations:tool?.annotations??{},requires:p.requires,restrict:inspected.restrict,schemas:inspected.schemas,readSchemas:inspected.readSchemas,readOnly:!inspected.opaque&&inspected.readOnly,opaque:inspected.opaque,trustedOnly:!!proc?.trustedOnly,variants:[],behavior:{declaration:p,dependencies:inspected.dependencies}})}let addRoute=(id,dependencies,inputSchema=objectInput)=>{let admin=id.startsWith("admin:");out.push({id,name:id.replace(/[^a-zA-Z0-9_-]/g,"_"),title:id,description:id,kind:admin?"admin":"rest",surface:admin?"admin":"public",surfaces:[admin?"admin":"rest"],triggers:dependencies.flatMap(d=>d.triggers),inputSchema,annotations:{},requires:admin?{auth:{all:["ctx.admin"]}}:dependencies[0]?.requires,restrict:mergeRestrict(dependencies.map(d=>d.restrict),!1),schemas:[...new Set(dependencies.flatMap(d=>d.schemas))].sort(),readSchemas:[...new Set(dependencies.flatMap(d=>d.readSchemas))].sort(),readOnly:dependencies.length>0&&dependencies.every(d=>d.readOnly)&&(!admin||id.startsWith("admin:GET ")||id==="admin:POST /operations/:name"||id==="admin:POST /webmcp/:tool"),opaque:dependencies.some(d=>d.opaque),trustedOnly:dependencies.some(d=>d.trustedOnly),variants:dependencies,behavior:dependencies.map(d=>({id:d.id,behavior:d.behavior,surfaces:d.surfaces,triggers:d.triggers,input:d.inputSchema,requires:d.requires,restrict:d.restrict,trustedOnly:d.trustedOnly}))})},declarations=[...out],adminOperations=new Set(mcpTools(plan,"admin").filter(t=>t.kind==="procedure").map(t=>`procedure:${t.source}`));for(let d of declarations)d.kind==="view"&&d.surfaces.includes("public")&&addRoute(`rest:GET /api/views/${d.id.slice(5)}`,[d],d.inputSchema);for(let[,t]of Object.entries(plan.triggers).sort(([a],[b])=>a<b?-1:a>b?1:0))if(t.source.kind==="http"){let d=declarations.find(d2=>d2.id===`procedure:${t.procedure}`);addRoute(`rest:${t.source.method} ${pathTemplate(t.source.path)}`,[d],d.inputSchema)}let generated=[];for(let[schema,s]of Object.entries(plan.schemas).sort(([a],[b])=>a<b?-1:a>b?1:0))if(s.publishing)for(let[verb,from,to]of publishingTransitions){addRoute(`procedure:${s.name}.${verb}`,[],publishingInput);let d=out.at(-1),inspected=inspectBehavior(plan,void 0,!1,void 0,void 0,[{schema,operation:verb==="publish"?"publish":"update"}]);Object.assign(d,{kind:"procedure",name:mcpToolNameSegment(`${verb}_${s.name}`),surface:"admin",surfaces:["admin"],requires:{auth:{all:["ctx.admin"]}},schemas:[...new Set([schema,...inspected.schemas])].sort(),readSchemas:[...new Set([schema,...inspected.readSchemas])].sort(),opaque:inspected.opaque,behavior:{schema,verb,from,to,dependencies:inspected.dependencies}}),generated.push(d)}for(let tool of mediaDefinitions()){addRoute(`admin:tool:${tool.name}`,[],tool.inputSchema);let d=out.at(-1);Object.assign(d,{name:tool.name,readOnly:tool.annotations.readOnlyHint,opaque:!tool.annotations.readOnlyHint,annotations:tool.annotations,behavior:{tool,schemas:plan.schemas}}),generated.push(d)}for(let route2 of adminRoutes){let deps=route2.includes("/views/:")||route2==="GET /view-batch"?declarations.filter(d2=>d2.kind==="view"&&(route2.includes("/developer/")||plan.views[d2.id.slice(5)]?.surface==="admin")):route2.includes("/operations/:")?declarations.filter(d2=>adminOperations.has(d2.id)):route2==="POST /webmcp/:tool"?declarations.filter(d2=>mcpTools(plan,"admin").some(t=>`${t.kind}:${t.source}`===d2.id)).concat(generated):[],generic=route2.includes("/entries")||route2.includes("/statistics")||route2==="GET /bootstrap";addRoute(`admin:${route2}`,deps,deps.length&&route2!=="GET /view-batch"?{...objectInput,anyOf:deps.map(d2=>({type:"object",properties:{[route2.includes("/webmcp/")?"tool":"name"]:{type:"string",const:route2.includes("/webmcp/")?d2.name:d2.id.slice(d2.kind==="view"?5:10)}}}))}:objectInput);let d=out.at(-1);if(generic){let operation=route2==="POST /entries"?"create":route2==="DELETE /entries/:id"?"delete":route2.startsWith("GET ")?void 0:route2.endsWith("/publish")?"publish":"update",inspected=inspectBehavior(plan,void 0,!1,void 0,void 0,operation?Object.keys(plan.schemas).map(schema=>({schema,operation})):[]);d.schemas=Object.keys(plan.schemas).sort(),d.readSchemas=d.schemas,d.opaque=inspected.opaque,d.behavior={schemas:plan.schemas,dependencies:inspected.dependencies}}}return[...new Map(out.map(d=>[d.id,d])).values()].sort((a,b)=>a.id<b.id?-1:a.id>b.id?1:0)}var tables=new WeakMap,route=/^(rest|admin):([A-Z]+) (\/.*)$/,normalized=new Set(["DELETE","GET","HEAD","OPTIONS","POST","PUT"]);function compile(describe){let table={rest:[],admin:[],tools:[]};for(let{name,_meta:meta}of describe.targets){let[,surface,method,template]=route.exec(meta.id)??[];if(surface==="rest")table.rest.push({method,pattern:new RegExp(routeSource(routePath(template)).source),view:meta.variants[0]?.id.startsWith("view:")===!0,meta});else if(surface==="admin")table.admin.push({method,pattern:new RegExp(routeSource(routePath(template),ADMIN_API_BASE).source),meta});else{let s=meta.surfaces,endpoints=meta.id.startsWith("admin:tool:")?["admin"]:meta.id.startsWith("view:")?s.includes("public")?["public"]:s.includes("internal")?[]:["admin"]:meta.id.startsWith("procedure:")?s.filter(x=>x==="public"||x==="admin"):[];endpoints.length&&table.tools.push({name,endpoints,meta})}}let order=id=>adminRoutes.indexOf(id.slice(6))>>>0,path=id=>routePath(id.slice(id.indexOf(" ")+1));return table.rest.sort((a,b)=>routeOrder(path(a.meta.id),path(b.meta.id))),table.admin.sort((a,b)=>order(a.meta.id)-order(b.meta.id)),table}function matchTarget(describe,request){let table=tables.get(describe);if(table||tables.set(describe,table=compile(describe)),request.surface==="mcp"){let path2=request.path?.replace(/\/+$/,""),endpoint=path2===void 0?void 0:path2==="/mcp"?"public":path2==="/mcp/admin"?"admin":null,found=table.tools.filter(t=>t.name===request.tool&&(endpoint===void 0||t.endpoints.includes(endpoint)));return endpoint!==null&&found.length&&found.every(t=>t.meta.id===found[0].meta.id)?found[0].meta:null}let method=normalized.has(request.method.toUpperCase())?request.method.toUpperCase():request.method,path=canonicalPath(request.path);return path===void 0?null:request.surface==="admin"?request.path!==ADMIN_API_BASE&&!request.path.startsWith(ADMIN_API_BASE+"/")?null:table.admin.find(r=>r.method===method&&r.pattern.test(path))?.meta??null:table.rest.find(r=>r.pattern.test(path)&&(r.method===method||method==="HEAD"&&r.method==="GET"&&r.view))?.meta??null}export{canonical,targetDefinitions,matchTarget};
|