@ggui-ai/cli 0.1.0-rc.3 → 0.2.0-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/README.md +2 -2
- package/dist/cli.js +83 -32
- package/dist/generation-probe.d.ts +28 -6
- package/dist/generation-probe.d.ts.map +1 -1
- package/dist/generation-probe.js +43 -26
- package/dist/mcp-backend.d.ts +38 -41
- package/dist/mcp-backend.d.ts.map +1 -1
- package/dist/mcp-backend.js +105 -135
- package/dist/persistent-secrets.js +8 -8
- package/dist/persistent-stores.d.ts +2 -2
- package/dist/persistent-stores.d.ts.map +1 -1
- package/dist/persistent-stores.js +8 -8
- package/dist/serve-command.d.ts +15 -15
- package/dist/serve-command.d.ts.map +1 -1
- package/dist/serve-command.js +19 -19
- package/dist/serve-fallback.d.ts +1 -1
- package/package.json +17 -17
package/dist/mcp-backend.js
CHANGED
|
@@ -19,62 +19,61 @@
|
|
|
19
19
|
* - `console: { sessionCookie: true }` — landing page at `/` +
|
|
20
20
|
* `/s/<shortCode>` viewer + same-origin HTTP-only cookie flow.
|
|
21
21
|
* - `shortCodeIndex: new InMemoryShortCodeIndex()` — required by
|
|
22
|
-
* the `sessionCookie` flow so `POST /ggui/console/
|
|
23
|
-
* can resolve a posted shortCode to `{
|
|
24
|
-
* In-memory is correct for the OSS first-run:
|
|
22
|
+
* the `sessionCookie` flow so `POST /ggui/console/render-cookie`
|
|
23
|
+
* can resolve a posted shortCode to the bound `{renderId, appId}`.
|
|
24
|
+
* In-memory is correct for the OSS first-run: render state lives
|
|
25
25
|
* in memory unless the operator opts into sqlite, so a matching
|
|
26
26
|
* index lifetime is what operators expect.
|
|
27
|
-
* - `mcpApps: {
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* lock in the OSS-split plan.
|
|
27
|
+
* - `mcpApps: { wsUrl }` — registers the `ggui_render` tool with
|
|
28
|
+
* MCP Apps `_meta.ui.*` declaration, serves `ui://ggui/render`,
|
|
29
|
+
* and advertises the `io.modelcontextprotocol/ui` capability.
|
|
30
|
+
* `wsUrl` is the live-channel URL the spawned iframe subscribes
|
|
31
|
+
* to; resolved host + port are mandatory so it points back at
|
|
32
|
+
* this same process. See §2.4.1 "sole entry-point tool" lock
|
|
33
|
+
* in the OSS-split plan.
|
|
35
34
|
*
|
|
36
35
|
* Embedding hosts that want a different shape (no landing page, no
|
|
37
36
|
* pairing, programmatic control) compose `createGguiServer()` directly
|
|
38
37
|
* rather than going through `ggui serve`.
|
|
39
38
|
*/
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
39
|
+
import { listThemes } from "@ggui-ai/design/themes";
|
|
40
|
+
import { compileUiOnDemand } from "@ggui-ai/dev-stack";
|
|
41
|
+
import { createLocalEmbeddingProvider } from "@ggui-ai/embedding-local";
|
|
42
|
+
import { RUNTIME_BUNDLE_URL_PATH } from "@ggui-ai/iframe-runtime/server";
|
|
43
|
+
import { composeWiredActionRouterFromMounts, createGguiServer, DEFAULT_BUILDER_APP_ID, FileSystemCodeStore, FixedWindowRateLimiter, InMemoryAuthAdapter, InMemoryQuotaStore, InMemoryShortCodeIndex, selectEmailSenderFromEnv, } from "@ggui-ai/mcp-server";
|
|
44
|
+
import { InMemoryAppMetadataStore, InMemoryVectorStore } from "@ggui-ai/mcp-server-core/in-memory";
|
|
45
|
+
import { PlaintextFileProviderKeyStore } from "@ggui-ai/mcp-server-core/plaintext";
|
|
46
|
+
import { createInstalledBlueprintsProvider, } from "@ggui-ai/mcp-server-handlers/renders";
|
|
47
|
+
import { createDeterministicPreviewEmitter } from "@ggui-ai/preview-a2ui/emitters";
|
|
48
|
+
import { randomUUID } from "node:crypto";
|
|
49
|
+
import { existsSync } from "node:fs";
|
|
50
|
+
import { createServer as createNetServer } from "node:net";
|
|
51
|
+
import { join } from "node:path";
|
|
52
|
+
import { getCodeCacheDir, getCredentialsFile, getEmbeddingCacheDir } from "./paths.js";
|
|
53
|
+
import { readOrMintHexSecret } from "./persistent-secrets.js";
|
|
55
54
|
/**
|
|
56
55
|
* Pick a free localhost TCP port by letting the OS assign one on a
|
|
57
56
|
* throwaway listener. Close the listener immediately and return the
|
|
58
57
|
* chosen number. Exported so the CLI (`cli.ts::runServeCommand`) and
|
|
59
58
|
* the integration tests can resolve `--port 0` to a concrete port
|
|
60
59
|
* BEFORE constructing the backend — {@link buildMcpServerBackend}
|
|
61
|
-
* captures the port in the composed `mcpApps.
|
|
60
|
+
* captures the port in the composed `mcpApps.wsUrl` / `runtime.url`
|
|
62
61
|
* at construction time, so the caller must know it up-front.
|
|
63
62
|
*
|
|
64
63
|
* Tiny race window: another process could claim the port between our
|
|
65
64
|
* `close()` and the caller's `listen()`. The CLI surfaces bind errors
|
|
66
65
|
* as a fatal exit; the OSS first-run path recovers by re-running.
|
|
67
66
|
*/
|
|
68
|
-
export function pickFreePort(host =
|
|
67
|
+
export function pickFreePort(host = "127.0.0.1") {
|
|
69
68
|
return new Promise((resolve, reject) => {
|
|
70
69
|
const probe = createNetServer();
|
|
71
|
-
probe.once(
|
|
70
|
+
probe.once("error", (err) => {
|
|
72
71
|
probe.close();
|
|
73
72
|
reject(err);
|
|
74
73
|
});
|
|
75
74
|
probe.listen(0, host, () => {
|
|
76
75
|
const addr = probe.address();
|
|
77
|
-
if (addr === null || typeof addr ===
|
|
76
|
+
if (addr === null || typeof addr === "string") {
|
|
78
77
|
probe.close();
|
|
79
78
|
reject(new Error(`pickFreePort: could not resolve a free port (got ${String(addr)})`));
|
|
80
79
|
return;
|
|
@@ -96,15 +95,15 @@ export function buildMcpServerBackend(opts) {
|
|
|
96
95
|
if (!Number.isInteger(opts.port) || opts.port <= 0 || opts.port > 65535) {
|
|
97
96
|
throw new Error(`buildMcpServerBackend: \`port\` must be a concrete TCP port (1..65535), got ${opts.port}. Resolve \`--port 0\` via pickFreePort() before composing.`);
|
|
98
97
|
}
|
|
99
|
-
// `baseUrl` feeds `
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
98
|
+
// `baseUrl` feeds `runtime.url`; `wsBaseUrl` feeds `mcpApps.wsUrl`.
|
|
99
|
+
// With `--public-base-url`, both derive from the operator-provided
|
|
100
|
+
// tunnel URL so the URLs the iframe receives resolve from the remote
|
|
101
|
+
// host's perspective. Without it, both fall back to the local bind.
|
|
102
|
+
// The shape was validated at flag-parse time (`http://`/`https://`,
|
|
103
|
+
// no trailing slash); we just substitute the scheme for ws/wss to
|
|
104
|
+
// derive the live-channel URL.
|
|
106
105
|
const baseUrl = opts.publicBaseUrl ?? `http://${opts.host}:${opts.port}`;
|
|
107
|
-
const wsBaseUrl = baseUrl.replace(/^http(s?):\/\//,
|
|
106
|
+
const wsBaseUrl = baseUrl.replace(/^http(s?):\/\//, "ws$1://");
|
|
108
107
|
// Router over the mount handler bundle. Computed once; absent
|
|
109
108
|
// (`null`) when no mounts are declared so the server falls through
|
|
110
109
|
// to agent-routed behavior.
|
|
@@ -136,7 +135,7 @@ export function buildMcpServerBackend(opts) {
|
|
|
136
135
|
});
|
|
137
136
|
// Per-IP rate limiter — only bound under `--public-demo` so an
|
|
138
137
|
// anonymous visitor can't burn the operator's BYOK budget by pumping
|
|
139
|
-
//
|
|
138
|
+
// ggui_render. Defaults: 30 generations / 10 minutes per IP. Operators
|
|
140
139
|
// who need different ceilings construct their own backend
|
|
141
140
|
// programmatically (calling `createGguiServer({ rateLimiter })`
|
|
142
141
|
// directly). Skipped under `--dev-allow-all` because that's the
|
|
@@ -186,8 +185,8 @@ export function buildMcpServerBackend(opts) {
|
|
|
186
185
|
const emailSenderSelection = selectEmailSenderFromEnv();
|
|
187
186
|
let emailSender;
|
|
188
187
|
let emailFromAddress;
|
|
189
|
-
const defaultFromAddress =
|
|
190
|
-
if (emailSenderSelection.kind ===
|
|
188
|
+
const defaultFromAddress = "ggui-serve <noreply@localhost>";
|
|
189
|
+
if (emailSenderSelection.kind === "ok") {
|
|
191
190
|
emailSender = emailSenderSelection.sender;
|
|
192
191
|
emailFromAddress = emailSenderSelection.fromAddress ?? defaultFromAddress;
|
|
193
192
|
}
|
|
@@ -195,26 +194,26 @@ export function buildMcpServerBackend(opts) {
|
|
|
195
194
|
// eslint-disable-next-line no-console -- one-shot boot warning, mirrors banner output
|
|
196
195
|
console.warn(`[ggui-cli] email-login: ${emailSenderSelection.reason}; falling back to ConsoleEmailSender (magic links print to terminal).`);
|
|
197
196
|
const fallback = selectEmailSenderFromEnv({
|
|
198
|
-
env: { GGUI_EMAIL_SENDER:
|
|
197
|
+
env: { GGUI_EMAIL_SENDER: "console" },
|
|
199
198
|
});
|
|
200
|
-
if (fallback.kind !==
|
|
199
|
+
if (fallback.kind !== "ok") {
|
|
201
200
|
throw new Error(`[ggui-cli] email-login: console fallback selection failed: ${fallback.reason}`);
|
|
202
201
|
}
|
|
203
202
|
emailSender = fallback.sender;
|
|
204
203
|
emailFromAddress = process.env.GGUI_EMAIL_FROM?.trim() || defaultFromAddress;
|
|
205
204
|
}
|
|
206
205
|
// Resolve cross-restart HMAC secrets. When the caller declared a
|
|
207
|
-
// persistent dir, read-or-mint `
|
|
206
|
+
// persistent dir, read-or-mint `ws-token-secret.hex` +
|
|
208
207
|
// `render-signer-secret.hex` (32 bytes / 64 hex chars, 0600). Both
|
|
209
208
|
// get threaded into `createGguiServer` below so the server stops
|
|
210
209
|
// minting fresh process-local secrets every boot — the precondition
|
|
211
|
-
// for any cached `_meta.ggui.
|
|
212
|
-
//
|
|
213
|
-
let
|
|
210
|
+
// for any cached `_meta["ai.ggui/render"].wsToken` surviving a
|
|
211
|
+
// restart. Absent dir = legacy ephemeral behavior.
|
|
212
|
+
let persistedWsTokenSecret;
|
|
214
213
|
let persistedRenderSignerSecret;
|
|
215
214
|
if (opts.persistentDir !== undefined) {
|
|
216
|
-
|
|
217
|
-
persistedRenderSignerSecret = readOrMintHexSecret(join(opts.persistentDir,
|
|
215
|
+
persistedWsTokenSecret = readOrMintHexSecret(join(opts.persistentDir, "ws-token-secret.hex"));
|
|
216
|
+
persistedRenderSignerSecret = readOrMintHexSecret(join(opts.persistentDir, "render-signer-secret.hex"));
|
|
218
217
|
}
|
|
219
218
|
// Resolve mcpInstructions: CLI flag wins over env var. Validated
|
|
220
219
|
// env-var values pass through to createGguiServer; invalid values
|
|
@@ -222,11 +221,11 @@ export function buildMcpServerBackend(opts) {
|
|
|
222
221
|
const envInstructions = process.env.GGUI_MCP_INSTRUCTIONS?.trim();
|
|
223
222
|
let resolvedMcpInstructions = opts.mcpInstructions;
|
|
224
223
|
if (resolvedMcpInstructions === undefined && envInstructions) {
|
|
225
|
-
if (envInstructions ===
|
|
226
|
-
envInstructions ===
|
|
227
|
-
envInstructions ===
|
|
228
|
-
envInstructions ===
|
|
229
|
-
envInstructions ===
|
|
224
|
+
if (envInstructions === "default" ||
|
|
225
|
+
envInstructions === "aggressive" ||
|
|
226
|
+
envInstructions === "always" ||
|
|
227
|
+
envInstructions === "minimal" ||
|
|
228
|
+
envInstructions === "off") {
|
|
230
229
|
resolvedMcpInstructions = envInstructions;
|
|
231
230
|
}
|
|
232
231
|
else {
|
|
@@ -242,11 +241,9 @@ export function buildMcpServerBackend(opts) {
|
|
|
242
241
|
// `listThemes()` either way; the file-mode override only affects the
|
|
243
242
|
// server's bound `theme` (rendered CSS variables), not the
|
|
244
243
|
// session/handler-side `themeId` chain.
|
|
245
|
-
const manifestThemePreset = opts.theme?.source ===
|
|
244
|
+
const manifestThemePreset = opts.theme?.source === "preset" ? opts.theme.preset : undefined;
|
|
246
245
|
const appMetadataStore = new InMemoryAppMetadataStore({
|
|
247
|
-
...(manifestThemePreset !== undefined
|
|
248
|
-
? { defaultThemeId: manifestThemePreset }
|
|
249
|
-
: {}),
|
|
246
|
+
...(manifestThemePreset !== undefined ? { defaultThemeId: manifestThemePreset } : {}),
|
|
250
247
|
// `ggui.json#app.gadgets` declarations (Leaflet, Mapbox, …). When
|
|
251
248
|
// omitted, the InMemoryAppMetadataStore falls back to
|
|
252
249
|
// STDLIB_GADGETS from @ggui-ai/protocol so the first-party hooks
|
|
@@ -262,11 +259,10 @@ export function buildMcpServerBackend(opts) {
|
|
|
262
259
|
...(opts.publicEnv !== undefined && Object.keys(opts.publicEnv).length > 0
|
|
263
260
|
? { defaultPublicEnv: opts.publicEnv }
|
|
264
261
|
: {}),
|
|
265
|
-
// `ggui.json#app.
|
|
266
|
-
// `
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
? { defaultMcpAppsMode: opts.defaultMcpAppsMode }
|
|
262
|
+
// `ggui.json#app.defaultDisplayMode`. Stamped per-push as
|
|
263
|
+
// `_meta.ui.displayMode`; controls only host-side presentation.
|
|
264
|
+
...(opts.defaultDisplayMode !== undefined
|
|
265
|
+
? { defaultDisplayMode: opts.defaultDisplayMode }
|
|
270
266
|
: {}),
|
|
271
267
|
});
|
|
272
268
|
// Materialize the vectorStore here so the marketplace-install
|
|
@@ -292,9 +288,7 @@ export function buildMcpServerBackend(opts) {
|
|
|
292
288
|
installedBlueprints: () => entries.flatMap((entry) => {
|
|
293
289
|
if (entry.manifest.contract === undefined)
|
|
294
290
|
return [];
|
|
295
|
-
const intent = entry.manifest.description ??
|
|
296
|
-
entry.manifest.name ??
|
|
297
|
-
entry.id;
|
|
291
|
+
const intent = entry.manifest.description ?? entry.manifest.name ?? entry.id;
|
|
298
292
|
return [
|
|
299
293
|
{
|
|
300
294
|
id: entry.id,
|
|
@@ -307,21 +301,21 @@ export function buildMcpServerBackend(opts) {
|
|
|
307
301
|
compile: async (entry) => {
|
|
308
302
|
const ui = byId.get(entry.id);
|
|
309
303
|
if (!ui) {
|
|
310
|
-
return { kind:
|
|
304
|
+
return { kind: "missing-entry", tried: [] };
|
|
311
305
|
}
|
|
312
306
|
const result = await compileUiOnDemand({
|
|
313
307
|
projectRoot,
|
|
314
308
|
manifestPath: ui.manifestPath,
|
|
315
309
|
manifest: ui.manifest,
|
|
316
310
|
});
|
|
317
|
-
if (result.kind ===
|
|
318
|
-
return { kind:
|
|
311
|
+
if (result.kind === "ok") {
|
|
312
|
+
return { kind: "ok", code: result.code };
|
|
319
313
|
}
|
|
320
|
-
if (result.kind ===
|
|
321
|
-
return { kind:
|
|
314
|
+
if (result.kind === "missing-entry") {
|
|
315
|
+
return { kind: "missing-entry", tried: result.tried };
|
|
322
316
|
}
|
|
323
317
|
return {
|
|
324
|
-
kind:
|
|
318
|
+
kind: "failure",
|
|
325
319
|
errors: result.errors.map((m) => m.text),
|
|
326
320
|
};
|
|
327
321
|
},
|
|
@@ -330,8 +324,7 @@ export function buildMcpServerBackend(opts) {
|
|
|
330
324
|
process.stderr.write(`[ggui serve] installed-blueprint ${issue.id}: ${issue.kind}: ${issue.message}\n`);
|
|
331
325
|
},
|
|
332
326
|
};
|
|
333
|
-
installedBlueprintsProvider =
|
|
334
|
-
createInstalledBlueprintsProvider(providerOptions);
|
|
327
|
+
installedBlueprintsProvider = createInstalledBlueprintsProvider(providerOptions);
|
|
335
328
|
}
|
|
336
329
|
// Fold the bridge into the generation deps so the push handler +
|
|
337
330
|
// handshake negotiator both consume it via `MatchBlueprintDeps.
|
|
@@ -351,8 +344,8 @@ export function buildMcpServerBackend(opts) {
|
|
|
351
344
|
providerKeys,
|
|
352
345
|
// Seeded per-app metadata store. Threaded so the in-process
|
|
353
346
|
// singleton is shared across `ggui_list_gadgets`,
|
|
354
|
-
// `ggui_list_themes`,
|
|
355
|
-
// resolution)
|
|
347
|
+
// `ggui_list_themes`, and the handshake (gadgets + theme default
|
|
348
|
+
// resolution).
|
|
356
349
|
appMetadataStore,
|
|
357
350
|
// Theme catalog resolver. Reads from `@ggui-ai/design`'s registry
|
|
358
351
|
// each call so runtime additions surface without a restart.
|
|
@@ -365,29 +358,23 @@ export function buildMcpServerBackend(opts) {
|
|
|
365
358
|
// process. Render-signer.secret rides on the `renderSigning`
|
|
366
359
|
// discriminated union (the `false` shape disables the layer
|
|
367
360
|
// entirely; we always want it on here, so build the object form).
|
|
368
|
-
...(
|
|
369
|
-
? { bootstrapSecret: persistedBootstrapSecret }
|
|
370
|
-
: {}),
|
|
361
|
+
...(persistedWsTokenSecret !== undefined ? { wsTokenSecret: persistedWsTokenSecret } : {}),
|
|
371
362
|
...(persistedRenderSignerSecret !== undefined
|
|
372
363
|
? { renderSigning: { secret: persistedRenderSignerSecret } }
|
|
373
364
|
: {}),
|
|
374
|
-
...(resolvedMcpInstructions !== undefined
|
|
375
|
-
? { mcpInstructions: resolvedMcpInstructions }
|
|
376
|
-
: {}),
|
|
365
|
+
...(resolvedMcpInstructions !== undefined ? { mcpInstructions: resolvedMcpInstructions } : {}),
|
|
377
366
|
// `--multi-tenant` flips the `/ggui/console/llm-keys` gate from
|
|
378
367
|
// admin-token to auth-adapter. Default scope derivation in the
|
|
379
368
|
// server picks up `userId` / `appId` from the resolved identity;
|
|
380
369
|
// operators with composite scopes pass their own `providerKeyScope`
|
|
381
370
|
// by composing `createGguiServer` directly.
|
|
382
|
-
...(opts.multiTenant
|
|
383
|
-
? { providerKeysGate: 'auth-adapter' }
|
|
384
|
-
: {}),
|
|
371
|
+
...(opts.multiTenant ? { providerKeysGate: "auth-adapter" } : {}),
|
|
385
372
|
// Gate OAuth login routes on publicBaseUrl. Without it the
|
|
386
373
|
// redirect_uri can't be composed; the admin transport still mounts
|
|
387
374
|
// so operators can paste credentials in advance.
|
|
388
375
|
...(opts.publicBaseUrl ? { publicBaseUrl: opts.publicBaseUrl } : {}),
|
|
389
376
|
...(rateLimiter ? { rateLimiter } : {}),
|
|
390
|
-
...(storage.
|
|
377
|
+
...(storage.renderStore ? { renderStore: storage.renderStore } : {}),
|
|
391
378
|
// Always pass the materialized vectorStore so the install bridge
|
|
392
379
|
// and the matcher share state. Passing only `storage.vectors`
|
|
393
380
|
// (set when the manifest declares sqlite) would, on an absent
|
|
@@ -406,16 +393,12 @@ export function buildMcpServerBackend(opts) {
|
|
|
406
393
|
? {
|
|
407
394
|
threads: {
|
|
408
395
|
store: storage.threadStore,
|
|
409
|
-
...(storage.threadDurability
|
|
410
|
-
? { durability: storage.threadDurability }
|
|
411
|
-
: {}),
|
|
396
|
+
...(storage.threadDurability ? { durability: storage.threadDurability } : {}),
|
|
412
397
|
},
|
|
413
398
|
}
|
|
414
399
|
: {}),
|
|
415
400
|
sessionChannel: true,
|
|
416
|
-
pairing: opts.keysFile
|
|
417
|
-
? { persistencePath: opts.keysFile }
|
|
418
|
-
: true,
|
|
401
|
+
pairing: opts.keysFile ? { persistencePath: opts.keysFile } : true,
|
|
419
402
|
// Email magic-link login. OSS first-run default mirrors
|
|
420
403
|
// `provisionalPreview` — wire a sender so magic links can be
|
|
421
404
|
// delivered. `selectEmailSenderFromEnv` reads
|
|
@@ -464,7 +447,7 @@ export function buildMcpServerBackend(opts) {
|
|
|
464
447
|
!!process.env.GOOGLE_API_KEY ||
|
|
465
448
|
!!process.env.OPENROUTER_API_KEY;
|
|
466
449
|
if (!credsFileExists && !anyEnvKey) {
|
|
467
|
-
return
|
|
450
|
+
return "/admin-login?next=%2Fadmin%2Fllm-keys";
|
|
468
451
|
}
|
|
469
452
|
return null;
|
|
470
453
|
}
|
|
@@ -479,10 +462,10 @@ export function buildMcpServerBackend(opts) {
|
|
|
479
462
|
// absent = no welcome page (SPA index handler still owns `/`).
|
|
480
463
|
...(opts.welcomePage ? { welcomePage: opts.welcomePage } : {}),
|
|
481
464
|
// Default to in-memory; caller substitutes a persistent impl
|
|
482
|
-
// (SqliteShortCodeIndex under `persistentDir`) so cached
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
465
|
+
// (SqliteShortCodeIndex under `persistentDir`) so cached short-code
|
|
466
|
+
// lookups survive a restart. Used by render-URL signing and
|
|
467
|
+
// legacy compat paths; post-R5 the primary mount path is the
|
|
468
|
+
// ui://ggui/render resource (auth-bearer-gated), not shortCodes.
|
|
486
469
|
shortCodeIndex: opts.shortCodeIndex ?? new InMemoryShortCodeIndex(),
|
|
487
470
|
// Content-addressable componentCode delivery. The OSS dev default
|
|
488
471
|
// is filesystem-backed at `~/.ggui/code-cache/` (override via
|
|
@@ -495,9 +478,7 @@ export function buildMcpServerBackend(opts) {
|
|
|
495
478
|
// found UIs in `blueprints.include`; absent = the server's
|
|
496
479
|
// `ggui_list_featured_blueprints` returns an empty catalog (the
|
|
497
480
|
// zero-config default).
|
|
498
|
-
...(opts.blueprintProvider
|
|
499
|
-
? { blueprintProvider: opts.blueprintProvider }
|
|
500
|
-
: {}),
|
|
481
|
+
...(opts.blueprintProvider ? { blueprintProvider: opts.blueprintProvider } : {}),
|
|
501
482
|
// Manifest-backed UI registry — resolves blueprint ids to
|
|
502
483
|
// compiled bundles for `ggui_render_blueprint`. Paired with
|
|
503
484
|
// `blueprintProvider` above (metadata vs. bundle resolution on
|
|
@@ -506,9 +487,7 @@ export function buildMcpServerBackend(opts) {
|
|
|
506
487
|
// Manifest-declared primitive catalogs. Present when `ggui serve`
|
|
507
488
|
// resolved `primitives.{packages,local}`; absent = in-memory-only
|
|
508
489
|
// server with an empty `server.primitiveCatalogs`.
|
|
509
|
-
...(opts.primitiveCatalogs
|
|
510
|
-
? { primitiveCatalogs: opts.primitiveCatalogs }
|
|
511
|
-
: {}),
|
|
490
|
+
...(opts.primitiveCatalogs ? { primitiveCatalogs: opts.primitiveCatalogs } : {}),
|
|
512
491
|
// Manifest-declared theme. Present when `ggui serve` resolved
|
|
513
492
|
// `ggui.json#theme`; absent = `createGguiServer` falls back to its
|
|
514
493
|
// `lightTheme`-backed default internally.
|
|
@@ -524,25 +503,18 @@ export function buildMcpServerBackend(opts) {
|
|
|
524
503
|
// bootstrap envelope without a restart. Both threaded through
|
|
525
504
|
// when present; absent = boot-baked behaviour (legacy).
|
|
526
505
|
...(opts.themeProvider ? { themeProvider: opts.themeProvider } : {}),
|
|
527
|
-
...(opts.onThemeConfigChange
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
506
|
+
...(opts.onThemeConfigChange ? { onThemeConfigChange: opts.onThemeConfigChange } : {}),
|
|
507
|
+
// Register `ggui_render` + serve `ui://ggui/render`. The `wsUrl`
|
|
508
|
+
// is the live-channel URL published on `_meta["ai.ggui/render"]`
|
|
509
|
+
// so iframes opened by an MCP Apps host (Claude Desktop, Claude
|
|
510
|
+
// Code, claude.ai) can subscribe back to this same process.
|
|
511
|
+
// First-party hosts (Studio, Portal, console) consume
|
|
512
|
+
// `_meta.ui.resourceUri` instead and never hit the URL directly.
|
|
534
513
|
mcpApps: {
|
|
535
|
-
// The `/r/` route serves the public self-contained shell that
|
|
536
|
-
// any MCP Apps host (Claude Desktop, Claude Code, claude.ai)
|
|
537
|
-
// can render directly — `structuredContent.url` lands on it
|
|
538
|
-
// as a full HTML page. First-party hosts (Studio, Portal,
|
|
539
|
-
// console) consume `_meta.ui.resourceUri` and never hit the
|
|
540
|
-
// URL directly, so the same prefix works for everyone.
|
|
541
|
-
renderBaseUrl: `${baseUrl}/r/`,
|
|
542
514
|
wsUrl: `${wsBaseUrl}/ws`,
|
|
543
515
|
},
|
|
544
516
|
// Task #382 — `runtimeUrl` MUST be absolute. The thin-shell HTML
|
|
545
|
-
// served from `ui://ggui/
|
|
517
|
+
// served from `ui://ggui/render` is mounted via `srcdoc` in most
|
|
546
518
|
// consumers (`<McpAppIframe>` default path for inline text), which
|
|
547
519
|
// gives the iframe `about:srcdoc` as its URL — a relative path
|
|
548
520
|
// like `/_ggui/iframe-runtime.js` resolves against `about:` and the
|
|
@@ -550,16 +522,16 @@ export function buildMcpServerBackend(opts) {
|
|
|
550
522
|
// URL built from the CLI's own known `baseUrl` makes srcdoc mount
|
|
551
523
|
// work without operator action. The server still serves the bundle
|
|
552
524
|
// at `/_ggui/iframe-runtime.js` under `runtimePath`; this overrides
|
|
553
|
-
// only the URL published on `_meta.ggui.
|
|
525
|
+
// only the URL published on `_meta["ai.ggui/render"].runtimeUrl`.
|
|
554
526
|
runtime: { url: `${baseUrl}${RUNTIME_BUNDLE_URL_PATH}` },
|
|
555
527
|
// OSS first-run default: provisional A2UI preview is on. The
|
|
556
528
|
// deterministic emitter from `@ggui-ai/preview-a2ui/emitters`
|
|
557
|
-
// adapts each
|
|
529
|
+
// adapts each render's `story.intent` into a small A2UI surface
|
|
558
530
|
// (heading → shell → teardown) and streams it on
|
|
559
531
|
// `_ggui:preview`. Embedded-ui's `/s/<shortCode>` viewer mounts
|
|
560
532
|
// `<ProvisionalRenderer>` to paint the frames as they arrive,
|
|
561
533
|
// so operators see a live UI build-up the first time they
|
|
562
|
-
// invoke `
|
|
534
|
+
// invoke `ggui_render` — before any LLM generation lands on OSS.
|
|
563
535
|
//
|
|
564
536
|
// Hosted + programmatic hosts opt-in by composing
|
|
565
537
|
// `createGguiServer()` directly and supplying their own
|
|
@@ -570,8 +542,8 @@ export function buildMcpServerBackend(opts) {
|
|
|
570
542
|
emitter: createDeterministicPreviewEmitter(),
|
|
571
543
|
},
|
|
572
544
|
// Generation wiring. Threaded through to
|
|
573
|
-
// `defaultHandlers.
|
|
574
|
-
// Absent = placeholder mode:
|
|
545
|
+
// `defaultHandlers.render.generation` → `createGguiRenderHandler`.
|
|
546
|
+
// Absent = placeholder mode: render mints renderIds + shortCodes
|
|
575
547
|
// + preview but does NOT produce componentCode. The CLI's
|
|
576
548
|
// `runServeCommand` probes BYOK (env → credentials file) at
|
|
577
549
|
// boot and only supplies this opt on a hit. See
|
|
@@ -581,9 +553,7 @@ export function buildMcpServerBackend(opts) {
|
|
|
581
553
|
// `/mcp` alongside ggui-native tools. No CLI config loader yet —
|
|
582
554
|
// programmatic hosts + integration tests compose mounts through
|
|
583
555
|
// this opt directly.
|
|
584
|
-
...(opts.mcpMounts && opts.mcpMounts.length > 0
|
|
585
|
-
? { mcpMounts: opts.mcpMounts }
|
|
586
|
-
: {}),
|
|
556
|
+
...(opts.mcpMounts && opts.mcpMounts.length > 0 ? { mcpMounts: opts.mcpMounts } : {}),
|
|
587
557
|
// wiredActionRouter composed from the same mount handlers. With
|
|
588
558
|
// mounts present, every mounted tool becomes wire-dispatchable
|
|
589
559
|
// from a generated UI's `actionSpec[name].dispatch.tool` (when
|
|
@@ -603,11 +573,11 @@ export function buildMcpServerBackend(opts) {
|
|
|
603
573
|
// 'off'`. Unrecognized values fall through to the server's
|
|
604
574
|
// default. Keeping the parse narrow so a typo yields default
|
|
605
575
|
// behavior (reject) rather than silent relaxation.
|
|
606
|
-
...(process.env[
|
|
607
|
-
process.env[
|
|
608
|
-
process.env[
|
|
576
|
+
...(process.env["GGUI_SCHEMA_COMPAT_MODE"] === "warn" ||
|
|
577
|
+
process.env["GGUI_SCHEMA_COMPAT_MODE"] === "off" ||
|
|
578
|
+
process.env["GGUI_SCHEMA_COMPAT_MODE"] === "reject"
|
|
609
579
|
? {
|
|
610
|
-
schemaCompatCheck: process.env[
|
|
580
|
+
schemaCompatCheck: process.env["GGUI_SCHEMA_COMPAT_MODE"],
|
|
611
581
|
}
|
|
612
582
|
: {}),
|
|
613
583
|
// Wired-tool per-call timeout override (default 30 s in
|
|
@@ -624,8 +594,8 @@ export function buildMcpServerBackend(opts) {
|
|
|
624
594
|
// Operators that need a different production timeout SHOULD compose
|
|
625
595
|
// `createGguiServer` directly rather than rely on env coupling.
|
|
626
596
|
...(() => {
|
|
627
|
-
const raw = process.env[
|
|
628
|
-
if (raw === undefined || raw ===
|
|
597
|
+
const raw = process.env["GGUI_WIRED_TIMEOUT_MS"];
|
|
598
|
+
if (raw === undefined || raw === "")
|
|
629
599
|
return {};
|
|
630
600
|
const parsed = Number(raw);
|
|
631
601
|
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
@@ -637,14 +607,14 @@ export function buildMcpServerBackend(opts) {
|
|
|
637
607
|
listen: async (port, host) => {
|
|
638
608
|
const httpServer = await server.listen(port, host);
|
|
639
609
|
const addr = httpServer.address();
|
|
640
|
-
if (!addr || typeof addr ===
|
|
641
|
-
throw new Error(
|
|
610
|
+
if (!addr || typeof addr === "string") {
|
|
611
|
+
throw new Error("server.address() returned an unexpected shape");
|
|
642
612
|
}
|
|
643
613
|
return addr.port;
|
|
644
614
|
},
|
|
645
615
|
close: () => server.close(),
|
|
646
616
|
toolCount: server.toolCount,
|
|
647
|
-
serverName:
|
|
617
|
+
serverName: "ggui-mcp-server",
|
|
648
618
|
serverVersion: opts.cliVersion,
|
|
649
619
|
primitiveCatalogCount: server.primitiveCatalogs.length,
|
|
650
620
|
themeSource: server.theme.source,
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* restart.
|
|
4
4
|
*
|
|
5
5
|
* Background: `createGguiServer` mints two HMAC secrets at boot when the
|
|
6
|
-
* caller doesn't pass them — `
|
|
7
|
-
* ggui.
|
|
8
|
-
* and the render-URL signer secret
|
|
9
|
-
*
|
|
6
|
+
* caller doesn't pass them — `wsTokenSecret` (signs the
|
|
7
|
+
* `_meta["ai.ggui/render"].wsToken` envelope so iframes can subscribe
|
|
8
|
+
* to the live-channel WS) and the render-URL signer secret. Both are
|
|
9
|
+
* process-local `randomBytes(32)` by default — fine for a single
|
|
10
10
|
* process lifetime, fatal for "revisit a claude.ai chat history after a
|
|
11
|
-
* restart": the cached
|
|
12
|
-
*
|
|
11
|
+
* restart": the cached wsToken fails HMAC verify against the freshly-
|
|
12
|
+
* minted secret.
|
|
13
13
|
*
|
|
14
14
|
* Persisting the secrets to a 0600-mode file makes those tokens valid
|
|
15
15
|
* across restarts, which is the precondition for any rehydrate path
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
* lowercase hex (32 bytes), no trailing newline, no JSON wrapper. Easy
|
|
18
18
|
* to inspect, easy to rotate (delete the file, restart, fresh secret).
|
|
19
19
|
*
|
|
20
|
-
* Security posture: these secrets are a
|
|
20
|
+
* Security posture: these secrets are a render-token signing key, NOT
|
|
21
21
|
* a long-lived credential like an OAuth client secret. Losing them
|
|
22
22
|
* means an attacker can mint MCP Apps bootstrap tokens for arbitrary
|
|
23
|
-
* `(
|
|
23
|
+
* `(renderId, appId)` pairs — same blast radius as before, just
|
|
24
24
|
* surviving restart. Mode 0600 on the file matches `~/.ggui/credentials
|
|
25
25
|
* .json` (BYOK keys) which has the same threat model.
|
|
26
26
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function
|
|
1
|
+
import type { RenderStore, VectorStore } from '@ggui-ai/mcp-server-core';
|
|
2
|
+
export declare function createPersistentRenderStore(persistentDir: string): Promise<RenderStore>;
|
|
3
3
|
export declare function createPersistentVectorStore(persistentDir: string): Promise<VectorStore>;
|
|
4
4
|
/**
|
|
5
5
|
* Compute the auto-default `keysFile` path. Returns `undefined` when
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistent-stores.d.ts","sourceRoot":"","sources":["../src/persistent-stores.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"persistent-stores.d.ts","sourceRoot":"","sources":["../src/persistent-stores.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAElC,wBAAsB,2BAA2B,CAC/C,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,CAAC,CAQtB;AAED,wBAAsB,2BAA2B,CAC/C,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,CAAC,CAQtB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE7D"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Default-on persistent stores under `<persistentDir>/`. Used when the
|
|
3
|
-
* operator hasn't declared `storage.
|
|
4
|
-
* `ggui.json` AND hasn't passed `--ephemeral`. Closes the
|
|
3
|
+
* operator hasn't declared `storage.renders` / `storage.vectors` in
|
|
4
|
+
* `ggui.json` AND hasn't passed `--ephemeral`. Closes the RenderStore
|
|
5
5
|
* leg of the rehydrate problem: without this, a restart drops every
|
|
6
|
-
*
|
|
6
|
+
* render row even though the HMAC secret + ShortCodeIndex now persist.
|
|
7
7
|
*
|
|
8
8
|
* Override precedence (caller enforces):
|
|
9
|
-
* 1. Explicit `ggui.json#storage.
|
|
9
|
+
* 1. Explicit `ggui.json#storage.renders` / `.vectors` (user wins)
|
|
10
10
|
* 2. These defaults (operator-friendly)
|
|
11
11
|
* 3. In-memory (`createGguiServer`'s internal fallback) (last resort)
|
|
12
12
|
*
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { mkdirSync } from 'node:fs';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
|
-
export async function
|
|
19
|
+
export async function createPersistentRenderStore(persistentDir) {
|
|
20
20
|
mkdirSync(persistentDir, { recursive: true });
|
|
21
|
-
const {
|
|
22
|
-
return new
|
|
23
|
-
filename: join(persistentDir, '
|
|
21
|
+
const { SqliteRenderStore } = await import('@ggui-ai/mcp-server-core/sqlite');
|
|
22
|
+
return new SqliteRenderStore({
|
|
23
|
+
filename: join(persistentDir, 'renders.sqlite'),
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
export async function createPersistentVectorStore(persistentDir) {
|