MCP-UI

Render interactive UI components from MCP tools

MCP-UI extends MCP to allow tools to return interactive HTML components like product cards, forms, and charts.

How It Works

User: "Show me the latest product"

MCP Tool returns HTML UI

┌─────────────────────────────────────────┐
│  Wireless Headphones - $99.99           │
│  [Add to Cart] [View Details]           │
└─────────────────────────────────────────┘

User clicks "Add to Cart" → Intent sent to your app

Handling Intents

Use useMCPUIIntents to handle user interactions from MCP-UI components:

import { CopilotProvider, useMCPTools, useMCPUIIntents } from '@yourgpt/copilot-sdk/react';
import { CopilotChat } from '@yourgpt/copilot-sdk/ui';

function App() {
  return (
    <CopilotProvider runtimeUrl="/api/chat">
      <Chat />
    </CopilotProvider>
  );
}

function Chat() {
  useMCPTools({
    name: "shop",
    transport: "http",
    url: "/api/mcp",
  });

  const { handleIntent } = useMCPUIIntents({
    onIntent: (action, data) => {
      if (action === "add_to_cart") {
        addToCart(data.productId, data.quantity);
      }
    },
    onNotify: (message, level) => {
      toast[level || "info"](message);
    },
    onPrompt: (text) => {
      setChatInput(text);
    },
    onLink: (url, newTab) => {
      if (url.startsWith("app://")) {
        router.push(url.replace("app://", "/"));
        return false; // Prevent default navigation
      }
    },
  });

  return <CopilotChat onUIIntent={handleIntent} />;
}

Intent Types

TypeHandlerDescription
toolonToolCallCall another MCP tool
intentonIntentSemantic action for your app
promptonPromptAdd text to chat input
notifyonNotifyShow notification
linkonLinkOpen a URL

Pre-built Components

The SDK includes ready-to-use components for displaying MCP connection status.

MCPPanel

Complete connection management panel:

import { MCPPanel } from '@yourgpt/copilot-sdk/ui';

<MCPPanel
  state={mcp.state}
  onConnect={mcp.connect}
  onDisconnect={mcp.disconnect}
  isLoading={mcp.isLoading}
  title="MCP360"
  collapsible
/>

MCPStatus

Simple connection status indicator:

import { MCPStatus } from '@yourgpt/copilot-sdk/ui';

<MCPStatus
  state={mcp.state.connectionState}
  serverName={mcp.state.serverInfo?.name}
  size="md"
  showLabel={true}
/>
StateColorLabel
disconnectedGray"Disconnected"
connectingYellow"Connecting..."
connectedGreen"Connected"
errorRed"Error"

MCPToolList

Display available tools:

import { MCPToolList } from '@yourgpt/copilot-sdk/ui';

<MCPToolList
  tools={mcp.state.tools}
  maxVisible={10}
  showSchema={true}
  onToolClick={(tool) => console.log(tool.name)}
/>

Creating MCP-UI Components

When building MCP servers, return UI content like this:

// MCP tool returning a product card
return {
  content: [
    { type: "text", text: "Here's the product:" },
    {
      type: "ui",
      resource: {
        uri: "ui://shop/product/123",
        mimeType: "text/html",
        content: `
          <div style="padding: 16px; border: 1px solid #ddd;">
            <h3>Wireless Headphones</h3>
            <p>$99.99</p>
            <button onclick="addToCart()">Add to Cart</button>
          </div>
          <script>
            function addToCart() {
              window.parent.postMessage({
                source: "mcp-ui",
                intent: {
                  type: "intent",
                  action: "add_to_cart",
                  data: { productId: "123", quantity: 1 }
                }
              }, "*");
            }
          </script>
        `,
        metadata: { title: "Product", height: "200px" },
      },
    },
  ],
};

Sending Intents from UI

// From within the iframe
window.parent.postMessage({
  source: "mcp-ui",
  intent: {
    type: "intent",          // or "tool", "prompt", "notify", "link"
    action: "add_to_cart",
    data: { productId: "123" }
  }
}, "*");

Security

MCP-UI content runs in sandboxed iframes. Default permissions: allow-scripts allow-forms.

// Custom sandbox permissions in metadata
metadata: {
  sandbox: ["allow-scripts", "allow-forms", "allow-same-origin"],
}

Be careful with allow-same-origin - it allows iframe access to parent cookies.

On this page