@ggui-ai/mcp-apps-react-native 0.0.1-placeholder → 0.9.0

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.
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Platform-agnostic JSON-RPC dispatch for `<McpAppIframe>` on React
3
+ * Native — the canonical RN MCP-Apps host primitive. The web side
4
+ * retired its `<McpAppIframe>` in favor of `<AppRenderer>` from
5
+ * `@mcp-ui/client`, which is built around iframes + a two-iframe
6
+ * sandbox-proxy model that has no meaningful WebView equivalent;
7
+ * `<McpAppIframe>` on RN stays as the host primitive for mobile.
8
+ *
9
+ * The host responds to:
10
+ *
11
+ * - `ping` → `{ok: true, pong: true}`.
12
+ * - `ui/initialize` → a spec-canonical `McpUiInitializeResult`
13
+ * whose `hostContext` carries `{locale, containerDimensions}`
14
+ * ONLY — the adapter-boundary rule (no outer-app state leaks).
15
+ * NEVER carries `toolOutput._meta` — see "Reading-B retired"
16
+ * note below.
17
+ * - `ui/open-link` with http(s) URLs → caller opens externally;
18
+ * other schemes → reject `unsupported-scheme`.
19
+ * - `tools/call` → caller-provided handler, or reject
20
+ * `no-tool-handler` when none.
21
+ * - any other method → `method_not_supported`.
22
+ *
23
+ * Notifications (no `id`) return `null` — the caller MUST NOT post a
24
+ * response back to the iframe.
25
+ *
26
+ * ── Reading-B retired (parity with iframe-runtime Phase 1.19b.3) ─────
27
+ *
28
+ * The legacy "stuff the `ai.ggui/render` slice on `ui/initialize`
29
+ * result.toolOutput._meta" path is retired here. iframe-runtime's
30
+ * App-class adoption means `App.connect()` does NOT expose
31
+ * `result.toolOutput`, so any meta crammed into the initialize
32
+ * response is silently dropped on the renderer side.
33
+ *
34
+ * The spec-canonical delivery channel is now {@link
35
+ * buildToolResultNotification} — a `ui/notifications/tool-result`
36
+ * notification (JSON-RPC) wrapping a `CallToolResult` whose `_meta`
37
+ * carries the single `ai.ggui/render` slice. The McpAppIframe host
38
+ * sends this notification immediately after the
39
+ * `ui/initialize` response when `meta` is supplied, so the renderer's
40
+ * `awaitToolResultMeta` window-message listener (autostart layer) or
41
+ * its App-mediated `toolresult` event catches the meta and boots the
42
+ * render. Wire shape matches what `@mcp-ui/client`'s `<AppRenderer
43
+ * toolResult={...}>` forwards on web.
44
+ *
45
+ * The shared host-role switch (`handleHostBridgeRequest`) lives in the
46
+ * sibling `components/mcp-apps-bridge.ts`; it covers the same methods
47
+ * but bakes in the ggui-server `/mcp-apps/tools-call` proxy URL,
48
+ * whereas this dispatcher leaves `tools/call` to a caller-supplied
49
+ * handler so the iframe stays generic across hosts.
50
+ */
51
+
52
+ import {
53
+ MCP_APP_BOOTSTRAP_FAILED_TYPE,
54
+ MCP_APP_LIFECYCLE_TYPE,
55
+ MCP_APP_OBSERVE_TYPE,
56
+ toMcpAppEnvelope,
57
+ type McpAppAiGguiRenderMeta,
58
+ } from '@ggui-ai/protocol/integrations/mcp-apps';
59
+ import {
60
+ LATEST_PROTOCOL_VERSION,
61
+ McpUiUpdateModelContextRequestSchema,
62
+ type McpUiInitializeResult,
63
+ } from '@modelcontextprotocol/ext-apps';
64
+ import type {
65
+ McpAppIframeDimensions,
66
+ McpAppIframeProps,
67
+ } from './types.js';
68
+
69
+ export interface HostBridgeRequest {
70
+ readonly jsonrpc?: '2.0';
71
+ readonly id?: number | string;
72
+ readonly method?: string;
73
+ readonly params?: Record<string, unknown>;
74
+ }
75
+
76
+ export interface HostBridgeResponse {
77
+ readonly jsonrpc: '2.0';
78
+ readonly id: number | string;
79
+ readonly result?: Record<string, unknown>;
80
+ readonly error?: { readonly code: number; readonly message: string };
81
+ }
82
+
83
+ export interface HostBridgeNotification {
84
+ readonly jsonrpc: '2.0';
85
+ readonly method: string;
86
+ readonly params?: Record<string, unknown>;
87
+ }
88
+
89
+ export interface HostBridgeContext {
90
+ readonly locale: string;
91
+ readonly containerDimensions: McpAppIframeDimensions;
92
+ readonly openLink: (url: string) => Promise<void> | void;
93
+ readonly onToolCall?: McpAppIframeProps['onToolCall'];
94
+ readonly onUpdateModelContext?: McpAppIframeProps['onUpdateModelContext'];
95
+ }
96
+
97
+ function isJsonRpcRequest(value: unknown): value is HostBridgeRequest {
98
+ if (value === null || typeof value !== 'object') return false;
99
+ const v = value as { jsonrpc?: unknown; method?: unknown };
100
+ return v.jsonrpc === '2.0' && typeof v.method === 'string';
101
+ }
102
+
103
+ function paramString(
104
+ params: HostBridgeRequest['params'],
105
+ key: string,
106
+ ): string {
107
+ if (!params) return '';
108
+ const v = params[key];
109
+ return typeof v === 'string' ? v : '';
110
+ }
111
+
112
+ function paramObject(
113
+ params: HostBridgeRequest['params'],
114
+ key: string,
115
+ ): Record<string, unknown> {
116
+ if (!params) return {};
117
+ const v = params[key];
118
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) return {};
119
+ return v as Record<string, unknown>;
120
+ }
121
+
122
+ /**
123
+ * Host-role method dispatcher. Returns a JSON-RPC response, or `null`
124
+ * when the request is malformed / a notification. Pure function —
125
+ * testable without a WebView present.
126
+ */
127
+ export async function dispatchHostBridgeRequest(
128
+ req: HostBridgeRequest,
129
+ ctx: HostBridgeContext,
130
+ ): Promise<HostBridgeResponse | null> {
131
+ if (!isJsonRpcRequest(req)) return null;
132
+ if (req.id === undefined) return null;
133
+
134
+ const id = req.id;
135
+
136
+ switch (req.method) {
137
+ case 'ping': {
138
+ return { jsonrpc: '2.0', id, result: { ok: true, pong: true } };
139
+ }
140
+ case 'ui/initialize': {
141
+ // Spec-canonical `McpUiInitializeResult`. The `@modelcontextprotocol/
142
+ // ext-apps` `App.connect` running inside the WebView zod-validates
143
+ // this response, and `protocolVersion` + `hostInfo` +
144
+ // `hostCapabilities` + `hostContext` are ALL required — the
145
+ // pre-App draft shape (`{theme, containerDimensions, locale}` at
146
+ // the top level) fails that gate and kills every mount before
147
+ // the renderer boots.
148
+ //
149
+ // ADAPTER BOUNDARY. `hostContext` carries `{locale,
150
+ // containerDimensions}` ONLY — no outer-app state leaks into
151
+ // the iframe. ggui theming rides the `ai.ggui/render.theme`
152
+ // slice (two-layer theming), never this handshake; the spec's
153
+ // `hostContext.styles` variable set is a closed union of
154
+ // spec-defined keys that ggui's overlay deliberately does not
155
+ // claim. First-party `ai.ggui/render` meta is delivered via the
156
+ // separate spec-canonical `ui/notifications/tool-result`
157
+ // notification (see {@link buildToolResultNotification}), NOT
158
+ // via this initialize response.
159
+ const requested = paramString(req.params, 'protocolVersion');
160
+ const result: McpUiInitializeResult = {
161
+ protocolVersion: requested.length > 0 ? requested : LATEST_PROTOCOL_VERSION,
162
+ // `hostInfo.version` is diagnostic-only; this tsc-built package
163
+ // has no build-stamp machinery (unlike iframe-runtime's esbuild
164
+ // `define`), and a hand-maintained literal would silently drift
165
+ // from the published version at every release.
166
+ hostInfo: { name: 'ggui-react-native', version: 'unstamped' },
167
+ hostCapabilities: {},
168
+ hostContext: {
169
+ locale: ctx.locale,
170
+ containerDimensions: ctx.containerDimensions,
171
+ },
172
+ };
173
+ return { jsonrpc: '2.0', id, result };
174
+ }
175
+ case 'ui/open-link': {
176
+ const url = paramString(req.params, 'url');
177
+ if (!/^https?:\/\//i.test(url)) {
178
+ return {
179
+ jsonrpc: '2.0',
180
+ id,
181
+ error: { code: -32602, message: 'unsupported-scheme' },
182
+ };
183
+ }
184
+ try {
185
+ await ctx.openLink(url);
186
+ return { jsonrpc: '2.0', id, result: { opened: true } };
187
+ } catch (err) {
188
+ return {
189
+ jsonrpc: '2.0',
190
+ id,
191
+ error: {
192
+ code: -32000,
193
+ message: `open_link_failed: ${String(err)}`,
194
+ },
195
+ };
196
+ }
197
+ }
198
+ case 'tools/call': {
199
+ if (ctx.onToolCall === undefined) {
200
+ return {
201
+ jsonrpc: '2.0',
202
+ id,
203
+ error: { code: -32000, message: 'no-tool-handler' },
204
+ };
205
+ }
206
+ const tool = paramString(req.params, 'name');
207
+ if (tool.length === 0) {
208
+ return {
209
+ jsonrpc: '2.0',
210
+ id,
211
+ error: { code: -32602, message: 'tools/call requires params.name' },
212
+ };
213
+ }
214
+ const args = paramObject(req.params, 'arguments');
215
+ try {
216
+ const result: unknown = await ctx.onToolCall(tool, args);
217
+ const wrapped: Record<string, unknown> =
218
+ result !== null && typeof result === 'object' && !Array.isArray(result)
219
+ ? (result as Record<string, unknown>)
220
+ : { value: result };
221
+ return { jsonrpc: '2.0', id, result: wrapped };
222
+ } catch (err) {
223
+ return {
224
+ jsonrpc: '2.0',
225
+ id,
226
+ error: {
227
+ code: -32000,
228
+ message: `tool_call_failed: ${String(err)}`,
229
+ },
230
+ };
231
+ }
232
+ }
233
+ case 'ui/update-model-context': {
234
+ // App → host context contribution (the renderer sends this on
235
+ // interaction). Forwarded to the caller when a handler is wired;
236
+ // WITHOUT one the honest answer stays `method_not_supported` —
237
+ // acking context we did not carry anywhere would mislead the app
238
+ // (first-integrator report, ggui#425 fourth finding).
239
+ if (ctx.onUpdateModelContext === undefined) {
240
+ return {
241
+ jsonrpc: '2.0',
242
+ id,
243
+ error: { code: -32601, message: 'method_not_supported' },
244
+ };
245
+ }
246
+ const parsed = McpUiUpdateModelContextRequestSchema.safeParse({
247
+ method: 'ui/update-model-context',
248
+ params: req.params,
249
+ });
250
+ if (!parsed.success) {
251
+ return {
252
+ jsonrpc: '2.0',
253
+ id,
254
+ error: { code: -32602, message: 'invalid ui/update-model-context params' },
255
+ };
256
+ }
257
+ await ctx.onUpdateModelContext(parsed.data.params);
258
+ return { jsonrpc: '2.0', id, result: {} };
259
+ }
260
+ default: {
261
+ return {
262
+ jsonrpc: '2.0',
263
+ id,
264
+ error: { code: -32601, message: 'method_not_supported' },
265
+ };
266
+ }
267
+ }
268
+ }
269
+
270
+ // =============================================================================
271
+ // Renderer → host envelope classification
272
+ // =============================================================================
273
+
274
+ export type RendererEnvelopeTag =
275
+ | 'bootstrap-failed'
276
+ | 'observability'
277
+ | 'lifecycle'
278
+ | 'jsonrpc'
279
+ | 'unknown';
280
+
281
+ /**
282
+ * Classify a raw message payload received from the WebView. The
283
+ * platform host routes each tag to the matching callback prop. The
284
+ * recognized `type` discriminators are the protocol-owned renderer →
285
+ * host envelope vocabulary (`@ggui-ai/protocol/integrations/mcp-apps`)
286
+ * — this classifier recognizes exactly the tags renderers emit, no
287
+ * phantom arms. (`ggui:renderer-ready` is deliberately untagged: it is
288
+ * an optional informational signal this host has no callback for, so
289
+ * it falls through to `unknown` and is dropped.)
290
+ */
291
+ export function classifyRendererEnvelope(data: unknown): RendererEnvelopeTag {
292
+ if (data === null || typeof data !== 'object') return 'unknown';
293
+ const d = data as { type?: unknown; jsonrpc?: unknown; method?: unknown };
294
+ if (typeof d.type === 'string') {
295
+ switch (d.type) {
296
+ case MCP_APP_BOOTSTRAP_FAILED_TYPE:
297
+ return 'bootstrap-failed';
298
+ case MCP_APP_OBSERVE_TYPE:
299
+ return 'observability';
300
+ case MCP_APP_LIFECYCLE_TYPE:
301
+ return 'lifecycle';
302
+ default:
303
+ break;
304
+ }
305
+ }
306
+ if (d.jsonrpc === '2.0' && typeof d.method === 'string') return 'jsonrpc';
307
+ return 'unknown';
308
+ }
309
+
310
+ export function buildDispatchActionNotification(
311
+ name: string,
312
+ data: unknown,
313
+ ): HostBridgeNotification {
314
+ return {
315
+ jsonrpc: '2.0',
316
+ method: name,
317
+ params: { data },
318
+ };
319
+ }
320
+
321
+ export function buildResourceTeardownNotification(): HostBridgeNotification {
322
+ return {
323
+ jsonrpc: '2.0',
324
+ method: 'ui/resource-teardown',
325
+ params: { reason: 'host_unmount' },
326
+ };
327
+ }
328
+
329
+ /**
330
+ * Build a spec-canonical `ui/notifications/tool-result` JSON-RPC
331
+ * notification carrying the `ai.ggui/render` slice on `params._meta`.
332
+ *
333
+ * Wire shape:
334
+ * ```
335
+ * {
336
+ * jsonrpc: '2.0',
337
+ * method: 'ui/notifications/tool-result',
338
+ * params: { // CallToolResult per MCP spec
339
+ * content: [],
340
+ * structuredContent: {},
341
+ * _meta: {
342
+ * 'ai.ggui/render': { sessionId, appId, runtimeUrl, ... },
343
+ * },
344
+ * },
345
+ * }
346
+ * ```
347
+ *
348
+ * Mirrors what `@mcp-ui/client`'s `<AppRenderer toolResult={...}>`
349
+ * forwards to its inner iframe on web (per MCP-Apps SEP-1865 and
350
+ * `McpUiToolResultNotification` in
351
+ * `@modelcontextprotocol/ext-apps/spec.types`). The renderer's
352
+ * `parseMetaFromToolResult` extractor
353
+ * (`packages/iframe-runtime/src/meta-parse.ts`) reads `params._meta`
354
+ * exactly.
355
+ *
356
+ * Sent immediately after the `ui/initialize` response when the host
357
+ * was given a `meta` prop, so the renderer's pre-handshake
358
+ * `awaitToolResultMeta` listener (Tier 2 in `bootSequence`) catches
359
+ * it and boots the render.
360
+ */
361
+ export function buildToolResultNotification(
362
+ meta: McpAppAiGguiRenderMeta,
363
+ ): HostBridgeNotification {
364
+ return {
365
+ jsonrpc: '2.0',
366
+ method: 'ui/notifications/tool-result',
367
+ params: {
368
+ content: [],
369
+ structuredContent: {},
370
+ _meta: toMcpAppEnvelope(meta),
371
+ },
372
+ };
373
+ }
374
+
375
+ // =============================================================================
376
+ // Resource → WebView mount-source derivation
377
+ // =============================================================================
378
+
379
+ /**
380
+ * RN WebView `source` shape. Matches react-native-webview's `source`
381
+ * prop.
382
+ */
383
+ export type ResourceWebViewSource =
384
+ | { readonly html: string; readonly baseUrl?: string }
385
+ | { readonly uri: string };
386
+
387
+ /**
388
+ * Compute the WebView `source` from an MCP Apps `ResourceContents`.
389
+ * Mirrors the web version's `deriveResourceMountSource` decision tree:
390
+ *
391
+ * 1. `text` present → `source={{html: text}}` (inline HTML;
392
+ * opaque origin, safest path).
393
+ * 2. `blob` + `mimeType` → data-URL fallback (native WebView treats
394
+ * this as a top-level URL load). Opaque origin too.
395
+ * 3. Else → `source={{uri}}` IF `uri` is http(s); else `null`
396
+ * (caller renders an empty WebView + emits a bootstrap failure).
397
+ */
398
+ export function deriveResourceMountSource(resource: {
399
+ readonly uri: string;
400
+ readonly mimeType?: string;
401
+ readonly text?: string;
402
+ readonly blob?: string;
403
+ }): ResourceWebViewSource | null {
404
+ if (typeof resource.text === 'string' && resource.text.length > 0) {
405
+ return { html: resource.text };
406
+ }
407
+ if (typeof resource.blob === 'string' && resource.blob.length > 0) {
408
+ const mime =
409
+ typeof resource.mimeType === 'string' && resource.mimeType.length > 0
410
+ ? resource.mimeType
411
+ : 'text/html';
412
+ return { uri: `data:${mime};base64,${resource.blob}` };
413
+ }
414
+ if (typeof resource.uri === 'string' && /^https?:\/\//i.test(resource.uri)) {
415
+ return { uri: resource.uri };
416
+ }
417
+ return null;
418
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `<McpAppIframe>` barrel — generic MCP Apps iframe host for React
3
+ * Native.
4
+ *
5
+ * Re-exported at the package root (`@ggui-ai/mcp-apps-react-native`) —
6
+ * consumers import from the root barrel, not this path.
7
+ */
8
+
9
+ export { McpAppIframe } from './McpAppIframe.js';
10
+ export type {
11
+ McpAppIframeDimensions,
12
+ McpAppIframePermissions,
13
+ McpAppIframeProps,
14
+ McpAppIframeRef,
15
+ } from './types.js';
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Public props + imperative-ref shape for `<McpAppIframe>` on React
3
+ * Native — the canonical MCP-Apps WebView host exported from
4
+ * `@ggui-ai/mcp-apps-react-native`.
5
+ *
6
+ * The web side retired `<McpAppIframe>` in favor of `<AppRenderer>`
7
+ * from `@mcp-ui/client`. That migration relied on a two-iframe
8
+ * sandbox-proxy architecture (cross-origin iframe nesting) that has
9
+ * no meaningful WebView equivalent — a `react-native-webview` WebView
10
+ * is itself a top-level browser surface, not a nestable origin. So on
11
+ * RN, `<McpAppIframe>` IS the spec-canonical host primitive.
12
+ *
13
+ * Wire shape parity with `<AppRenderer>` is preserved on every other
14
+ * surface: spec-canonical `ui/initialize` handshake (adapter-boundary
15
+ * response), `ui/notifications/tool-result` for render-meta delivery,
16
+ * `tools/call` outbound routing, `ui/open-link` delegated to
17
+ * `Linking.openURL`, `ui/resource-teardown` notification at unmount,
18
+ * and observability / lifecycle envelopes posted upward.
19
+ */
20
+
21
+ import type { ResourceContents } from '@modelcontextprotocol/sdk/types.js';
22
+ import type {
23
+ ObservabilityEvent,
24
+ ProtocolError,
25
+ } from '@ggui-ai/iframe-runtime';
26
+ import type {
27
+ McpAppAiGguiRenderMeta,
28
+ McpAppLifecycleEvent,
29
+ } from '@ggui-ai/protocol/integrations/mcp-apps';
30
+
31
+ /**
32
+ * Container-dimensions hint forwarded verbatim to the iframe via
33
+ * `ui/initialize.result.hostContext.containerDimensions`.
34
+ */
35
+ export interface McpAppIframeDimensions {
36
+ readonly width?: number;
37
+ readonly height?: number;
38
+ readonly maxWidth?: number;
39
+ readonly maxHeight?: number;
40
+ }
41
+
42
+ /**
43
+ * Permissions-Policy hint forwarded to the WebView (RN). Spec-canonical
44
+ * field names. Native WebView enforces these through platform-specific
45
+ * permission prompts — on web these map to the iframe `allow` attribute.
46
+ */
47
+ export interface McpAppIframePermissions {
48
+ readonly camera?: boolean;
49
+ readonly microphone?: boolean;
50
+ readonly geolocation?: boolean;
51
+ readonly clipboardWrite?: boolean;
52
+ }
53
+
54
+ /**
55
+ * Props for `<McpAppIframe>` on React Native.
56
+ *
57
+ * Host obligations:
58
+ * - Host MUST mount WebView with `source` derived from `resource`:
59
+ * `source={{html: resource.text}}` for inline HTML; `source={{uri:
60
+ * resource.uri}}` for URL resources (http(s) only).
61
+ * - `ui/initialize` replies carry ONLY `{theme, containerDimensions,
62
+ * locale}` — adapter-boundary rule.
63
+ * - `tools/call` forwards to `onToolCall`; absent handler → reject
64
+ * `no-tool-handler`.
65
+ * - `ui/open-link` with `https?://` → delegates to `Linking.openURL`;
66
+ * other schemes → reject `unsupported-scheme`.
67
+ * - Unknown method → `method_not_supported`.
68
+ */
69
+ export interface McpAppIframeProps {
70
+ /**
71
+ * The MCP Apps resource to render. Structurally compatible with
72
+ * `@modelcontextprotocol/sdk`'s `ResourceContents` (same TS type).
73
+ *
74
+ * Shape:
75
+ * - `uri` — required. Identifies the resource.
76
+ * - `mimeType?` — used when deriving a data-URL from a `blob`.
77
+ * - `text?` — when present, host mounts via `source={{html}}`
78
+ * (inline HTML).
79
+ * - `blob?` — when present (and no `text`), host mounts via a
80
+ * data-URL derived from the base64 blob + `mimeType` (defaults
81
+ * `text/html`).
82
+ * - Else → host mounts via `source={{uri}}` — caller is
83
+ * responsible for the URI being `http(s)://` (WebView rejects
84
+ * non-http schemes via `originWhitelist`).
85
+ */
86
+ readonly resource: ResourceContents;
87
+
88
+ /**
89
+ * Locale string forwarded to `ui/initialize.result.hostContext.locale`.
90
+ * On RN, defaults to `'en-US'` when absent (no `navigator` available).
91
+ */
92
+ readonly locale?: string;
93
+
94
+ /**
95
+ * Called when the embedded app sends `ui/update-model-context` —
96
+ * content the app wants added to the host's model/conversation
97
+ * context (the ggui renderer sends this on user interaction). When
98
+ * wired, the request is schema-validated and acked with an empty
99
+ * result; the host owns delivering the content to its agent. When
100
+ * absent, the host answers `method_not_supported` — the honest
101
+ * response for context it would not carry anywhere.
102
+ */
103
+ readonly onUpdateModelContext?: (
104
+ params: import('@modelcontextprotocol/ext-apps').McpUiUpdateModelContextRequest['params'],
105
+ ) => Promise<void> | void;
106
+
107
+ /**
108
+ * Container-dimensions hint. Mirrored to the View element's style
109
+ * AND echoed to the iframe via
110
+ * `ui/initialize.result.hostContext.containerDimensions`.
111
+ */
112
+ readonly containerDimensions?: McpAppIframeDimensions;
113
+
114
+ /**
115
+ * Permissions-Policy hint. Passed through to the platform WebView's
116
+ * media/geolocation gating; does NOT leak into `ui/initialize`.
117
+ */
118
+ readonly permissions?: McpAppIframePermissions;
119
+
120
+ /**
121
+ * Opt-in first-party `ai.ggui/render` meta delivery for ggui
122
+ * renderer WebViews.
123
+ *
124
+ * **When set:** the host fires a spec-canonical
125
+ * `ui/notifications/tool-result` JSON-RPC notification immediately
126
+ * after responding to the renderer's `ui/initialize` request,
127
+ * wrapping a `CallToolResult` whose `_meta` carries the single
128
+ * `ai.ggui/render` slice from the supplied {@link
129
+ * McpAppAiGguiRenderMeta}. The renderer's `parseMetaFromToolResult`
130
+ * extractor (`packages/iframe-runtime/src/meta-parse.ts`) reads
131
+ * `params._meta` and uses it to fetch the renderer bundle, open the
132
+ * WebSocket, and bootstrap the render. This matches the wire shape
133
+ * `<AppRenderer toolResult={...}>` forwards on web and the
134
+ * `McpUiToolResultNotification` shape in
135
+ * `@modelcontextprotocol/ext-apps/spec.types`. The `ui/initialize`
136
+ * response itself is unchanged — it carries the adapter-boundary
137
+ * `{theme, containerDimensions, locale}` fields ONLY.
138
+ *
139
+ * **When absent (default):** no `ui/notifications/tool-result`
140
+ * notification is sent. The renderer falls through to its other
141
+ * boot paths (inline `__GGUI_META__` global from a self-contained
142
+ * shell, or the per-app live channel). Third-party MCP App
143
+ * WebViews MUST NOT be given `meta` here — leaking outer-app state
144
+ * into a generic MCP App is exactly the adapter-boundary
145
+ * violation the rule exists to prevent.
146
+ *
147
+ * **Rule of thumb:** set this exactly when the WebView was spawned by
148
+ * following ggui's own resource URI (`ui://ggui/render` or
149
+ * `ui://ggui/render/<sessionId>` etc.) and the host is responsible
150
+ * for wiring the meta forward — e.g. the console's
151
+ * `<McpAppIframe>` mount feeds it the meta fetched from
152
+ * `GET /ggui/console/session-resource`. Do NOT set this for any
153
+ * WebView loading content authored outside ggui's render-resource
154
+ * surface.
155
+ *
156
+ * **Recursive case.** The renderer itself hosts third-party MCP App
157
+ * iframes via `packages/iframe-runtime/src/mcp-app-iframe-host.ts`.
158
+ * That nested host MUST NOT forward `meta` regardless of the outer
159
+ * host's posture — first-party meta delivery does NOT cascade
160
+ * through to third-party content the renderer is itself hosting.
161
+ */
162
+ readonly meta?: McpAppAiGguiRenderMeta;
163
+
164
+ /**
165
+ * Caller-provided handler for `tools/call` dispatches from the
166
+ * iframe. The host forwards `(toolName, args)` and awaits; the
167
+ * resolved value becomes the JSON-RPC `result`, rejections become
168
+ * `-32000` errors. Absent handler = every `tools/call` is rejected
169
+ * with `no-tool-handler`.
170
+ */
171
+ readonly onToolCall?: (
172
+ toolName: string,
173
+ args: Record<string, unknown>,
174
+ ) => Promise<unknown>;
175
+
176
+ /**
177
+ * Surfaced on every ProtocolError the host classifies from the
178
+ * WebView (today: the `ggui:bootstrap-failed` envelope, which also
179
+ * carries the renderer's version-handshake rejection as reason
180
+ * `UPGRADE_REQUIRED`). Hosts pattern-match on `err.kind`. Handlers
181
+ * MUST NOT throw.
182
+ */
183
+ readonly onError?: (err: ProtocolError) => void;
184
+
185
+ /**
186
+ * Surfaced for every {@link ObservabilityEvent} the iframe emits
187
+ * via `postMessage({type:'ggui:observe', event})`. Handlers MUST
188
+ * tolerate unknown `event.kind` values (extensibly-closed union).
189
+ */
190
+ readonly onObserve?: (event: ObservabilityEvent) => void;
191
+
192
+ /**
193
+ * Optional callback for every {@link McpAppLifecycleEvent} the iframe
194
+ * emits via `postMessage({type:'ggui:lifecycle', event})`.
195
+ *
196
+ * Lifecycle is **always mirrored to the outer `<View>`** via
197
+ * `accessibilityValue={{text: state}}` regardless of whether this
198
+ * callback is bound — this is the canonical observation surface
199
+ * (RN equivalent of the web `data-ggui-mcp-app-iframe-lifecycle`
200
+ * attribute that E2E + console inspectors pin on). On RN, lifecycle
201
+ * is mirrored via `accessibilityValue={{text: state}}` on the host
202
+ * `<View>` so RN testing libraries (`@testing-library/react-native`)
203
+ * can query the same observable surface as web E2E selectors. Hosts
204
+ * that want richer reactive state should bind `onLifecycle` directly.
205
+ *
206
+ * Handlers MUST tolerate any future `state` values per the
207
+ * `McpAppLifecycleState` closed union — adding a new state requires a
208
+ * protocol change, but legacy hosts MUST not crash on a state they
209
+ * don't recognise (the host filters known states before mirroring;
210
+ * unknown states reach this callback as opaque strings the host
211
+ * neither mirrors nor blocks).
212
+ */
213
+ readonly onLifecycle?: (event: McpAppLifecycleEvent) => void;
214
+ }
215
+
216
+ /**
217
+ * Imperative ref shape. Mirror of the web version — keep stable so
218
+ * host-side tooling (e.g. a test-action panel) works against either
219
+ * platform.
220
+ */
221
+ export interface McpAppIframeRef {
222
+ /**
223
+ * Dispatch a JSON-RPC notification into the WebView. Fire-and-
224
+ * forget; the iframe MUST NOT respond.
225
+ */
226
+ readonly dispatchAction: (name: string, data: unknown) => void;
227
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Pure helpers for chat-shaped integrations. Populated in Chunk 1.2.
3
+ * Do NOT use `export *` here — named re-exports only (parity-test policy).
4
+ */
5
+ export { useRafThrottled } from './useRafThrottled';
6
+ export { extractRenderFromToolResult, extractSessionIdFromToolResult } from './render';
7
+ export {
8
+ invokeMessageToContentGroups,
9
+ contentGroupsToConversationMessages,
10
+ conversationMessagesToInvokeHistory,
11
+ type ContentGroup,
12
+ } from './message-groups';