<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MCP Apps Extension (SEP-1865) - Complete Guide</title>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #6e7681;
--border-color: #30363d;
--accent-blue: #58a6ff;
--accent-green: #3fb950;
--accent-purple: #a371f7;
--accent-orange: #d29922;
--accent-red: #f85149;
--code-bg: #0d1117;
--font-mono: 'SF Mono', 'Fira Code', 'Monaco', monospace;
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-sans);
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
font-size: 16px;
}
/* Layout */
.container {
display: grid;
grid-template-columns: 280px 1fr;
min-height: 100vh;
}
/* Sidebar */
.sidebar {
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
padding: 24px;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
.sidebar h1 {
font-size: 18px;
color: var(--text-primary);
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.sidebar .badge {
font-size: 10px;
background: var(--accent-orange);
color: var(--bg-primary);
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
}
.sidebar .subtitle {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 24px;
}
.nav-section {
margin-bottom: 20px;
}
.nav-section-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-muted);
margin-bottom: 8px;
font-weight: 600;
}
.nav-link {
display: block;
color: var(--text-secondary);
text-decoration: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 14px;
margin-bottom: 2px;
transition: all 0.15s;
}
.nav-link:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.nav-link.active {
background: rgba(88, 166, 255, 0.1);
color: var(--accent-blue);
}
/* Main Content */
main {
padding: 48px 64px;
max-width: 900px;
}
h2 {
font-size: 32px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
color: var(--text-primary);
}
h3 {
font-size: 22px;
margin: 32px 0 16px;
color: var(--text-primary);
}
h4 {
font-size: 16px;
margin: 24px 0 12px;
color: var(--accent-blue);
font-weight: 600;
}
p {
margin-bottom: 16px;
color: var(--text-secondary);
}
section {
margin-bottom: 64px;
}
/* Code blocks */
pre {
background: var(--code-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
overflow-x: auto;
margin: 16px 0;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.5;
}
code {
font-family: var(--font-mono);
font-size: 0.9em;
background: var(--bg-tertiary);
padding: 2px 6px;
border-radius: 4px;
color: var(--accent-blue);
}
pre code {
background: none;
padding: 0;
color: var(--text-primary);
}
.code-comment { color: var(--text-muted); }
.code-keyword { color: var(--accent-purple); }
.code-string { color: var(--accent-green); }
.code-type { color: var(--accent-orange); }
.code-function { color: var(--accent-blue); }
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 14px;
}
th, td {
text-align: left;
padding: 12px;
border: 1px solid var(--border-color);
}
th {
background: var(--bg-tertiary);
font-weight: 600;
color: var(--text-primary);
}
td {
color: var(--text-secondary);
}
/* Cards */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin: 16px 0;
}
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
transition: border-color 0.2s;
}
.card:hover {
border-color: var(--accent-blue);
}
.card h5 {
color: var(--text-primary);
font-size: 16px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.card p {
font-size: 14px;
margin: 0;
}
.card .icon {
width: 32px;
height: 32px;
background: var(--bg-tertiary);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
/* Diagrams */
.diagram {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.4;
overflow-x: auto;
white-space: pre;
}
.diagram .highlight {
color: var(--accent-blue);
}
.diagram .muted {
color: var(--text-muted);
}
.diagram .green {
color: var(--accent-green);
}
.diagram .purple {
color: var(--accent-purple);
}
.diagram .orange {
color: var(--accent-orange);
}
/* Callouts */
.callout {
padding: 16px 20px;
border-radius: 8px;
margin: 16px 0;
border-left: 4px solid;
}
.callout-info {
background: rgba(88, 166, 255, 0.1);
border-color: var(--accent-blue);
}
.callout-warning {
background: rgba(210, 153, 34, 0.1);
border-color: var(--accent-orange);
}
.callout-success {
background: rgba(63, 185, 80, 0.1);
border-color: var(--accent-green);
}
.callout-danger {
background: rgba(248, 81, 73, 0.1);
border-color: var(--accent-red);
}
.callout p {
margin: 0;
color: var(--text-primary);
}
.callout strong {
display: block;
margin-bottom: 4px;
}
/* Lists */
ul, ol {
margin: 16px 0 16px 24px;
color: var(--text-secondary);
}
li {
margin-bottom: 8px;
}
/* Hero section */
.hero {
text-align: center;
padding: 48px 0;
margin-bottom: 48px;
border-bottom: 1px solid var(--border-color);
}
.hero h2 {
font-size: 42px;
border: none;
margin-bottom: 16px;
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero p {
font-size: 18px;
max-width: 600px;
margin: 0 auto 24px;
}
.hero-badges {
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.hero-badge {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
color: var(--text-secondary);
}
.hero-badge strong {
color: var(--text-primary);
}
/* Flow diagram */
.flow-step {
display: flex;
align-items: flex-start;
gap: 16px;
margin: 16px 0;
}
.flow-number {
width: 28px;
height: 28px;
background: var(--accent-blue);
color: var(--bg-primary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
flex-shrink: 0;
}
.flow-content {
flex: 1;
}
.flow-content h5 {
color: var(--text-primary);
margin-bottom: 4px;
}
.flow-content p {
margin: 0;
font-size: 14px;
}
/* Responsive */
@media (max-width: 900px) {
.container {
grid-template-columns: 1fr;
}
.sidebar {
display: none;
}
main {
padding: 24px;
}
}
/* API Reference tables */
.api-table {
font-size: 13px;
}
.api-table code {
font-size: 12px;
}
.api-table td:first-child {
font-family: var(--font-mono);
color: var(--accent-blue);
white-space: nowrap;
}
.method-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.method-request {
background: rgba(88, 166, 255, 0.2);
color: var(--accent-blue);
}
.method-notification {
background: rgba(63, 185, 80, 0.2);
color: var(--accent-green);
}
</style>
</head>
<body>
<div class="container">
<aside class="sidebar">
<h1>MCP Apps <span class="badge">DRAFT</span></h1>
<div class="subtitle">SEP-1865 Extension Guide</div>
<nav>
<div class="nav-section">
<div class="nav-section-title">Overview</div>
<a href="#introduction" class="nav-link">Introduction</a>
<a href="#key-concepts" class="nav-link">Key Concepts</a>
<a href="#architecture" class="nav-link">Architecture</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Core Components</div>
<a href="#ui-resources" class="nav-link">UI Resources</a>
<a href="#tool-linkage" class="nav-link">Tool-UI Linkage</a>
<a href="#app-bridge" class="nav-link">AppBridge Protocol</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Security</div>
<a href="#security-model" class="nav-link">Security Model</a>
<a href="#sandbox" class="nav-link">Sandbox Architecture</a>
<a href="#csp" class="nav-link">CSP Configuration</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Implementation</div>
<a href="#server-side" class="nav-link">Server Implementation</a>
<a href="#client-side" class="nav-link">Building UIs</a>
<a href="#host-side" class="nav-link">Host Implementation</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Reference</div>
<a href="#api-reference" class="nav-link">API Reference</a>
<a href="#css-variables" class="nav-link">CSS Variables</a>
<a href="#examples" class="nav-link">Examples</a>
</div>
</nav>
</aside>
<main>
<div class="hero">
<h2>MCP Apps Extension</h2>
<p>A standardized extension for MCP servers to deliver interactive user interfaces to AI hosts, transforming chatbots into full-stack application runtimes.</p>
<div class="hero-badges">
<span class="hero-badge"><strong>SEP-1865</strong></span>
<span class="hero-badge">Protocol: <strong>2025-11-21</strong></span>
<span class="hero-badge">Status: <strong>Draft</strong></span>
<span class="hero-badge">By: <strong>Anthropic + OpenAI + Community</strong></span>
</div>
</div>
<!-- Introduction -->
<section id="introduction">
<h2>Introduction</h2>
<p>MCP Apps Extension (SEP-1865) defines a standard way for MCP servers to provide interactive HTML user interfaces that render directly in AI chat interfaces. Instead of tools returning just text or JSON, they can now return full interactive applications.</p>
<h3>Before vs After</h3>
<table>
<thead>
<tr>
<th>Traditional MCP</th>
<th>MCP Apps</th>
</tr>
</thead>
<tbody>
<tr>
<td>Tools return text/JSON only</td>
<td>Tools return text + interactive UI</td>
</tr>
<tr>
<td>"Here's your data as JSON"</td>
<td>Interactive tables, charts, forms</td>
</tr>
<tr>
<td>Copy-paste to use results</td>
<td>Click buttons, drag-drop, real-time updates</td>
</tr>
<tr>
<td>Text-only responses</td>
<td>Rich media, visualizations, controls</td>
</tr>
<tr>
<td>Stateless tool calls</td>
<td>Persistent UI sessions with bidirectional communication</td>
</tr>
</tbody>
</table>
<h3>Key Design Decisions</h3>
<div class="card-grid">
<div class="card">
<h5><span class="icon">📦</span> Predeclared Resources</h5>
<p>UI templates are registered as resources, enabling prefetching, caching, and security review before execution.</p>
</div>
<div class="card">
<h5><span class="icon">🔌</span> MCP Transport</h5>
<p>Uses JSON-RPC over postMessage, reusing existing MCP infrastructure for auditability.</p>
</div>
<div class="card">
<h5><span class="icon">🔒</span> Security First</h5>
<p>Mandatory iframe sandboxing with CSP enforcement. All communication is auditable.</p>
</div>
<div class="card">
<h5><span class="icon">🎨</span> Framework Agnostic</h5>
<p>HTML-only MVP supports any framework. React hooks provided for convenience.</p>
</div>
</div>
</section>
<!-- Key Concepts -->
<section id="key-concepts">
<h2>Key Concepts</h2>
<h3>The Core Flow</h3>
<div class="diagram"><span class="muted">Traditional MCP:</span>
┌─────────┐ tool call ┌────────────┐
│ <span class="highlight">Host</span> │ ─────────────────► │ <span class="purple">MCP Server</span> │
│(Claude) │ ◄───────────────── │ │
└─────────┘ text response └────────────┘
<span class="muted">MCP Apps:</span>
┌─────────┐ tool call ┌────────────┐
│ <span class="highlight">Host</span> │ ─────────────────► │ <span class="purple">MCP Server</span> │
│(Claude) │ ◄───────────────── │ │
└─────────┘ <span class="green">text + UI ref</span> └────────────┘
│ │
│ fetch UI resource │
│ ◄────────────────────────────┤
▼
┌─────────────────────────────────────┐
│ <span class="orange">Interactive HTML App in Sandbox</span> │
│ (can send messages back to chat) │
└─────────────────────────────────────┘</div>
<h3>Components</h3>
<h4>1. UI Resources</h4>
<p>HTML applications registered with the special MIME type <code>text/html;profile=mcp-app</code> and a <code>ui://</code> URI scheme.</p>
<h4>2. Tool-UI Linkage</h4>
<p>Tools reference their UI via the <code>_meta.ui.resourceUri</code> field, telling the host which UI to render when the tool is called.</p>
<h4>3. AppBridge</h4>
<p>The bidirectional communication layer between the host and the sandboxed UI app, using JSON-RPC over postMessage.</p>
<h4>4. Security Sandbox</h4>
<p>All UIs run in sandboxed iframes with strict CSP, preventing access to the host's DOM, cookies, or network (unless explicitly allowed).</p>
</section>
<!-- Architecture -->
<section id="architecture">
<h2>Architecture</h2>
<h3>Double-Iframe Sandbox (Web Hosts)</h3>
<p>For maximum security, web-based hosts use a double-iframe architecture with different origins:</p>
<div class="diagram">┌──────────────────────────────────────────────────────────────┐
│ <span class="highlight">Host Application</span> (origin: localhost:8080) │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ <span class="purple">Sandbox Proxy iframe</span> (origin: localhost:8081) │ │
│ │ sandbox="allow-scripts allow-same-origin" │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ <span class="orange">MCP App iframe</span> (srcdoc, null origin) │ │ │
│ │ │ sandbox="allow-scripts" │ │ │
│ │ │ CSP: strict, no external resources by default │ │ │
│ │ │ │ │ │
│ │ │ <span class="green">Your interactive UI runs here</span> │ │ │
│ │ │ • Cannot access parent origins │ │ │
│ │ │ • Cannot make network requests (by default) │ │ │
│ │ │ • Communicates only via postMessage │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘</div>
<h3>Communication Flow</h3>
<div class="flow-step">
<div class="flow-number">1</div>
<div class="flow-content">
<h5>Host discovers resources & tools</h5>
<p>Calls <code>resources/list</code> and <code>tools/list</code> to find UI-enabled tools.</p>
</div>
</div>
<div class="flow-step">
<div class="flow-number">2</div>
<div class="flow-content">
<h5>User calls tool</h5>
<p>Host creates sandboxed iframe and loads the UI resource.</p>
</div>
</div>
<div class="flow-step">
<div class="flow-number">3</div>
<div class="flow-content">
<h5>UI initializes</h5>
<p>Guest UI sends <code>ui/initialize</code>, host responds with capabilities and context.</p>
</div>
</div>
<div class="flow-step">
<div class="flow-number">4</div>
<div class="flow-content">
<h5>Data exchange</h5>
<p>Host sends tool input/result, UI can call tools, send messages, request links.</p>
</div>
</div>
<div class="flow-step">
<div class="flow-number">5</div>
<div class="flow-content">
<h5>Interactive phase</h5>
<p>Bidirectional communication continues until teardown.</p>
</div>
</div>
</section>
<!-- UI Resources -->
<section id="ui-resources">
<h2>UI Resources</h2>
<h3>Resource Declaration</h3>
<pre><code><span class="code-comment">// In resources/list response</span>
{
<span class="code-string">"uri"</span>: <span class="code-string">"ui://my-server/dashboard.html"</span>,
<span class="code-string">"name"</span>: <span class="code-string">"dashboard"</span>,
<span class="code-string">"description"</span>: <span class="code-string">"Interactive dashboard UI"</span>,
<span class="code-string">"mimeType"</span>: <span class="code-string">"text/html;profile=mcp-app"</span>,
<span class="code-string">"_meta"</span>: {
<span class="code-string">"ui"</span>: {
<span class="code-string">"csp"</span>: {
<span class="code-string">"connectDomains"</span>: [<span class="code-string">"https://api.example.com"</span>],
<span class="code-string">"resourceDomains"</span>: [<span class="code-string">"https://cdn.example.com"</span>]
}
}
}
}</code></pre>
<h3>Resource Content</h3>
<pre><code><span class="code-comment">// In resources/read response</span>
{
<span class="code-string">"contents"</span>: [{
<span class="code-string">"uri"</span>: <span class="code-string">"ui://my-server/dashboard.html"</span>,
<span class="code-string">"mimeType"</span>: <span class="code-string">"text/html;profile=mcp-app"</span>,
<span class="code-string">"text"</span>: <span class="code-string">"<!DOCTYPE html><html>...</html>"</span>
}]
}</code></pre>
<div class="callout callout-info">
<p><strong>URI Scheme</strong></p>
<p>The <code>ui://</code> scheme is reserved for MCP Apps. The format is <code>ui://server-name/resource-path</code>.</p>
</div>
</section>
<!-- Tool Linkage -->
<section id="tool-linkage">
<h2>Tool-UI Linkage</h2>
<h3>Linking a Tool to UI</h3>
<pre><code><span class="code-comment">// Tool definition with UI linkage</span>
{
<span class="code-string">"name"</span>: <span class="code-string">"get_weather"</span>,
<span class="code-string">"description"</span>: <span class="code-string">"Get weather with interactive visualization"</span>,
<span class="code-string">"inputSchema"</span>: { ... },
<span class="code-string">"_meta"</span>: {
<span class="code-string">"ui"</span>: {
<span class="code-string">"resourceUri"</span>: <span class="code-string">"ui://weather-server/dashboard.html"</span>,
<span class="code-string">"visibility"</span>: [<span class="code-string">"model"</span>, <span class="code-string">"app"</span>]
}
}
}</code></pre>
<h3>Visibility Options</h3>
<table>
<thead>
<tr>
<th>Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>["model", "app"]</code></td>
<td>Default. Visible to both AI agent and app.</td>
</tr>
<tr>
<td><code>["model"]</code></td>
<td>Only visible to AI agent, hidden from app.</td>
</tr>
<tr>
<td><code>["app"]</code></td>
<td>Only callable by app from same server.</td>
</tr>
</tbody>
</table>
<h3>Tool Result with UI</h3>
<pre><code><span class="code-comment">// Tool execution returns data + triggers UI</span>
{
<span class="code-string">"content"</span>: [{
<span class="code-string">"type"</span>: <span class="code-string">"text"</span>,
<span class="code-string">"text"</span>: <span class="code-string">"{\"temperature\": 72, \"condition\": \"sunny\"}"</span>
}],
<span class="code-string">"_meta"</span>: {
<span class="code-string">"ui/resourceUri"</span>: <span class="code-string">"ui://weather-server/dashboard.html"</span>
}
}</code></pre>
</section>
<!-- AppBridge Protocol -->
<section id="app-bridge">
<h2>AppBridge Protocol</h2>
<p>The AppBridge handles all communication between the host and guest UI using JSON-RPC 2.0 over postMessage.</p>
<h3>Requests (UI → Host)</h3>
<table class="api-table">
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>ui/initialize</td>
<td>Handshake with host, exchange capabilities</td>
</tr>
<tr>
<td>ui/message</td>
<td>Send message to host's chat interface</td>
</tr>
<tr>
<td>ui/open-link</td>
<td>Request host to open external URL</td>
</tr>
<tr>
<td>ui/request-display-mode</td>
<td>Request inline/fullscreen/PiP mode</td>
</tr>
<tr>
<td>tools/call</td>
<td>Execute tool on MCP server</td>
</tr>
<tr>
<td>tools/list</td>
<td>List available tools</td>
</tr>
<tr>
<td>resources/read</td>
<td>Read a resource from server</td>
</tr>
</tbody>
</table>
<h3>Notifications (Host → UI)</h3>
<table class="api-table">
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>ui/notifications/tool-input</td>
<td>Complete tool arguments</td>
</tr>
<tr>
<td>ui/notifications/tool-input-partial</td>
<td>Streaming partial arguments</td>
</tr>
<tr>
<td>ui/notifications/tool-result</td>
<td>Tool execution result</td>
</tr>
<tr>
<td>ui/notifications/tool-cancelled</td>
<td>Tool was cancelled</td>
</tr>
<tr>
<td>ui/notifications/host-context-changed</td>
<td>Theme/viewport/locale changed</td>
</tr>
</tbody>
</table>
</section>
<!-- Security Model -->
<section id="security-model">
<h2>Security Model</h2>
<div class="callout callout-danger">
<p><strong>Security is Mandatory</strong></p>
<p>All UI content MUST run in sandboxed iframes. There are no exceptions. Host implementations that skip sandboxing are non-compliant.</p>
</div>
<h3>Security Layers</h3>
<div class="card-grid">
<div class="card">
<h5><span class="icon">🔐</span> Iframe Sandbox</h5>
<p>Standard sandbox attributes prevent DOM access, cookie access, and JavaScript context escape.</p>
</div>
<div class="card">
<h5><span class="icon">🛡️</span> CSP Enforcement</h5>
<p>Content Security Policy restricts network requests and resource loading.</p>
</div>
<div class="card">
<h5><span class="icon">📋</span> Auditable Comms</h5>
<p>All messages use JSON-RPC format, enabling logging and security review.</p>
</div>
<div class="card">
<h5><span class="icon">👁️</span> Resource Review</h5>
<p>Hosts can inspect UI HTML before rendering, detecting malicious patterns.</p>
</div>
</div>
</section>
<!-- Sandbox -->
<section id="sandbox">
<h2>Sandbox Architecture</h2>
<h3>Sandbox Attributes</h3>
<pre><code><span class="code-comment"><!-- Outer iframe (sandbox proxy) --></span>
<iframe sandbox=<span class="code-string">"allow-scripts allow-same-origin"</span>></iframe>
<span class="code-comment"><!-- Inner iframe (guest UI) --></span>
<iframe sandbox=<span class="code-string">"allow-scripts"</span> srcdoc=<span class="code-string">"..."</span>></iframe></code></pre>
<h3>What's Blocked</h3>
<ul>
<li>Access to parent window's DOM or JavaScript context</li>
<li>Reading cookies or localStorage from host origin</li>
<li>Making network requests (unless CSP allows specific domains)</li>
<li>Opening popups or navigating the top frame</li>
<li>Accessing clipboard, geolocation, or other sensitive APIs</li>
</ul>
<h3>What's Allowed</h3>
<ul>
<li>Running JavaScript within the sandboxed context</li>
<li>Communicating with host via postMessage</li>
<li>Rendering UI with full HTML/CSS capabilities</li>
<li>Making requests to allowed domains (via CSP)</li>
</ul>
</section>
<!-- CSP -->
<section id="csp">
<h2>CSP Configuration</h2>
<h3>Default CSP (Most Restrictive)</h3>
<pre><code>default-src 'none';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
media-src 'self' data:;
connect-src 'none';
frame-src 'none';
object-src 'none';
base-uri 'self';</code></pre>
<h3>Custom CSP via Metadata</h3>
<pre><code><span class="code-string">"_meta"</span>: {
<span class="code-string">"ui"</span>: {
<span class="code-string">"csp"</span>: {
<span class="code-comment">// Allow API calls to these origins</span>
<span class="code-string">"connectDomains"</span>: [
<span class="code-string">"https://api.example.com"</span>,
<span class="code-string">"wss://realtime.example.com"</span>
],
<span class="code-comment">// Allow loading resources from these origins</span>
<span class="code-string">"resourceDomains"</span>: [
<span class="code-string">"https://cdn.example.com"</span>,
<span class="code-string">"https://*.cloudflare.com"</span>
]
}
}
}</code></pre>
</section>
<!-- Server Implementation -->
<section id="server-side">
<h2>Server Implementation</h2>
<h3>Using Standard MCP SDK</h3>
<pre><code><span class="code-keyword">import</span> { McpServer } <span class="code-keyword">from</span> <span class="code-string">"@modelcontextprotocol/sdk/server/mcp.js"</span>;
<span class="code-keyword">const</span> RESOURCE_MIME_TYPE = <span class="code-string">"text/html;profile=mcp-app"</span>;
<span class="code-keyword">const</span> RESOURCE_URI_META_KEY = <span class="code-string">"ui/resourceUri"</span>;
<span class="code-keyword">const</span> server = <span class="code-keyword">new</span> <span class="code-type">McpServer</span>({ name: <span class="code-string">"my-app"</span>, version: <span class="code-string">"1.0.0"</span> });
<span class="code-comment">// Register UI resource</span>
server.<span class="code-function">resource</span>(
<span class="code-string">"ui://my-app/dashboard.html"</span>,
<span class="code-string">"ui://my-app/dashboard.html"</span>,
{ mimeType: RESOURCE_MIME_TYPE },
<span class="code-keyword">async</span> () => ({
contents: [{
uri: <span class="code-string">"ui://my-app/dashboard.html"</span>,
mimeType: RESOURCE_MIME_TYPE,
text: html
}]
})
);
<span class="code-comment">// Register tool with UI linkage</span>
server.<span class="code-function">tool</span>(
<span class="code-string">"show-dashboard"</span>,
<span class="code-string">"Opens interactive dashboard"</span>,
{ data: z.string() },
<span class="code-keyword">async</span> (args) => ({
content: [{ type: <span class="code-string">"text"</span>, text: JSON.stringify(args) }],
_meta: { [RESOURCE_URI_META_KEY]: <span class="code-string">"ui://my-app/dashboard.html"</span> }
})
);</code></pre>
</section>
<!-- Client Side -->
<section id="client-side">
<h2>Building UIs</h2>
<h3>Vanilla JavaScript</h3>
<pre><code><span class="code-keyword">import</span> { App, PostMessageTransport } <span class="code-keyword">from</span> <span class="code-string">"@modelcontextprotocol/ext-apps"</span>;
<span class="code-keyword">const</span> app = <span class="code-keyword">new</span> <span class="code-type">App</span>({
appInfo: { name: <span class="code-string">"My App"</span>, version: <span class="code-string">"1.0.0"</span> }
});
<span class="code-comment">// Receive tool input</span>
app.ontoolinput = (input) => {
console.<span class="code-function">log</span>(<span class="code-string">"Tool args:"</span>, input.arguments);
renderUI(input.arguments);
};
<span class="code-comment">// Receive tool result</span>
app.ontoolresult = (result) => {
console.<span class="code-function">log</span>(<span class="code-string">"Tool result:"</span>, result);
updateUI(result);
};
<span class="code-comment">// Send message to chat</span>
button.<span class="code-function">addEventListener</span>(<span class="code-string">"click"</span>, () => {
app.<span class="code-function">sendMessage</span>(<span class="code-string">"user"</span>, [
{ type: <span class="code-string">"text"</span>, text: <span class="code-string">"User clicked submit!"</span> }
]);
});
<span class="code-comment">// Connect to host</span>
<span class="code-keyword">await</span> app.<span class="code-function">connect</span>(<span class="code-keyword">new</span> <span class="code-type">PostMessageTransport</span>(window.parent));</code></pre>
<h3>React</h3>
<pre><code><span class="code-keyword">import</span> { useApp, useHostStyleVariables } <span class="code-keyword">from</span> <span class="code-string">"@modelcontextprotocol/ext-apps/react"</span>;
<span class="code-keyword">function</span> <span class="code-function">MyApp</span>() {
<span class="code-keyword">const</span> { app, isConnected, hostContext } = <span class="code-function">useApp</span>({
appInfo: { name: <span class="code-string">"My App"</span>, version: <span class="code-string">"1.0.0"</span> }
});
<span class="code-comment">// Apply host theme CSS variables</span>
<span class="code-function">useHostStyleVariables</span>();
<span class="code-keyword">const</span> handleClick = () => {
app?.<span class="code-function">sendMessage</span>(<span class="code-string">"user"</span>, [
{ type: <span class="code-string">"text"</span>, text: <span class="code-string">"Button clicked!"</span> }
]);
};
<span class="code-keyword">if</span> (!isConnected) <span class="code-keyword">return</span> <div>Connecting...</div>;
<span class="code-keyword">return</span> (
<button onClick={handleClick}>
Send Message
</button>
);
}</code></pre>
</section>
<!-- Host Implementation -->
<section id="host-side">
<h2>Host Implementation</h2>
<h3>Using AppBridge</h3>
<pre><code><span class="code-keyword">import</span> { AppBridge, PostMessageTransport }
<span class="code-keyword">from</span> <span class="code-string">"@modelcontextprotocol/ext-apps/app-bridge"</span>;
<span class="code-keyword">const</span> bridge = <span class="code-keyword">new</span> <span class="code-type">AppBridge</span>(
mcpClient,
{ name: <span class="code-string">"My Host"</span>, version: <span class="code-string">"1.0.0"</span> },
{
openLinks: {},
serverTools: { listChanged: <span class="code-keyword">true</span> },
logging: {}
}
);
<span class="code-comment">// Handle messages from UI</span>
bridge.onmessage = <span class="code-keyword">async</span> (params) => {
console.<span class="code-function">log</span>(<span class="code-string">"Message from app:"</span>, params);
<span class="code-keyword">return</span> {};
};
<span class="code-comment">// Handle link requests</span>
bridge.onopenlink = <span class="code-keyword">async</span> (params) => {
window.<span class="code-function">open</span>(params.url, <span class="code-string">"_blank"</span>);
<span class="code-keyword">return</span> {};
};
<span class="code-comment">// Connect to sandboxed iframe</span>
<span class="code-keyword">await</span> bridge.<span class="code-function">connect</span>(
<span class="code-keyword">new</span> <span class="code-type">PostMessageTransport</span>(iframe.contentWindow)
);
<span class="code-comment">// Send tool data to UI</span>
bridge.<span class="code-function">sendToolInput</span>({ arguments: toolArgs });
bridge.<span class="code-function">sendToolResult</span>(toolResult);</code></pre>
</section>
<!-- API Reference -->
<section id="api-reference">
<h2>API Reference</h2>
<h3>App Class (Guest UI)</h3>
<table class="api-table">
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>connect(transport)</td>
<td>Establish connection to host</td>
</tr>
<tr>
<td>callServerTool(name, args)</td>
<td>Execute tool on MCP server</td>
</tr>
<tr>
<td>sendMessage(role, content)</td>
<td>Send message to chat</td>
</tr>
<tr>
<td>sendLog(level, message)</td>
<td>Send log message to host</td>
</tr>
<tr>
<td>sendOpenLink(url)</td>
<td>Request to open URL</td>
</tr>
<tr>
<td>sendSizeChanged(w, h)</td>
<td>Notify size change</td>
</tr>
<tr>
<td>requestDisplayMode(mode)</td>
<td>Request display mode change</td>
</tr>
</tbody>
</table>
<h3>App Event Handlers</h3>
<table class="api-table">
<thead>
<tr>
<th>Handler</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>ontoolinput</td>
<td>Called with complete tool arguments</td>
</tr>
<tr>
<td>ontoolinputpartial</td>
<td>Called with streaming partial arguments</td>
</tr>
<tr>
<td>ontoolresult</td>
<td>Called with tool execution result</td>
</tr>
<tr>
<td>ontoolcancelled</td>
<td>Called when tool is cancelled</td>
</tr>
<tr>
<td>onhostcontextchanged</td>
<td>Called on theme/viewport changes</td>
</tr>
</tbody>
</table>
<h3>AppBridge Class (Host)</h3>
<table class="api-table">
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>connect(transport)</td>
<td>Connect to guest UI</td>
</tr>
<tr>
<td>sendToolInput(params)</td>
<td>Send tool arguments</td>
</tr>
<tr>
<td>sendToolInputPartial(params)</td>
<td>Send streaming partial args</td>
</tr>
<tr>
<td>sendToolResult(params)</td>
<td>Send tool result</td>
</tr>
<tr>
<td>sendToolCancelled(params)</td>
<td>Notify cancellation</td>
</tr>
<tr>
<td>sendSandboxResourceReady(html)</td>
<td>Send HTML to sandbox proxy</td>
</tr>
<tr>
<td>setHostContext(context)</td>
<td>Update host context</td>
</tr>
</tbody>
</table>
</section>
<!-- CSS Variables -->
<section id="css-variables">
<h2>Standardized CSS Variables</h2>
<p>Hosts provide CSS custom properties for theme consistency. Apps should define fallbacks.</p>
<h3>Colors</h3>
<table>
<thead>
<tr>
<th>Category</th>
<th>Variables</th>
</tr>
</thead>
<tbody>
<tr>
<td>Background</td>
<td><code>--color-background-primary</code>, <code>-secondary</code>, <code>-tertiary</code>, <code>-inverse</code></td>
</tr>
<tr>
<td>Text</td>
<td><code>--color-text-primary</code>, <code>-secondary</code>, <code>-tertiary</code>, <code>-inverse</code></td>
</tr>
<tr>
<td>Border</td>
<td><code>--color-border-primary</code>, <code>-secondary</code>, <code>-tertiary</code></td>
</tr>
<tr>
<td>Status</td>
<td><code>--color-*-info</code>, <code>-danger</code>, <code>-success</code>, <code>-warning</code></td>
</tr>
</tbody>
</table>
<h3>Typography</h3>
<table>
<thead>
<tr>
<th>Category</th>
<th>Variables</th>
</tr>
</thead>
<tbody>
<tr>
<td>Fonts</td>
<td><code>--font-sans</code>, <code>--font-mono</code></td>
</tr>
<tr>
<td>Weights</td>
<td><code>--font-weight-normal</code>, <code>-medium</code>, <code>-semibold</code>, <code>-bold</code></td>
</tr>
<tr>
<td>Text Sizes</td>
<td><code>--font-text-xs-size</code>, <code>-sm</code>, <code>-md</code>, <code>-lg</code></td>
</tr>
</tbody>
</table>
<h3>Layout</h3>
<table>
<thead>
<tr>
<th>Category</th>
<th>Variables</th>
</tr>
</thead>
<tbody>
<tr>
<td>Border Radius</td>
<td><code>--border-radius-xs</code>, <code>-sm</code>, <code>-md</code>, <code>-lg</code>, <code>-xl</code>, <code>-full</code></td>
</tr>
<tr>
<td>Shadows</td>
<td><code>--shadow-hairline</code>, <code>-sm</code>, <code>-md</code>, <code>-lg</code></td>
</tr>
</tbody>
</table>
</section>
<!-- Examples -->
<section id="examples">
<h2>Examples</h2>
<p>The ext-apps repository includes these example implementations:</p>
<div class="card-grid">
<div class="card">
<h5>📊 Budget Allocator</h5>
<p>Interactive budget allocation with donut charts, sparklines, and sliders.</p>
</div>
<div class="card">
<h5>📈 Scenario Modeler</h5>
<p>SaaS financial projections with 12-month forecasts and templates.</p>
</div>
<div class="card">
<h5>🖥️ System Monitor</h5>
<p>Real-time CPU/memory monitoring with live charts.</p>
</div>
<div class="card">
<h5>🌐 Wiki Explorer</h5>
<p>Force-directed graph visualization of Wikipedia links.</p>
</div>
<div class="card">
<h5>🎨 Three.js Server</h5>
<p>3D scene rendering with OrbitControls and post-processing.</p>
</div>
<div class="card">
<h5>📱 QR Generator</h5>
<p>Simple QR code generation and display.</p>
</div>
</div>
<div class="callout callout-success">
<p><strong>This POC Repository</strong></p>
<p>This documentation is part of the <code>mcp-apps-poc</code> repository which includes working examples of Quick Tasks, Data Visualization, Form Builder, System Monitor, and Code Playground.</p>
</div>
</section>
</main>
</div>
<script>
// Highlight active nav link on scroll
const sections = document.querySelectorAll('section[id]');
const navLinks = document.querySelectorAll('.nav-link');
function updateActiveLink() {
const scrollPos = window.scrollY + 100;
sections.forEach(section => {
const top = section.offsetTop;
const height = section.offsetHeight;
const id = section.getAttribute('id');
if (scrollPos >= top && scrollPos < top + height) {
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === `#${id}`) {
link.classList.add('active');
}
});
}
});
}
window.addEventListener('scroll', updateActiveLink);
updateActiveLink();
</script>
</body>
</html>