Skip to main content
MCP Apps SDK 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 shapeModel can callView can callRenders a ViewRequired metadata
UI-launching toolYesUsually yesYes_meta.ui.resourceUri plus optional visibility.
App-only helper toolNoYesNo_meta.ui.visibility: ["app"]. resourceUri is optional.
Backend-only standard toolYesNoNoNo 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

PieceWhere it appearsWhat it does
Tool definitiontools/listGives the model a callable tool with name, description, inputSchema, and optional outputSchema.
Tool annotationstool.annotationsGives hosts safety hints such as read-only, destructive, idempotent, or external-system behavior.
Tool UI metadatatool._meta.ui.resourceUriPoints the host at the HTML resource to render after the tool call.
Resource definitionresources/list or tool-linked discoveryDeclares the ui:// resource and text/html;profile=mcp-app MIME type.
Resource contentsresources/readReturns the HTML document and any iframe metadata under contents[]._meta.ui.
Tool resulttools/call resultReturns model-readable content and UI-readable structuredContent.
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.

Minimal Contract

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.
AnnotationUse it forExample
readOnlyHintTools that do not modify state.Search, list, preview, load dashboard.
destructiveHintTools that can delete, overwrite, cancel, charge, or send.Cancel order, delete record, submit payment.
idempotentHintTools that are safe to retry with the same arguments.Save draft by stable id, refresh cache.
openWorldHintTools 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.
annotations: {
  readOnlyHint: true,
  openWorldHint: false,
},
_meta: {
  ui: {
    resourceUri: "ui://orders/view.html",
    visibility: ["model", "app"],
  },
}
See 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:
_meta: {
  ui: {
    resourceUri: "ui://orders/view.html",
    visibility: ["model", "app"],
  },
}
FieldRequirementNotes
_meta.ui.resourceUriRequired for UI-launching toolsMust match the resource URI exactly. Use ui://. Optional for app-only helpers that do not render a View.
_meta.ui.visibilityOptionalDefaults to ["model", "app"]. Use ["app"] for UI-only actions.
_meta["ui/resourceUri"]DeprecatedregisterAppTool 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.
{
  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,
        },
      },
    },
  ],
}
FieldRequirementNotes
uriRequiredMust match the tool’s _meta.ui.resourceUri.
mimeTypeRequiredUse RESOURCE_MIME_TYPE, which is text/html;profile=mcp-app.
text or blobRequiredReturn a valid HTML document as text or base64 HTML.
_meta.ui.cspRequired when loading outside originsDeclare fetch, script, style, font, image, media, and nested iframe origins.
_meta.ui.permissionsOptionalHosts may grant requested browser capabilities, but apps must handle denial.
_meta.ui.domainOptionalHost-specific stable sandbox origin for CORS, OAuth callbacks, or origin-restricted APIs.
_meta.ui.prefersBorderRecommendedSet this explicitly because host defaults vary.

Tool Results

Use both MCP result channels:
return {
  content: [{ type: 'text', text: JSON.stringify(result) }],
  structuredContent: result,
};
FieldAudienceGuidance
contentModel and text-only clientsInclude a short text summary. For structured results, MCP recommends also serializing the JSON into a text block for compatibility.
structuredContentHost, View, and typed clientsPut the object your UI renders here. If you declare outputSchema, this object must match it.
_metaHost or component-only metadataUse sparingly. Do not rely on it for model-visible facts.
isErrorHost and modelSet 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.
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:
CheckWhy it matters
initialize advertises MCP Apps supportLets the server decide whether to send UI metadata or fall back to text tools.
tools/list includes _meta.uiGives MCP Apps hosts the resourceUri and visibility contract.
resources/read returns app HTMLHosts render the returned HTML in a sandboxed iframe.
HTML uses text/html;profile=mcp-appDistinguishes an MCP App View from a plain MCP resource.
CSP lists every external originFetches, scripts, images, fonts, media, and nested iframes are otherwise blocked.
Tool annotations match behaviorLets hosts apply useful review UI and avoid treating writes as reads.
outputSchema matches structuredContentLets clients validate the typed tool result that the View consumes.
content includes text fallbackKeeps text-only MCP clients and older hosts useful.
App-only helpers use visibilityStops the model from calling UI-only actions such as refresh or submit.

Common Failures

SymptomLikely causeFix
The tool runs but no UI appearsMissing or mismatched _meta.ui.resourceUriCheck that the tool URI and resource URI are identical.
The iframe is blankWrong MIME type or invalid HTMLUse RESOURCE_MIME_TYPE and return a full HTML document.
Images, scripts, or fetch calls failMissing CSP metadataAdd the origin to resourceDomains, connectDomains, or frameDomains.
The model calls a refresh or pagination toolTool is visible to "model"Set visibility: ["app"].
The View receives hard-to-parse textTool only returned contentAdd structuredContent and an outputSchema.
A text-only host gets unusable outputTool only returned structuredContentAlso return model-readable content.

Build an MCP App Server

Complete server example with capability detection and fallback tools.

Add a UI to an MCP Tool

Convert a standard MCP tool into a UI-rendering MCP App.

Tool _meta

Full reference for resource linkage and tool visibility.

Tool Annotations

Add host-facing safety hints to MCP App tools.

Resource _meta

Full reference for CSP, permissions, stable domains, and borders.