> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metabind.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Render MCP Apps natively on iOS and Android, without a WebView

> Host MCP Apps in a native iOS or Android app. Render MCP App tool results as SwiftUI or Jetpack Compose instead of HTML in a WKWebView or WebView, using the Metabind SDKs for Swift and Kotlin.

To host MCP Apps in an iOS or Android app, you can load each tool's HTML view in a `WKWebView` or `WebView`, or you can draw the tool's UI with the platform's own UI toolkit. The Metabind SDKs do the second. An MCP App built on Metabind renders as SwiftUI on iOS, macOS, and visionOS, and as Jetpack Compose on Android. The same MCP App renders as HTML in Claude, ChatGPT, and other MCP hosts.

This page covers how MCP Apps are usually rendered, what changes when the UI renders natively, how the SDKs and the Metabind server agree on a format, and the minimum code for iOS and Android.

## How MCP Apps are usually rendered

The MCP Apps extension to MCP ([SEP-1865](https://github.com/modelcontextprotocol/ext-apps)) delivers a tool's user interface as a `ui://` resource with the MIME type `text/html;profile=mcp-app`. A tool names its resource in `_meta.ui.resourceUri`. The host reads the resource, loads the HTML in a sandboxed iframe, and exchanges JSON-RPC 2.0 messages with it over `postMessage`: tool input, tool results, and requests from the view such as tool calls.

Web hosts usually do this with the MCP-UI client SDK (`@mcp-ui/client`) or the App Bridge from the ext-apps SDK (`@modelcontextprotocol/ext-apps/app-bridge`). A native app does the same thing with a `WKWebView` on Apple platforms or a `WebView` on Android: it loads the HTML and implements the `postMessage` bridge.

This works with every MCP App server, because the HTML view is the format the MCP Apps specification defines. The result is a web page inside your app. The page draws its own controls, sets its own text sizes, and exposes the accessibility tree its HTML builds.

## What changes when the UI renders natively

On Metabind, an Interactive Tool's UI is a [BindJS](/bindjs/introduction) component. The BindJS runtime runs the component's code in an isolated JavaScript context and produces a view tree. The SDK draws that tree with SwiftUI or Jetpack Compose, as part of your app's view hierarchy.

* **Controls.** BindJS `Button`, `Toggle`, `TextField`, `Slider`, and `List` render as the corresponding SwiftUI and Compose components. See the mapping table in [Native rendering](/guides/concepts/native-rendering).
* **Text size.** BindJS text styles such as `body`, `headline`, and `caption` map to SwiftUI text styles, which scale with Dynamic Type. On Android, text sizes are set in `sp`, which follows the system font size setting.
* **Accessibility.** `accessibilityLabel`, `accessibilityHint`, and `accessibilityValue` map to SwiftUI accessibility modifiers and to Compose semantics. VoiceOver and TalkBack read tool UI the same way they read the rest of your app.
* **Gestures and animation.** Scrolling, gestures, and animation run through SwiftUI and Compose, so they behave like the rest of your app.
* **Performance.** There is no web view to create and no page to load for each result. On Apple platforms, `MCPAppsClient` caches a project's decoded component package, so later results from the same package version skip the full decode. The Apple client also accepts `prefetchUIResources` in `MCPAppsClient.Configuration`, which fetches every `ui://` resource as soon as `listTools()` returns.

Components call back into your app through `useMCPHost()`: tool calls, follow-up messages, model context updates, and opening links. The Assistant SDK wires these on both platforms.

## How the format is negotiated

The SDKs and the Metabind server agree on a format during the MCP handshake, using the extension MCP Apps hosts already use to declare HTML support.

<Steps>
  <Step title="The client declares the formats it renders">
    On `initialize`, `MCPAppsClient` lists the MIME types it can render in the `io.modelcontextprotocol/ui` extension. The iOS and Android clients send:

    ```json theme={null}
    {
      "method": "initialize",
      "params": {
        "protocolVersion": "2025-03-26",
        "capabilities": {
          "extensions": {
            "io.modelcontextprotocol/ui": {
              "mimeTypes": ["application/vnd.bindjs+json", "text/html;profile=mcp-app"]
            }
          }
        },
        "clientInfo": { "name": "MCPAppsHost", "version": "1.0.0" }
      }
    }
    ```
  </Step>

  <Step title="The server returns the format the client declared">
    The Metabind server stores those MIME types with the MCP session. When the client reads a tool's `ui://` resource, the server returns the BindJS bundle (`application/vnd.bindjs+json`) to a client that declared it, and the HTML view (`text/html;profile=mcp-app`) to a client that did not. The tool definition and the resource URI are the same for every client.
  </Step>

  <Step title="The SDK renders by MIME type">
    The SDK picks a renderer from the MIME type of the resource it receives. BindJS renders as SwiftUI or Compose. HTML renders in a `WKWebView` with a restrictive Content Security Policy and external navigation blocked on Apple platforms, and in a `WebView` on Android. Host calls through `useMCPHost()` apply to BindJS content.
  </Step>
</Steps>

Claude, ChatGPT, and other MCP hosts render HTML views, so they receive the HTML representation and render the same component in their sandboxed iframe with the BindJS React renderer. You publish once, and each client receives the format it declared.

On Apple platforms, the declared MIME types come from the client's content resolvers. The default is `[BindJSResolver(), HTMLResolver()]`. To declare BindJS without the HTML fallback, pass one resolver:

```swift theme={null}
let client = MCPAppsClient(
    url: URL(string: "https://mcp.metabind.ai/<org>/projects/<project>")!,
    headers: ["authorization": "Bearer \(metabindApiKey)"],
    resolvers: [BindJSResolver()]
)
```

The Android `MCPAppsClient` always declares both types.

## Quick start: iOS, macOS, and visionOS

Add `https://github.com/metabindai/metabind-apple` with Swift Package Manager and link the product you need: `MCPAppsHost` to render individual tool results, or `MetabindAI` for the Assistant SDK. See [installation](/guides/assistant-sdk/ios-sdk#install-via-swift-package-manager) and [authentication patterns](/guides/assistant-sdk/ios-sdk#authentication-patterns). The MCP server URL for a Metabind project is `https://mcp.metabind.ai/<org>/projects/<project>`.

### Render a single tool result with MCPAppsHost

Use `MCPAppsHost` when your app decides which tool to call, for example on a product screen or a dashboard tile, and you don't need a conversation.

```swift theme={null}
import SwiftUI
import MCPAppsHost

struct ProductCardView: View {
    @State private var client = MCPAppsClient(
        url: URL(string: "https://mcp.metabind.ai/<org>/projects/<project>")!,
        headers: ["authorization": "Bearer \(metabindApiKey)"]
    )
    @State private var session: MCPAppSession?

    var body: some View {
        Group {
            if let session {
                MCPAppView(session: session)
            } else {
                ProgressView()
            }
        }
        .task {
            // tools/list carries each tool's ui:// resource URI in _meta.ui.
            guard let tool = try? await client.listTools()
                .first(where: { $0.name == "show_product" }) else { return }

            let call = SimpleMCPToolCall(
                id: UUID().uuidString,
                name: tool.name,
                arguments: ["sku": "SOFA-001"],
                toolDefinition: tool
            )
            // Fetches the ui:// resource, calls the tool, and renders the result.
            session = MCPAppSession(toolCall: call, server: client)
        }
    }
}
```

`MCPAppSession` reads the tool's `ui://` resource, calls the tool, and moves through the `loading`, `active`, and `completed` phases. `MCPAppView` renders each phase. Pass the tool definition from `listTools()`, because it carries the resource URI. A `SimpleMCPToolCall` without a tool definition still calls the tool, and the session shows the tool's text and image content instead of a view.

Other options:

* If you already called the tool, `MCPAppSession(toolCall:completedWith:server:)` fetches the view and renders your result.
* For your own loading and error UI, use `MCPAppView(session:content:placeholder:)` or `MCPAppView(session:phase:)`.
* To give components host capabilities outside the Assistant SDK, attach a bridge with `.mcpHostBridge(_:)`.

`MCPAppsHost` contains no language model or agent loop. It connects to any MCP server over HTTP. A resource the server returns as HTML displays in the `WKWebView` fallback.

### Embed the Assistant SDK

`MetabindAI` adds the conversation on top of `MCPAppsHost`. `MetabindAssistantView` runs the conversation through the Metabind Agent proxy and renders each Interactive Tool result inline as SwiftUI:

```swift theme={null}
import SwiftUI
import MetabindAI

struct AssistantScreen: View {
    @State private var assistant = MetabindAssistant(
        serverURL: URL(string: "https://mcp.metabind.ai/<org>/projects/<project>")!,
        serverHeaders: ["authorization": "Bearer \(metabindApiKey)"],
        provider: MetabindAgentProvider(
            apiKey: metabindApiKey,
            orgId: "<org>",
            projectId: "<project>"
        )
    )

    var body: some View {
        MetabindAssistantView(assistant: assistant)
    }
}
```

`MetabindAssistant` builds its `MCPAppsClient` from `serverURL` and `serverHeaders`, so format negotiation works as described above. One Metabind API key authenticates both the MCP server and the Agent proxy. The [iOS SDK](/guides/assistant-sdk/ios-sdk) guide covers bring-your-own-key mode and previews. [Custom host UI](/guides/assistant-sdk/custom-host-ui) covers replacing the chat surface.

## Quick start: Android

Add `ai.metabind:metabindai-android`, which brings in `ai.metabind:mcpappshost-android` and the BindJS Compose renderer, `ai.metabind:bindjs-android`. The packages are published to GitHub Packages; see [installation](/guides/assistant-sdk/android-sdk#installation) for the repository configuration.

### Embed the Assistant SDK

```kotlin theme={null}
import ai.metabind.ai.MetabindAssistant
import ai.metabind.ai.MetabindAssistantView

@Composable
fun AssistantScreen(apiKey: String, org: String, project: String) {
    val assistant = remember {
        MetabindAssistant(apiKey = apiKey, orgId = org, projectId = project)
    }
    MetabindAssistantView(assistant = assistant)
}
```

The assistant derives the MCP server URL from `orgId` and `projectId`, runs the conversation through the Agent proxy, and renders each Interactive Tool result as Compose. Keep the `MetabindAssistant` at a longer-lived scope, such as a ViewModel, and call `close()` when you discard it. The Android SDK has no bring-your-own-key provider. The Agent proxy holds the language model key, so none ships in your APK. See the [Android SDK](/guides/assistant-sdk/android-sdk) guide.

### Place a tool result outside the chat

`MetabindToolView` is the renderer `MetabindAssistantView` uses for each tool result. Use it to show a result on another screen, in a sheet, or in a tab. It takes the assistant, which supplies host calls, and a `ToolUIContent` from `assistant.toolUIContent`, which is keyed by tool call ID, the same ID as the `TOOL` message the call produced:

```kotlin theme={null}
val content = assistant.toolUIContent.collectAsState().value[message.id]
if (content != null) {
    MetabindToolView(
        assistant = assistant,
        toolName = message.toolName ?: "",
        content = content,
    )
}
```

### Fetch a tool's UI with MCPAppsClient

`MCPAppsClient` in `:mcpappshost` performs the same negotiation without the assistant. `ToolUIContent.fromResource`, in `:metabindai`, parses the resource into `ToolUIContent.BindJS` or `ToolUIContent.Html`:

```kotlin theme={null}
import ai.metabind.ai.ToolUIContent
import ai.metabind.mcpappshost.MCPAppsClient
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive

suspend fun loadProductCard(apiKey: String, org: String, project: String): ToolUIContent? {
    val client = MCPAppsClient(
        url = "https://mcp.metabind.ai/$org/projects/$project",
        headers = mapOf("authorization" to "Bearer $apiKey")
    )
    val tool = client.listTools().firstOrNull { it.name == "show_product" } ?: return null
    val resourceUri = tool.ui?.resourceUri ?: return null
    val arguments = JsonObject(mapOf("sku" to JsonPrimitive("SOFA-001")))

    val resource = client.readResource(resourceUri)
    val result = client.callTool(tool.name, arguments)
    return ToolUIContent.fromResource(resource, arguments)
        .withResult(result.textContent, result.isError)
}
```

To render `ToolUIContent.BindJS` without a `MetabindAssistant`, drive `BindJSView` from `bindjs-android` directly. [`MetabindToolView.kt`](https://github.com/metabindai/metabind-android/blob/main/metabindai/src/main/java/ai/metabind/ai/MetabindToolView.kt) shows the runtime setup and host wiring.

## Web

On the web, the [Web SDK](/guides/assistant-sdk/web-sdk) renders Interactive Tool output as HTML in sandboxed iframes through `@mcp-ui/client`, the same way MCP hosts do.

## Related

<CardGroup cols={2}>
  <Card title="Native rendering" icon="layer-group" href="/guides/concepts/native-rendering">
    How one BindJS definition becomes SwiftUI, Jetpack Compose, and React.
  </Card>

  <Card title="iOS SDK" icon="apple" href="/guides/assistant-sdk/ios-sdk">
    Install, configure, and embed the Assistant SDK on iOS, macOS, and visionOS.
  </Card>

  <Card title="Android SDK" icon="android" href="/guides/assistant-sdk/android-sdk">
    Install, configure, and embed the Assistant SDK on Android.
  </Card>

  <Card title="Web SDK" icon="globe" href="/guides/assistant-sdk/web-sdk">
    The Assistant SDK for React apps.
  </Card>

  <Card title="Custom host UI" icon="paintbrush" href="/guides/assistant-sdk/custom-host-ui">
    Replace the default chat surface with your own.
  </Card>
</CardGroup>
