---
title: "useMcpApp() bridge"
description: "data, hostContext, sendPrompt, callTool, openLink, display mode, model context, and downloads from inside the iframe."
canonical_url: "https://mcp-toolkit.nuxt.dev/apps/use-mcp-app"
---
# useMcpApp() bridge

> data, hostContext, sendPrompt, callTool, openLink, display mode, model context, and downloads from inside the iframe.

## `useMcpApp()`

The single client-side composable, **auto-imported into every MCP App SFC**. It returns everything the iframe needs to talk to the host:

```ts
const {
  initialData,        // Ref<T | null>          — first payload the host pushes, never updated after
  data,               // Ref<T | null>          — structuredContent pushed by the host, refreshed by callTool
  meta,               // Ref<ToolResultMeta | null> — _meta of the result that set data, hidden from the model
  loading,            // Ref<boolean>           — true until first payload arrives or the call fails
  error,              // Ref<Error | null>      — bridge / transport / payload / callTool errors
  pending,            // Ref<boolean>           — true while a callTool() is in flight
  hostContext,        // Ref<HostContext | null> — theme, displayMode, locale, …
  hostCapabilities,   // Ref<HostCapabilities | null> — what the host supports
  callTool,           // (name, params?) => Promise<T | null> — re-invoke any MCP tool
  sendPrompt,         // (prompt: string) => void              — push a message into the chat
  openLink,           // (url: string) => void                 — ask the host to open a URL
  requestDisplayMode, // (mode) => Promise<DisplayMode> — switch inline / fullscreen / pip
  updateModelContext, // ({ content?, structuredContent? }) => Promise<void> — context for the next turn
  downloadFile,       // (contents) => Promise<void> — save files through the host
} = useMcpApp<MyPayload>()
```

Pass your payload type as the generic to get full inference downstream.

### `initialData` & `data`

`initialData` is a **frozen snapshot** of the first `structuredContent` the host pushes (or `window.openai.toolOutput` on ChatGPT). It never changes after that — use it for bootstrap values such as IDs or query params that later `callTool` results might overwrite in `data`.

`data` is populated once the host pushes the tool result, then refreshed by `callTool` or later `tool-result` pushes. `loading` starts as `true` and becomes `false` after the first payload arrives. When the host reports an error result or cancels the call, or a `callTool` fails, `loading` becomes `false` and `error` holds the reason. Use `pending` for in-flight `callTool()` refreshes:

```vue
<script setup lang="ts">
const { initialData, data, callTool } = useMcpApp<{ listId: string }>()

async function refresh() {
  const result = await callTool('list_todos', { listId: initialData.value!.listId })
  // `data` now holds the latest tool result; `initialData` still has `{ listId }`
}
</script>
```

```vue
<template>
  <section v-if="loading" class="skeleton" />
  <section v-else-if="data" class="content">
    {{ data.swatches.length }} swatches from {{ data.base }}
  </section>
</template>
```

### `meta` — View-Only Data

`meta` holds the `_meta` of the tool result that set `data` and is refreshed together with it. Hosts hand `_meta` to the view but keep it out of the model's context, so put payloads only the view needs there, such as SVG markup or chart series. For the app's own tool, the toolkit merges its `ui`, `openai/*` and `handler` keys over yours, so namespace your keys:

```ts
handler: async ({ name }) => ({
  structuredContent: { name },
  _meta: { 'my-app/icon': { svg: await renderIcon(name) } },
}),
```

```ts
const { meta } = useMcpApp<{ name: string }>()
const svg = computed(() => (meta.value?.['my-app/icon'] as { svg: string } | undefined)?.svg)
```

### `hostContext`

The host hands the iframe a context object during the `ui/initialize` handshake and sends `ui/notifications/host-context-changed` whenever part of it changes, for example when the user switches theme or the app goes fullscreen. `hostContext` merges each update, so it stays current. Use it to **adapt to dark mode, fullscreen, or a fixed iframe size**:

```ts
type DisplayMode = 'inline' | 'fullscreen' | 'pip'

interface HostContext {
  theme?: 'light' | 'dark'
  displayMode?: DisplayMode
  availableDisplayModes?: DisplayMode[]
  containerDimensions?: { width?: number, height?: number, maxWidth?: number, maxHeight?: number }
  locale?: string
  timeZone?: string
  platform?: 'web' | 'desktop' | 'mobile'
}
```

```vue
<script setup lang="ts">
const { hostContext } = useMcpApp()
const isDark = computed(() => hostContext.value?.theme === 'dark')
const isFullscreen = computed(() => hostContext.value?.displayMode === 'fullscreen')
</script>

<template>
  <main :data-theme="isDark ? 'dark' : 'light'" :data-mode="isFullscreen ? 'fullscreen' : 'inline'">
    …
  </main>
</template>
```

<callout color="info" icon="i-lucide-info">

`hostContext` is `null` on the very first paint and populates after the handshake (typically <50 ms). Always use a fallback in your template.

</callout>

### `hostCapabilities`

The capabilities the host announced in the handshake, `null` until it completes. A key is present when the host supports the feature (`openLinks`, `downloadFile`, `updateModelContext`, `message`, `serverTools`, …). Check it before offering an action the host may lack — Claude on iOS, for example, has no `downloadFile`:

```vue
<script setup lang="ts">
const { hostCapabilities } = useMcpApp()
const canDownload = computed(() => !!hostCapabilities.value?.downloadFile)
</script>
```

