@ggui-ai/iframe-runtime 0.1.0-rc.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/LICENSE +201 -0
- package/README.md +33 -0
- package/dist/bootstrap.d.ts +72 -0
- package/dist/bootstrap.js +500 -0
- package/dist/canvas/animator/animator-host.d.ts +63 -0
- package/dist/canvas/animator/animator-host.js +105 -0
- package/dist/canvas/animator/animator-pill.d.ts +42 -0
- package/dist/canvas/animator/animator-pill.js +119 -0
- package/dist/canvas/animator/state-machine.d.ts +106 -0
- package/dist/canvas/animator/state-machine.js +150 -0
- package/dist/canvas/canvas-shell.d.ts +77 -0
- package/dist/canvas/canvas-shell.js +117 -0
- package/dist/canvas/display-mode-policy.d.ts +104 -0
- package/dist/canvas/display-mode-policy.js +98 -0
- package/dist/canvas/mount.d.ts +150 -0
- package/dist/canvas/mount.js +202 -0
- package/dist/canvas/nav-stack.d.ts +133 -0
- package/dist/canvas/nav-stack.js +172 -0
- package/dist/channel-transport.d.ts +182 -0
- package/dist/channel-transport.js +344 -0
- package/dist/channels/channel-payload.d.ts +20 -0
- package/dist/channels/channel-payload.js +34 -0
- package/dist/channels/data.d.ts +48 -0
- package/dist/channels/data.js +103 -0
- package/dist/channels/drain-ack.d.ts +13 -0
- package/dist/channels/drain-ack.js +15 -0
- package/dist/channels/feedback.d.ts +14 -0
- package/dist/channels/feedback.js +19 -0
- package/dist/channels/index.d.ts +7 -0
- package/dist/channels/index.js +7 -0
- package/dist/channels/props-update.d.ts +53 -0
- package/dist/channels/props-update.js +130 -0
- package/dist/channels/push.d.ts +47 -0
- package/dist/channels/push.js +54 -0
- package/dist/channels/system.d.ts +26 -0
- package/dist/channels/system.js +44 -0
- package/dist/compiled-validators.d.ts +40 -0
- package/dist/compiled-validators.js +74 -0
- package/dist/context-observer.d.ts +205 -0
- package/dist/context-observer.js +312 -0
- package/dist/gadget-loader.d.ts +118 -0
- package/dist/gadget-loader.js +136 -0
- package/dist/globals.d.ts +234 -0
- package/dist/globals.js +101 -0
- package/dist/host-context-emitter.d.ts +91 -0
- package/dist/host-context-emitter.js +213 -0
- package/dist/iframe-runtime.js +261 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +4 -0
- package/dist/lifecycle.d.ts +66 -0
- package/dist/lifecycle.js +49 -0
- package/dist/mcp-app-iframe-host.d.ts +69 -0
- package/dist/mcp-app-iframe-host.js +214 -0
- package/dist/observability.d.ts +225 -0
- package/dist/observability.js +28 -0
- package/dist/protocol-error.d.ts +199 -0
- package/dist/protocol-error.js +116 -0
- package/dist/provisional-renderer.d.ts +62 -0
- package/dist/provisional-renderer.js +323 -0
- package/dist/react-renderer.d.ts +95 -0
- package/dist/react-renderer.js +343 -0
- package/dist/registry-subscribe.d.ts +102 -0
- package/dist/registry-subscribe.js +319 -0
- package/dist/runtime.d.ts +531 -0
- package/dist/runtime.js +2623 -0
- package/dist/server.d.ts +36 -0
- package/dist/server.js +72 -0
- package/dist/stack-item-renderer.d.ts +103 -0
- package/dist/stack-item-renderer.js +270 -0
- package/dist/stack.d.ts +90 -0
- package/dist/stack.js +118 -0
- package/dist/status-dom.d.ts +72 -0
- package/dist/status-dom.js +51 -0
- package/dist/system-cards/NoCredentialsCard.d.ts +33 -0
- package/dist/system-cards/NoCredentialsCard.js +189 -0
- package/dist/system-cards/ProtocolProbeCard.d.ts +64 -0
- package/dist/system-cards/ProtocolProbeCard.js +463 -0
- package/dist/system-cards/QrCode.d.ts +46 -0
- package/dist/system-cards/QrCode.js +55 -0
- package/dist/system-cards/host-detect.d.ts +31 -0
- package/dist/system-cards/host-detect.js +108 -0
- package/dist/system-cards/host-intents.d.ts +85 -0
- package/dist/system-cards/host-intents.js +170 -0
- package/dist/system-cards/index.d.ts +70 -0
- package/dist/system-cards/index.js +134 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.js +3 -0
- package/dist/validation.d.ts +83 -0
- package/dist/validation.js +106 -0
- package/dist/wire-config.d.ts +214 -0
- package/dist/wire-config.js +290 -0
- package/package.json +81 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ggui-ai/iframe-runtime` package entry — type-export surface for
|
|
3
|
+
* non-iframe consumers (host apps, the `<McpAppIframe>` wrapper,
|
|
4
|
+
* tests). The runtime artifact is `dist/iframe-runtime.js` (built by
|
|
5
|
+
* `esbuild.config.mjs` from `src/runtime.ts`); this index does NOT
|
|
6
|
+
* re-export the runtime side-effects.
|
|
7
|
+
*
|
|
8
|
+
* Surface:
|
|
9
|
+
* - `BootstrapParseFailureReason` — closed union of bootstrap-parse
|
|
10
|
+
* failure reasons.
|
|
11
|
+
* - `BootstrapParseResult` — discriminated union returned by
|
|
12
|
+
* `parseBootstrap()`.
|
|
13
|
+
* - `parseBootstrap` — the parser itself (consumers wanting to
|
|
14
|
+
* validate a bootstrap before spawning the iframe).
|
|
15
|
+
* - `RendererBootFailureReason` / `RendererBootFailedMessage` — the
|
|
16
|
+
* postMessage envelope shape parents observe on boot failure.
|
|
17
|
+
* - `ProtocolError` — canonical typed union for every failure
|
|
18
|
+
* the renderer classifies outward, wired to the postMessage
|
|
19
|
+
* envelope and the `_ggui:contract-error` WS channel.
|
|
20
|
+
* - `BootstrapFailureReason` — extensibly-closed union of every
|
|
21
|
+
* reason the 'bootstrap' variant can carry. Consolidates
|
|
22
|
+
* parse-time and post-parse codes in one place.
|
|
23
|
+
* - `ProtocolErrorEmitter` — function signature for the
|
|
24
|
+
* caller-side sink.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately NOT exported: `RendererWebSocketManager`, `subscribe`,
|
|
27
|
+
* `bootSequence`, `StackModel`. Those are the renderer's INTERNAL
|
|
28
|
+
* runtime contract and should not become a load-bearing public API.
|
|
29
|
+
*/
|
|
30
|
+
export type { BootstrapParseFailureReason, BootstrapParseResult, } from './types.js';
|
|
31
|
+
export { parseBootstrap, parseBootstrapFromUiInitialize, parseBootstrapFromGlobal, parseBootstrapFromToolResult, validateBootstrapMeta, } from './bootstrap.js';
|
|
32
|
+
export type { RendererBootFailureReason, RendererBootFailedMessage, SelfContainedBootstrap, } from './runtime.js';
|
|
33
|
+
export type { ProtocolError, ProtocolErrorEmitter, BootstrapFailureReason, } from './protocol-error.js';
|
|
34
|
+
export { defaultProtocolErrorEmitter, fromBootstrapFailure, fromClientContractViolation, fromContractErrorPayload, fromTransportFailure, fromAuthFailure, fromUpgradeRequired, fromUnknown, } from './protocol-error.js';
|
|
35
|
+
export type { ObservabilityEvent, ObservabilityEmitter, ObservabilityMessage, WiredToolInvokedEvent, ContractErrorEmittedEvent, SchemaVersionMismatchEvent, SubscribeFailedEvent, AuthRequiredEvent, UnknownObservabilityEvent, } from './observability.js';
|
|
36
|
+
export { postObservabilityToParent } from './observability.js';
|
|
37
|
+
export type { LifecycleEmitter } from './lifecycle.js';
|
|
38
|
+
export { postLifecycleToParent, makeLifecycleEvent } from './lifecycle.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { parseBootstrap, parseBootstrapFromUiInitialize, parseBootstrapFromGlobal, parseBootstrapFromToolResult, validateBootstrapMeta, } from './bootstrap.js';
|
|
2
|
+
export { defaultProtocolErrorEmitter, fromBootstrapFailure, fromClientContractViolation, fromContractErrorPayload, fromTransportFailure, fromAuthFailure, fromUpgradeRequired, fromUnknown, } from './protocol-error.js';
|
|
3
|
+
export { postObservabilityToParent } from './observability.js';
|
|
4
|
+
export { postLifecycleToParent, makeLifecycleEvent } from './lifecycle.js';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer-side emission of MCP App lifecycle events — the postMessage
|
|
3
|
+
* envelope the iframe posts to its parent on every mount-state
|
|
4
|
+
* transition. Mirrors the `<McpAppIframe>` host's outer-DOM mirror
|
|
5
|
+
* (`data-ggui-mcp-app-iframe-lifecycle="<state>"`); together they form
|
|
6
|
+
* a first-class wire surface so observers (E2E tests, accessibility
|
|
7
|
+
* scanners, third-party hosts, console inspectors) can read mount
|
|
8
|
+
* state without reaching into iframe-internal DOM.
|
|
9
|
+
*
|
|
10
|
+
* Lives here (not in `runtime.ts`) so spec-level tests can import the
|
|
11
|
+
* helper without dragging in the runtime's autostart side effects.
|
|
12
|
+
*
|
|
13
|
+
* **Producer obligations** (locked by
|
|
14
|
+
* {@link import('@ggui-ai/protocol/integrations/mcp-apps').McpAppLifecycleMessage}):
|
|
15
|
+
* - `mounting` MUST fire before bundle eval / first WS attempt.
|
|
16
|
+
* - Exactly one terminal state (`code-ready` or `error`) MUST follow
|
|
17
|
+
* for every successful boot attempt.
|
|
18
|
+
* - `disconnected` MAY follow `code-ready` on WS close; a subsequent
|
|
19
|
+
* `code-ready` MAY follow on successful reconnect (not required).
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
import type { McpAppLifecycleEvent, McpAppLifecycleState } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
24
|
+
/**
|
|
25
|
+
* Caller sink for lifecycle events. Mirrors the
|
|
26
|
+
* {@link import('./observability.js').ObservabilityEmitter} posture —
|
|
27
|
+
* tests inject a recorder; production binds the postMessage-to-parent
|
|
28
|
+
* default {@link postLifecycleToParent}.
|
|
29
|
+
*
|
|
30
|
+
* Handlers MUST NOT throw — lifecycle events are fire-and-forget. A
|
|
31
|
+
* throwing handler would mask the underlying state transition (the
|
|
32
|
+
* renderer has already committed to the new state by the time emit
|
|
33
|
+
* runs).
|
|
34
|
+
*
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
export type LifecycleEmitter = (event: McpAppLifecycleEvent) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Default emitter — posts a {@link McpAppLifecycleMessage} envelope to
|
|
40
|
+
* `window.parent`. Mirrors `postObservabilityToParent` /
|
|
41
|
+
* `postBootFailure` posture: swallows postMessage failure when the
|
|
42
|
+
* parent is unreachable so a detached parent doesn't crash the iframe.
|
|
43
|
+
*
|
|
44
|
+
* Non-browser import graphs (vitest+jsdom before a window mock lands)
|
|
45
|
+
* have no `window.parent`; the early return guards that case so tests
|
|
46
|
+
* importing this helper can do so before staging their `window` mocks.
|
|
47
|
+
*
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
export declare function postLifecycleToParent(event: McpAppLifecycleEvent): void;
|
|
51
|
+
/**
|
|
52
|
+
* Build a lifecycle event payload. Centralises the `state` →
|
|
53
|
+
* `McpAppLifecycleEvent` projection so call sites in
|
|
54
|
+
* `runtime.ts` don't repeat the `{state, ...optional}` shape and so
|
|
55
|
+
* the additive-extension semantic of `stackItemId` / `error` is
|
|
56
|
+
* single-sourced.
|
|
57
|
+
*
|
|
58
|
+
* @public
|
|
59
|
+
*/
|
|
60
|
+
export declare function makeLifecycleEvent(state: McpAppLifecycleState, options?: {
|
|
61
|
+
readonly stackItemId?: string;
|
|
62
|
+
readonly error?: {
|
|
63
|
+
readonly code: string;
|
|
64
|
+
readonly message: string;
|
|
65
|
+
};
|
|
66
|
+
}): McpAppLifecycleEvent;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default emitter — posts a {@link McpAppLifecycleMessage} envelope to
|
|
3
|
+
* `window.parent`. Mirrors `postObservabilityToParent` /
|
|
4
|
+
* `postBootFailure` posture: swallows postMessage failure when the
|
|
5
|
+
* parent is unreachable so a detached parent doesn't crash the iframe.
|
|
6
|
+
*
|
|
7
|
+
* Non-browser import graphs (vitest+jsdom before a window mock lands)
|
|
8
|
+
* have no `window.parent`; the early return guards that case so tests
|
|
9
|
+
* importing this helper can do so before staging their `window` mocks.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export function postLifecycleToParent(event) {
|
|
14
|
+
if (typeof window === 'undefined' || window.parent === null)
|
|
15
|
+
return;
|
|
16
|
+
const message = { type: 'ggui:lifecycle', event };
|
|
17
|
+
try {
|
|
18
|
+
window.parent.postMessage(message, '*');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Parent unreachable (detached window) — best-effort fire-and-
|
|
22
|
+
// forget. Matches postObservabilityToParent's swallow posture.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build a lifecycle event payload. Centralises the `state` →
|
|
27
|
+
* `McpAppLifecycleEvent` projection so call sites in
|
|
28
|
+
* `runtime.ts` don't repeat the `{state, ...optional}` shape and so
|
|
29
|
+
* the additive-extension semantic of `stackItemId` / `error` is
|
|
30
|
+
* single-sourced.
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
export function makeLifecycleEvent(state, options) {
|
|
35
|
+
// Avoid emitting `undefined` keys — the protocol's structural lock
|
|
36
|
+
// (mcp-apps.test.ts "shape lock") asserts a populated event has
|
|
37
|
+
// exactly state + stackItemId + error keys, not an Object.keys-
|
|
38
|
+
// observable `undefined` field.
|
|
39
|
+
if (options === undefined)
|
|
40
|
+
return { state };
|
|
41
|
+
const { stackItemId, error } = options;
|
|
42
|
+
return {
|
|
43
|
+
state,
|
|
44
|
+
...(typeof stackItemId === 'string' && stackItemId.length > 0
|
|
45
|
+
? { stackItemId }
|
|
46
|
+
: {}),
|
|
47
|
+
...(error !== undefined ? { error } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanilla-TS port of
|
|
3
|
+
* `@ggui-ai/react::components/McpAppsStackItemRenderer.tsx`. Used
|
|
4
|
+
* when a ggui stack item's `kind === 'mcpApps'` — the renderer
|
|
5
|
+
* mounts a nested iframe that embeds a foreign MCP App.
|
|
6
|
+
*
|
|
7
|
+
* The iframe plays the MCP Apps HOST role for the embedded content:
|
|
8
|
+
*
|
|
9
|
+
* - `ui/initialize` → respond with minimal context (theme +
|
|
10
|
+
* containerDimensions + locale).
|
|
11
|
+
* - `tools/call` → forward to the ggui server's
|
|
12
|
+
* `/mcp-apps/tools-call` endpoint. The server enforces source
|
|
13
|
+
* connector scoping + `_meta.ui.visibility: ['app']`.
|
|
14
|
+
* - any other method → respond with `method_not_supported`.
|
|
15
|
+
*
|
|
16
|
+
* Adapter-boundary rule (ENFORCED):
|
|
17
|
+
* - MCP Apps lifecycle messages from the embedded iframe do NOT
|
|
18
|
+
* mutate the outer ggui session state / contracts — the host
|
|
19
|
+
* never forwards them to ggui-server as session-level events.
|
|
20
|
+
* - `ui/initialize` intentionally DOES NOT forward outer ggui
|
|
21
|
+
* session state (stack, actionSpec, streamSpec) to the iframe.
|
|
22
|
+
*
|
|
23
|
+
* On detach, a host-initiated `ui/resource-teardown` notification
|
|
24
|
+
* is posted to the iframe BEFORE the element leaves the DOM so the
|
|
25
|
+
* embedded app can persist state / release resources cleanly. Post-
|
|
26
|
+
* detach the contentWindow is null in browsers + jsdom; we cache it
|
|
27
|
+
* at mount so the teardown message can still reach the view.
|
|
28
|
+
*
|
|
29
|
+
* Recursive mount safety: a foreign MCP App loaded inside this
|
|
30
|
+
* iframe can in turn render a `ui://ggui/session` resource via its
|
|
31
|
+
* own host implementation. That recursion stays bounded because
|
|
32
|
+
* each nested iframe runs the same adapter-boundary rule — outer
|
|
33
|
+
* ggui state never leaks into the inner renderer's session through
|
|
34
|
+
* this host path. Host-to-host sandboxing is browser-enforced by
|
|
35
|
+
* the iframe's `sandbox="allow-scripts"` attribute.
|
|
36
|
+
*/
|
|
37
|
+
import type { McpAppsStackItem } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
38
|
+
export interface McpAppIframeMountOptions {
|
|
39
|
+
readonly stackItem: McpAppsStackItem;
|
|
40
|
+
/** Session id — threaded into the proxy URL so server-side
|
|
41
|
+
* connector scoping works. */
|
|
42
|
+
readonly sessionId: string;
|
|
43
|
+
/**
|
|
44
|
+
* Base URL of the ggui server (origin). The host appends
|
|
45
|
+
* `/mcp-apps/resource` + `/mcp-apps/tools-call` to this. Should
|
|
46
|
+
* NOT have a trailing slash. Empty string = same-origin.
|
|
47
|
+
*/
|
|
48
|
+
readonly serverBaseUrl?: string;
|
|
49
|
+
/** Optional theme override for the `ui/initialize` context. */
|
|
50
|
+
readonly theme?: Readonly<Record<string, string>>;
|
|
51
|
+
/** Optional locale; defaults to `navigator.language` when
|
|
52
|
+
* available. */
|
|
53
|
+
readonly locale?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface McpAppIframeMount {
|
|
56
|
+
/** Tear down — posts `ui/resource-teardown` then removes the iframe. */
|
|
57
|
+
unmount(): void;
|
|
58
|
+
/** The mounted iframe element — exposed for test assertions. */
|
|
59
|
+
readonly element: HTMLIFrameElement;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Mount the MCP Apps iframe into `container`. The container is
|
|
63
|
+
* cleared; the iframe becomes its sole child.
|
|
64
|
+
*
|
|
65
|
+
* The host-bridge listener attaches to `window.message` globally;
|
|
66
|
+
* `source !== iframe.contentWindow` guard ensures we only respond
|
|
67
|
+
* to our own iframe's messages.
|
|
68
|
+
*/
|
|
69
|
+
export declare function mountMcpAppIframe(container: HTMLElement, opts: McpAppIframeMountOptions): McpAppIframeMount;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/** Default permissive-but-safe theme for iframes that don't override. */
|
|
2
|
+
const DEFAULT_THEME = {
|
|
3
|
+
'--color-primary': '#0284c7',
|
|
4
|
+
'--color-surface': '#ffffff',
|
|
5
|
+
'--color-text': '#111111',
|
|
6
|
+
'--font-family': 'system-ui, -apple-system, sans-serif',
|
|
7
|
+
'--border-radius-md': '8px',
|
|
8
|
+
};
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Helpers
|
|
11
|
+
// =============================================================================
|
|
12
|
+
function composeResourceUrl(opts) {
|
|
13
|
+
const base = opts.serverBaseUrl.replace(/\/$/, '');
|
|
14
|
+
const qs = new URLSearchParams({
|
|
15
|
+
session: opts.sessionId,
|
|
16
|
+
item: opts.itemId,
|
|
17
|
+
});
|
|
18
|
+
return `${base}/mcp-apps/resource?${qs.toString()}`;
|
|
19
|
+
}
|
|
20
|
+
function composeToolsCallUrl(serverBaseUrl) {
|
|
21
|
+
return `${serverBaseUrl.replace(/\/$/, '')}/mcp-apps/tools-call`;
|
|
22
|
+
}
|
|
23
|
+
function buildSandboxAttr() {
|
|
24
|
+
// MCP Apps hosts grant the minimum sandbox tokens needed. V1 keeps
|
|
25
|
+
// us permissive-enough for scripted + form-submitting content but
|
|
26
|
+
// does NOT grant same-origin (that would let the iframe read the
|
|
27
|
+
// host document). Ports the host-SDK version verbatim.
|
|
28
|
+
return 'allow-scripts allow-forms';
|
|
29
|
+
}
|
|
30
|
+
function buildAllowAttr(stackItem) {
|
|
31
|
+
const perms = stackItem.permissions;
|
|
32
|
+
if (!perms)
|
|
33
|
+
return undefined;
|
|
34
|
+
const parts = [];
|
|
35
|
+
if (perms.camera)
|
|
36
|
+
parts.push("camera 'self'");
|
|
37
|
+
if (perms.microphone)
|
|
38
|
+
parts.push("microphone 'self'");
|
|
39
|
+
if (perms.geolocation)
|
|
40
|
+
parts.push("geolocation 'self'");
|
|
41
|
+
if (perms.clipboardWrite)
|
|
42
|
+
parts.push("clipboard-write 'self'");
|
|
43
|
+
return parts.length > 0 ? parts.join('; ') : undefined;
|
|
44
|
+
}
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Mount
|
|
47
|
+
// =============================================================================
|
|
48
|
+
/**
|
|
49
|
+
* Mount the MCP Apps iframe into `container`. The container is
|
|
50
|
+
* cleared; the iframe becomes its sole child.
|
|
51
|
+
*
|
|
52
|
+
* The host-bridge listener attaches to `window.message` globally;
|
|
53
|
+
* `source !== iframe.contentWindow` guard ensures we only respond
|
|
54
|
+
* to our own iframe's messages.
|
|
55
|
+
*/
|
|
56
|
+
export function mountMcpAppIframe(container, opts) {
|
|
57
|
+
container.replaceChildren();
|
|
58
|
+
const serverBaseUrl = opts.serverBaseUrl ?? '';
|
|
59
|
+
const resourceUrl = composeResourceUrl({
|
|
60
|
+
serverBaseUrl,
|
|
61
|
+
sessionId: opts.sessionId,
|
|
62
|
+
itemId: opts.stackItem.id,
|
|
63
|
+
});
|
|
64
|
+
const toolsCallUrl = composeToolsCallUrl(serverBaseUrl);
|
|
65
|
+
const dims = {
|
|
66
|
+
width: opts.stackItem.containerDimensions?.width,
|
|
67
|
+
height: opts.stackItem.containerDimensions?.height,
|
|
68
|
+
maxWidth: opts.stackItem.containerDimensions?.maxWidth,
|
|
69
|
+
maxHeight: opts.stackItem.containerDimensions?.maxHeight,
|
|
70
|
+
};
|
|
71
|
+
const iframe = container.ownerDocument.createElement('iframe');
|
|
72
|
+
iframe.setAttribute('data-ggui-mcp-apps', 'iframe');
|
|
73
|
+
iframe.setAttribute('data-ggui-stack-item-id', opts.stackItem.id);
|
|
74
|
+
iframe.setAttribute('data-ggui-connector-id', opts.stackItem.source.connectorId);
|
|
75
|
+
iframe.src = resourceUrl;
|
|
76
|
+
iframe.title = opts.stackItem.description ?? 'MCP App';
|
|
77
|
+
iframe.setAttribute('sandbox', buildSandboxAttr());
|
|
78
|
+
const allowAttr = buildAllowAttr(opts.stackItem);
|
|
79
|
+
if (allowAttr !== undefined)
|
|
80
|
+
iframe.setAttribute('allow', allowAttr);
|
|
81
|
+
iframe.style.width = dims.width !== undefined ? `${dims.width}px` : '100%';
|
|
82
|
+
iframe.style.height = dims.height !== undefined ? `${dims.height}px` : '480px';
|
|
83
|
+
iframe.style.maxWidth = dims.maxWidth !== undefined ? `${dims.maxWidth}px` : '100%';
|
|
84
|
+
if (dims.maxHeight !== undefined)
|
|
85
|
+
iframe.style.maxHeight = `${dims.maxHeight}px`;
|
|
86
|
+
iframe.style.border = '1px solid #e5e5e5';
|
|
87
|
+
iframe.style.borderRadius = '8px';
|
|
88
|
+
iframe.style.display = 'block';
|
|
89
|
+
container.appendChild(iframe);
|
|
90
|
+
// Cache contentWindow — on detach, `iframe.contentWindow` is null
|
|
91
|
+
// so the cached handle is the only way `ui/resource-teardown` can
|
|
92
|
+
// still reach the view.
|
|
93
|
+
let cachedIframeWindow = iframe.contentWindow;
|
|
94
|
+
const listener = (ev) => {
|
|
95
|
+
void handleHostBridgeRequest(ev);
|
|
96
|
+
};
|
|
97
|
+
async function handleHostBridgeRequest(ev) {
|
|
98
|
+
// Adapter-boundary guard: only our own iframe's contentWindow is
|
|
99
|
+
// allowed. This matches the ref-guard in
|
|
100
|
+
// McpAppsStackItemRenderer.tsx.
|
|
101
|
+
if (iframe.contentWindow === null || ev.source !== iframe.contentWindow)
|
|
102
|
+
return;
|
|
103
|
+
const req = ev.data;
|
|
104
|
+
if (req === null || typeof req !== 'object')
|
|
105
|
+
return;
|
|
106
|
+
if (req.jsonrpc !== '2.0' || typeof req.method !== 'string')
|
|
107
|
+
return;
|
|
108
|
+
const id = req.id ?? 0;
|
|
109
|
+
let response;
|
|
110
|
+
switch (req.method) {
|
|
111
|
+
case 'ui/initialize': {
|
|
112
|
+
response = {
|
|
113
|
+
jsonrpc: '2.0',
|
|
114
|
+
id,
|
|
115
|
+
result: {
|
|
116
|
+
theme: opts.theme ?? DEFAULT_THEME,
|
|
117
|
+
containerDimensions: dims,
|
|
118
|
+
locale: opts.locale ??
|
|
119
|
+
(typeof navigator !== 'undefined' ? navigator.language : 'en-US'),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case 'tools/call': {
|
|
125
|
+
const tool = typeof req.params?.['name'] === 'string' ? req.params['name'] : '';
|
|
126
|
+
const rawArgs = req.params?.['arguments'];
|
|
127
|
+
const args = rawArgs !== null && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
128
|
+
? rawArgs
|
|
129
|
+
: {};
|
|
130
|
+
if (tool.length === 0) {
|
|
131
|
+
response = {
|
|
132
|
+
jsonrpc: '2.0',
|
|
133
|
+
id,
|
|
134
|
+
error: { code: -32602, message: 'tools/call requires params.name' },
|
|
135
|
+
};
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const resp = await fetch(toolsCallUrl, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: { 'Content-Type': 'application/json' },
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
session: opts.sessionId,
|
|
144
|
+
item: opts.stackItem.id,
|
|
145
|
+
tool,
|
|
146
|
+
arguments: args,
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
if (!resp.ok) {
|
|
150
|
+
const body = (await resp.json().catch(() => ({})));
|
|
151
|
+
response = {
|
|
152
|
+
jsonrpc: '2.0',
|
|
153
|
+
id,
|
|
154
|
+
error: {
|
|
155
|
+
code: resp.status === 403 ? -32003 : -32000,
|
|
156
|
+
message: body.error ?? `proxy_error_${resp.status}`,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const result = (await resp.json());
|
|
162
|
+
response = { jsonrpc: '2.0', id, result };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
response = {
|
|
167
|
+
jsonrpc: '2.0',
|
|
168
|
+
id,
|
|
169
|
+
error: {
|
|
170
|
+
code: -32000,
|
|
171
|
+
message: `proxy_fetch_failed: ${String(err)}`,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
default: {
|
|
178
|
+
response = {
|
|
179
|
+
jsonrpc: '2.0',
|
|
180
|
+
id,
|
|
181
|
+
error: { code: -32601, message: 'method_not_supported' },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
iframe.contentWindow?.postMessage(response, '*');
|
|
186
|
+
}
|
|
187
|
+
window.addEventListener('message', listener);
|
|
188
|
+
return {
|
|
189
|
+
element: iframe,
|
|
190
|
+
unmount() {
|
|
191
|
+
window.removeEventListener('message', listener);
|
|
192
|
+
// Host-initiated `ui/resource-teardown` — posted BEFORE the
|
|
193
|
+
// iframe leaves the DOM so its contentWindow is still live.
|
|
194
|
+
// Post-detach the contentWindow is null; the cached handle is
|
|
195
|
+
// a belt-and-suspenders fallback.
|
|
196
|
+
const win = iframe.contentWindow ?? cachedIframeWindow;
|
|
197
|
+
try {
|
|
198
|
+
win?.postMessage({
|
|
199
|
+
jsonrpc: '2.0',
|
|
200
|
+
method: 'ui/resource-teardown',
|
|
201
|
+
params: { reason: 'host_unmount' },
|
|
202
|
+
}, '*');
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Best-effort — teardown notification is a courtesy, not a
|
|
206
|
+
// correctness contract. A thrown postMessage (detached win)
|
|
207
|
+
// silently falls through to element removal.
|
|
208
|
+
}
|
|
209
|
+
cachedIframeWindow = null;
|
|
210
|
+
if (iframe.parentNode !== null)
|
|
211
|
+
iframe.parentNode.removeChild(iframe);
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability events emitted by the renderer iframe → surfaced to
|
|
3
|
+
* the MCP Apps host via the `<McpAppIframe>` wrapper's `onObserve`
|
|
4
|
+
* prop. Complementary to `ProtocolError`:
|
|
5
|
+
*
|
|
6
|
+
* - `ProtocolError` fires on FAILURES (typed classification of a
|
|
7
|
+
* protocol violation or transport error).
|
|
8
|
+
* - `ObservabilityEvent` fires on HAPPY PATHS + failures alike
|
|
9
|
+
* (telemetry + inspection signal the host can display in a
|
|
10
|
+
* SessionInspector-style view).
|
|
11
|
+
*
|
|
12
|
+
* The host wrapper passes events through to `onObserve` opaquely.
|
|
13
|
+
*
|
|
14
|
+
* Design:
|
|
15
|
+
*
|
|
16
|
+
* - Discriminated union on `kind`. Each arm carries only the fields
|
|
17
|
+
* the event semantically requires — no optional everything-bags.
|
|
18
|
+
* - Extensibly closed via the `(string & {})` tail on `kind`. Adding
|
|
19
|
+
* a new arm does NOT bump the protocol version —
|
|
20
|
+
* hosts built against older typings fall through to the unknown
|
|
21
|
+
* branch and MUST render gracefully (e.g., `JSON.stringify(event)`
|
|
22
|
+
* fallback in an inspector row).
|
|
23
|
+
* - `@ggui-ai/iframe-runtime` owns this union, NOT `@ggui-ai/protocol`.
|
|
24
|
+
* Observability events are a renderer ↔ host implementation seam,
|
|
25
|
+
* not a wire-format contract between arbitrary protocol peers, so
|
|
26
|
+
* they stay out of the protocol package.
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export type ObservabilityEvent = WiredToolInvokedEvent | ContractErrorEmittedEvent | SchemaVersionMismatchEvent | SubscribeFailedEvent | AuthRequiredEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | UnknownObservabilityEvent;
|
|
31
|
+
/**
|
|
32
|
+
* Fired when a wired action successfully dispatched to a registered
|
|
33
|
+
* MCP tool. Hosts surface this as a dispatch row in SessionInspector.
|
|
34
|
+
* Emitted by the server-side router (session-channel.ts) via
|
|
35
|
+
* postMessage relay through the renderer.
|
|
36
|
+
*
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export interface WiredToolInvokedEvent {
|
|
40
|
+
readonly kind: 'wired-tool-invoked';
|
|
41
|
+
readonly toolName: string;
|
|
42
|
+
readonly actionName?: string;
|
|
43
|
+
readonly dispatchedAt: string;
|
|
44
|
+
readonly latencyMs?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fired when the server emits a `_ggui:contract-error` envelope on
|
|
48
|
+
* the live channel. Mirrors the envelope's `code` field for host-side
|
|
49
|
+
* inspection without requiring the host to parse channel frames.
|
|
50
|
+
*
|
|
51
|
+
* @public
|
|
52
|
+
*/
|
|
53
|
+
export interface ContractErrorEmittedEvent {
|
|
54
|
+
readonly kind: 'contract-error-emitted';
|
|
55
|
+
readonly code: string;
|
|
56
|
+
readonly toolName: string;
|
|
57
|
+
readonly actionName?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Fired when the protocol-version handshake rejects the connection.
|
|
61
|
+
* Parallel to `UpgradeRequiredError` — the host can choose to render
|
|
62
|
+
* an inline upgrade prompt instead of treating it as a terminal
|
|
63
|
+
* failure.
|
|
64
|
+
*
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
67
|
+
export interface SchemaVersionMismatchEvent {
|
|
68
|
+
readonly kind: 'schema-version-mismatch';
|
|
69
|
+
readonly observedVersion: string;
|
|
70
|
+
readonly acceptedVersions: readonly string[];
|
|
71
|
+
readonly observedBy: 'client' | 'server';
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Fired when subscribe fails for any reason the renderer classifies
|
|
75
|
+
* as non-fatal to the session (e.g., transient network jitter that
|
|
76
|
+
* the reconnect ladder is handling). Terminal subscribe failures
|
|
77
|
+
* still route through `ProtocolError`.
|
|
78
|
+
*
|
|
79
|
+
* @public
|
|
80
|
+
*/
|
|
81
|
+
export interface SubscribeFailedEvent {
|
|
82
|
+
readonly kind: 'subscribe-failed';
|
|
83
|
+
readonly reason: string;
|
|
84
|
+
readonly message?: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Fired when the server emits a `system` frame with
|
|
88
|
+
* `action: 'auth_required'` — the agent needs the end-user to authorize
|
|
89
|
+
* an OAuth service (Google, Slack, etc.) before a wired tool can
|
|
90
|
+
* proceed. The host listens via `<McpAppIframe onObserve>` and renders
|
|
91
|
+
* a consent overlay OUTSIDE the iframe, then redirects the user to
|
|
92
|
+
* `authUrl` to complete the OAuth flow.
|
|
93
|
+
*
|
|
94
|
+
* Observability-only — the wire signal is already fully typed on
|
|
95
|
+
* `@ggui-ai/protocol`'s `SystemPayload` (live-channel.ts). This event is a
|
|
96
|
+
* renderer↔host projection of that payload, not a new protocol surface,
|
|
97
|
+
* matching the vendor-neutral-separation posture of every
|
|
98
|
+
* other `ObservabilityEvent` kind.
|
|
99
|
+
*
|
|
100
|
+
* Fields map 1:1 from `SystemPayload`:
|
|
101
|
+
* - `provider` ← `serviceId` (canonical identifier the credential
|
|
102
|
+
* store keys off — e.g., `"google"`, `"slack"`).
|
|
103
|
+
* - `authUrl` ← `consentUrl` (URL the host opens in a popup / new tab
|
|
104
|
+
* to initiate the OAuth consent flow).
|
|
105
|
+
* - `displayName` / `scopes` / `message` are optional hints the host
|
|
106
|
+
* can surface in the consent overlay; absent means "render with
|
|
107
|
+
* generic copy".
|
|
108
|
+
*
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
export interface AuthRequiredEvent {
|
|
112
|
+
readonly kind: 'auth-required';
|
|
113
|
+
/**
|
|
114
|
+
* Canonical identifier of the service needing authorization (e.g.
|
|
115
|
+
* `"google"`, `"slack"`). Maps to `SystemPayload.serviceId`.
|
|
116
|
+
*/
|
|
117
|
+
readonly provider: string;
|
|
118
|
+
/**
|
|
119
|
+
* URL the host opens in a popup / new tab to initiate the OAuth
|
|
120
|
+
* consent flow. Maps to `SystemPayload.consentUrl`.
|
|
121
|
+
*/
|
|
122
|
+
readonly authUrl: string;
|
|
123
|
+
/**
|
|
124
|
+
* Human-readable service name (e.g. `"Google"`, `"Slack"`). Optional
|
|
125
|
+
* — absent when the server doesn't supply one.
|
|
126
|
+
*/
|
|
127
|
+
readonly displayName?: string;
|
|
128
|
+
/**
|
|
129
|
+
* OAuth scopes the agent is requesting. Optional.
|
|
130
|
+
*/
|
|
131
|
+
readonly scopes?: readonly string[];
|
|
132
|
+
/**
|
|
133
|
+
* Human-readable message explaining why access is needed. Optional.
|
|
134
|
+
*/
|
|
135
|
+
readonly message?: string;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Fired by the channel-transport router when it picks a transport
|
|
139
|
+
* for a `streamSpec[ch].source.tool` channel. Hosts can inspect the
|
|
140
|
+
* WS-vs-poll decision per channel in the SessionInspector activity
|
|
141
|
+
* feed.
|
|
142
|
+
*
|
|
143
|
+
* @public
|
|
144
|
+
*/
|
|
145
|
+
export interface ChannelTransportPickedEvent {
|
|
146
|
+
readonly kind: 'channel-transport-picked';
|
|
147
|
+
readonly stackItemId: string;
|
|
148
|
+
readonly channelName: string;
|
|
149
|
+
readonly transport: 'ws' | 'poll';
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Fired by the channel-transport router when a WS-bound channel
|
|
153
|
+
* falls back to iframe polling. `'ws-disconnect'`
|
|
154
|
+
* = WS dropped, transient. `'channel-not-local'` = server explicitly
|
|
155
|
+
* said it can't subscribe-for this tool (sticky for the channel's
|
|
156
|
+
* lifetime).
|
|
157
|
+
*
|
|
158
|
+
* @public
|
|
159
|
+
*/
|
|
160
|
+
export interface ChannelTransportFallbackEvent {
|
|
161
|
+
readonly kind: 'channel-transport-fallback';
|
|
162
|
+
readonly stackItemId: string;
|
|
163
|
+
readonly channelName: string;
|
|
164
|
+
readonly reason: 'ws-disconnect' | 'channel-not-local';
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Fired by the channel-transport router when it re-sends
|
|
168
|
+
* `channel_subscribe` for a channel after the WS reconnects.
|
|
169
|
+
*
|
|
170
|
+
* @public
|
|
171
|
+
*/
|
|
172
|
+
export interface ChannelTransportResubscribedEvent {
|
|
173
|
+
readonly kind: 'channel-transport-resubscribed';
|
|
174
|
+
readonly stackItemId: string;
|
|
175
|
+
readonly channelName: string;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Catch-all branch for event kinds the host's typings don't recognize.
|
|
179
|
+
* Lets the union stay extensible without forcing protocol-version
|
|
180
|
+
* bumps when new kinds are added. Hosts MUST render unknown events
|
|
181
|
+
* gracefully — default: show as raw JSON in the inspector.
|
|
182
|
+
*
|
|
183
|
+
* @public
|
|
184
|
+
*/
|
|
185
|
+
export interface UnknownObservabilityEvent {
|
|
186
|
+
readonly kind: string & {};
|
|
187
|
+
readonly [field: string]: unknown;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Shape of the postMessage envelope the renderer emits to its parent
|
|
191
|
+
* when an observability event fires. `<McpAppIframe>` listens for this
|
|
192
|
+
* and forwards the `event` field to `onObserve`.
|
|
193
|
+
*
|
|
194
|
+
* @public
|
|
195
|
+
*/
|
|
196
|
+
export interface ObservabilityMessage {
|
|
197
|
+
readonly type: 'ggui:observe';
|
|
198
|
+
readonly event: ObservabilityEvent;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Caller sink for every {@link ObservabilityEvent} the renderer
|
|
202
|
+
* classifies. Mirrors the {@link import('./protocol-error.js').ProtocolErrorEmitter}
|
|
203
|
+
* posture — injection slot so tests can record, production binds a
|
|
204
|
+
* postMessage-to-parent default.
|
|
205
|
+
*
|
|
206
|
+
* Handlers MUST NOT throw. Observability is fire-and-forget — the
|
|
207
|
+
* renderer has already completed the observed side-effect by the time
|
|
208
|
+
* the emitter runs; a throwing handler would mask the real signal.
|
|
209
|
+
*
|
|
210
|
+
* @public
|
|
211
|
+
*/
|
|
212
|
+
export type ObservabilityEmitter = (event: ObservabilityEvent) => void;
|
|
213
|
+
/**
|
|
214
|
+
* Default emitter — posts an {@link ObservabilityMessage} to
|
|
215
|
+
* `window.parent`. Mirrors `postBootFailure` / `postRendererReady`
|
|
216
|
+
* posture in `runtime.ts`: swallows postMessage failure so a detached
|
|
217
|
+
* parent doesn't crash the iframe.
|
|
218
|
+
*
|
|
219
|
+
* Lives here (not in `runtime.ts`) so tests exercising individual
|
|
220
|
+
* emission sites can import the same default without dragging in the
|
|
221
|
+
* runtime's module side-effects.
|
|
222
|
+
*
|
|
223
|
+
* @public
|
|
224
|
+
*/
|
|
225
|
+
export declare function postObservabilityToParent(event: ObservabilityEvent): void;
|