@ggui-ai/mcp-server 0.2.0-alpha.3 → 0.2.0-alpha.4
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.
|
@@ -83,9 +83,9 @@ const DEFAULT_PRESET_BODY = [
|
|
|
83
83
|
' • `ggui_consume` and `ggui_update` operate on EXISTING renders identified by `renderId` and require NO handshake. The `renderId` was minted by an earlier `ggui_render` call; it identifies the live iframe the user is interacting with. Calling `ggui_handshake` before `ggui_consume`/`ggui_update` is a category error — it would mint a SEPARATE new render that the user can\'t see, orphaning whichever live iframe the gesture targeted.',
|
|
84
84
|
' • Each `renderId` is the agent\'s persistent handle to one specific live iframe. Reuse it across consume/update for as long as the user is interacting with that UI; only call `ggui_handshake` again when you need to spawn a genuinely new UI surface.',
|
|
85
85
|
'',
|
|
86
|
-
' 1. ggui_handshake — FIRST CALL for any UI. Negotiate a contract for the next render. Post {intent, blueprintDraft: {contract, variance?, generator?}}. `variance` is optional and
|
|
86
|
+
' 1. ggui_handshake — FIRST CALL for any UI. Negotiate a contract for the next render. Post {intent, blueprintDraft: {contract, variance?, generator?}}. `variance` is optional and steers cache lookup + gen by axis: `persona` ("minimalist", "playful"), `aesthetic` ("dense", "spacious"), `seedPrompt` (deterministic seed). HANDSHAKE IS FORGIVING — it ALWAYS returns a handshakeId + a CONFORMING contract, even when your draft is malformed: the server cache-matches an existing blueprint OR repairs your draft until it passes validation. The suggestion carries origin: cache (an existing blueprint matched — delivered instantly) | agent (your draft was already clean) | synth (your draft had problems and the server repaired it; `suggestion.validationFindings` lists exactly what was wrong so you learn for next time). CRITICAL: a synth result is NOT an error and NOT a request to retry — the returned contract is READY. Accept it and call ggui_render. Do NOT re-call ggui_handshake hoping for a different origin — that is a loop that wastes turns and changes nothing.',
|
|
87
87
|
'',
|
|
88
|
-
' 2. ggui_render — deliver the UI. handshakeId is REQUIRED (from step 1); calling render without it fails with `handshake_not_found`. Send {handshakeId, decision: {kind:"accept"}} to use the suggestion verbatim, OR {handshakeId, decision: {kind:"override", blueprintDraft:{...}}} to
|
|
88
|
+
' 2. ggui_render — deliver the UI. handshakeId is REQUIRED (from step 1); calling render without it fails with `handshake_not_found`. Send {handshakeId, decision: {kind:"accept"}} to use the handshake suggestion verbatim — THE NORMAL PATH, since the contract is already conforming — OR {handshakeId, decision: {kind:"override", blueprintDraft:{...}}} to commit to a FRESH draft of your own. OVERRIDE IS STRICT: a forced contract MUST already conform — the server does NOT repair it (override means "use mine exactly"). Prefer accept; override only when the suggestion genuinely is not what you want AND your replacement contract is correct. If the contract declares propsSpec, props is REQUIRED. Response includes the minted `renderId` — your handle for every follow-up call. Handshake records are SINGLE-USE and expire after 10 minutes. On error: `handshake_not_found` → call ggui_handshake again; `contract_violation` / `missing_props` (props don\'t match propsSpec) → fix props and retry with the SAME handshakeId; `override_contract_invalid` (your forced override draft failed validation) → drop the override, re-run ggui_handshake with that draft (which WILL repair it), and accept the suggestion — do NOT retry override with the same draft.',
|
|
89
89
|
'',
|
|
90
90
|
' 3. NEXT STEP — read the render response. If it carries a `nextStep` field, call that tool with the given args. Render only emits `nextStep` when the contract declared a non-empty actionSpec (i.e., the UI has interactive buttons/forms); in that case nextStep names ggui_consume and your job is to long-poll for the user\'s gesture. If the render response has NO nextStep, the UI is pure-display (props only, no actionSpec) — you can end your turn; the user reads the UI and types their next prompt when they\'re ready.',
|
|
91
91
|
'',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llm-backed-negotiator.d.ts","sourceRoot":"","sources":["../src/llm-backed-negotiator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,KAAK,EACV,iBAAiB,EAEjB,YAAY,EACZ,cAAc,EACd,uBAAuB,EACvB,wBAAwB,EACxB,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAGL,KAAK,mBAAmB,EAExB,KAAK,2BAA2B,EAEjC,MAAM,sCAAsC,CAAC;AAC9C,OAAO,
|
|
1
|
+
{"version":3,"file":"llm-backed-negotiator.d.ts","sourceRoot":"","sources":["../src/llm-backed-negotiator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,KAAK,EACV,iBAAiB,EAEjB,YAAY,EACZ,cAAc,EACd,uBAAuB,EACvB,wBAAwB,EACxB,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAGL,KAAK,mBAAmB,EAExB,KAAK,2BAA2B,EAEjC,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAA4B,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAE/E,OAAO,KAAK,EACV,SAAS,EAIV,MAAM,mBAAmB,CAAC;AAmB3B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,GAAG,SAAS,CAmD9F;AA6ED;;;GAGG;AACH,MAAM,WAAW,gCAAgC;IAC/C;;;;;;OAMG;IACH,UAAU,EAAE,CACV,GAAG,EAAE,cAAc,KAEjB;QAAE,SAAS,EAAE,YAAY,CAAC;QAAC,WAAW,EAAE,cAAc,CAAA;KAAE,GACxD,OAAO,CAAC;QAAE,SAAS,EAAE,YAAY,CAAC;QAAC,WAAW,EAAE,cAAc,CAAA;KAAE,GAAG,IAAI,CAAC,GACxE,IAAI,CAAC;IACT;;;OAGG;IACH,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,KAAK,CAAC,EAAE;QACN,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC;QACtC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;KACnC,CAAC;IACF;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,2BAA2B,CAAC;CACnD;AA6CD;;;;;;;;GAQG;AACH,wBAAgB,kCAAkC,CAChD,IAAI,EAAE,gCAAgC,GACrC,mBAAmB,CAiKrB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,2wDAoBgN,CAAC;AAqC7P;;;;;;;;GAQG;AACH,wBAAgB,gCAAgC,CAC9C,UAAU,EAAE,SAAS,SAAS,EAAE,EAChC,OAAO,EAAE,uBAAuB,GAC/B,MAAM,CA2CR;AA8CD;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,OAAO,GAAG,wBAAwB,CAyBpF"}
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
* infrastructure isn't bound.
|
|
52
52
|
*/
|
|
53
53
|
import { DEFAULT_GENERATOR_SLUG, matchBlueprint, } from "@ggui-ai/mcp-server-handlers/renders";
|
|
54
|
-
import {
|
|
54
|
+
import { ensureConformingContract } from "@ggui-ai/negotiator";
|
|
55
|
+
import { dataContractSchema, lintContract } from "@ggui-ai/protocol";
|
|
55
56
|
import { blueprintKey } from "@ggui-ai/protocol/blueprint-key";
|
|
56
57
|
import { selectAdapter } from "@ggui-ai/ui-gen/providers";
|
|
57
58
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -196,9 +197,22 @@ async function anthropicCallStructured(args) {
|
|
|
196
197
|
}
|
|
197
198
|
const DEFAULT_GEN_LATENCY_MS = 30_000;
|
|
198
199
|
function buildCreateFallback(draftContract, reason, _estimatedLatencyMs) {
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
200
|
+
// No-LLM fallback (no BYOK creds, or an operational error during synth).
|
|
201
|
+
// The handshake backstop (validateContract) THROWS on a malformed draft,
|
|
202
|
+
// and there is no LLM here to repair it — so deterministically
|
|
203
|
+
// substitute the trivially-conforming empty contract (+ loud findings)
|
|
204
|
+
// when the draft fails the gate. NEVER return a raw malformed draft into
|
|
205
|
+
// the throwing backstop (that would revive the handshake-retry loop).
|
|
206
|
+
// A clean draft is kept verbatim (origin: agent).
|
|
207
|
+
const lint = lintContract(draftContract);
|
|
208
|
+
const contract = lint.errors.length === 0 ? dataContractSchema.parse(draftContract) : {};
|
|
209
|
+
const findings = lint.errors.map((e) => ({
|
|
210
|
+
code: e.code,
|
|
211
|
+
severity: "error",
|
|
212
|
+
path: e.path,
|
|
213
|
+
message: e.message,
|
|
214
|
+
}));
|
|
215
|
+
const contractHash = blueprintKey(contract);
|
|
202
216
|
const suggestion = {
|
|
203
217
|
origin: "agent",
|
|
204
218
|
rationale: reason,
|
|
@@ -208,12 +222,13 @@ function buildCreateFallback(draftContract, reason, _estimatedLatencyMs) {
|
|
|
208
222
|
generator: "ui-gen-default-haiku-4-5",
|
|
209
223
|
variance: {},
|
|
210
224
|
},
|
|
225
|
+
...(findings.length > 0 ? { validationFindings: findings } : {}),
|
|
211
226
|
};
|
|
212
227
|
return {
|
|
213
228
|
action: "create",
|
|
214
229
|
reason,
|
|
215
230
|
suggestion,
|
|
216
|
-
effectiveContract:
|
|
231
|
+
effectiveContract: contract,
|
|
217
232
|
};
|
|
218
233
|
}
|
|
219
234
|
/**
|
|
@@ -242,7 +257,12 @@ export function createLlmBackedHandshakeNegotiator(deps) {
|
|
|
242
257
|
// — the negotiator stays useful when the registry is cold,
|
|
243
258
|
// when matchBlueprint hiccups, or when the deployment skipped
|
|
244
259
|
// the cache deps entirely.
|
|
245
|
-
|
|
260
|
+
// Cache match needs a SHAPE-VALID contract — a malformed draft
|
|
261
|
+
// (wrapper-nesting, bad type spelling, …) can't canonical-key-match
|
|
262
|
+
// a registered blueprint, so skip the probe and fall straight to
|
|
263
|
+
// validate/repair via ensureConformingContract.
|
|
264
|
+
const parsedDraft = dataContractSchema.safeParse(draftContract);
|
|
265
|
+
if (deps.cache && parsedDraft.success) {
|
|
246
266
|
try {
|
|
247
267
|
const matchDeps = {
|
|
248
268
|
registry: deps.cache,
|
|
@@ -250,7 +270,7 @@ export function createLlmBackedHandshakeNegotiator(deps) {
|
|
|
250
270
|
};
|
|
251
271
|
const matchResult = await matchBlueprint(matchDeps, ctx.appId, {
|
|
252
272
|
intent,
|
|
253
|
-
contract:
|
|
273
|
+
contract: parsedDraft.data,
|
|
254
274
|
});
|
|
255
275
|
if (matchResult.strategy === "exact-key") {
|
|
256
276
|
const matched = matchResult.blueprint;
|
|
@@ -297,85 +317,38 @@ export function createLlmBackedHandshakeNegotiator(deps) {
|
|
|
297
317
|
}
|
|
298
318
|
try {
|
|
299
319
|
const llm = buildLlmCaller(creds.selection, creds.providerKey);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
// its optional `readRenderState` callback, which the OSS
|
|
313
|
-
// path doesn't wire here, so a stable placeholder works.
|
|
314
|
-
renderId: "handshake",
|
|
315
|
-
includeSharedPool: false,
|
|
316
|
-
},
|
|
320
|
+
// Create path (no cache hit): the agent's DRAFT is the basis.
|
|
321
|
+
// ensureConformingContract validates it against the deterministic
|
|
322
|
+
// gate (lintContract / validateContract) and, on errors, repairs
|
|
323
|
+
// it in place via the bounded LLM loop — GUARANTEEING a contract
|
|
324
|
+
// that passes the handshake backstop, so the handshake never
|
|
325
|
+
// hard-fails on a malformed draft. origin: 'agent' = draft was
|
|
326
|
+
// already clean; 'synth' = repaired (or minimal-conforming
|
|
327
|
+
// fallback when unrepairable).
|
|
328
|
+
const conforming = await ensureConformingContract({ llm }, {
|
|
329
|
+
intent,
|
|
330
|
+
draft: draftContract,
|
|
331
|
+
...(gadgets !== undefined ? { appGadgets: gadgets } : {}),
|
|
317
332
|
});
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
blueprintMeta: {
|
|
333
|
-
blueprintId,
|
|
334
|
-
contractHash,
|
|
335
|
-
generator: "ui-gen-default-haiku-4-5",
|
|
336
|
-
variance: {},
|
|
337
|
-
},
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
else if (contractHash === draftHash) {
|
|
341
|
-
suggestion = {
|
|
342
|
-
origin: "agent",
|
|
343
|
-
rationale: decision.reasoning ?? "novel-but-clean contract",
|
|
344
|
-
blueprintMeta: {
|
|
345
|
-
blueprintId: `bp_${randomUUID()}`,
|
|
346
|
-
contractHash,
|
|
347
|
-
generator: "ui-gen-default-haiku-4-5",
|
|
348
|
-
variance: {},
|
|
349
|
-
},
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
suggestion = {
|
|
354
|
-
origin: "synth",
|
|
355
|
-
rationale: decision.reasoning ?? "synth amended contract",
|
|
356
|
-
blueprintMeta: {
|
|
357
|
-
blueprintId: `bp_${randomUUID()}`,
|
|
358
|
-
contractHash,
|
|
359
|
-
generator: "ui-gen-default-haiku-4-5",
|
|
360
|
-
variance: {},
|
|
361
|
-
},
|
|
362
|
-
amendments: {
|
|
363
|
-
contractDiff: [
|
|
364
|
-
{
|
|
365
|
-
op: "replace",
|
|
366
|
-
path: "",
|
|
367
|
-
value: contract,
|
|
368
|
-
},
|
|
369
|
-
],
|
|
370
|
-
reasoning: decision.reasoning ?? "negotiator-amended contract",
|
|
371
|
-
},
|
|
372
|
-
};
|
|
373
|
-
}
|
|
333
|
+
const contractHash = blueprintKey(conforming.contract);
|
|
334
|
+
const suggestion = {
|
|
335
|
+
origin: conforming.origin,
|
|
336
|
+
rationale: conforming.reasoning,
|
|
337
|
+
blueprintMeta: {
|
|
338
|
+
blueprintId: `bp_${randomUUID()}`,
|
|
339
|
+
contractHash,
|
|
340
|
+
generator: "ui-gen-default-haiku-4-5",
|
|
341
|
+
variance: {},
|
|
342
|
+
},
|
|
343
|
+
...(conforming.findings.length > 0
|
|
344
|
+
? { validationFindings: conforming.findings }
|
|
345
|
+
: {}),
|
|
346
|
+
};
|
|
374
347
|
return {
|
|
375
|
-
action:
|
|
376
|
-
reason:
|
|
348
|
+
action: "create",
|
|
349
|
+
reason: conforming.reasoning,
|
|
377
350
|
suggestion,
|
|
378
|
-
effectiveContract: contract,
|
|
351
|
+
effectiveContract: conforming.contract,
|
|
379
352
|
};
|
|
380
353
|
}
|
|
381
354
|
catch (err) {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
import type { RenderStore, VectorStore } from "@ggui-ai/mcp-server-core";
|
|
35
35
|
import { type McpAppAiGguiRenderMeta } from "@ggui-ai/protocol/integrations/mcp-apps";
|
|
36
36
|
import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
37
|
-
export declare const GGUI_RENDER_SHELL_HTML = "<!doctype html>\n<html lang=\"en\" style=\"height:100%\"><head><meta charset=\"utf-8\"><title>ggui render</title></head>\n<body style=\"margin:0;height:100%;min-height:480px\"><div id=\"ggui-root\" data-ggui-shell=\"thin\" style=\"height:100%;min-height:480px\"></div>\n<script>\n(function(){'use strict';\n// MCP Apps shell. Speaks the canonical postMessage protocol from\n// @modelcontextprotocol/ext-apps:\n// 1. iframe -> host: ui/initialize\n// 2. iframe -> host: ui/notifications/initialized\n// 3. host -> iframe: ui/notifications/tool-result (per CallToolResult)\n// On tool-result, read the slice envelope from _meta (spec-canonical\n// CallToolResult _meta at the top level, or the first-party\n// params.toolOutput._meta shape), set window.__GGUI_META__ to the\n// envelope, fetch runtime as blob, inject as script.\n// Runtime auto-mounts inline. No nested iframe (claudemcpcontent.com\n// CSP frame-src forbids cross-origin frames). State machine matches\n// buildSelfContainedShell so the same runtime mounts both paths.\n//\n// R5 (2026-05-26): the historic /r/<shortCode> HTTP fallback was\n// removed along with the bearer-by-obscurity model -- hosts that strip\n// _meta no longer have a recovery path here. Spec-canonical hosts\n// deliver _meta inline and are unaffected.\n//\n// Phase B (2026-05-27): the historic two-slice envelope\n// (`ai.ggui/session` + `ai.ggui/stack-item`) collapsed to ONE flat\n// `ai.ggui/render` slice. runtimeUrl now lives directly on the\n// render slice; renderId is the canonical identity.\nvar rpcId=1,pending={};\nvar rootEl=document.getElementById('ggui-root');\nrootEl.style.cssText='display:flex;flex-direction:column;height:100%;min-height:300px;margin:0';\nvar mounted=false;\nfunction setOverlay(text){\n if(mounted)return;\n rootEl.innerHTML='<div style=\"font:14px system-ui,sans-serif;padding:24px;color:#666\">'+text+'</div>';\n}\nfunction postNotification(method,params){\n try{window.parent.postMessage({jsonrpc:'2.0',method:method,params:params||{}},'*');}catch(e){}\n}\nfunction postRpc(method,params){\n return new Promise(function(res,rej){\n var id=rpcId++;pending[id]={res:res,rej:rej};\n try{window.parent.postMessage({jsonrpc:'2.0',id:id,method:method,params:params||{}},'*');}\n catch(e){delete pending[id];rej(e);}\n });\n}\nfunction postBootstrapFailed(reason,message){\n // Surface every shell-layer bootstrap-failure path as a typed\n // RendererBootFailedMessage envelope so hosts pinning the C9\n // error pane (McpAppIframe onError, IframeErrorPane) see the\n // failure instead of staring at the inert overlay until the test\n // times out. Reason codes match BootstrapFailureReason.\n try{window.parent.postMessage({type:'ggui:bootstrap-failed',reason:reason,message:message},'*');}catch(e){}\n}\nasync function mountFromMeta(envelope){\n if(mounted)return;\n // Slice envelope shape (Phase B): { \"ai.ggui/render\": { renderId,\n // appId, runtimeUrl, ... } }. runtimeUrl on the render slice is\n // the only load-bearing field at the shell layer \u2014 it tells us\n // which iframe-runtime bundle to fetch. Everything else is\n // optional; the runtime decides at boot time based on the meta\n // it reads from window.__GGUI_META__.\n var renderSlice=envelope&&envelope['ai.ggui/render'];\n var runtimeUrl=renderSlice&&renderSlice.runtimeUrl;\n if(!envelope||typeof runtimeUrl!=='string'){\n setOverlay('Bootstrap payload malformed.');\n postBootstrapFailed('BOOTSTRAP_MALFORMED','Bootstrap payload malformed.');\n return;\n }\n setOverlay('Loading UI\u2026');\n window.__GGUI_META__=envelope;\n // Load the runtime bundle via a direct cross-origin script tag\n // (governed by CSP script-src) instead of fetch + Blob (governed by\n // CSP connect-src). claude.ai's claudemcpcontent.com iframe CSP\n // forbids cross-origin connect-src so fetch throws TypeError\n // 'Failed to fetch', but allows cross-origin script-src when the\n // bundle responds with the right CORS headers (the iframe-runtime\n // mount sets them). The self-contained shell already uses this\n // pattern; legacy postMessage shell now matches it.\n try{\n var s=document.createElement('script');\n s.type='module';\n // crossorigin=anonymous opts into CORS-mode error reporting.\n // Without it, cross-origin script tags get a sanitized\n // \"script error\" with no details, masking the real cause\n // (CSP block, CORS reject, module-evaluation throw). With it,\n // the error event surfaces the actual message in the iframe\n // console -- the bundle ships ACAO=* so credentialed mode is\n // unnecessary.\n s.crossOrigin='anonymous';\n s.src=runtimeUrl;\n s.onload=function(){mounted=true;};\n s.onerror=function(e){\n var msg='Runtime bundle failed to load: '+(e&&e.message||'script error');\n setOverlay(msg);\n postBootstrapFailed('BUNDLE_FETCH_FAILED',msg);\n };\n rootEl.innerHTML='';\n document.body.appendChild(s);\n }catch(e){\n var msg='Runtime bundle failed to load: '+(e&&e.message||e);\n setOverlay(msg);\n postBootstrapFailed('BUNDLE_FETCH_FAILED',msg);\n }\n}\nfunction readMetaFromInitResult(result){\n if(!result||typeof result!=='object')return null;\n var toolOutput=result.toolOutput;\n if(!toolOutput||typeof toolOutput!=='object')return null;\n var meta=toolOutput._meta;\n if(!meta||typeof meta!=='object')return null;\n // Single slice-envelope key (Phase B: ai.ggui/render). Only the\n // render slice's runtimeUrl is load-bearing at the shell layer;\n // the runtime reads everything else off window.__GGUI_META__\n // after we set it.\n var renderSlice=meta['ai.ggui/render'];\n if(!renderSlice||typeof renderSlice!=='object')return null;\n if(typeof renderSlice.runtimeUrl!=='string')return null;\n return meta;\n}\nfunction hasAiGguiMetaPlaceholder(result){\n // Detect the protocol-violation case where the host signaled\n // \"I tried to deliver meta\" (toolOutput._meta carries an\n // ai.ggui/render key) but it's malformed. Fail-fast with\n // BOOTSTRAP_META_MISSING rather than waiting forever.\n if(!result||typeof result!=='object')return false;\n var toolOutput=result.toolOutput;\n if(!toolOutput||typeof toolOutput!=='object')return false;\n var meta=toolOutput._meta;\n if(!meta||typeof meta!=='object')return false;\n return 'ai.ggui/render' in meta;\n}\nfunction readMetaFromCallToolResult(params){\n // MCP Apps spec (specification/2026-01-26/apps.mdx:1145-1155):\n // ui/notifications/tool-result\n // params: CallToolResult // Standard MCP type\n // So params IS the CallToolResult and _meta lives at the top\n // level (NOT under params.toolOutput, which is where the\n // first-party McpAppIframe convention wraps it). Spec-compliant\n // hosts (Claude Desktop, claude.ai Connector, Claude Code) deliver\n // slice-envelope material here.\n if(!params||typeof params!=='object')return null;\n var meta=params._meta;\n if(!meta||typeof meta!=='object')return null;\n var renderSlice=meta['ai.ggui/render'];\n if(!renderSlice||typeof renderSlice!=='object')return null;\n if(typeof renderSlice.runtimeUrl!=='string')return null;\n return meta;\n}\nwindow.addEventListener('message',function(ev){\n var m=ev&&ev.data;\n if(!m||m.jsonrpc!=='2.0')return;\n if(m.id!=null&&pending[m.id]){\n var p=pending[m.id];delete pending[m.id];\n if(m.error)p.rej(m.error);else p.res(m.result);\n return;\n }\n if(m.method==='ui/notifications/tool-result'){\n // Spec-compliant hosts: m.params IS the CallToolResult; _meta is\n // at the top level. Try this FIRST so Claude Desktop / claude.ai\n // Connector / Claude Code land here.\n var specB=readMetaFromCallToolResult(m.params);\n if(specB){mountFromMeta(specB);return;}\n // First-party McpAppIframe convention: slice envelope nested under\n // params.toolOutput._meta. First-party hosts (Studio, Portal,\n // console) use this shape for both init-response and post-init\n // notification.\n var bb=readMetaFromInitResult(m.params);\n if(bb){mountFromMeta(bb);return;}\n // R5 (2026-05-26) -- the /r/<shortCode> HTTP fallback was removed\n // along with the bearer-by-obscurity model. Hosts that strip\n // _meta on the tool-result wire have no fallback path here;\n // spec-canonical hosts deliver meta inline and land in the two\n // branches above.\n }\n});\nsetOverlay('Initializing\u2026');\nvar initTimer=setTimeout(function(){\n setOverlay('Host did not respond to ui/initialize within 3s.');\n},3000);\npostRpc('ui/initialize',{\n appCapabilities:{},\n appInfo:{name:'ggui-session',version:'1.0.0'},\n protocolVersion:'2026-01-26'\n}).then(function(result){\n clearTimeout(initTimer);\n postNotification('ui/notifications/initialized',{});\n // Path B: slice envelope inline in ui/initialize result. Hosts\n // using <McpAppIframe>'s first-party dispatch deliver meta here\n // and never send a separate ui/notifications/tool-result.\n var b=readMetaFromInitResult(result);\n if(b){mountFromMeta(b);return;}\n // Protocol-violation surface: host signalled an attempt\n // (toolOutput._meta carries ai.ggui/render) but it's malformed.\n // Fail fast so hosts pinning a typed error envelope don't sit on\n // the Path-A waiting overlay forever.\n if(hasAiGguiMetaPlaceholder(result)){\n var msg='ui/initialize result missing valid ai.ggui/render.runtimeUrl';\n setOverlay(msg);\n postBootstrapFailed('BOOTSTRAP_META_MISSING',msg);\n return;\n }\n // Path A: wait for the host to send ui/notifications/tool-result\n // with structuredContent.{url, shortCode}. MCP Apps hosts that don't\n // implement the reading-B inline-meta convention land here.\n setOverlay('Waiting for tool result\u2026');\n}).catch(function(e){\n clearTimeout(initTimer);\n setOverlay('ui/initialize failed: '+(e&&e.message||JSON.stringify(e)));\n});\n})();\n</script></body></html>";
|
|
37
|
+
export declare const GGUI_RENDER_SHELL_HTML = "<!doctype html>\n<html lang=\"en\" style=\"height:100%;background-color:var(--ggui-color-surface, #1e293b)\"><head><meta charset=\"utf-8\"><title>ggui render</title></head>\n<body style=\"margin:0;height:100%;min-height:480px;background-color:var(--ggui-color-surface, #1e293b)\"><div id=\"ggui-root\" data-ggui-shell=\"thin\" style=\"height:100%;min-height:480px\"></div>\n<script>\n(function(){'use strict';\n// MCP Apps shell. Speaks the canonical postMessage protocol from\n// @modelcontextprotocol/ext-apps:\n// 1. iframe -> host: ui/initialize\n// 2. iframe -> host: ui/notifications/initialized\n// 3. host -> iframe: ui/notifications/tool-result (per CallToolResult)\n// On tool-result, read the slice envelope from _meta (spec-canonical\n// CallToolResult _meta at the top level, or the first-party\n// params.toolOutput._meta shape), set window.__GGUI_META__ to the\n// envelope, fetch runtime as blob, inject as script.\n// Runtime auto-mounts inline. No nested iframe (claudemcpcontent.com\n// CSP frame-src forbids cross-origin frames). State machine matches\n// buildSelfContainedShell so the same runtime mounts both paths.\n//\n// R5 (2026-05-26): the historic /r/<shortCode> HTTP fallback was\n// removed along with the bearer-by-obscurity model -- hosts that strip\n// _meta no longer have a recovery path here. Spec-canonical hosts\n// deliver _meta inline and are unaffected.\n//\n// Phase B (2026-05-27): the historic two-slice envelope\n// (`ai.ggui/session` + `ai.ggui/stack-item`) collapsed to ONE flat\n// `ai.ggui/render` slice. runtimeUrl now lives directly on the\n// render slice; renderId is the canonical identity.\nvar rpcId=1,pending={};\nvar rootEl=document.getElementById('ggui-root');\nrootEl.style.cssText='display:flex;flex-direction:column;height:100%;min-height:300px;margin:0';\nvar mounted=false;\nfunction setOverlay(text){\n if(mounted)return;\n rootEl.innerHTML='<div style=\"font:14px system-ui,sans-serif;padding:24px;color:#666\">'+text+'</div>';\n}\nfunction postNotification(method,params){\n try{window.parent.postMessage({jsonrpc:'2.0',method:method,params:params||{}},'*');}catch(e){}\n}\nfunction postRpc(method,params){\n return new Promise(function(res,rej){\n var id=rpcId++;pending[id]={res:res,rej:rej};\n try{window.parent.postMessage({jsonrpc:'2.0',id:id,method:method,params:params||{}},'*');}\n catch(e){delete pending[id];rej(e);}\n });\n}\nfunction postBootstrapFailed(reason,message){\n // Surface every shell-layer bootstrap-failure path as a typed\n // RendererBootFailedMessage envelope so hosts pinning the C9\n // error pane (McpAppIframe onError, IframeErrorPane) see the\n // failure instead of staring at the inert overlay until the test\n // times out. Reason codes match BootstrapFailureReason.\n try{window.parent.postMessage({type:'ggui:bootstrap-failed',reason:reason,message:message},'*');}catch(e){}\n}\nasync function mountFromMeta(envelope){\n if(mounted)return;\n // Slice envelope shape (Phase B): { \"ai.ggui/render\": { renderId,\n // appId, runtimeUrl, ... } }. runtimeUrl on the render slice is\n // the only load-bearing field at the shell layer \u2014 it tells us\n // which iframe-runtime bundle to fetch. Everything else is\n // optional; the runtime decides at boot time based on the meta\n // it reads from window.__GGUI_META__.\n var renderSlice=envelope&&envelope['ai.ggui/render'];\n var runtimeUrl=renderSlice&&renderSlice.runtimeUrl;\n if(!envelope||typeof runtimeUrl!=='string'){\n setOverlay('Bootstrap payload malformed.');\n postBootstrapFailed('BOOTSTRAP_MALFORMED','Bootstrap payload malformed.');\n return;\n }\n setOverlay('Loading UI\u2026');\n window.__GGUI_META__=envelope;\n // Load the runtime bundle via a direct cross-origin script tag\n // (governed by CSP script-src) instead of fetch + Blob (governed by\n // CSP connect-src). claude.ai's claudemcpcontent.com iframe CSP\n // forbids cross-origin connect-src so fetch throws TypeError\n // 'Failed to fetch', but allows cross-origin script-src when the\n // bundle responds with the right CORS headers (the iframe-runtime\n // mount sets them). The self-contained shell already uses this\n // pattern; legacy postMessage shell now matches it.\n try{\n var s=document.createElement('script');\n s.type='module';\n // crossorigin=anonymous opts into CORS-mode error reporting.\n // Without it, cross-origin script tags get a sanitized\n // \"script error\" with no details, masking the real cause\n // (CSP block, CORS reject, module-evaluation throw). With it,\n // the error event surfaces the actual message in the iframe\n // console -- the bundle ships ACAO=* so credentialed mode is\n // unnecessary.\n s.crossOrigin='anonymous';\n s.src=runtimeUrl;\n s.onload=function(){mounted=true;};\n s.onerror=function(e){\n var msg='Runtime bundle failed to load: '+(e&&e.message||'script error');\n setOverlay(msg);\n postBootstrapFailed('BUNDLE_FETCH_FAILED',msg);\n };\n rootEl.innerHTML='';\n document.body.appendChild(s);\n }catch(e){\n var msg='Runtime bundle failed to load: '+(e&&e.message||e);\n setOverlay(msg);\n postBootstrapFailed('BUNDLE_FETCH_FAILED',msg);\n }\n}\nfunction readMetaFromInitResult(result){\n if(!result||typeof result!=='object')return null;\n var toolOutput=result.toolOutput;\n if(!toolOutput||typeof toolOutput!=='object')return null;\n var meta=toolOutput._meta;\n if(!meta||typeof meta!=='object')return null;\n // Single slice-envelope key (Phase B: ai.ggui/render). Only the\n // render slice's runtimeUrl is load-bearing at the shell layer;\n // the runtime reads everything else off window.__GGUI_META__\n // after we set it.\n var renderSlice=meta['ai.ggui/render'];\n if(!renderSlice||typeof renderSlice!=='object')return null;\n if(typeof renderSlice.runtimeUrl!=='string')return null;\n return meta;\n}\nfunction hasAiGguiMetaPlaceholder(result){\n // Detect the protocol-violation case where the host signaled\n // \"I tried to deliver meta\" (toolOutput._meta carries an\n // ai.ggui/render key) but it's malformed. Fail-fast with\n // BOOTSTRAP_META_MISSING rather than waiting forever.\n if(!result||typeof result!=='object')return false;\n var toolOutput=result.toolOutput;\n if(!toolOutput||typeof toolOutput!=='object')return false;\n var meta=toolOutput._meta;\n if(!meta||typeof meta!=='object')return false;\n return 'ai.ggui/render' in meta;\n}\nfunction readMetaFromCallToolResult(params){\n // MCP Apps spec (specification/2026-01-26/apps.mdx:1145-1155):\n // ui/notifications/tool-result\n // params: CallToolResult // Standard MCP type\n // So params IS the CallToolResult and _meta lives at the top\n // level (NOT under params.toolOutput, which is where the\n // first-party McpAppIframe convention wraps it). Spec-compliant\n // hosts (Claude Desktop, claude.ai Connector, Claude Code) deliver\n // slice-envelope material here.\n if(!params||typeof params!=='object')return null;\n var meta=params._meta;\n if(!meta||typeof meta!=='object')return null;\n var renderSlice=meta['ai.ggui/render'];\n if(!renderSlice||typeof renderSlice!=='object')return null;\n if(typeof renderSlice.runtimeUrl!=='string')return null;\n return meta;\n}\nwindow.addEventListener('message',function(ev){\n var m=ev&&ev.data;\n if(!m||m.jsonrpc!=='2.0')return;\n if(m.id!=null&&pending[m.id]){\n var p=pending[m.id];delete pending[m.id];\n if(m.error)p.rej(m.error);else p.res(m.result);\n return;\n }\n if(m.method==='ui/notifications/tool-result'){\n // Spec-compliant hosts: m.params IS the CallToolResult; _meta is\n // at the top level. Try this FIRST so Claude Desktop / claude.ai\n // Connector / Claude Code land here.\n var specB=readMetaFromCallToolResult(m.params);\n if(specB){mountFromMeta(specB);return;}\n // First-party McpAppIframe convention: slice envelope nested under\n // params.toolOutput._meta. First-party hosts (Studio, Portal,\n // console) use this shape for both init-response and post-init\n // notification.\n var bb=readMetaFromInitResult(m.params);\n if(bb){mountFromMeta(bb);return;}\n // R5 (2026-05-26) -- the /r/<shortCode> HTTP fallback was removed\n // along with the bearer-by-obscurity model. Hosts that strip\n // _meta on the tool-result wire have no fallback path here;\n // spec-canonical hosts deliver meta inline and land in the two\n // branches above.\n }\n});\nsetOverlay('Initializing\u2026');\nvar initTimer=setTimeout(function(){\n setOverlay('Host did not respond to ui/initialize within 3s.');\n},3000);\npostRpc('ui/initialize',{\n appCapabilities:{},\n appInfo:{name:'ggui-session',version:'1.0.0'},\n protocolVersion:'2026-01-26'\n}).then(function(result){\n clearTimeout(initTimer);\n postNotification('ui/notifications/initialized',{});\n // Path B: slice envelope inline in ui/initialize result. Hosts\n // using <McpAppIframe>'s first-party dispatch deliver meta here\n // and never send a separate ui/notifications/tool-result.\n var b=readMetaFromInitResult(result);\n if(b){mountFromMeta(b);return;}\n // Protocol-violation surface: host signalled an attempt\n // (toolOutput._meta carries ai.ggui/render) but it's malformed.\n // Fail fast so hosts pinning a typed error envelope don't sit on\n // the Path-A waiting overlay forever.\n if(hasAiGguiMetaPlaceholder(result)){\n var msg='ui/initialize result missing valid ai.ggui/render.runtimeUrl';\n setOverlay(msg);\n postBootstrapFailed('BOOTSTRAP_META_MISSING',msg);\n return;\n }\n // Path A: wait for the host to send ui/notifications/tool-result\n // with structuredContent.{url, shortCode}. MCP Apps hosts that don't\n // implement the reading-B inline-meta convention land here.\n setOverlay('Waiting for tool result\u2026');\n}).catch(function(e){\n clearTimeout(initTimer);\n setOverlay('ui/initialize failed: '+(e&&e.message||JSON.stringify(e)));\n});\n})();\n</script></body></html>";
|
|
38
38
|
/**
|
|
39
39
|
* CSP `script-src` source expression that authorises the inline
|
|
40
40
|
* `<script>` block of {@link GGUI_RENDER_SHELL_HTML} when it executes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-apps-outbound.d.ts","sourceRoot":"","sources":["../src/mcp-apps-outbound.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAWzE,OAAO,EAML,KAAK,sBAAsB,EAC5B,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"mcp-apps-outbound.d.ts","sourceRoot":"","sources":["../src/mcp-apps-outbound.ts"],"names":[],"mappings":"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"}
|
|
@@ -327,9 +327,40 @@ postRpc('ui/initialize',{
|
|
|
327
327
|
});
|
|
328
328
|
})();
|
|
329
329
|
`;
|
|
330
|
+
// `--ggui-color-surface` is injected at `:root` on this document's
|
|
331
|
+
// `<head>` by the iframe-runtime at boot (react-renderer.ts ->
|
|
332
|
+
// `<style id="ggui-theme-vars">`), so the `var()` resolves to the
|
|
333
|
+
// active theme's exact per-mode surface color at runtime. The static
|
|
334
|
+
// `#1e293b` fallback (the dark-mode surface) covers the pre-resolve
|
|
335
|
+
// first paint + browsers that drop unresolved custom properties.
|
|
336
|
+
//
|
|
337
|
+
// # Why paint the document background here (Safari white-canvas fix)
|
|
338
|
+
//
|
|
339
|
+
// This shell is ALWAYS the **top-level document of a standalone served
|
|
340
|
+
// iframe** (`ui://ggui/render`, the `/r/<shortCode>` viewer, the
|
|
341
|
+
// sandbox-proxy inner-iframe `srcdoc`). It is NEVER inlined into a host
|
|
342
|
+
// page — the host wrapper (`<McpAppIframe>`) only ever loads it AS an
|
|
343
|
+
// iframe document, never as part of its own DOM. So painting `html` /
|
|
344
|
+
// `body` here is structurally unreachable by inline embedding and
|
|
345
|
+
// cannot impose a background on a host page.
|
|
346
|
+
//
|
|
347
|
+
// The design scope root (`.ggui-rcr-*`) and the sandbox-proxy outer
|
|
348
|
+
// document stay `background-color: transparent` by design (so a host
|
|
349
|
+
// themeing the chat surface shows through AROUND the rendered content).
|
|
350
|
+
// But nothing painted the rendered-content document's OWN backdrop:
|
|
351
|
+
// Chrome composited the transparent iframe document over the dark host
|
|
352
|
+
// app behind it (looked dark), while Safari renders a transparent
|
|
353
|
+
// iframe document's backdrop as the opaque UA `Canvas` color (white) —
|
|
354
|
+
// the per-browser divergence the bug reported. The component itself
|
|
355
|
+
// themed correctly (its scoped vars resolve); only the page behind it
|
|
356
|
+
// diverged. Painting the served document's own surface here removes the
|
|
357
|
+
// dependency on a browser honoring iframe transparency.
|
|
358
|
+
//
|
|
359
|
+
// Value-resolution only — no `--ggui-*` token added or renamed.
|
|
360
|
+
const GGUI_RENDER_SHELL_SURFACE = `var(--ggui-color-surface, #1e293b)`;
|
|
330
361
|
export const GGUI_RENDER_SHELL_HTML = `<!doctype html>
|
|
331
|
-
<html lang="en" style="height:100
|
|
332
|
-
<body style="margin:0;height:100%;min-height:480px"><div id="ggui-root" data-ggui-shell="thin" style="height:100%;min-height:480px"></div>
|
|
362
|
+
<html lang="en" style="height:100%;background-color:${GGUI_RENDER_SHELL_SURFACE}"><head><meta charset="utf-8"><title>ggui render</title></head>
|
|
363
|
+
<body style="margin:0;height:100%;min-height:480px;background-color:${GGUI_RENDER_SHELL_SURFACE}"><div id="ggui-root" data-ggui-shell="thin" style="height:100%;min-height:480px"></div>
|
|
333
364
|
<script>${GGUI_RENDER_SHELL_SCRIPT_BODY}</script></body></html>`;
|
|
334
365
|
/**
|
|
335
366
|
* CSP `script-src` source expression that authorises the inline
|
|
@@ -657,9 +688,19 @@ export function buildSelfContainedShell(opts) {
|
|
|
657
688
|
.replace(/"/g, """)
|
|
658
689
|
.replace(/</g, "<")
|
|
659
690
|
.replace(/>/g, ">");
|
|
691
|
+
// Standalone served-iframe document — paint its own surface backdrop
|
|
692
|
+
// so the canvas behind the rendered component never depends on a
|
|
693
|
+
// browser honoring iframe transparency (the Safari white-canvas bug).
|
|
694
|
+
// Same gating rationale as `GGUI_RENDER_SHELL_HTML`: this is ALWAYS a
|
|
695
|
+
// top-level iframe document (the `__GGUI_META__` self-contained shell
|
|
696
|
+
// for claude.ai / per-render resource shells), never inlined into a
|
|
697
|
+
// host page, so painting `html`/`body` here cannot regress inline
|
|
698
|
+
// embedding. `var(--ggui-color-surface, …)` resolves to the active
|
|
699
|
+
// theme's per-mode surface once the iframe-runtime injects the `:root`
|
|
700
|
+
// theme vars; the static dark fallback covers pre-resolve paint.
|
|
660
701
|
return `<!doctype html>
|
|
661
|
-
<html lang="en"><head><meta charset="utf-8"><title>ggui render</title></head>
|
|
662
|
-
<body>
|
|
702
|
+
<html lang="en" style="background-color:${GGUI_RENDER_SHELL_SURFACE}"><head><meta charset="utf-8"><title>ggui render</title></head>
|
|
703
|
+
<body style="background-color:${GGUI_RENDER_SHELL_SURFACE}">
|
|
663
704
|
<div id="ggui-root" data-ggui-shell="self-contained"></div>
|
|
664
705
|
<script>window.__GGUI_META__ = ${json};</script>
|
|
665
706
|
<script type="module" crossorigin="anonymous" src="${safeRuntimeUrl}"></script>
|
|
@@ -679,9 +720,12 @@ export function buildSelfContainedShell(opts) {
|
|
|
679
720
|
* @public
|
|
680
721
|
*/
|
|
681
722
|
export function buildSelfContainedLoadingShell(renderId) {
|
|
723
|
+
// Standalone served-iframe loading document — same surface-backdrop
|
|
724
|
+
// paint + gating as the thin/self-contained shells so the canvas is
|
|
725
|
+
// dark while the render is still in flight (no Safari white flash).
|
|
682
726
|
return `<!doctype html>
|
|
683
|
-
<html lang="en"><head><meta charset="utf-8"><title>ggui render</title></head>
|
|
684
|
-
<body>
|
|
727
|
+
<html lang="en" style="background-color:${GGUI_RENDER_SHELL_SURFACE}"><head><meta charset="utf-8"><title>ggui render</title></head>
|
|
728
|
+
<body style="background-color:${GGUI_RENDER_SHELL_SURFACE}">
|
|
685
729
|
<div id="ggui-root" data-ggui-shell="loading" data-ggui-render-id="${renderId
|
|
686
730
|
.replace(/&/g, "&")
|
|
687
731
|
.replace(/"/g, """)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/mcp-server",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.4",
|
|
4
4
|
"description": "Self-hosted MCP server for the ggui protocol. An HTTP/MCP binding layer over @ggui-ai/mcp-server-handlers and the reference adapters from @ggui-ai/mcp-server-core — run it via `ggui serve`.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ggui",
|
|
@@ -32,16 +32,16 @@
|
|
|
32
32
|
"resend": "^6.12.2",
|
|
33
33
|
"ws": "^8.20.1",
|
|
34
34
|
"zod": "^4.3.6",
|
|
35
|
-
"@ggui-ai/console": "0.2.0-alpha.
|
|
36
|
-
"@ggui-ai/mcp-server-core": "0.2.0-alpha.
|
|
37
|
-
"@ggui-ai/
|
|
38
|
-
"@ggui-ai/
|
|
39
|
-
"@ggui-ai/negotiator": "0.2.0-alpha.
|
|
40
|
-
"@ggui-ai/preview-a2ui": "0.2.0-alpha.
|
|
41
|
-
"@ggui-ai/project-config": "0.2.0-alpha.
|
|
42
|
-
"@ggui-ai/protocol": "0.2.0-alpha.
|
|
43
|
-
"@ggui-ai/ui-gen": "0.2.0-alpha.
|
|
44
|
-
"@ggui-ai/ui-registry": "0.2.0-alpha.
|
|
35
|
+
"@ggui-ai/console": "0.2.0-alpha.4",
|
|
36
|
+
"@ggui-ai/mcp-server-core": "0.2.0-alpha.4",
|
|
37
|
+
"@ggui-ai/iframe-runtime": "0.2.0-alpha.4",
|
|
38
|
+
"@ggui-ai/mcp-server-handlers": "0.2.0-alpha.4",
|
|
39
|
+
"@ggui-ai/negotiator": "0.2.0-alpha.4",
|
|
40
|
+
"@ggui-ai/preview-a2ui": "0.2.0-alpha.4",
|
|
41
|
+
"@ggui-ai/project-config": "0.2.0-alpha.4",
|
|
42
|
+
"@ggui-ai/protocol": "0.2.0-alpha.4",
|
|
43
|
+
"@ggui-ai/ui-gen": "0.2.0-alpha.4",
|
|
44
|
+
"@ggui-ai/ui-registry": "0.2.0-alpha.4"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/express": "^4.17.21",
|