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

# MCP App Tool and Resource Contract

> A field-by-field server checklist for MCP App tools, ui:// HTML resources, _meta.ui.resourceUri, tool annotations, structuredContent, outputSchema, CSP metadata, and app-only tool visibility.

<Badge color="green">MCP Apps SDK</Badge>

An MCP App is a normal MCP tool plus a `ui://` HTML resource. The tool returns data. The resource renders the UI. The link between them is `_meta.ui.resourceUri`.

Use this page as the server-side contract to check before testing in a host.

## Choose the Right Tool Shape

Most MCP App servers have three tool shapes. Use this table before adding metadata:

| Tool shape                 | Model can call | View can call | Renders a View | Required metadata                                          |
| -------------------------- | -------------- | ------------- | -------------- | ---------------------------------------------------------- |
| UI-launching tool          | Yes            | Usually yes   | Yes            | `_meta.ui.resourceUri` plus optional `visibility`.         |
| App-only helper tool       | No             | Yes           | No             | `_meta.ui.visibility: ["app"]`. `resourceUri` is optional. |
| Backend-only standard tool | Yes            | No            | No             | No MCP Apps metadata. Use normal MCP tool metadata.        |

Use UI-launching tools for the actions a user can ask the model to perform, such as "show orders." Use app-only helper tools for actions the rendered View triggers directly, such as refresh, pagination, autosave, or form submit. Use backend-only tools when the result belongs in the conversation and no UI should render.

## Required Pieces for a UI Tool

| Piece               | Where it appears                          | What it does                                                                                            |
| ------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Tool definition     | `tools/list`                              | Gives the model a callable tool with `name`, `description`, `inputSchema`, and optional `outputSchema`. |
| Tool annotations    | `tool.annotations`                        | Gives hosts safety hints such as read-only, destructive, idempotent, or external-system behavior.       |
| Tool UI metadata    | `tool._meta.ui.resourceUri`               | Points the host at the HTML resource to render after the tool call.                                     |
| Resource definition | `resources/list` or tool-linked discovery | Declares the `ui://` resource and `text/html;profile=mcp-app` MIME type.                                |
| Resource contents   | `resources/read`                          | Returns the HTML document and any iframe metadata under `contents[]._meta.ui`.                          |
| Tool result         | `tools/call` result                       | Returns model-readable `content` and UI-readable `structuredContent`.                                   |

<Note>
  MCP Apps are optional. Servers should check the host's MCP Apps capability before registering UI
  metadata, then fall back to text-only MCP tools when the host does not support
  `text/html;profile=mcp-app`.
</Note>

## Minimal Contract

```ts theme={null}
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import {
  registerAppResource,
  registerAppTool,
  RESOURCE_MIME_TYPE,
} from '@modelcontextprotocol/ext-apps/server';
import { z } from 'zod';

const server = new McpServer({ name: 'orders', version: '1.0.0' });
const resourceUri = 'ui://orders/view.html';

registerAppResource(
  server,
  'Orders View',
  resourceUri,
  { mimeType: RESOURCE_MIME_TYPE },
  async () => ({
    contents: [
      {
        uri: resourceUri,
        mimeType: RESOURCE_MIME_TYPE,
        text: '<!doctype html><html><body><div id="root"></div></body></html>',
        _meta: {
          ui: {
            csp: {
              connectDomains: ['https://api.example.com'],
              resourceDomains: ['https://cdn.example.com'],
            },
            prefersBorder: true,
          },
        },
      },
    ],
  })
);

registerAppTool(
  server,
  'search-orders',
  {
    title: 'Search Orders',
    description: 'Search orders and display the results.',
    inputSchema: { query: z.string() },
    outputSchema: {
      query: z.string(),
      orders: z.array(z.object({ id: z.string(), total: z.number() })),
    },
    annotations: {
      readOnlyHint: true,
      openWorldHint: false,
    },
    _meta: {
      ui: {
        resourceUri,
        visibility: ['model', 'app'],
      },
    },
  },
  async ({ query }) => {
    const result = {
      query,
      orders: [{ id: 'ord_123', total: 128.5 }],
    };

    return {
      content: [{ type: 'text', text: JSON.stringify(result) }],
      structuredContent: result,
    };
  }
);
```