### `sendPrompt(prompt)` — Follow-Ups

Push a message into the chat as if the user had typed it. The LLM then routes it like any other request — including invoking another MCP App:

```vue
<button @click="sendPrompt(`Use ${swatch.name} (${swatch.hex}) as the brand colour.`)">
  Use this colour
</button>
```

The host receives the prompt as if the user had typed it. The LLM may reply, call another tool, or open a different MCP App in response — **app-to-app workflows** fall out of this primitive.

<callout color="warning" icon="i-lucide-triangle-alert">

Follow-ups are best-effort. Hosts that implement `ui/message` forward the prompt cleanly. ChatGPT acknowledges the request but doesn't always re-render the next tool inline (an upstream limitation).

</callout>

### `callTool(name, params)` — In-Place Refresh

Re-invoke any MCP tool from the iframe. The result replaces `data` automatically:

```vue
<script setup lang="ts">
const { data, pending, callTool } = useMcpApp<PalettePayload>()

async function refresh(base: string) {
  await callTool('color-picker', { base })
}
</script>
```

Use this for filters, pagination, refresh buttons — anything that changes the query without a full chat round-trip.

### `openLink(url)`

Sandbox iframes can't open windows. `openLink` asks the host to do it for you (e.g. open a booking confirmation in a new browser tab):

```vue
<button @click="openLink(`https://example.com/colors/${swatch.hex.slice(1)}`)">
  View on the web
</button>
```

<callout color="info" icon="i-lucide-info">

Add the target host to `csp.connectDomains` if you also need to `fetch()` it from the iframe.

</callout>

### `requestDisplayMode(mode)`

Ask the host to show the app `inline`, `fullscreen` or as picture-in-picture (`pip`). It resolves with the mode the host actually set, which can differ from the one you asked for (claude.ai answers `pip` with `inline`), and updates `hostContext.displayMode`. When `hostContext.availableDisplayModes` does not include the mode, nothing is sent and it resolves with the current mode. In ChatGPT the request goes through `window.openai.requestDisplayMode`.

```vue
<script setup lang="ts">
const { hostContext, requestDisplayMode } = useMcpApp()
const isFullscreen = computed(() => hostContext.value?.displayMode === 'fullscreen')
</script>

<template>
  <button @click="requestDisplayMode(isFullscreen ? 'inline' : 'fullscreen')">
    {{ isFullscreen ? 'Collapse' : 'Expand' }}
  </button>
</template>
```

### `updateModelContext({ content, structuredContent })`

Tell the model what the user is looking at without sending a message. Each call replaces the context this app sent before; the host includes it in the model's next turn. The model reads this text as context, so don't pass through untrusted content such as fetched web pages or other users' input. It rejects when the host does not announce `hostCapabilities.updateModelContext`.

```ts
const { updateModelContext } = useMcpApp()

await updateModelContext({
  content: [{ type: 'text', text: `The user selected ${swatch.name} (${swatch.hex}).` }],
  structuredContent: { selected: swatch.hex },
})
```

<callout color="info" icon="i-lucide-info">

Claude acknowledges the update but does not add it to the prompt by itself — the model reads it on demand through its `read_widget_context` tool.

</callout>

### `downloadFile(contents)`

Sandboxed iframes can't trigger downloads. `downloadFile` hands the host MCP resources to save — embedded (`type: 'resource'` with `text` or base64 `blob`) or linked (`type: 'resource_link'`, fetched by the host). The host derives the file name from the last URI segment and usually asks the user to confirm.

```ts
const { downloadFile } = useMcpApp()

await downloadFile([{
  type: 'resource',
  resource: { uri: 'file:///palette.json', mimeType: 'application/json', text: JSON.stringify(palette) },
}])
```

It rejects when the host lacks `hostCapabilities.downloadFile`, and with `cancelled: true` when the user cancels or the host denies the download. A `resource_link` must use `http` or `https`; any other scheme rejects with `code: -32602` before the host is asked. Fall back to `openLink` on hosts without the capability.

### Errors

`requestDisplayMode`, `updateModelContext` and `downloadFile` reject with an `McpAppRequestError`: `code` and `data` carry the host's JSON-RPC error, `cancelled` marks a declined download. `updateModelContext` and `downloadFile` reject with `code: -32601` when the host lacks the capability; `requestDisplayMode` resolves with the current mode instead when the host does not offer the requested one. All three reject without sending anything when the app runs outside a host or the `ui/initialize` handshake did not complete.

```ts
import type { McpAppRequestError } from '@nuxtjs/mcp-toolkit/app'

try {
  await downloadFile([file])
}
catch (error) {
  if (!(error as McpAppRequestError).cancelled) showError()
}
```

## `useToolCall()`

`callTool` writes its result into `data`. To call a tool without touching `data` — say, an app-only tool that fetches file contents for a download — import `useToolCall`:

```ts
import { useToolCall } from '@nuxtjs/mcp-toolkit/app'

const png = useToolCall<{ filename: string, png: string }>('get_icon_png')

const result = await png.call({ iconId }) // `png.pending`, `png.error`, `png.result`, `png.meta` track the call; a result with `isError` sets `png.error`
```

Both `callTool` and `useToolCall` wait for the `ui/initialize` handshake before they send `tools/call`, so calling a tool on mount is safe.


## Sitemap

See the full [sitemap](https://mcp-toolkit.nuxt.dev/sitemap.md) for all pages.
