@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.
- package/LICENSE +201 -0
- package/README.md +58 -4
- package/package.json +84 -4
- package/src/McpAppIframe/McpAppIframe.tsx +368 -0
- package/src/McpAppIframe/dispatch.ts +418 -0
- package/src/McpAppIframe/index.ts +15 -0
- package/src/McpAppIframe/types.ts +227 -0
- package/src/chat-helpers/index.ts +12 -0
- package/src/chat-helpers/message-groups.ts +124 -0
- package/src/chat-helpers/render.ts +65 -0
- package/src/chat-helpers/useRafThrottled.ts +34 -0
- package/src/components/ErrorBoundary.tsx +96 -0
- package/src/components/GguiProvider.tsx +188 -0
- package/src/components/UiFeedback.tsx +233 -0
- package/src/components/mcp-apps-bridge.ts +388 -0
- package/src/context/GguiContext.ts +121 -0
- package/src/hooks/useAppState.ts +24 -0
- package/src/index.ts +150 -0
- package/src/invoke/index.ts +8 -0
- package/src/invoke/sse-parse.ts +79 -0
- package/src/invoke/useInvoke.ts +502 -0
- package/src/test-setup.ts +85 -0
- package/src/theme/ThemeProvider.tsx +129 -0
- package/src/theme/index.ts +29 -0
- package/src/theme/tokens.ts +201 -0
- package/src/theme/types.ts +94 -0
- package/src/types/react-test-renderer.d.ts +50 -0
|
@@ -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';
|