## Tool Annotations

Use standard MCP `annotations` to describe what the tool does. Use `_meta.ui` to describe MCP App behavior such as View linkage and visibility.

| Annotation        | Use it for                                                 | Example                                       |
| ----------------- | ---------------------------------------------------------- | --------------------------------------------- |
| `readOnlyHint`    | Tools that do not modify state.                            | Search, list, preview, load dashboard.        |
| `destructiveHint` | Tools that can delete, overwrite, cancel, charge, or send. | Cancel order, delete record, submit payment.  |
| `idempotentHint`  | Tools that are safe to retry with the same arguments.      | Save draft by stable id, refresh cache.       |
| `openWorldHint`   | Tools that interact with systems outside your backend.     | Email, payment, public web, third-party APIs. |

Tool visibility and annotations are independent. An app-only helper can still be destructive, and a UI-launching model tool can be read-only.

```ts theme={null}
annotations: {
  readOnlyHint: true,
  openWorldHint: false,
},
_meta: {
  ui: {
    resourceUri: "ui://orders/view.html",
    visibility: ["model", "app"],
  },
}
```

See [Tool Annotations](/mcp-apps/server/tool-annotations) for examples of read-only UI tools, app-only write helpers, and destructive actions.

## Tool Metadata

Put UI linkage on the tool, not in the tool result:

```ts theme={null}
_meta: {
  ui: {
    resourceUri: "ui://orders/view.html",
    visibility: ["model", "app"],
  },
}
```

| Field                     | Requirement                     | Notes                                                                                                        |
| ------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `_meta.ui.resourceUri`    | Required for UI-launching tools | Must match the resource URI exactly. Use `ui://`. Optional for app-only helpers that do not render a View.   |
| `_meta.ui.visibility`     | Optional                        | Defaults to `["model", "app"]`. Use `["app"]` for UI-only actions.                                           |
| `_meta["ui/resourceUri"]` | Deprecated                      | `registerAppTool` writes compatibility metadata for older hosts. New code should use `_meta.ui.resourceUri`. |

## Resource Contents

The resource is the HTML template. The tool result is the data. Keep them separate so hosts can read, inspect, and cache UI HTML before the tool runs.

```ts theme={null}
{
  contents: [
    {
      uri: "ui://orders/view.html",
      mimeType: "text/html;profile=mcp-app",
      text: html,
      _meta: {
        ui: {
          csp: {
            connectDomains: ["https://api.example.com"],
            resourceDomains: ["https://cdn.example.com"],
          },
          permissions: { clipboardWrite: {} },
          prefersBorder: true,
        },
      },
    },
  ],
}
```

| Field                    | Requirement                           | Notes                                                                                     |
| ------------------------ | ------------------------------------- | ----------------------------------------------------------------------------------------- |
| `uri`                    | Required                              | Must match the tool's `_meta.ui.resourceUri`.                                             |
| `mimeType`               | Required                              | Use `RESOURCE_MIME_TYPE`, which is `text/html;profile=mcp-app`.                           |
| `text` or `blob`         | Required                              | Return a valid HTML document as text or base64 HTML.                                      |
| `_meta.ui.csp`           | Required when loading outside origins | Declare fetch, script, style, font, image, media, and nested iframe origins.              |
| `_meta.ui.permissions`   | Optional                              | Hosts may grant requested browser capabilities, but apps must handle denial.              |
| `_meta.ui.domain`        | Optional                              | Host-specific stable sandbox origin for CORS, OAuth callbacks, or origin-restricted APIs. |
| `_meta.ui.prefersBorder` | Recommended                           | Set this explicitly because host defaults vary.                                           |

## Tool Results

Use both MCP result channels:

```ts theme={null}
return {
  content: [{ type: 'text', text: JSON.stringify(result) }],
  structuredContent: result,
};
```

| Field               | Audience                        | Guidance                                                                                                                            |
| ------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `content`           | Model and text-only clients     | Include a short text summary. For structured results, MCP recommends also serializing the JSON into a text block for compatibility. |
| `structuredContent` | Host, View, and typed clients   | Put the object your UI renders here. If you declare `outputSchema`, this object must match it.                                      |
| `_meta`             | Host or component-only metadata | Use sparingly. Do not rely on it for model-visible facts.                                                                           |
| `isError`           | Host and model                  | Set `true` for tool execution errors that should still return a tool result. Use JSON-RPC errors for protocol failures.             |

