---
title: React Native host helpers (MCP Apps)
description: @ggui-ai/mcp-apps-react-native — host-helper library for embedding ggui views in a React Native or Expo app via <McpAppIframe>, the WebView-backed MCP Apps host. Not an SDK: rendering semantics live in the mounted view.
---

:::tip[React Native host: `<McpAppIframe>`]
On React Native the host primitive is `<McpAppIframe>` — a `react-native-webview` WebView that speaks the MCP Apps host protocol. It is RN-only; there is no `<McpAppIframe>` on web, where the host is `<AppRenderer>` from `@mcp-ui/client` (see [React host helpers](/sdk/react/)).

The web migration to `<AppRenderer>` relied on a two-iframe cross-origin sandbox proxy. A WebView is itself a top-level browser surface, not a nestable origin, so that architecture has no RN equivalent — which is why `<McpAppIframe>` remains the canonical host here.
:::

`@ggui-ai/mcp-apps-react-native` is a **host-helper library, not an SDK**. It gives your app the host half of the MCP Apps contract — mount a UI resource, answer `ui/initialize`, relay `tools/call`, open links, tear down. Rendering semantics (freeze latch, history epochs, error surfaces) live inside the mounted view, not in your tree.

## Installation

```bash
npm install @ggui-ai/mcp-apps-react-native@0.10.0
```

Peer dependencies, installed by your app: `react` (18 or 19), `react-native` (>= 0.70), `react-native-webview` (>= 13), and `@modelcontextprotocol/sdk`.

| Import path                                   | Contents                                                               |
| --------------------------------------------- | ---------------------------------------------------------------------- |
| `@ggui-ai/mcp-apps-react-native`              | `<McpAppIframe>`, `<GguiProvider>`, `useInvoke`, `<UiFeedback>`, theme |
| `@ggui-ai/mcp-apps-react-native/chat-helpers` | Message-grouping + render-extraction helpers                           |

---

## Mounting a render — `<McpAppIframe>`

Give the component the UI resource from a tool result plus the render metadata carried on `_meta["ai.ggui/render"]`. It mounts the resource in a WebView and runs the host side of the handshake:

```tsx
import { useRef } from "react";
import { McpAppIframe } from "@ggui-ai/mcp-apps-react-native";
import type { McpAppIframeProps, McpAppIframeRef } from "@ggui-ai/mcp-apps-react-native";

export function AgentCard({
  resource,
  meta,
  callTool,
}: {
  resource: McpAppIframeProps["resource"];
  meta: McpAppIframeProps["meta"];
  callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>;
}) {
  const ref = useRef<McpAppIframeRef>(null);

  return (
    <McpAppIframe
      ref={ref}
      resource={resource}
      meta={meta}
      containerDimensions={{ width: 360, maxHeight: 640 }}
      onToolCall={callTool}
      onError={(err) => console.warn("render error", err.kind)}
      onObserve={(event) => console.log("observe", event.kind)}
      onLifecycle={(event) => console.log("lifecycle", event.state)}
    />
  );
}
```

### Props

| Prop                   | Type                                                      | Notes                                                                                                               |
| ---------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `resource`             | `ResourceContents`                                        | Required. `text` mounts as inline HTML, `blob` as a data URL, otherwise the `uri` is loaded (`http(s)` only).       |
| `meta`                 | `McpAppAiGguiRenderMeta`                                  | First-party ggui renders only. Delivered as a `ui/notifications/tool-result` right after the initialize response.   |
| `onToolCall`           | `(toolName, args) => Promise<unknown>`                    | Relay for `tools/call`. Without it every call is rejected `no-tool-handler`.                                        |
| `onUpdateModelContext` | `(params) => Promise<void> \| void`                       | Receives `ui/update-model-context`. Without it the host answers `method_not_supported`.                             |
| `containerDimensions`  | `{ width?, height?, maxWidth?, maxHeight? }`              | Styles the outer `<View>` and is echoed on `ui/initialize.result.hostContext`.                                      |
| `permissions`          | `{ camera?, microphone?, geolocation?, clipboardWrite? }` | Platform WebView gating. Never reaches `ui/initialize`.                                                             |
| `locale`               | `string`                                                  | Forwarded as `hostContext.locale`. Defaults to `en-US` — RN has no `navigator`.                                     |
| `onError`              | `(err: ProtocolError) => void`                            | Classified failures, including the `ggui:bootstrap-failed` envelope. Handlers must not throw.                       |
| `onObserve`            | `(event: ObservabilityEvent) => void`                     | The `ggui:observe` channel. Tolerate unknown `event.kind`.                                                          |
| `onLifecycle`          | `(event: McpAppLifecycleEvent) => void`                   | Also mirrored on the outer `<View>` as `accessibilityValue={{ text: state }}` whether or not you bind the callback. |

The imperative ref exposes one method, `dispatchAction(name, data)`, which posts a fire-and-forget JSON-RPC notification into the WebView.

`ui/open-link` with an `http(s)` URL is delegated to `Linking.openURL`; other schemes are rejected `unsupported-scheme`. Set `meta` only for WebViews you spawned from ggui's own render resource URI — passing it to third-party MCP App content leaks outer-app state across the adapter boundary.

---

## Relationship to the web package

The two packages are twins, and a parity test enforces it. `chat-helpers/message-groups.ts`, `chat-helpers/render.ts`, and `chat-helpers/useRafThrottled.ts` are byte-identical across both packages, and `components/GguiProvider.tsx` and `components/UiFeedback.tsx` are documented platform-delta twins whose exported surfaces must match. A one-sided edit fails both suites.

Two deliberate differences:

- **`useMcpAppsChat` is web-only.** RN's `chat-helpers` ships the platform-neutral subset — `invokeMessageToContentGroups`, `contentGroupsToConversationMessages`, `conversationMessagesToInvokeHistory`, `extractRenderFromToolResult`, `extractSessionIdFromToolResult`, `useRafThrottled`. Drive the stream with `useInvoke` (mounted inside `<GguiProvider>`) and own your message history — the pattern is the [chat-own-storage cookbook](/cookbook/chat-own-storage/).
- **`<McpAppIframe>` is RN-only**, for the sandbox-proxy reason above.

Everything else lines up: the same `ProtocolError` union, the same `ObservabilityEvent` catalog, the same protocol types, and a React Native theme system (`ThemeProvider`, `useTheme`) mirroring the web design tokens.

## What was removed

The legacy in-tree render family — `GguiRender`, `DynamicComponent`, `WebViewRenderer`, `NativeRegistry`, and their WebSocket stack — was deleted from this package. Generated component code runs inside the WebView, not in your React Native tree; there is no in-process renderer to import.