@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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Request-only MCP implementation, excluded from Worker startup. */
|
|
2
2
|
import { authorized, authorizeTarget } from "./authorization.js";
|
|
3
3
|
import { forwarded, isAdmin, ContractError, checkOrigin, surfaceCaller, surfaceError, wire } from "./contracts.js";
|
|
4
|
+
import { draining } from "./drain.js";
|
|
4
5
|
import { callToolLazy as callTool } from "./mcp-call.js";
|
|
5
|
-
import { RESOURCE_MIME_TYPE, linkSurface } from "./mcp-apps.js";
|
|
6
|
+
import { RESOURCE_MIME_TYPE, linkSurface, resourcesOf } from "./mcp-apps.js";
|
|
6
7
|
const text = (value, locale) => {
|
|
7
8
|
if (typeof value === "string")
|
|
8
9
|
return value;
|
|
@@ -18,11 +19,18 @@ function observe(listener, event) {
|
|
|
18
19
|
catch { }
|
|
19
20
|
}
|
|
20
21
|
const paging = (schema) => ({ ...schema, properties: { ...schema.properties, limit: { type: "integer", minimum: 1, maximum: 500 }, cursor: { type: "string" } } });
|
|
22
|
+
/**
|
|
23
|
+
* Characters of `JSON.stringify(page)` an MCP View page may hold. A result carries its rows twice (text block and
|
|
24
|
+
* `structuredContent`) and hosts are reported to truncate near 150k characters, so one copy stays at 60k.
|
|
25
|
+
* ponytail: one fixed number; raise it or make it an option if hosts publish their limits.
|
|
26
|
+
*/
|
|
27
|
+
const MCP_PAGE_BUDGET = 60_000;
|
|
21
28
|
/** The scopes a challenge asks for: the credential's, plus what the request needs. */
|
|
22
29
|
const union = (scopes, extra) => [...new Set([...scopes, ...extra])].join(" ");
|
|
23
30
|
/** The handler for `/mcp` (public tools) and `/mcp/admin` (tools for `admin` roles); other paths are undefined. */
|
|
24
31
|
export function handler(tools, options, apps = {}, reads = []) {
|
|
25
|
-
|
|
32
|
+
const allowed = options.allowedOrigins ?? [];
|
|
33
|
+
return draining(async (request, env, ctx) => {
|
|
26
34
|
const access = options.gateway ? (await import("./host-access.js")).accessRequest(ctx, options) : undefined;
|
|
27
35
|
let resolved;
|
|
28
36
|
try {
|
|
@@ -30,6 +38,18 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
30
38
|
const url = new URL(request.url), path = url.pathname.replace(/\/+$/, ""), surface = path === "/mcp" ? "public" : path === "/mcp/admin" ? "admin" : undefined;
|
|
31
39
|
if (!surface)
|
|
32
40
|
return;
|
|
41
|
+
// MCP transport MUST: a present Origin is a browser, answered only for loopback or `allowedOrigins`. The request's own
|
|
42
|
+
// host is not implied, since a DNS-rebinding page sends Origin == its rebound Host; a missing Origin is a server client.
|
|
43
|
+
if (request.headers.has("origin")) {
|
|
44
|
+
const sdk = await import("@modelcontextprotocol/server");
|
|
45
|
+
const foreign = sdk.originValidationResponse(request, [...sdk.localhostAllowedOrigins(), ...allowed]);
|
|
46
|
+
if (foreign) {
|
|
47
|
+
access?.finish(new ContractError("AUTH_DENIED", "Origin is not allowed.", "request:origin"));
|
|
48
|
+
observe(options.onObservation, { kind: "request-refused", at: Date.now(), surface, status: 403 });
|
|
49
|
+
observe(options.onRefusal, { at: Date.now(), status: 403, reason: "origin" });
|
|
50
|
+
return foreign;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
33
53
|
const select = async () => (typeof tools === "function" ? await tools(request, env) : tools).filter(tool => tool.surface === surface);
|
|
34
54
|
// Dynamic admin tables stay behind the admin gate; cache classification uses generated metadata only.
|
|
35
55
|
let selected = surface === "public" ? await select() : [];
|
|
@@ -74,7 +94,7 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
74
94
|
observe(options.onRefusal, { at: Date.now(), status: result.status, reason: "credential" });
|
|
75
95
|
}
|
|
76
96
|
if (result.status === 401)
|
|
77
|
-
scope
|
|
97
|
+
scope = union(scope === undefined ? requiredScopes : scope.split(" "), options.authorizationScopes ?? []);
|
|
78
98
|
return (await import("./host-caller.js")).hostFailure(error, resolved, `mcp ${path}`, bare ? undefined : 'Bearer ' + (scope ? (result.status === 403 ? 'error="insufficient_scope", ' : '') + 'scope="' + scope + '", ' : '') + 'resource_metadata="' + (options.resourceMetadata ?? url.origin + '/.well-known/oauth-protected-resource/mcp') + '"');
|
|
79
99
|
};
|
|
80
100
|
let caller;
|
|
@@ -100,7 +120,9 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
100
120
|
}
|
|
101
121
|
if (surface === "admin")
|
|
102
122
|
selected = await select();
|
|
103
|
-
|
|
123
|
+
// Before per-caller filtering, so every caller of a deploy sees one catalogue version.
|
|
124
|
+
const version = catalogueVersion(selected);
|
|
125
|
+
const { McpServer, ProtocolError, ProtocolErrorCode, createMcpHandler, fromJsonSchema } = await import("@modelcontextprotocol/server");
|
|
104
126
|
if (request.method === "POST" && !cookieOnly && !options.gateway)
|
|
105
127
|
await parse();
|
|
106
128
|
if (tooLarge && access)
|
|
@@ -149,7 +171,17 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
149
171
|
}
|
|
150
172
|
}
|
|
151
173
|
const app = options.apps?.[surface];
|
|
152
|
-
|
|
174
|
+
let links;
|
|
175
|
+
if (app) {
|
|
176
|
+
const sources = (Array.isArray(app) ? app : [app]);
|
|
177
|
+
const dynamic = sources.some(source => typeof source === "function");
|
|
178
|
+
const resolved = resourcesOf(await Promise.all(sources.map(source => typeof source === "function" ? source({ env, request }) : source)));
|
|
179
|
+
// Equal tool names say nothing about request-bound resource identity or ownership.
|
|
180
|
+
const current = linkSurface(resolved, selected, dynamic ? undefined : apps[surface]);
|
|
181
|
+
if (!dynamic)
|
|
182
|
+
apps[surface] = current;
|
|
183
|
+
links = current.links;
|
|
184
|
+
}
|
|
153
185
|
if (options.authorize) {
|
|
154
186
|
try {
|
|
155
187
|
for (const message of Array.isArray(parsedBody) ? parsedBody : [parsedBody]) {
|
|
@@ -161,7 +193,8 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
161
193
|
tool.check?.(caller);
|
|
162
194
|
await authorizeTarget(options.authorize, tool.target ?? { kind: tool.page ? "view" : "procedure", name: tool.name }, caller);
|
|
163
195
|
}
|
|
164
|
-
|
|
196
|
+
// an App's HTML is rendered for the same tools the caller's tools/list shows
|
|
197
|
+
if ((Array.isArray(parsedBody) ? parsedBody : [parsedBody]).some(message => message?.method === "tools/list" || message?.method === "resources/read")) {
|
|
165
198
|
const visible = [];
|
|
166
199
|
for (const tool of selected) {
|
|
167
200
|
if (caller.kind === "anonymous" && tool.closed || !options.gateway && caller.kind === "user" && caller.credential === "oauth" && tool.scopes?.some(scope => !caller.scopes.includes(scope)))
|
|
@@ -184,17 +217,31 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
184
217
|
return challenge(error);
|
|
185
218
|
}
|
|
186
219
|
}
|
|
220
|
+
const serverInfo = { name: "aotter.mantle", ...options.serverInfo, version: options.serverInfo?.version ?? version };
|
|
187
221
|
// Host calls execute in the SDK callback, after transport validation.
|
|
188
222
|
const handler = createMcpHandler(() => {
|
|
189
223
|
const schema = (value) => fromJsonSchema(value, { getValidator() { return (input) => ({ valid: true, data: input, errorMessage: undefined }); } });
|
|
190
|
-
|
|
224
|
+
// Tools capability after construction: given to the constructor, the SDK installs its tools/call handler before the wrap below.
|
|
225
|
+
const server = new McpServer(serverInfo, options.instructions ? { instructions: options.instructions } : {});
|
|
226
|
+
server.server.registerCapabilities({ tools: { listChanged: false } });
|
|
191
227
|
if (host)
|
|
192
228
|
server.server.onclose = () => access?.finish(isRecord(parsedBody) && parsedBody.method === "initialize" ? undefined : new ContractError("INPUT_VALIDATION_FAILED", "MCP request refused.", "mcp", { status: 400 }));
|
|
193
|
-
const appMeta = links ? registerApps(server, links, parsedBody) : undefined;
|
|
229
|
+
const appMeta = links ? registerApps(server, links, parsedBody, new Set(selected.map(tool => tool.name)), { env, request }) : undefined;
|
|
230
|
+
// A host's stale copy names a tool this catalogue lacks: say so, with the catalogue version, instead of the SDK's bare "not found".
|
|
231
|
+
// Wrapped where the SDK stores it, so transport and request validation still answer first.
|
|
232
|
+
const registered = new Set(), set = server.server.setRequestHandler.bind(server.server);
|
|
233
|
+
server.server.setRequestHandler = ((method, handler) => set(method, method === "tools/call" && typeof handler === "function" ? (request, context) => {
|
|
234
|
+
if (!registered.has(request.params.name))
|
|
235
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `No tool named ${JSON.stringify(request.params.name)} exists in the current catalogue (catalogue version ${serverInfo.version}).`);
|
|
236
|
+
return handler(request, context);
|
|
237
|
+
} : handler));
|
|
194
238
|
for (const tool of selected) {
|
|
195
239
|
const meta = appMeta?.(tool.name);
|
|
196
240
|
if (meta === false)
|
|
197
241
|
continue;
|
|
242
|
+
registered.add(tool.name);
|
|
243
|
+
// Not part of structured output: the version, and on failure the current input contract.
|
|
244
|
+
const about = (contract) => { const text = [tool.version && `tool version ${tool.version}`, contract && `input schema ${JSON.stringify(tool.page ? paging(tool.inputSchema) : tool.inputSchema)}`].filter(Boolean).join("; "); return text ? [{ type: "text", text }] : []; };
|
|
198
245
|
server.registerTool(tool.name, { ...(meta ? { _meta: meta } : {}), ...(tool.title ? { title: text(tool.title, locale) } : {}), description: text(tool.description, locale) ?? text(tool.title, locale) ?? tool.name, inputSchema: schema(tool.page ? paging(tool.inputSchema) : tool.inputSchema), ...(tool.outputSchema ? { outputSchema: schema(tool.outputSchema) } : {}), ...(tool.annotations ? { annotations: tool.annotations } : {}) }, async (raw) => {
|
|
199
246
|
const metadata = options.onObservation ? { kind: "invocation", surface, invocationId: crypto.randomUUID(), tool: tool.name, caller: Object.freeze(caller.kind === "user" ? { ...caller, scopes: Object.freeze([...caller.scopes]) } : { ...caller }) } : undefined;
|
|
200
247
|
const started = metadata ? performance.now() : 0;
|
|
@@ -206,10 +253,34 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
206
253
|
tool.check?.(caller);
|
|
207
254
|
if (tool.run && (!host || !tool.readOnly))
|
|
208
255
|
checkOrigin(request, caller);
|
|
209
|
-
|
|
256
|
+
let db;
|
|
257
|
+
const run = (input) => callTool(tool, input, () => db ??= options.db(env), caller, options.handlers, access?.dryRun ? () => ({ dryRun: true, event: access.finish() }) : undefined);
|
|
258
|
+
let data = await run(raw);
|
|
259
|
+
// A page is cut before the first row that overflows the budget: the same page re-runs with the limit that fits, so the cursor is the View's own.
|
|
260
|
+
// Data can change between reads, so the re-run is measured again. A View without an order is one page and refuses a smaller limit: it stays whole.
|
|
261
|
+
for (let tries = 0; tool.page && isRecord(data) && Array.isArray(data.rows); tries++) {
|
|
262
|
+
const rows = data.rows, total = JSON.stringify(data).length;
|
|
263
|
+
if (total <= MCP_PAGE_BUDGET)
|
|
264
|
+
break;
|
|
265
|
+
if (rows.length === 1 || tries === 4)
|
|
266
|
+
break;
|
|
267
|
+
const sizes = rows.map(row => JSON.stringify(row).length);
|
|
268
|
+
let size = total - sizes.reduce((sum, n) => sum + n, 0) - (rows.length - 1), fit = 0;
|
|
269
|
+
for (const n of sizes) {
|
|
270
|
+
size += n + (fit ? 1 : 0);
|
|
271
|
+
if (size > MCP_PAGE_BUDGET)
|
|
272
|
+
break;
|
|
273
|
+
fit++;
|
|
274
|
+
}
|
|
275
|
+
const cut = await run({ ...raw, limit: Math.max(1, fit) }).catch(() => undefined);
|
|
276
|
+
if (cut === undefined)
|
|
277
|
+
break;
|
|
278
|
+
data = cut;
|
|
279
|
+
}
|
|
210
280
|
if (host)
|
|
211
281
|
access?.finish();
|
|
212
|
-
|
|
282
|
+
const text = JSON.stringify(data), notice = tool.page && text.length > MCP_PAGE_BUDGET;
|
|
283
|
+
return { ...(meta ? { _meta: { "net.aotter.mantle/tool": tool.name } } : {}), content: [{ type: "text", text }, ...about(false), ...(notice ? [{ type: "text", text: `This page is larger than the ${MCP_PAGE_BUDGET}-character page size budget (a single row over it, or a View without an order, is returned whole), so a host may truncate it. Continue with nextCursor, if any.` }] : [])], ...(data && typeof data === "object" && !Array.isArray(data) ? { structuredContent: data } : {}) };
|
|
213
284
|
}
|
|
214
285
|
catch (error) {
|
|
215
286
|
if (host)
|
|
@@ -220,7 +291,7 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
220
291
|
if (diagnostic.path === "request:origin")
|
|
221
292
|
observe(options.onRefusal, { at: Date.now(), status: 403, reason: "origin" });
|
|
222
293
|
const payload = { diagnostics: [diagnostic] };
|
|
223
|
-
return { ...(meta ? { _meta: { "net.aotter.mantle/tool": tool.name } } : {}), isError: true, content: [{ type: "text", text: JSON.stringify(payload) }], ...(tool.outputSchema ? {} : { structuredContent: payload }) };
|
|
294
|
+
return { ...(meta ? { _meta: { "net.aotter.mantle/tool": tool.name } } : {}), isError: true, content: [{ type: "text", text: JSON.stringify(payload) }, ...about(outcome === "failed")], ...(tool.outputSchema ? {} : { structuredContent: payload }) };
|
|
224
295
|
}
|
|
225
296
|
finally {
|
|
226
297
|
if (metadata)
|
|
@@ -228,6 +299,11 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
228
299
|
}
|
|
229
300
|
});
|
|
230
301
|
}
|
|
302
|
+
// An empty catalogue still answers tools/list and refuses calls the same way.
|
|
303
|
+
if (registered.size === 0) {
|
|
304
|
+
server.server.setRequestHandler("tools/list", () => ({ tools: [] }));
|
|
305
|
+
server.server.setRequestHandler("tools/call", () => ({ content: [] }));
|
|
306
|
+
}
|
|
231
307
|
return server;
|
|
232
308
|
}, { legacy: "stateless", maxRequestBodySize: 1024 * 1024, onerror: error => console.error("[mantle mcp] request failed", error) });
|
|
233
309
|
const response = await handler.fetch(request, { parsedBody });
|
|
@@ -241,7 +317,12 @@ export function handler(tools, options, apps = {}, reads = []) {
|
|
|
241
317
|
access?.finish(error);
|
|
242
318
|
return (await import("./host-caller.js")).hostFailure(error, resolved, "mcp");
|
|
243
319
|
}
|
|
244
|
-
};
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/** A hash of the tools an endpoint serves (`surface:name:version`, sorted), as 32-bit FNV-1a hex: it changes with any tool's definition. */
|
|
323
|
+
function catalogueVersion(tools) {
|
|
324
|
+
const text = JSON.stringify(tools.map(tool => `${tool.surface}:${tool.name}:${tool.version ?? ""}`).sort());
|
|
325
|
+
return [...text].reduce((h, c) => Math.imul(h ^ c.codePointAt(0), 16777619) >>> 0, 2166136261).toString(16).padStart(8, "0");
|
|
245
326
|
}
|
|
246
327
|
const CLIENT_CAPABILITIES_META_KEY = "io.modelcontextprotocol/clientCapabilities";
|
|
247
328
|
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -259,22 +340,24 @@ function clientUiSupport(message) {
|
|
|
259
340
|
}
|
|
260
341
|
return "unknown";
|
|
261
342
|
}
|
|
262
|
-
function appMeta(r) {
|
|
263
|
-
const ui = { ...(
|
|
343
|
+
function appMeta(r, csp = r.csp) {
|
|
344
|
+
const ui = { ...(csp ? { csp } : {}), ...(r.permissions ? { permissions: r.permissions } : {}), ...(r.domain !== undefined ? { domain: r.domain } : {}), ...(r.prefersBorder !== undefined ? { prefersBorder: r.prefersBorder } : {}) };
|
|
264
345
|
return Object.keys(ui).length > 0 ? { ui } : undefined;
|
|
265
346
|
}
|
|
266
347
|
/** Registration uses the official SDK; HTML loaders run only on resources/read. */
|
|
267
|
-
function registerApps(server, links, message) {
|
|
348
|
+
function registerApps(server, links, message, tools, request) {
|
|
268
349
|
const enabled = clientUiSupport(message) !== "unsupported";
|
|
269
350
|
if (enabled)
|
|
270
351
|
for (const resource of links.resources) {
|
|
271
352
|
const meta = appMeta(resource);
|
|
272
353
|
server.registerResource(resource.name, resource.uri, { mimeType: RESOURCE_MIME_TYPE, title: resource.title, description: resource.description, ...(meta ? { _meta: meta } : {}) }, async () => {
|
|
273
354
|
try {
|
|
274
|
-
|
|
355
|
+
const loaded = typeof resource.html === "string" ? resource.html : await resource.html({ tools, ...request });
|
|
356
|
+
const page = typeof loaded === "string" ? { html: loaded } : loaded, read = page.csp ? appMeta(resource, page.csp) : meta;
|
|
357
|
+
return { contents: [{ uri: resource.uri, mimeType: RESOURCE_MIME_TYPE, text: page.html, ...(read ? { _meta: read } : {}) }] };
|
|
275
358
|
}
|
|
276
|
-
catch {
|
|
277
|
-
console.error("[mantle mcp] resource rendering failed");
|
|
359
|
+
catch (error) {
|
|
360
|
+
console.error("[mantle mcp] resource rendering failed:", error instanceof Error ? error.message : String(error));
|
|
278
361
|
throw new Error("Internal error.");
|
|
279
362
|
}
|
|
280
363
|
});
|
package/dist/kernel/mcp.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/** MCP request handling for a generated tool table; the official SDK loads on the first MCP request. */
|
|
2
2
|
import type { Caller, Target, Authorize, Gateway, RequestContext, GatewayOptions } from "./contracts.js";
|
|
3
3
|
import type { Conversions } from "./surface.js";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { type McpAppsSource } from "./mcp-apps.js";
|
|
5
|
+
type Call = typeof import("./mcp-call.js");
|
|
6
|
+
export declare const callTool: Call["callTool"];
|
|
7
|
+
export declare const callToolLazy: Call["callToolLazy"];
|
|
8
|
+
export type { McpApps, McpAppsSource, McpAppResource, McpAppRender, McpAppPage, McpAppCsp } from "./mcp-apps.js";
|
|
6
9
|
/** Options of a generated `mcp(...)`; it adds required `handlers` when the app declares ref Procedures. */
|
|
7
10
|
export interface Options<Env, Database> extends GatewayOptions {
|
|
8
|
-
apps?: Readonly<Partial<Record<"public" | "admin",
|
|
11
|
+
apps?: Readonly<Partial<Record<"public" | "admin", McpAppsSource | readonly McpAppsSource[]>>>;
|
|
9
12
|
authorize?: Authorize;
|
|
10
13
|
gateway?: Gateway;
|
|
11
14
|
fields?: import("./user-fields.js").UserFields;
|
|
@@ -27,7 +30,11 @@ export interface Options<Env, Database> extends GatewayOptions {
|
|
|
27
30
|
}[];
|
|
28
31
|
};
|
|
29
32
|
requiredScopes?: readonly string[];
|
|
33
|
+
/** Additional scopes requested by an unauthenticated OAuth challenge (for example offline_access), not an API scope floor. */
|
|
34
|
+
authorizationScopes?: readonly string[];
|
|
30
35
|
resourceMetadata?: string;
|
|
36
|
+
/** Browser origins MCP answers besides loopback, compared by hostname: `https://console.example` or the official SDK's bare `console.example`. The site's own origin is not implied. */
|
|
37
|
+
allowedOrigins?: readonly string[];
|
|
31
38
|
onObservation?: (event: McpObservation) => void | Promise<void>;
|
|
32
39
|
onRefusal?: (event: CallerRefusal) => void | Promise<void>;
|
|
33
40
|
}
|
|
@@ -79,6 +86,8 @@ export interface Tool<Database, Handlers = never> {
|
|
|
79
86
|
readonly idempotentHint?: boolean;
|
|
80
87
|
};
|
|
81
88
|
readonly inputSchema: unknown;
|
|
89
|
+
/** The listed definition's version; results carry it so a model holding an older definition can tell. */
|
|
90
|
+
readonly version?: string;
|
|
82
91
|
/** Declares an auth requirement: anonymous callers are challenged. */
|
|
83
92
|
readonly closed?: boolean;
|
|
84
93
|
/** OAuth scopes the requirement names. */
|
package/dist/kernel/mcp.js
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
import { linkApps, linkSurface, resourcesOf } from "./mcp-apps.js";
|
|
2
|
-
export
|
|
2
|
+
export const callTool = async (...args) => (await import("./mcp-call.js")).callTool(...args);
|
|
3
|
+
export const callToolLazy = async (...args) => (await import("./mcp-call.js")).callToolLazy(...args);
|
|
3
4
|
/** The selected MCP routes; all request work loads only when an MCP request arrives. */
|
|
4
5
|
export function mcp(tools, options, reads = []) {
|
|
5
6
|
let serve;
|
|
6
|
-
//
|
|
7
|
+
// Check known static App configuration now; factories wait for the request caller gate.
|
|
7
8
|
const linked = {};
|
|
8
9
|
const apps = {};
|
|
9
10
|
for (const surface of ["public", "admin"]) {
|
|
10
11
|
const declared = options.apps?.[surface];
|
|
11
12
|
if (declared) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Read each static contributor's resources getter once, including mixed arrays.
|
|
14
|
+
const sources = (Array.isArray(declared) ? declared : [declared])
|
|
15
|
+
.map(source => typeof source === "function" ? source : resourcesOf(source));
|
|
16
|
+
const fixed = resourcesOf(sources.filter(source => typeof source !== "function"));
|
|
17
|
+
const dynamic = sources.some(source => typeof source === "function");
|
|
18
|
+
apps[surface] = dynamic ? sources : fixed;
|
|
19
|
+
// Known static conflicts still fail before any request or dynamic factory runs.
|
|
20
|
+
if (typeof tools === "function")
|
|
21
|
+
linkApps(fixed);
|
|
22
|
+
else {
|
|
23
|
+
const current = linkSurface(fixed, tools.filter(tool => tool.surface === surface));
|
|
24
|
+
if (!dynamic)
|
|
25
|
+
linked[surface] = current;
|
|
26
|
+
}
|
|
15
27
|
}
|
|
16
28
|
}
|
|
17
|
-
options = { ...options, apps };
|
|
29
|
+
options = { ...options, apps, allowedOrigins: originHostnames(options.allowedOrigins) };
|
|
18
30
|
return async (request, env, ctx) => {
|
|
19
31
|
if (!/^\/mcp(?:\/admin)?\/*$/.test(new URL(request.url).pathname))
|
|
20
32
|
return;
|
|
@@ -23,3 +35,16 @@ export function mcp(tools, options, reads = []) {
|
|
|
23
35
|
return serve(request, env, ctx);
|
|
24
36
|
};
|
|
25
37
|
}
|
|
38
|
+
/** `allowedOrigins` as the hostnames the official SDK compares; a bad entry fails here, not on each request. */
|
|
39
|
+
function originHostnames(origins = []) {
|
|
40
|
+
return Object.freeze(origins.map(origin => {
|
|
41
|
+
let url;
|
|
42
|
+
try {
|
|
43
|
+
url = new URL(/^[a-z][a-z\d+.-]*:\/\//i.test(origin) ? origin : `http://${origin}`);
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
if (origin.includes("*") || !url?.hostname || url.username || url.password || url.pathname !== "/" || url.search || url.hash)
|
|
47
|
+
throw new TypeError(`mcp allowedOrigins: "${origin}" is not an origin (https://host[:port]) or a host[:port]; wildcards never match.`);
|
|
48
|
+
return url.hostname;
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/** Route matching and caller enforcement load on the first request. */
|
|
2
2
|
import { forwarded, ContractError, checkOrigin, surfaceCaller } from "./contracts.js";
|
|
3
3
|
import { decode, invalid } from "./surface.js";
|
|
4
|
+
import { draining } from "./drain.js";
|
|
4
5
|
const isApi = (path) => path === "/api" || path.startsWith("/api/");
|
|
5
6
|
const fail = message => invalid(message, "rest");
|
|
6
7
|
/** Shared matching/auth/error envelope; selected dispatch is supplied by the surface. */
|
|
7
8
|
export function router(routes, options, send) {
|
|
8
|
-
return async (request, env, ctx) => {
|
|
9
|
+
return draining(async (request, env, ctx) => {
|
|
9
10
|
let caller;
|
|
10
11
|
const isForwarded = forwarded(ctx, options.gateway);
|
|
11
12
|
const access = options.gateway ? (await import("./host-access.js")).accessRequest(ctx, options) : undefined;
|
|
@@ -59,5 +60,5 @@ export function router(routes, options, send) {
|
|
|
59
60
|
access?.finish(error);
|
|
60
61
|
return (await import("./host-caller.js")).hostFailure(error, caller, `rest ${request.method} ${new URL(request.url).pathname}`);
|
|
61
62
|
}
|
|
62
|
-
};
|
|
63
|
+
});
|
|
63
64
|
}
|
package/dist/kernel/scalars.js
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
import { ContractError } from "./contracts.js";
|
|
7
7
|
export function encodeTimestamptz(v) {
|
|
8
8
|
if (v instanceof Date) {
|
|
9
|
-
|
|
9
|
+
const micros = v.getTime() * 1000;
|
|
10
|
+
if (!Number.isSafeInteger(micros))
|
|
10
11
|
throw new ContractError("SQL_TYPE", "Invalid Date is not a timestamp");
|
|
11
|
-
return
|
|
12
|
+
return micros;
|
|
12
13
|
}
|
|
13
14
|
// hours 00-23: Date.parse would roll T24:00 over to the next day
|
|
14
15
|
const m = /^(\d{4}-\d\d-\d\d)[T ]((?:[01]\d|2[0-3]):\d\d(?::\d\d)?)(?:\.(\d{1,6}))?(Z|[+-]\d\d(?::?\d\d)?)$/.exec(v.trim());
|
package/dist/kernel/site.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Dialect } from "./dialect/common.js";
|
|
|
5
5
|
import type { MantleSite, SiteDefaults, SiteConfig, MediaPurposePolicy } from "./site-types.js";
|
|
6
6
|
export type { MantleSite, SiteDefaults, SiteConfig, MediaStorage, MediaAsset, MediaLibrary, MediaPurposePolicy } from "./site-types.js";
|
|
7
7
|
export declare function createSite(db: MediaDatabase, d: Pick<Dialect, "param" | "escapeLike" | "likeEscape" | "like">, caller: Caller, defaults: SiteDefaults, policies: readonly MediaPurposePolicy[]): MantleSite;
|
|
8
|
-
/** 0.2 /admin/api/site projection; the app supplies its selected capabilities
|
|
8
|
+
/** 0.2 /admin/api/site projection; the app supplies its selected capabilities. */
|
|
9
9
|
export declare function siteInfo(config: SiteConfig, options: {
|
|
10
10
|
origin: string;
|
|
11
11
|
media?: boolean;
|
|
@@ -14,7 +14,6 @@ export declare function siteInfo(config: SiteConfig, options: {
|
|
|
14
14
|
public?: string | null;
|
|
15
15
|
admin?: string | null;
|
|
16
16
|
};
|
|
17
|
-
extensions?: readonly unknown[];
|
|
18
17
|
siteSettings?: boolean;
|
|
19
18
|
}): {
|
|
20
19
|
publicUrl: string;
|
|
@@ -28,7 +27,6 @@ export declare function siteInfo(config: SiteConfig, options: {
|
|
|
28
27
|
invitationEmail: boolean;
|
|
29
28
|
statistics: boolean;
|
|
30
29
|
};
|
|
31
|
-
extensions: readonly unknown[];
|
|
32
30
|
brand: string;
|
|
33
31
|
title: string;
|
|
34
32
|
description: string;
|
package/dist/kernel/site.js
CHANGED
|
@@ -23,10 +23,10 @@ export function createSite(db, d, caller, defaults, policies) {
|
|
|
23
23
|
return read();
|
|
24
24
|
}, media(storage) { admin(); return mediaLibrary(db, d, storage, policies); } };
|
|
25
25
|
}
|
|
26
|
-
/** 0.2 /admin/api/site projection; the app supplies its selected capabilities
|
|
26
|
+
/** 0.2 /admin/api/site projection; the app supplies its selected capabilities. */
|
|
27
27
|
export function siteInfo(config, options) {
|
|
28
28
|
const { origin, ...stored } = config;
|
|
29
29
|
const publicUrl = URL.canParse(origin) ? origin : options.origin;
|
|
30
30
|
const at = (path) => path ? new URL(path, publicUrl).href : null;
|
|
31
|
-
return { ...stored, publicUrl, mcpEndpoints: { public: at(options.mcpEndpoints?.public), admin: at(options.mcpEndpoints?.admin) }, capabilities: { siteSettings: options.siteSettings ?? true, media: !!options.media, invitationEmail: !!options.invitationEmail, statistics: false }
|
|
31
|
+
return { ...stored, publicUrl, mcpEndpoints: { public: at(options.mcpEndpoints?.public), admin: at(options.mcpEndpoints?.admin) }, capabilities: { siteSettings: options.siteSettings ?? true, media: !!options.media, invitationEmail: !!options.invitationEmail, statistics: false } };
|
|
32
32
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** /llms.txt and /sitemap.xml bodies; loaded only when an index is requested, so page Workers do not pay for it at startup. */
|
|
2
|
+
const escape = (v) => String(v).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
3
|
+
const label = (v) => String(v).replace(/[\r\n]/g, " ").replace(/[\\[\]]/g, "\\$&");
|
|
4
|
+
const size = (s) => new TextEncoder().encode(s).length;
|
|
5
|
+
export async function webIndex(sitemap, options, pages, list, validUrl) {
|
|
6
|
+
const cap = sitemap ? 50000 : 5000, limit = sitemap ? 50 * 1024 * 1024 : 1024 * 1024;
|
|
7
|
+
const start = sitemap ? '<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' : "# " + label(options.title) + "\n\n> " + label(options.description) + "\n\n## Pages\n\n", end = sitemap ? "</urlset>" : "\n";
|
|
8
|
+
const links = [];
|
|
9
|
+
let visited = 0, requests = 0, bytes = size(start + end);
|
|
10
|
+
if (bytes > limit)
|
|
11
|
+
throw new TypeError("Web index header exceeds its byte limit.");
|
|
12
|
+
pagesLoop: for (const entry of pages) {
|
|
13
|
+
if (!entry.page.list)
|
|
14
|
+
continue;
|
|
15
|
+
let cursor;
|
|
16
|
+
do {
|
|
17
|
+
if (requests >= (sitemap ? 100 : 10))
|
|
18
|
+
break pagesLoop;
|
|
19
|
+
requests++;
|
|
20
|
+
const page = await list(entry.page.list, cursor);
|
|
21
|
+
for (const row of page.rows) {
|
|
22
|
+
if (visited++ >= cap)
|
|
23
|
+
break pagesLoop;
|
|
24
|
+
const url = validUrl(entry.path, row, entry.page.view);
|
|
25
|
+
if (!url)
|
|
26
|
+
continue;
|
|
27
|
+
const line = sitemap ? "<url><loc>" + escape(url) + "</loc></url>" : "- [" + label(row.title ?? url) + "](" + url + "): " + label(row.description ?? "") + "\n";
|
|
28
|
+
const length = size(line);
|
|
29
|
+
if (bytes + length > limit)
|
|
30
|
+
break pagesLoop;
|
|
31
|
+
bytes += length;
|
|
32
|
+
links.push(line);
|
|
33
|
+
}
|
|
34
|
+
if (visited >= cap)
|
|
35
|
+
break pagesLoop;
|
|
36
|
+
cursor = page.nextCursor;
|
|
37
|
+
} while (cursor);
|
|
38
|
+
}
|
|
39
|
+
return start + links.join("") + end;
|
|
40
|
+
}
|
package/dist/kernel/web.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { type Caller, type Gateway, type RequestContext } from "./contracts.js";
|
|
3
3
|
import type { UserFields } from "./user-fields.js";
|
|
4
4
|
import { type Conversions } from "./surface.js";
|
|
5
|
-
/** An app's public Views: row type and input keys per View, and the Views a list may name (no
|
|
5
|
+
/** An app's public Views: row type and input keys per View, and the Views a list may name (input that accepts no fields). */
|
|
6
6
|
export interface App {
|
|
7
7
|
Rows: Record<string, any>;
|
|
8
8
|
Inputs: Record<string, string>;
|
|
@@ -33,6 +33,8 @@ export type Options<A extends App, Database, P extends Record<string, Page<A>>,
|
|
|
33
33
|
/** One public View: its query-string reader, paged boundary, bind conversions and boolean input keys. */
|
|
34
34
|
export interface View<Database> {
|
|
35
35
|
readonly check?: (caller: Caller) => void;
|
|
36
|
+
/** The input rejects an empty query, so a list cannot name this View. */
|
|
37
|
+
readonly unlisted?: true;
|
|
36
38
|
readonly query: (params: URLSearchParams) => {
|
|
37
39
|
input: Record<string, unknown>;
|
|
38
40
|
};
|
package/dist/kernel/web.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/** Web output for a generated public View table: /llms.txt, /sitemap.xml, Markdown pages and head tags. SQL owns visibility. */
|
|
2
2
|
import { forwarded, wire, ContractError } from "./contracts.js";
|
|
3
3
|
import { convert, decode } from "./surface.js";
|
|
4
|
-
const escape = (v) => String(v).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
5
|
-
const label = (v) => String(v).replace(/[\r\n]/g, " ").replace(/[\\[\]]/g, "\\$&");
|
|
6
|
-
const size = (s) => new TextEncoder().encode(s).length;
|
|
7
4
|
const NO_STORE = { "cache-control": "no-store" };
|
|
8
5
|
const notFound = () => new Response(null, { status: 404, headers: NO_STORE });
|
|
9
6
|
export function web(options, table) {
|
|
@@ -13,6 +10,9 @@ export function web(options, table) {
|
|
|
13
10
|
convert(input, view.convert);
|
|
14
11
|
return view.page(db, caller, input, { limit: 500, cursor });
|
|
15
12
|
};
|
|
13
|
+
for (const [path, page] of Object.entries(options.pages))
|
|
14
|
+
if (page.list && views.get(page.list)?.unlisted)
|
|
15
|
+
throw new TypeError(`Page ${path} lists View '${page.list}', whose input rejects an empty query; a list View must accept no input.`);
|
|
16
16
|
const origin = new URL(options.origin);
|
|
17
17
|
if (!/^https?:$/.test(origin.protocol) || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash)
|
|
18
18
|
throw new TypeError("origin must be an HTTP(S) origin.");
|
|
@@ -73,40 +73,9 @@ export function web(options, table) {
|
|
|
73
73
|
const ctx = await context(request, env, execution);
|
|
74
74
|
caller = ctx.caller;
|
|
75
75
|
if (index) {
|
|
76
|
-
const sitemap = path === "/sitemap.xml"
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
let visited = 0, requests = 0, bytes = size(start + end);
|
|
80
|
-
if (bytes > limit)
|
|
81
|
-
throw new TypeError("Web index header exceeds its byte limit.");
|
|
82
|
-
pagesLoop: for (const entry of pages) {
|
|
83
|
-
if (!entry.page.list)
|
|
84
|
-
continue;
|
|
85
|
-
let cursor;
|
|
86
|
-
do {
|
|
87
|
-
if (requests >= (sitemap ? 100 : 10))
|
|
88
|
-
break pagesLoop;
|
|
89
|
-
requests++;
|
|
90
|
-
const page = await query(entry.page.list, ctx.db, ctx.caller, new URLSearchParams(), cursor);
|
|
91
|
-
for (const row of page.rows) {
|
|
92
|
-
if (visited++ >= cap)
|
|
93
|
-
break pagesLoop;
|
|
94
|
-
const url = validUrl(entry.path, row, entry.page.view);
|
|
95
|
-
if (!url)
|
|
96
|
-
continue;
|
|
97
|
-
const line = sitemap ? "<url><loc>" + escape(url) + "</loc></url>" : "- [" + label(row.title ?? url) + "](" + url + "): " + label(row.description ?? "") + "\n";
|
|
98
|
-
const length = size(line);
|
|
99
|
-
if (bytes + length > limit)
|
|
100
|
-
break pagesLoop;
|
|
101
|
-
bytes += length;
|
|
102
|
-
links.push(line);
|
|
103
|
-
}
|
|
104
|
-
if (visited >= cap)
|
|
105
|
-
break pagesLoop;
|
|
106
|
-
cursor = page.nextCursor;
|
|
107
|
-
} while (cursor);
|
|
108
|
-
}
|
|
109
|
-
return response(request, start + links.join("") + end, sitemap ? "application/xml" : "text/plain", ctx.caller);
|
|
76
|
+
const sitemap = path === "/sitemap.xml";
|
|
77
|
+
const body = await (await import("./web-index.js")).webIndex(sitemap, options, pages, (list, cursor) => query(list, ctx.db, ctx.caller, new URLSearchParams(), cursor), validUrl);
|
|
78
|
+
return response(request, body, sitemap ? "application/xml" : "text/plain", ctx.caller);
|
|
110
79
|
}
|
|
111
80
|
const exact = match(path);
|
|
112
81
|
if (exact)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { userReferences } from "../domain/userReferences.js";
|
|
2
2
|
/** Selected application bindings: compiled checks/native SQL; only the app depends on Admin adapters. */
|
|
3
|
-
import {
|
|
3
|
+
import { isPrimaryCollection } from "../build/compiler.js";
|
|
4
4
|
import { enumOptions } from "../build/compiler.js";
|
|
5
5
|
import { databaseImport, isSqlite, literal as q } from "../domain/entry.js";
|
|
6
6
|
import { conversions, authLines } from "../domain/input.js";
|
|
@@ -83,8 +83,7 @@ export function emitAdminBindings(input, refuse) {
|
|
|
83
83
|
continue;
|
|
84
84
|
}
|
|
85
85
|
const defaults = Object.fromEntries(Object.entries(checks(s, true).properties ?? {}).flatMap(([key, p]) => p.default !== undefined ? [[key, p.default]] : []));
|
|
86
|
-
const
|
|
87
|
-
const primary = !s.translates && (!descriptor.parent || descriptor.nav?.standalone === true);
|
|
86
|
+
const primary = isPrimaryCollection(s, Object.values(plan.schemas));
|
|
88
87
|
const schemaCode = `{
|
|
89
88
|
name:${q(s.name)},primary:${primary},descriptor:allCollections[${q(s.name)}],publishing:${!!s.publishing},readOnly:${s.schema.readOnly === true || declared.some(f => s.fields[f] === "geo")},fields:names${n},defaults:${q(defaults)},
|
|
90
89
|
binding:${entrySpec},
|