## App-Only Helper Tools

Set `visibility: ["app"]` when the View needs a server round trip that the model should not see, such as refresh, pagination, autosave, or form submit.

An app-only helper tool may include `resourceUri` when it belongs to a specific View, but it does not have to. Omitting `resourceUri` means the helper is callable from the app connection but does not tell the host to render a new iframe when the tool is called.

```ts theme={null}
registerAppTool(
  server,
  'refresh-orders',
  {
    description: 'Refresh order data for the app UI.',
    inputSchema: { query: z.string() },
    _meta: {
      ui: {
        visibility: ['app'],
      },
    },
  },
  async ({ query }) => {
    const result = await searchOrders(query);

    return {
      content: [{ type: 'text', text: JSON.stringify(result) }],
      structuredContent: result,
    };
  }
);
```

The host must hide app-only tools from the model tool list and reject View calls to tools whose visibility does not include `"app"`.

## Server Checklist

Before testing in a real host, verify the server response shape:

| Check                                      | Why it matters                                                                    |
| ------------------------------------------ | --------------------------------------------------------------------------------- |
| `initialize` advertises MCP Apps support   | Lets the server decide whether to send UI metadata or fall back to text tools.    |
| `tools/list` includes `_meta.ui`           | Gives MCP Apps hosts the `resourceUri` and `visibility` contract.                 |
| `resources/read` returns app HTML          | Hosts render the returned HTML in a sandboxed iframe.                             |
| HTML uses `text/html;profile=mcp-app`      | Distinguishes an MCP App View from a plain MCP resource.                          |
| CSP lists every external origin            | Fetches, scripts, images, fonts, media, and nested iframes are otherwise blocked. |
| Tool annotations match behavior            | Lets hosts apply useful review UI and avoid treating writes as reads.             |
| `outputSchema` matches `structuredContent` | Lets clients validate the typed tool result that the View consumes.               |
| `content` includes text fallback           | Keeps text-only MCP clients and older hosts useful.                               |
| App-only helpers use `visibility`          | Stops the model from calling UI-only actions such as refresh or submit.           |

## Common Failures

| Symptom                                      | Likely cause                                 | Fix                                                                       |
| -------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------- |
| The tool runs but no UI appears              | Missing or mismatched `_meta.ui.resourceUri` | Check that the tool URI and resource URI are identical.                   |
| The iframe is blank                          | Wrong MIME type or invalid HTML              | Use `RESOURCE_MIME_TYPE` and return a full HTML document.                 |
| Images, scripts, or fetch calls fail         | Missing CSP metadata                         | Add the origin to `resourceDomains`, `connectDomains`, or `frameDomains`. |
| The model calls a refresh or pagination tool | Tool is visible to `"model"`                 | Set `visibility: ["app"]`.                                                |
| The View receives hard-to-parse text         | Tool only returned `content`                 | Add `structuredContent` and an `outputSchema`.                            |
| A text-only host gets unusable output        | Tool only returned `structuredContent`       | Also return model-readable `content`.                                     |

## Related

<Card horizontal title="Build an MCP App Server" icon="server" href="/mcp-apps/server/build-server">
  Complete server example with capability detection and fallback tools.
</Card>

<Card horizontal title="Add a UI to an MCP Tool" icon="page" href="/mcp-apps/server/add-ui-to-tool">
  Convert a standard MCP tool into a UI-rendering MCP App.
</Card>

<Card horizontal title="Tool _meta" icon="page" href="/mcp-apps/server/tool-meta">
  Full reference for resource linkage and tool visibility.
</Card>

<Card horizontal title="Tool Annotations" icon="shield-check" href="/mcp-apps/server/tool-annotations">
  Add host-facing safety hints to MCP App tools.
</Card>

<Card horizontal title="Resource _meta" icon="page" href="/mcp-apps/server/resource-meta">
  Full reference for CSP, permissions, stable domains, and borders.
</Card>
