@ggui-ai/mcp-apps-react-native 0.10.0 → 0.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/mcp-apps-react-native",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "React Native SDK for ggui — components and hooks for embedding agent-generated UIs in mobile apps.",
5
5
  "keywords": [
6
6
  "ggui",
@@ -58,9 +58,9 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@modelcontextprotocol/ext-apps": "^1.7.2",
61
- "@ggui-ai/design": "0.10.0",
62
- "@ggui-ai/protocol": "0.10.0",
63
- "@ggui-ai/iframe-runtime": "0.10.0"
61
+ "@ggui-ai/design": "0.11.0",
62
+ "@ggui-ai/protocol": "0.11.0",
63
+ "@ggui-ai/iframe-runtime": "0.11.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@modelcontextprotocol/sdk": "^1.27.0",
@@ -68,6 +68,7 @@ import {
68
68
  classifyRendererEnvelope,
69
69
  deriveResourceMountSource,
70
70
  dispatchHostBridgeRequest,
71
+ readSizeChangedNotification,
71
72
  type HostBridgeContext,
72
73
  type HostBridgeRequest,
73
74
  type HostBridgeResponse,
@@ -107,6 +108,7 @@ export const McpAppIframe = forwardRef<McpAppIframeRef, McpAppIframeProps>(
107
108
  onError,
108
109
  onObserve,
109
110
  onLifecycle,
111
+ onSizeChanged,
110
112
  },
111
113
  ref,
112
114
  ) {
@@ -226,6 +228,16 @@ export const McpAppIframe = forwardRef<McpAppIframeRef, McpAppIframeProps>(
226
228
  return;
227
229
  }
228
230
  case 'jsonrpc': {
231
+ // `ui/notifications/size-changed` (ggui#589 rider 5) — an
232
+ // id-less NOTIFICATION, so it never reaches the request
233
+ // dispatcher below (which returns null for notifications;
234
+ // before this branch the announce was dropped and content
235
+ // taller than the container clipped). Surface it and stop.
236
+ const sizeChange = readSizeChangedNotification(payload);
237
+ if (sizeChange !== null) {
238
+ onSizeChanged?.(sizeChange);
239
+ return;
240
+ }
229
241
  const req = payload as HostBridgeRequest;
230
242
  const response = await dispatchHostBridgeRequest(req, ctxRef.current);
231
243
  if (response) deliverToWebView(response);
@@ -256,7 +268,7 @@ export const McpAppIframe = forwardRef<McpAppIframeRef, McpAppIframeProps>(
256
268
  return;
257
269
  }
258
270
  },
259
- [deliverToWebView, onError, onObserve, onLifecycle],
271
+ [deliverToWebView, onError, onObserve, onLifecycle, onSizeChanged],
260
272
  );
261
273
 
262
274
  // Mount-source null → caller gave an unmountable resource. Emit
@@ -306,7 +318,11 @@ export const McpAppIframe = forwardRef<McpAppIframeRef, McpAppIframeProps>(
306
318
 
307
319
  if (mountSource === null) {
308
320
  // Render an empty View so the caller still sees a slot; onError
309
- // already fired the bootstrap-failed error.
321
+ // already fired the bootstrap-failed error. NO card chrome —
322
+ // the HOST owns borders/radius entirely (ggui#589 round 6: the
323
+ // hardcoded #e5e5e5 border ran at its own radius inside the
324
+ // embedder's rounded rim — the two-radii mismatch was the
325
+ // store-frame rejection).
310
326
  return (
311
327
  <View
312
328
  testID="mcp-app-iframe-empty"
@@ -315,9 +331,6 @@ export const McpAppIframe = forwardRef<McpAppIframeRef, McpAppIframeProps>(
315
331
  height: dims.height ?? 480,
316
332
  ...(dims.maxWidth !== undefined ? { maxWidth: dims.maxWidth } : {}),
317
333
  ...(dims.maxHeight !== undefined ? { maxHeight: dims.maxHeight } : {}),
318
- borderWidth: 1,
319
- borderColor: '#e5e5e5',
320
- borderRadius: 8,
321
334
  }}
322
335
  />
323
336
  );
@@ -337,14 +350,15 @@ export const McpAppIframe = forwardRef<McpAppIframeRef, McpAppIframeProps>(
337
350
  {...(lifecycleState !== null
338
351
  ? { accessibilityValue: { text: lifecycleState } }
339
352
  : {})}
353
+ // NO card chrome (ggui#589 round 6) — the HOST owns borders/
354
+ // radius/silhouette entirely, the same doctrine as the round-5
355
+ // frameless ruling one layer down. `overflow: 'hidden'` stays:
356
+ // it is containment (clip the WebView to the slot), not chrome.
340
357
  style={{
341
358
  width: dims.width ?? '100%',
342
359
  height: dims.height ?? 480,
343
360
  ...(dims.maxWidth !== undefined ? { maxWidth: dims.maxWidth } : {}),
344
361
  ...(dims.maxHeight !== undefined ? { maxHeight: dims.maxHeight } : {}),
345
- borderWidth: 1,
346
- borderColor: '#e5e5e5',
347
- borderRadius: 8,
348
362
  overflow: 'hidden',
349
363
  }}
350
364
  >
@@ -307,6 +307,43 @@ export function classifyRendererEnvelope(data: unknown): RendererEnvelopeTag {
307
307
  return 'unknown';
308
308
  }
309
309
 
310
+ /**
311
+ * Read a spec-canonical `ui/notifications/size-changed` NOTIFICATION
312
+ * (ggui#589 rider 5). The view sends it when its rendered content
313
+ * changes size (ext-apps `App.sendSizeChanged` / its autoResize
314
+ * observer); before this reader existed, the jsonrpc branch dropped
315
+ * every id-less message (`dispatchHostBridgeRequest` returns null for
316
+ * notifications) and content taller than the container clipped.
317
+ *
318
+ * Returns the validated `{width?, height?}` params, or `null` when the
319
+ * payload is not this notification OR carries nothing usable — each
320
+ * dimension must be a finite non-negative number to pass; a payload
321
+ * where NEITHER dimension survives is `null` (nothing to resize on).
322
+ * Pure function — testable without a WebView present.
323
+ */
324
+ export function readSizeChangedNotification(
325
+ data: unknown,
326
+ ): { width?: number; height?: number } | null {
327
+ if (data === null || typeof data !== 'object') return null;
328
+ const d = data as { jsonrpc?: unknown; method?: unknown; params?: unknown };
329
+ if (d.jsonrpc !== '2.0' || d.method !== 'ui/notifications/size-changed') {
330
+ return null;
331
+ }
332
+ if (d.params === null || typeof d.params !== 'object' || Array.isArray(d.params)) {
333
+ return null;
334
+ }
335
+ const params = d.params as { width?: unknown; height?: unknown };
336
+ const dim = (v: unknown): number | undefined =>
337
+ typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined;
338
+ const width = dim(params.width);
339
+ const height = dim(params.height);
340
+ if (width === undefined && height === undefined) return null;
341
+ return {
342
+ ...(width !== undefined ? { width } : {}),
343
+ ...(height !== undefined ? { height } : {}),
344
+ };
345
+ }
346
+
310
347
  export function buildDispatchActionNotification(
311
348
  name: string,
312
349
  data: unknown,
@@ -211,6 +211,26 @@ export interface McpAppIframeProps {
211
211
  * neither mirrors nor blocks).
212
212
  */
213
213
  readonly onLifecycle?: (event: McpAppLifecycleEvent) => void;
214
+
215
+ /**
216
+ * Fired when the view announces a content-size change via the
217
+ * spec-canonical `ui/notifications/size-changed` notification
218
+ * (ext-apps `App.sendSizeChanged` / its autoResize observer).
219
+ * Both dimensions are optional per the spec — the common autoResize
220
+ * case announces `height` only. Values are validated finite
221
+ * non-negative numbers before they reach this callback; malformed
222
+ * announcements are dropped silently (the same tolerant posture as
223
+ * every other envelope).
224
+ *
225
+ * The host does NOT resize itself: `containerDimensions` stays the
226
+ * embedder's declaration. Bind this to drive your own container
227
+ * sizing (autoResize) — without it, content taller than the
228
+ * container clips.
229
+ */
230
+ readonly onSizeChanged?: (size: {
231
+ readonly width?: number;
232
+ readonly height?: number;
233
+ }) => void;
214
234
  }
215
235
 
216
236
  /**