@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.
- package/README.md +27 -5
- package/dist/auth/adapter.js +9 -6
- package/dist/auth/caller.js +2 -1
- package/dist/auth/identity.d.ts +2 -2
- package/dist/auth/oauth.d.ts +33 -1
- package/dist/auth/oauth.js +121 -28
- package/dist/auth/routes.js +6 -4
- package/dist/build/compiler.d.ts +44 -122
- package/dist/build/compiler.js +202 -277
- package/dist/build/spec/domain/model/ManifestGrammar.d.ts +6 -5
- package/dist/build/spec/domain/model/SqlIr.d.ts +2 -0
- package/dist/build/spec/domain/service/PlanFingerprint.d.ts +2 -0
- package/dist/build/spec/kernel/diagnostic.d.ts +1 -1
- package/dist/domain/describe/{chunk-ZIQVLA7X.js → chunk-ZVXBGP7Y.js} +69 -8
- package/dist/domain/describe/index.js +1 -1
- package/dist/domain/describe/target-hash.js +1 -1
- package/dist/domain/describe/targets.d.ts +1 -1
- package/dist/domain/generated-tools.js +2 -0
- package/dist/domain/paging.js +41 -15
- package/dist/infrastructure/bundle.js +118 -15
- package/dist/infrastructure/cli.js +11 -15
- package/dist/infrastructure/generate.d.ts +1 -3
- package/dist/infrastructure/generate.js +11 -3
- package/dist/infrastructure/generateAuth.js +57 -0
- package/dist/infrastructure/inputs.js +8 -3
- package/dist/infrastructure/wrangler.js +50 -12
- package/dist/kernel/binds.d.ts +0 -3
- package/dist/kernel/contracts.d.ts +1 -1
- package/dist/kernel/contracts.js +4 -4
- package/dist/kernel/dialect/common.d.ts +0 -1
- package/dist/kernel/drain.d.ts +5 -0
- package/dist/kernel/drain.js +35 -0
- package/dist/kernel/driver/postgres.js +6 -2
- package/dist/kernel/host-caller.d.ts +1 -0
- package/dist/kernel/host-caller.js +1 -0
- package/dist/kernel/mcp-apps.d.ts +29 -9
- package/dist/kernel/mcp-apps.js +3 -3
- package/dist/kernel/mcp-handler.js +101 -18
- package/dist/kernel/mcp.d.ts +12 -3
- package/dist/kernel/mcp.js +31 -6
- package/dist/kernel/rest-router.js +3 -2
- package/dist/kernel/scalars.js +3 -2
- package/dist/kernel/site.d.ts +1 -3
- package/dist/kernel/site.js +2 -2
- package/dist/kernel/web-index.js +40 -0
- package/dist/kernel/web.d.ts +3 -1
- package/dist/kernel/web.js +6 -37
- package/dist/usecase/adminBindings.js +2 -3
- package/dist/usecase/adminClient.js +87 -0
- package/dist/usecase/adminDescriptors.js +19 -65
- package/dist/usecase/generate.js +5 -4
- package/dist/usecase/mcp-media.js +2 -2
- package/dist/usecase/mcp.js +16 -12
- package/dist/usecase/paging.js +2 -3
- package/dist/usecase/publishing.js +3 -3
- package/dist/usecase/site.js +0 -1
- package/dist/usecase/tool-version.js +9 -0
- package/dist/usecase/translations.js +1 -1
- package/dist/usecase/validation.js +128 -19
- package/dist/usecase/web.js +3 -2
- package/docs/examples/README.md +1 -1
- package/docs/examples/app-shell/README.md +45 -3
- package/docs/examples/app-shell/package.json +14 -6
- package/docs/examples/app-shell/smoke-kit.mjs +54 -0
- package/docs/examples/app-shell/src/auth.ts +6 -3
- package/docs/examples/app-shell/src/service.ts +3 -3
- package/docs/examples/app-shell/tsconfig.json +4 -1
- package/docs/examples/app-shell/worker-configuration.d.ts +2 -2
- package/docs/examples/app-shell/wrangler.jsonc +5 -4
- package/docs/examples/club-events/manifests/club-events.yaml +7 -0
- package/docs/examples/club-events/src/handlers.ts +1 -1
- package/docs/examples/generated-web/README.md +6 -2
- package/docs/examples/generated-web/wrangler.jsonc +1 -0
- package/docs/examples/purchasing/README.md +1 -1
- package/docs/examples/purchasing/manifests/purchasing.yaml +17 -1
- package/docs/examples/purchasing/smoke.mjs +10 -4
- package/docs/examples/purchasing/src/admin.tsx +13 -0
- package/docs/examples/purchasing/src/handlers.ts +1 -1
- package/docs/examples/purchasing/wrangler.jsonc +5 -4
- package/docs/examples/recipes/wrangler.jsonc +5 -4
- package/docs/handbook/cloudflare/authentication.md +43 -6
- package/docs/handbook/cloudflare/chatgpt-sites.md +4 -2
- package/docs/handbook/cloudflare/deploy-and-operate.md +45 -1
- package/docs/handbook/cloudflare/media-r2.md +2 -3
- package/docs/handbook/cloudflare/service-entry.md +68 -31
- package/docs/handbook/concepts/authorization.md +1 -1
- package/docs/handbook/concepts/mcp-and-agents.md +19 -0
- package/docs/handbook/concepts/runtime-and-adapters.md +1 -1
- package/docs/handbook/examples/hub.md +1 -1
- package/docs/handbook/guides/admin-ui.md +107 -1
- package/docs/handbook/guides/agent-setup.md +27 -9
- package/docs/handbook/guides/agent-workflow-acceptance.md +222 -0
- package/docs/handbook/guides/mcp-apps.md +522 -6
- package/docs/handbook/guides/native-admin.md +300 -51
- package/docs/handbook/guides/oauth.md +119 -5
- package/docs/handbook/guides/public-pages.md +2 -2
- package/docs/handbook/guides/size-and-startup.md +1 -1
- package/docs/handbook/guides/typed-queries.md +35 -0
- package/docs/handbook/navigation.json +0 -4
- package/docs/handbook/reference/admin-descriptors.md +0 -35
- package/docs/handbook/reference/procedure.md +6 -2
- package/docs/handbook/reference/schema.md +13 -3
- package/docs/handbook/reference/surface.md +129 -15
- package/docs/handbook/reference/trigger.md +43 -1
- package/docs/handbook/reference/view.md +1 -1
- package/docs/handbook/releases/index.md +74 -2
- package/docs/handbook/start/overview.md +1 -1
- package/docs/handbook/start/project-and-cli.md +16 -8
- package/docs/handbook/start/quickstart-worker.md +1 -1
- package/docs/skills/develop/SKILL.md +46 -18
- package/docs/skills/integrate/SKILL.md +4 -9
- package/docs/skills/media-gc/SKILL.md +7 -5
- package/docs/skills/provision/SKILL.md +18 -12
- package/docs/skills/update/SKILL.md +19 -18
- package/docs/upgrade-0.1-to-0.2.md +6 -6
- package/package.json +2 -1
- package/skills/README.md +11 -16
- package/skills/mantle/SKILL.md +39 -17
- package/skills/mantle/references/local-testing.md +9 -0
- package/skills/mantle/references/web.md +1 -1
- package/skills/mantle/references/website.md +26 -22
- package/docs/examples/app-shell/src/admin.ts +0 -10
- package/docs/examples/purchasing/src/admin.ts +0 -12
- package/docs/handbook/guides/admin-extensions.md +0 -20
- package/docs/skills/develop/mcp-app.md +0 -39
- package/skills/.keep +0 -0
|
@@ -7,13 +7,19 @@ endpoint accepts one contributor or an array; Admin uses the same public hook:
|
|
|
7
7
|
import type { McpApps } from '@aotter/mantle/mcp';
|
|
8
8
|
import { adminMcpApps } from '@aotter/mantle-admin/mcp';
|
|
9
9
|
import { mcp } from './generated/mcp.js';
|
|
10
|
+
import { mcpAppCatalogues } from './generated/admin-descriptors.js';
|
|
11
|
+
|
|
12
|
+
declare const db: (env: Env) => D1Database; // as in service composition
|
|
13
|
+
declare const caller: Parameters<typeof mcp>[0]['caller'];
|
|
14
|
+
declare function loadAppHtml(): string;
|
|
10
15
|
|
|
11
16
|
const app: McpApps = { resources: [{
|
|
12
|
-
uri: 'ui://my-app/records', name: 'my-app-records',
|
|
17
|
+
uri: 'ui://my-app/records/build-1', name: 'my-app-records',
|
|
13
18
|
html: () => loadAppHtml(), renders: ['records'],
|
|
14
19
|
}] };
|
|
15
20
|
const handler = mcp({ db, caller, apps: {
|
|
16
|
-
public: app,
|
|
21
|
+
public: app,
|
|
22
|
+
admin: adminMcpApps({ catalogue: mcpAppCatalogues.admin }), // or an array with your own contributors
|
|
17
23
|
} });
|
|
18
24
|
```
|
|
19
25
|
|
|
@@ -22,13 +28,171 @@ HTML is reusable static content; caller-specific data comes from tool results.
|
|
|
22
28
|
Build the front end with the official `@modelcontextprotocol/ext-apps` client.
|
|
23
29
|
`renders` links tools to `_meta.ui.resourceUri`; `appOnly` adds
|
|
24
30
|
`visibility: ["app"]`. Both name tools actually exposed on that endpoint.
|
|
25
|
-
Resources may supply CSP, permissions and `
|
|
31
|
+
Resources may supply CSP, permissions, `prefersBorder` and `domain`; each is
|
|
32
|
+
passed through as the resource's `_meta.ui`. `domain` is the dedicated sandbox
|
|
33
|
+
origin for hosts that serve each App from its own origin; use the form that
|
|
34
|
+
host documents.
|
|
35
|
+
|
|
36
|
+
## Package and application ownership
|
|
37
|
+
|
|
38
|
+
Core (`@aotter/mantle`) owns tools, resources, endpoint links and server authorization,
|
|
39
|
+
validation, scope and OCC. It has no React, DOM or Admin dependency. The optional
|
|
40
|
+
`@aotter/mantle-ui` owns reusable presentation and interaction controllers: pass it
|
|
41
|
+
data and invocation callbacks; it knows no MCP connection, cookie, role or Admin API.
|
|
42
|
+
`@aotter/mantle-admin` owns the Admin product, its existing hooks/surface transport
|
|
43
|
+
and the thin adapter to the pinned official ext-apps client. The official client
|
|
44
|
+
owns protocol negotiation, messages and connection lifecycle; the separate browser
|
|
45
|
+
source/origin guard is not a protocol implementation.
|
|
46
|
+
|
|
47
|
+
Applications own domain components and build entries. A frontend MCP App uses its
|
|
48
|
+
own React entry, official ext-apps and optionally mantle-ui, registered through
|
|
49
|
+
Core's generic resources. It needs no Admin package or Admin session. See the
|
|
50
|
+
[standalone frontend example](../../examples/frontend-mcp-app/README.md) for a
|
|
51
|
+
small owner-scoped example. `public` names the frontend endpoint, not anonymous
|
|
52
|
+
access: use the application's verified Caller and generated authorization rules.
|
|
53
|
+
UI visibility and `appOnly` metadata grant no execution authority.
|
|
54
|
+
|
|
55
|
+
The default Admin App uses the default composition. Custom Admin Apps use the same
|
|
56
|
+
`defineAdmin` entry, slots and hooks, with `pages[].mcp` for explicit page opt-in;
|
|
57
|
+
an application may also supply its own resource. There is no extra Mantle bridge
|
|
58
|
+
package, provider, slot registry or query cache to install for these paths.
|
|
59
|
+
|
|
60
|
+
When combining a custom renderer with Admin, partition ownership explicitly:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// Both views already belong to this endpoint. Custom resources use their own prefix.
|
|
64
|
+
const adminApps = [
|
|
65
|
+
adminMcpApps({ catalogue: mcpAppCatalogues.admin, renders: ['pending_requests'] }),
|
|
66
|
+
{ resources: [{ uri: 'ui://my-app/details/build-2', name: 'my-details',
|
|
67
|
+
html: () => loadAppHtml(), renders: ['request_details'] }] },
|
|
68
|
+
];
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Register the array on `apps.admin`. Leaving Admin's default ownership of all Views
|
|
72
|
+
while another resource claims one is an error, independent of registration order.
|
|
73
|
+
|
|
74
|
+
An HTML loader receives `{ tools, env, request }`: `tools` is the same tool names this caller's
|
|
75
|
+
`tools/list` returns on the endpoint (with `authorize` configured, only those
|
|
76
|
+
it allows), so the HTML it renders names no tool the caller cannot see; `env` is
|
|
77
|
+
the environment the handler was called with (a Worker's bindings, such as
|
|
78
|
+
`ASSETS`) and `request` the `resources/read` request, for its origin. Loaders
|
|
79
|
+
that take less, or nothing, keep working. `adminMcpApps` uses it: the catalogue
|
|
80
|
+
it embeds holds only the Views and row actions in `tools`. A host that caches
|
|
81
|
+
the resource across callers may hold an older catalogue; a View it does not
|
|
82
|
+
describe still shows its rows, with no action to open.
|
|
83
|
+
|
|
84
|
+
`adminMcpApps({ catalogue })` takes `mcpAppCatalogues.admin`, which `mantle generate --admin` writes to
|
|
85
|
+
`generated/admin-descriptors.ts` (pass it as is). It needs no `html`: its default loader reads the file
|
|
86
|
+
`mantle bundle` built, `/admin/mcp-app.html`, through `env.ASSETS.fetch` on a
|
|
87
|
+
Worker, and from `dist/client/admin/mcp-app.html` on Bun (`file` names another
|
|
88
|
+
path). The Worker's assets binding must be named `ASSETS` (otherwise pass `html`);
|
|
89
|
+
the loader follows the one same-origin redirect the default `html_handling` answers a
|
|
90
|
+
`.html` path with. Pass `html` (a string or a loader) to serve the App from elsewhere. The
|
|
91
|
+
file carries the `csp` your `defineAdmin({ mcp: { csp } })` declared; a loader may return
|
|
92
|
+
`{ html, csp }` instead of a string, and the handler sends that `csp` as `_meta.ui.csp` on
|
|
93
|
+
`resources/read`; it replaces the resource's own `csp` rather than merging with it. A loader
|
|
94
|
+
that throws answers the host with a redacted error and logs the message on the server.
|
|
26
95
|
|
|
27
|
-
|
|
96
|
+
Resource URIs are immutable cache identities. By default Admin contributes an async
|
|
97
|
+
resource source: after the request passes the origin/identity/Admin gates, Admin
|
|
98
|
+
reads its built asset and hashes the HTML, full static catalogue and material
|
|
99
|
+
resource metadata into `ui://mantle-admin/admin/<digest>`. A new UI build or catalogue
|
|
100
|
+
gets a new URI, used consistently by tools and resource reads. Workers cache only
|
|
101
|
+
the raw immutable asset/identity per contributor and ASSETS binding; caller-filtered
|
|
102
|
+
catalogues are embedded per read. Bun rereads the file so rebuilding in the same
|
|
103
|
+
process changes the identity. Run `mantle bundle` and deploy its assets with the server;
|
|
104
|
+
missing assets can fail discovery when Admin resolves the build identity.
|
|
105
|
+
|
|
106
|
+
An explicit `uri` keeps application-owned identity. A custom `html` function must
|
|
107
|
+
supply a build-specific `uri`: Core cannot infer its build or safely memoize its
|
|
108
|
+
request-dependent output. A raw `html` string may use the automatic identity.
|
|
109
|
+
|
|
110
|
+
### Keep old conversations working after an upgrade
|
|
111
|
+
|
|
112
|
+
A new URI does not invalidate a host's saved tool definitions or old conversation
|
|
113
|
+
cards. Keep previously deployed pages with `adminMcpApps({ catalogue, previous })`.
|
|
114
|
+
`previous` is an array of `AdminMcpSnapshot` from `@aotter/mantle-admin/mcp`:
|
|
115
|
+
`{ uri, html, csp?, domain?, title? }`. Each `html` includes the original `mantle-catalog`
|
|
116
|
+
script (the page returned by `resources/read`, not the raw bundle). The SDK serves
|
|
117
|
+
its original HTML and metadata at its original URI, filters its catalogue against
|
|
118
|
+
the current caller's tools on every read, and gives only the current build renderer
|
|
119
|
+
ownership. A removed/denied tool gains no authority through an old page.
|
|
120
|
+
|
|
121
|
+
Store these snapshots in the application's source or durable artifact storage;
|
|
122
|
+
an isolate cache or untracked `dist/` is insufficient for a clean CI deployment.
|
|
123
|
+
Keep an explicit retention window. Old pages outside that window and incompatible
|
|
124
|
+
tool renames/input/output changes need an application migration. Never answer an
|
|
125
|
+
old digest with the latest HTML. Host metadata refresh is a development/recovery
|
|
126
|
+
operation, not a normal compatible-upgrade requirement.
|
|
127
|
+
|
|
128
|
+
For the default built loader, share the exact declaration in
|
|
129
|
+
`src/mcp-app-definition.ts` (no `html` or `previous`):
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { mcpAppCatalogues } from '../generated/admin-descriptors.ts';
|
|
133
|
+
export const appDefinition = { catalogue: mcpAppCatalogues.admin };
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Keep a committed `mcp-app-previous.json` at the project root (initially `[]`). Load
|
|
137
|
+
it only inside the service's MCP branch, before constructing the handler, so other
|
|
138
|
+
routes do not parse archived full-page HTML at startup:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const { default: previous } = await import('../mcp-app-previous.json', { with: { type: 'json' } });
|
|
142
|
+
serveMcpHandler ??= serveMcp(tools, { ...options, apps: {
|
|
143
|
+
admin: adminMcpApps({ ...appDefinition, previous }),
|
|
144
|
+
} });
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Keep custom `renders`, catalogue extensions, `domain` and `title` in the shared
|
|
148
|
+
definition too. Node 22.18+ can read these generated `.ts` modules; a TypeScript
|
|
149
|
+
app using `.ts` import paths enables `allowImportingTsExtensions` with `noEmit`.
|
|
150
|
+
Keep snapshots outside the directory containing `admin.tsx`: Tailwind scans that
|
|
151
|
+
directory, and compiled HTML/JavaScript inside a snapshot can be mistaken for
|
|
152
|
+
utility class names and change subsequent CSS builds.
|
|
153
|
+
|
|
154
|
+
After a successful deployment, archive the **exact asset just deployed** with
|
|
155
|
+
this application-owned `scripts/archive-mcp-app.mjs`, then commit the snapshot
|
|
156
|
+
for the next build. Run it in the deployment script, not as a manual cache fix:
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
160
|
+
import { adminMcpApps } from '@aotter/mantle-admin/mcp';
|
|
161
|
+
import { appDefinition } from '../src/mcp-app-definition.ts';
|
|
162
|
+
|
|
163
|
+
const path = 'mcp-app-previous.json';
|
|
164
|
+
const previous = JSON.parse(readFileSync(path, 'utf8'));
|
|
165
|
+
const context = { env: {}, request: new Request('https://archive.invalid/mcp') };
|
|
166
|
+
const source = adminMcpApps({ ...appDefinition,
|
|
167
|
+
html: readFileSync('dist/client/admin/mcp-app.html', 'utf8') });
|
|
168
|
+
const { resources: [resource] } = typeof source === 'function' ? await source(context) : source;
|
|
169
|
+
const tools = new Set([...Object.keys(appDefinition.catalogue.views),
|
|
170
|
+
...Object.keys(appDefinition.catalogue.actions)]);
|
|
171
|
+
const loaded = typeof resource.html === 'string' ? resource.html
|
|
172
|
+
: await resource.html({ ...context, tools });
|
|
173
|
+
const page = typeof loaded === 'string' ? { html: loaded } : loaded;
|
|
174
|
+
const snapshot = { uri: resource.uri, title: resource.title, ...page,
|
|
175
|
+
...(resource.domain === undefined ? {} : { domain: resource.domain }) };
|
|
176
|
+
// This application's policy: retain the last two deployed pages for future builds.
|
|
177
|
+
writeFileSync(path, JSON.stringify([...previous.filter(p => p.uri !== snapshot.uri), snapshot].slice(-2)) + '\n');
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
This archives static UI/catalogue metadata, never rows, cookies or OAuth tokens.
|
|
181
|
+
Custom loaders must archive their actual immutable resource, including CSP/domain,
|
|
182
|
+
instead of using this default-asset recipe. Compare the archived URI with deployed
|
|
183
|
+
`tools/list`/`resources/read`; a mismatching declaration or stale local build is
|
|
184
|
+
not deployment evidence.
|
|
185
|
+
|
|
186
|
+
Core accepts static contributors or async `(context: { env, request }) => McpApps`
|
|
187
|
+
sources, including mixed arrays. Static declarations are checked at construction;
|
|
188
|
+
async sources resolve after the endpoint's origin and caller gates, then their
|
|
189
|
+
links are checked without reusing another request's resource links. Request-bound
|
|
190
|
+
tool tables check on the first matching request and recheck changed tool names. Duplicate
|
|
28
191
|
resource URIs or names, conflicting tool ownership, and absent or foreign-endpoint
|
|
29
192
|
tools throw `ContractError` with code `MCP_APP_CONFLICT`. HTML loaders never run during import or handler construction. Treat descriptors as
|
|
30
|
-
immutable after construction. HTML
|
|
31
|
-
|
|
193
|
+
immutable after construction. Static HTML loaders run only on `resources/read`;
|
|
194
|
+
async sources may read immutable build assets to declare their identity. Failures
|
|
195
|
+
return a redacted internal error. Core never reads a build file or computes a UI digest.
|
|
32
196
|
|
|
33
197
|
Generated media-enabled handlers keep the same cache while resolving their tool
|
|
34
198
|
table against each request's environment. Database handles, storage and Caller
|
|
@@ -42,3 +206,355 @@ Clients advertise `io.modelcontextprotocol/ui` and MIME type
|
|
|
42
206
|
requests, in `params._meta["io.modelcontextprotocol/clientCapabilities"]`.
|
|
43
207
|
An explicit capability declaration without UI support receives no UI resources, UI tool metadata or App-only tools. A stateless request with no capability declaration is treated as unknown and retains App support. Other tools retain their existing validation,
|
|
44
208
|
authorization, caller, origin and database behavior.
|
|
209
|
+
|
|
210
|
+
## The Admin App in a host
|
|
211
|
+
|
|
212
|
+
> **Verification status:** automated AppBridge coverage exists, but the compatibility claims in this section have not yet been verified manually in real ChatGPT or Claude hosts.
|
|
213
|
+
|
|
214
|
+
The Admin App speaks MCP Apps `2026-01-26` to the host and asks only for what
|
|
215
|
+
the host advertises in `hostCapabilities`:
|
|
216
|
+
|
|
217
|
+
- Without `serverTools` it is read-only: rows show, no row action opens and no
|
|
218
|
+
list refresh is attempted.
|
|
219
|
+
- `format: "uri"` values with an `http(s)` URL open through `ui/open-link`
|
|
220
|
+
when the host offers `openLinks`, and are plain text otherwise. The sandbox
|
|
221
|
+
blocks navigation, so the App never uses `window.open` or `target="_blank"`.
|
|
222
|
+
- After a row action succeeds, `ui/update-model-context` tells the model the
|
|
223
|
+
tool, the row id and the outcome, for its next turn. The App never sends
|
|
224
|
+
`ui/message`, which would speak for the user.
|
|
225
|
+
- It declares `availableDisplayModes: ["inline", "fullscreen"]` and offers a
|
|
226
|
+
full-screen toggle (`ui/request-display-mode`) when the host lists
|
|
227
|
+
`fullscreen` in its context.
|
|
228
|
+
- `appInfo.version` is the `@aotter/mantle-admin` version.
|
|
229
|
+
|
|
230
|
+
The pinned official client validates host context. Invalid protocol data is not
|
|
231
|
+
reinterpreted by a second Mantle wire parser; connection failures are visible.
|
|
232
|
+
Mantle applies only presentation fields and style variables it supports. `locale` and
|
|
233
|
+
`timeZone` format dates (an unknown zone falls back to the browser's);
|
|
234
|
+
`safeAreaInsets` become root padding; `containerDimensions` bound the root: a
|
|
235
|
+
fixed or maximum height scrolls, and the reported size stays within it.
|
|
236
|
+
`theme: "dark"` sets both `data-theme` and the `.dark` class. Only the spec's
|
|
237
|
+
style variables are applied; [Theme](#theme) says how they meet your own.
|
|
238
|
+
|
|
239
|
+
A View result carries its rows twice, as the JSON text block and as
|
|
240
|
+
`structuredContent`. Hosts truncate long tool results (about 150k characters
|
|
241
|
+
is reported for some), and a truncated text block is no longer JSON. So an MCP
|
|
242
|
+
View page is cut by size: `limit` still accepts 1 to 500, but a page holds only
|
|
243
|
+
as many of those rows as fit 60,000 characters of JSON. It stops before the first
|
|
244
|
+
row that overflows, and the next cursor resumes at that row, so no row is skipped
|
|
245
|
+
or repeated. A single row over the budget is returned alone, with its cursor, and a
|
|
246
|
+
View without an order (one page, no cursor) is returned whole; any page still over the
|
|
247
|
+
budget carries a notice that hosts may truncate it. The default is 50 rows. REST and native paging keep their 500-row limit.
|
|
248
|
+
|
|
249
|
+
Checks against real hosts need an account and a public HTTPS endpoint, so
|
|
250
|
+
they are manual: add the endpoint as a custom connector in each host, call a
|
|
251
|
+
View tool with an App, and check light and dark themes, a narrow window,
|
|
252
|
+
full screen, a row action, and a link. Check too that the host advertises
|
|
253
|
+
`hostCapabilities.serverTools`; without it the App is read-only. The automated interoperability test
|
|
254
|
+
uses the official `AppBridge`; your app runs its generated version ([Run the compatibility test](#run-the-compatibility-test)).
|
|
255
|
+
|
|
256
|
+
## Customising Admin for MCP hosts
|
|
257
|
+
|
|
258
|
+
`mantle bundle` builds the Admin MCP App from the same entry as the browser Admin
|
|
259
|
+
(`src/admin.tsx`, or the built-in one) into one self-contained file,
|
|
260
|
+
`<assets>/admin/mcp-app.html`, which `adminMcpApps` serves as
|
|
261
|
+
`ui://mantle-admin/admin/<digest>` by default. Shared View cells and opted-in pages reach the host, with application plugins,
|
|
262
|
+
Tailwind classes and CSS. Browser-only record/input overrides do not mount there.
|
|
263
|
+
|
|
264
|
+
### Which slots render where
|
|
265
|
+
|
|
266
|
+
| Slot | Browser | MCP host |
|
|
267
|
+
| --- | --- | --- |
|
|
268
|
+
| `Shell`, `Sidebar`, `Header`, `Brand`, `Home`, `CollectionList`, `list.actions:<schema>` | yes | never: the App has no shell, sidebar or router |
|
|
269
|
+
| `cell:view:<view>.<column>` | yes | yes: each column of the rows the host's View tool returned |
|
|
270
|
+
| `cell:<schema>.<field>`, `input:<schema>.<field>`, `record.aside:<schema>`, `record.actions:<schema>`, `RecordView` | yes | never: browser-only record/collection slots; their `surface` is `"browser"` (`mantle bundle` reminds you) |
|
|
271
|
+
| `pages[]`, `nav`, `home`, nav badges | yes | only a page that declares `mcp` (below) |
|
|
272
|
+
|
|
273
|
+
A component for a `both` slot gets `surface` (`"browser"` or `"mcp-app"`). The
|
|
274
|
+
record/collection slots keep `surface: "browser"` for an explicit, truthful contract.
|
|
275
|
+
Other browser-only slots have no `surface` prop.
|
|
276
|
+
|
|
277
|
+
Migration (#1710): remove unreachable `surface === "mcp-app"` branches from
|
|
278
|
+
record/input/collection overrides. Their surface is now typed as the browser
|
|
279
|
+
literal. Move shared interactions to a View cell or opted-in page; changing
|
|
280
|
+
this type does not add a record editor, entry tool, or automatic CRUD to MCP.
|
|
281
|
+
|
|
282
|
+
### Record input versus Procedure action input
|
|
283
|
+
|
|
284
|
+
`input:requests.title` wraps the browser record editor's `value`, `onChange`,
|
|
285
|
+
`readOnly` and JSON Schema property. It does not customize an MCP action dialog.
|
|
286
|
+
The native App builds that dialog from the Procedure's declared input schema,
|
|
287
|
+
binds the row identity and observed version, validates input, and runs the
|
|
288
|
+
existing authorized Procedure tool. There is no View `actions` field.
|
|
289
|
+
Declare `Procedure.spec.target: { schema: requests, id: id, version: expectedVersion }`
|
|
290
|
+
and expose it with a Trigger whose `source: { kind: mcp, surface: admin }` and
|
|
291
|
+
`target: { procedure: approve }`. Return the target row's unchanged `id` and
|
|
292
|
+
`version` in the View, preserving their column provenance (do not replace them
|
|
293
|
+
with a computed label, joined row identity or synthetic version). Admin derives
|
|
294
|
+
row bindings from that target and the View's proven columns. See
|
|
295
|
+
[Procedure targets and row placement](native-admin.md#collection-navigation-and-operation-placement)
|
|
296
|
+
and the shipped [purchasing manifest](../../examples/purchasing/manifests/purchasing.yaml)
|
|
297
|
+
for complete Procedure/Trigger syntax. Changing the UI grants no execution rights.
|
|
298
|
+
|
|
299
|
+
For custom MCP interaction, use an opted-in page with `useView` and
|
|
300
|
+
`useOperation` instead of trying to render a record editor. The shared-page example below demonstrates page opt-in;
|
|
301
|
+
[Native Admin](native-admin.md) describes generated typed hooks. An operation
|
|
302
|
+
component follows the same public contract:
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
// In a typed opted-in page; replace approve/input with your generated names.
|
|
306
|
+
function Approve({ id }: { id: string }) {
|
|
307
|
+
const { run, pending } = useOperation("approve");
|
|
308
|
+
const [message, setMessage] = useState("");
|
|
309
|
+
return <><button disabled={!run || pending} onClick={() => {
|
|
310
|
+
void run?.({ id }).then(() => setMessage("Done"), () => setMessage("Not available"));
|
|
311
|
+
}}>Approve</button><p role="status">{message}</p></>;
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Import `useState` from React and `useOperation` from your generated
|
|
316
|
+
`admin-client` (its types are tied to your manifests). Include observed-version
|
|
317
|
+
input when the Procedure declares OCC; never replace it with an unguarded write.
|
|
318
|
+
Without serverTools capability, `run` is absent and this button stays disabled.
|
|
319
|
+
A missing or denied tool is reported as unavailable when invoked; an enabled
|
|
320
|
+
button does not establish permission. To ship a record-oriented MCP UI today, explicitly
|
|
321
|
+
declare its read View and write Procedure; `useEntry` is unavailable in MCP.
|
|
322
|
+
|
|
323
|
+
### Rules for a component that renders in a host
|
|
324
|
+
|
|
325
|
+
1. **Data only through the hooks.** `useView` and `useOperation` call the View's
|
|
326
|
+
or Procedure's admin MCP tool through the host, as the signed-in caller.
|
|
327
|
+
`useEntries`, `useEntry`, `useCollectionTotal`, `useMe` and `useBadges` have no
|
|
328
|
+
tool and read `unavailable`: declare the data a component needs as an admin
|
|
329
|
+
View. Without the host's `serverTools`, `useOperation().run` is `undefined`.
|
|
330
|
+
No `fetch` to `/admin/api`, no `document.cookie`: the build refuses both.
|
|
331
|
+
2. **Links through the host.** `<AdminLink>` and `useOpenLink()` open an http(s)
|
|
332
|
+
URL through `ui/open-link`; the sandbox blocks `window.open` and
|
|
333
|
+
`target="_blank"`. An `/admin/...` path has no origin in a host, so
|
|
334
|
+
`<AdminLink>` shows it as text.
|
|
335
|
+
3. **No assumed width.** The host sets the frame; a narrow one and a full screen
|
|
336
|
+
must both work.
|
|
337
|
+
4. **Remote assets only from origins you declare.** Something from another origin
|
|
338
|
+
loads only if `defineAdmin({ mcp: { csp } })` names it in the list its use needs:
|
|
339
|
+
`resourceDomains` for an image, font, stylesheet or script,
|
|
340
|
+
`connectDomains` for `fetch`, `WebSocket`, `EventSource` and `import()`,
|
|
341
|
+
`frameDomains` for an iframe (`baseUriDomains` is for `<base>`). Each entry is an
|
|
342
|
+
origin, `https://host[:port]` or `https://*.host`; the build refuses `*`, a bare
|
|
343
|
+
scheme or a path. You declare it once: the build writes it into
|
|
344
|
+
`mcp-app.html` and `adminMcpApps` sends it as `_meta.ui.csp` on `resources/read`
|
|
345
|
+
(not on `resources/list`, which names no HTML). Prefer assets bundled into your entry: they are inlined.
|
|
346
|
+
5. **Branch on the surface if you must, but both branches render.**
|
|
347
|
+
`useAdminSurface()` and the `surface` prop say where you are; a component that
|
|
348
|
+
shows nothing in a host is a hole in the host's view.
|
|
349
|
+
|
|
350
|
+
### Theme
|
|
351
|
+
|
|
352
|
+
The App's colours come from three layers, later over earlier:
|
|
353
|
+
|
|
354
|
+
1. Admin's default tokens (`@aotter/mantle-ui/tokens.css`: the shadcn variables
|
|
355
|
+
`--background`, `--primary`, `--chart-1` and the rest).
|
|
356
|
+
2. Your own CSS, which your entry imports: your `--primary` or `.dark {}`
|
|
357
|
+
overrides apply in the App as they do in the browser Admin.
|
|
358
|
+
3. The host's style variables, for neutral colours, radii, text sizes and the
|
|
359
|
+
font only, laid over the tokens. The table lists, for each host variable the
|
|
360
|
+
App reads, the Tailwind theme variable it feeds.
|
|
361
|
+
|
|
362
|
+
| Host variable | App theme variable |
|
|
363
|
+
| --- | --- |
|
|
364
|
+
| `--color-background-primary` | `--color-background`, `--color-card`, `--color-popover` |
|
|
365
|
+
| `--color-text-primary` | `--color-foreground`, `--color-card-foreground`, `--color-popover-foreground`, `--color-secondary-foreground`, `--color-accent-foreground` |
|
|
366
|
+
| `--color-background-secondary` | `--color-muted`, `--color-secondary`, `--color-accent` |
|
|
367
|
+
| `--color-text-secondary` | `--color-muted-foreground` |
|
|
368
|
+
| `--color-border-primary` | `--color-border`, `--color-input` |
|
|
369
|
+
| `--color-ring-primary` | `--color-ring` |
|
|
370
|
+
| `--color-text-danger` | `--color-destructive` |
|
|
371
|
+
| `--border-radius-sm` | `--radius-sm` |
|
|
372
|
+
| `--border-radius-md` | `--radius-md` |
|
|
373
|
+
| `--border-radius-lg` | `--radius-lg` |
|
|
374
|
+
| `--font-text-xs-size` | `--text-xs` |
|
|
375
|
+
| `--font-text-xs-line-height` | `--text-xs--line-height` |
|
|
376
|
+
| `--font-text-sm-size` | `--text-sm` |
|
|
377
|
+
| `--font-text-sm-line-height` | `--text-sm--line-height` |
|
|
378
|
+
| `--font-text-md-size` | `--text-base` |
|
|
379
|
+
| `--font-text-md-line-height` | `--text-base--line-height` |
|
|
380
|
+
| `--font-sans` | `--font-app` (the App's font) |
|
|
381
|
+
|
|
382
|
+
`--font-mono` and `--font-weight-*` apply as the host gives them. The primary
|
|
383
|
+
colours (`--color-primary`, `--color-primary-foreground`) and the chart colours
|
|
384
|
+
(`--color-chart-1` to `-5`) are your brand's and are never the host's.
|
|
385
|
+
`--sidebar-*` and `--app-*` do not apply in a host. The host's theme marks the
|
|
386
|
+
root with both `data-theme` and the `.dark` class, so your `.dark {}` block and
|
|
387
|
+
`dark:` classes follow it; a host that sends no theme gets the system's colour scheme (`prefers-color-scheme`), as the same `.dark` class, and a theme the host sends wins over it.
|
|
388
|
+
The variables a host does not send fall back to the tokens.
|
|
389
|
+
|
|
390
|
+
### A page for an MCP App view: `pages[].mcp`
|
|
391
|
+
|
|
392
|
+
A custom page may also be an App view. Name a View that has an admin MCP tool:
|
|
393
|
+
|
|
394
|
+
```tsx
|
|
395
|
+
export default defineAdmin(admin, {
|
|
396
|
+
pages: [{
|
|
397
|
+
path: "queue", title: "Queue", element: <QueuePage />,
|
|
398
|
+
header: { description: "Items awaiting review", actions: <QueueActions /> },
|
|
399
|
+
mcp: { renders: "pending-requests" },
|
|
400
|
+
}],
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
The explicit `header` object shares the native title, localized description and
|
|
405
|
+
React actions with browser Admin. Its actions must use the same host-safe hooks
|
|
406
|
+
and capability fallbacks as the page. `header: false` delegates the heading to
|
|
407
|
+
the element; omit `header` to preserve element-only MCP presentation (and the
|
|
408
|
+
browser's native title). Do not render a second h1 inside `element` when using
|
|
409
|
+
the native heading.
|
|
410
|
+
|
|
411
|
+
When the host's tool is that View's tool, the App shows `<QueuePage />` instead
|
|
412
|
+
of the default rows table, in the host's frame and theme. The page reads its own data
|
|
413
|
+
through the hooks (`useView("pending-requests", input)` calls the View's tool through
|
|
414
|
+
the host), so use only `both` hooks and components, and render something sensible
|
|
415
|
+
when a hook reads `unavailable`.
|
|
416
|
+
The generated `admin-client.ts` records, for each View, `true` when it is an admin MCP tool and `null` when not; `tsc`
|
|
417
|
+
refuses a `renders` that is misspelt, names a collection, or names a View with no
|
|
418
|
+
admin MCP tool. When several pages name one View, the first is shown.
|
|
419
|
+
|
|
420
|
+
### What `mantle bundle` checks
|
|
421
|
+
|
|
422
|
+
`buildAdmin`, which `mantle bundle` runs, builds the App and then refuses it,
|
|
423
|
+
naming the module (and the slot, when it can tell) and pointing here, if the
|
|
424
|
+
file:
|
|
425
|
+
|
|
426
|
+
- contains `/admin/api`, which is the browser's route and has no session in a host;
|
|
427
|
+
- reads `document.cookie`;
|
|
428
|
+
- uses a remote origin (`http(s):` or `//`; `ws(s):` for sockets) that
|
|
429
|
+
`defineAdmin({ mcp: { csp } })` does not declare in the list that use needs.
|
|
430
|
+
|
|
431
|
+
The check is best effort: it reads the built text, so it catches only what it can
|
|
432
|
+
see as a string. It catches `src` and `poster` attributes and props, `<iframe src>`
|
|
433
|
+
(against `frameDomains`), every URL of a `srcset` and of a CSS `image-set()`, `url()`
|
|
434
|
+
and `@import`, a `<link href>`, `setAttribute("src" | "href", "https://…")`, and a
|
|
435
|
+
remote string literal given to `fetch()`, `new WebSocket()`, `new EventSource()` or
|
|
436
|
+
`import()` (against `connectDomains`). It does not treat `href` on a link
|
|
437
|
+
(`<a>`, `<AdminLink>`) as a load: that is navigation, which opens through the host.
|
|
438
|
+
A URL put together at run time is not seen; the generated compatibility test, which
|
|
439
|
+
loads the App under the spec's default CSP and fails on any network request, is
|
|
440
|
+
the backstop at run time. `document.cookie` in Mantle's own library code (the kit's
|
|
441
|
+
sidebar remembers its state in one) does not fail the build; in yours it does.
|
|
442
|
+
|
|
443
|
+
`window.open` and `target="_blank"` in your own code only warn. The whole selected
|
|
444
|
+
MCP entry graph is checked, not only the slots a host renders. By default it is
|
|
445
|
+
the Admin entry, so browser-only code (a `Shell`, a `Home`, a page) imported there
|
|
446
|
+
must avoid direct `/admin/api` fetches and cookies too. Shared components read
|
|
447
|
+
through the hooks, which use the browser route in the browser and host tools in
|
|
448
|
+
a host. For integrations that need browser APIs, select a separate entry below.
|
|
449
|
+
The build prints the App's size.
|
|
450
|
+
|
|
451
|
+
### Isolate browser-only integrations
|
|
452
|
+
|
|
453
|
+
`pages[].mcp` opts a page into rendering; it does not remove other imports from
|
|
454
|
+
the build. To isolate browser-only integrations, add `admin.mcp.tsx` beside the
|
|
455
|
+
Worker entry and `admin.tsx`. `mantle bundle` discovers this optional file and
|
|
456
|
+
uses it for `mcp-app.html`; the browser SPA and preview keep `admin.tsx`. With the
|
|
457
|
+
build API, pass `buildAdmin({ entry: "src/admin.tsx", mcpEntry: "src/admin.mcp.tsx", outDir: "dist/client/admin" })`.
|
|
458
|
+
Without the optional file/option, the shared-entry behavior is unchanged.
|
|
459
|
+
|
|
460
|
+
Keep a single host-safe config and components module, composed through the same
|
|
461
|
+
`defineAdmin` contract in both roots. For example, with an admin View `queue`:
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
// src/admin-shared.tsx
|
|
465
|
+
import type { AdminConfig } from "@aotter/mantle-admin/app";
|
|
466
|
+
import type { AppAdminTypes } from "../generated/admin-client.js";
|
|
467
|
+
import { QueuePage } from "./queue-page.js"; // native hooks, host-safe components
|
|
468
|
+
import "./shared.css";
|
|
469
|
+
export const shared = {
|
|
470
|
+
pages: [{ path: "queue", title: "Queue", element: <QueuePage />, mcp: { renders: "queue" } }],
|
|
471
|
+
} satisfies AdminConfig<AppAdminTypes>;
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
// src/admin.tsx: only the browser root imports this integration
|
|
476
|
+
import { defineAdmin } from "@aotter/mantle-admin/app";
|
|
477
|
+
import { admin } from "../generated/admin-client.js";
|
|
478
|
+
import { shared } from "./admin-shared.js";
|
|
479
|
+
import { BrowserReport } from "./browser-report.js";
|
|
480
|
+
export default defineAdmin(admin, {
|
|
481
|
+
...shared,
|
|
482
|
+
pages: [...shared.pages, { path: "browser-report", title: "Browser report", element: <BrowserReport /> }],
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
```tsx
|
|
487
|
+
// src/admin.mcp.tsx: no import of admin.tsx or browser-report.js
|
|
488
|
+
import { defineAdmin } from "@aotter/mantle-admin/app";
|
|
489
|
+
import { admin } from "../generated/admin-client.js";
|
|
490
|
+
import { shared } from "./admin-shared.js";
|
|
491
|
+
export default defineAdmin(admin, shared);
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Only code excluded from the MCP graph may use browser-only integrations. The
|
|
495
|
+
selected MCP entry is still read in Node to obtain its definition: do not access
|
|
496
|
+
`window` or `document` at module initialization there. An explicit entry that
|
|
497
|
+
fails to load fails the build; it does not fall back to the browser entry.
|
|
498
|
+
The MCP guard, declared CSP, catalogue, resource ownership and built-HTML cache
|
|
499
|
+
identity apply to the selected graph exactly as before. Entry selection changes
|
|
500
|
+
no server permissions and adds no provider or transport.
|
|
501
|
+
|
|
502
|
+
### Run the compatibility test
|
|
503
|
+
|
|
504
|
+
For an interactive test in another official host, load the HTML returned by an
|
|
505
|
+
authenticated MCP `resources/read`, discovered through `resources/list` or the
|
|
506
|
+
tool's UI resource link. Do not serve the raw built `mcp-app.html` directly: the
|
|
507
|
+
server embeds the caller-filtered catalogue when reading the resource, and
|
|
508
|
+
without that catalogue native row actions are absent. The generated test below
|
|
509
|
+
supplies its own synthetic catalogue and checks rendering, not a real host's
|
|
510
|
+
authorization flow or a completed action dialog.
|
|
511
|
+
|
|
512
|
+
The build check reads text; this test runs the App. `mantle generate --admin` writes
|
|
513
|
+
`generated/admin-mcp-app.check.mjs`. After `npm run build`, run
|
|
514
|
+
`node --test generated/admin-mcp-app.check.mjs` (the examples name it `npm test`;
|
|
515
|
+
`MANTLE_MCP_APP_HTML` points it at another built file). It loads the built
|
|
516
|
+
`mcp-app.html` in a host page built on the official `AppBridge`. The App is in a
|
|
517
|
+
sandboxed iframe from its own origin, under the spec's default CSP, so it cannot
|
|
518
|
+
reach the host page. For each View whose `cell:view:` slot you override and each
|
|
519
|
+
`pages[].mcp` page it sends sample rows made from the catalogue's column schemas
|
|
520
|
+
(three full rows and one with every nullable column null), in a light and a dark
|
|
521
|
+
host. When the file records nothing customised it runs the first View of the
|
|
522
|
+
catalogue; when the file has no record at all, every View. It fails on:
|
|
523
|
+
|
|
524
|
+
- a network request that leaves the frame (the failure names the URL; origins you
|
|
525
|
+
declared in `mcp.csp`, for the kind of use they were declared for, are answered
|
|
526
|
+
empty, since the test has no network);
|
|
527
|
+
- a console error, including a CSP refusal, or an uncaught exception;
|
|
528
|
+
- an App that does not show the result (it still waits, or is empty), that does not
|
|
529
|
+
take the host's dark theme, or that never reports a size.
|
|
530
|
+
|
|
531
|
+
After the result it waits until the App has made no request and logged nothing for
|
|
532
|
+
400 ms, at most 5 s. The test checks the slots a host renders today: `cell:view:` and
|
|
533
|
+
pages. `input:`, `record.aside:`, `record.actions:`, `cell:<schema>.<field>` and
|
|
534
|
+
`RecordView` are browser-only, so the generated compatibility check does not
|
|
535
|
+
claim to test them. A Procedure action form does not mount these slots.
|
|
536
|
+
|
|
537
|
+
It needs `playwright-core` (`npm install -D playwright-core`) and Chrome or Chromium:
|
|
538
|
+
run `npx playwright-core install chromium`, or set `MANTLE_TEST_CHROMIUM` to a Chrome or
|
|
539
|
+
Chromium executable. With no browser the test fails, saying so; it never skips. Import
|
|
540
|
+
`checkMcpApp` from `@aotter/mantle-admin/testing` to call it yourself. For a real host
|
|
541
|
+
(Claude's custom connector, ChatGPT), follow the manual checks under
|
|
542
|
+
[The Admin App in a host](#the-admin-app-in-a-host).
|
|
543
|
+
|
|
544
|
+
### Hosts without MCP Apps
|
|
545
|
+
|
|
546
|
+
A host that does not advertise the MCP Apps extension gets no `ui://` resource
|
|
547
|
+
and no `_meta.ui` on the tools. Its tool results are the same complete JSON
|
|
548
|
+
(`content` text and `structuredContent`); the App only shows them. Nothing an
|
|
549
|
+
application customises changes what a tool returns.
|
|
550
|
+
|
|
551
|
+
### Upgrading from the prebuilt App
|
|
552
|
+
|
|
553
|
+
Earlier versions shipped the App prebuilt as `@aotter/mantle-admin/mcp-app`
|
|
554
|
+
(`mantleAppHtml`), which ignored your Admin entry. That export is gone. Remove
|
|
555
|
+
the import and the `html:` option from `adminMcpApps({ catalogue })`, run
|
|
556
|
+
`mantle generate` again (the generated `admin-client.ts` now records which Views are admin
|
|
557
|
+
MCP tools) and `mantle bundle`; the App is in `<assets>/admin/mcp-app.html`, and
|
|
558
|
+
your entry's CSS, components and pages are now in it. The Worker's assets
|
|
559
|
+
binding must serve `/admin/mcp-app.html` (it does for the Admin build's output
|
|
560
|
+
directory); on Bun, `dist/client/admin/mcp-app.html` must exist at run time.
|