MCP-UI Complete Tutorial: Building Next-Gen UI Experiences for AI Agents
⏱️ Estimated Reading Time: 15 minutes
Introduction to MCP-UI
The Model Context Protocol (MCP) has revolutionized how AI agents interact with external systems. However, traditional text-based interactions often fall short when dealing with complex data visualization or interactive workflows. This is where MCP-UI comes in - a groundbreaking SDK that enables rich, interactive user interfaces within MCP servers.
MCP-UI allows developers to create dynamic UI components that can be rendered directly in MCP-compatible clients, providing users with intuitive visual interfaces for complex operations. Whether you’re building data dashboards, form interfaces, or interactive visualizations, MCP-UI bridges the gap between AI agents and human-friendly interfaces.
What is MCP-UI?
MCP-UI is an open-source SDK that extends the Model Context Protocol to support rich user interface components. It enables MCP servers to return not just text responses, but fully interactive UI elements that can be rendered in compatible clients.
Key Features
- Multiple Content Types: Support for raw HTML, external URLs, and remote DOM components
- Framework Flexibility: Works with React, Web Components, and vanilla JavaScript
- Security First: All UI content runs in sandboxed iframes for maximum security
- Interactive Actions: UI components can trigger tool calls and interact with the agent
- Cross-Platform: Compatible with multiple MCP hosts including Postman, Goose, and Smithery
Architecture Overview
MCP-UI follows a client-server architecture where:
- Server Side: Creates UI resources using the MCP-UI server SDK
- Client Side: Renders UI resources using the MCP-UI client renderer
- Communication: UI actions are communicated back to the server via events
Installation and Setup
Prerequisites
Before getting started, ensure you have:
- Node.js 16+ (for TypeScript development)
- Ruby 3.0+ (for Ruby development)
- Basic understanding of MCP concepts
- Familiarity with React or Web Components
TypeScript Installation
# Using npm
npm install @mcp-ui/server @mcp-ui/client
# Using pnpm
pnpm add @mcp-ui/server @mcp-ui/client
# Using yarn
yarn add @mcp-ui/server @mcp-ui/client
Ruby Installation
gem install mcp_ui_server
Building Your First MCP-UI Component
Let’s start with a simple example that demonstrates the core concepts of MCP-UI.
TypeScript Example: Interactive Greeting
Server-Side Implementation
import { createUIResource } from '@mcp-ui/server';
// Simple HTML greeting
const createGreetingResource = (name: string) => {
return createUIResource({
uri: `ui://greeting/${Date.now()}`,
content: {
type: 'rawHtml',
htmlString: `
<div style="padding: 20px; border: 2px solid #007acc; border-radius: 8px; background: #f0f8ff;">
<h2 style="color: #007acc; margin: 0 0 10px 0;">Hello, ${name}!</h2>
<p style="margin: 0; color: #333;">Welcome to MCP-UI tutorial.</p>
<button onclick="window.parent.postMessage({type: 'tool', payload: {toolName: 'nextStep', params: {action: 'continue'}}}, '*')"
style="margin-top: 15px; padding: 8px 16px; background: #007acc; color: white; border: none; border-radius: 4px; cursor: pointer;">
Continue Tutorial
</button>
</div>
`
},
encoding: 'text'
});
};
// Usage in your MCP server tool
export const greetingTool = {
name: 'create_greeting',
description: 'Create an interactive greeting UI',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Name to greet' }
},
required: ['name']
},
handler: async (args: { name: string }) => {
const resource = createGreetingResource(args.name);
return {
content: [
{
type: 'resource',
resource: resource
}
]
};
}
};
Client-Side Rendering
import React from 'react';
import { UIResourceRenderer } from '@mcp-ui/client';
interface MCPUIAppProps {
mcpResource: any;
}
function MCPUIApp({ mcpResource }: MCPUIAppProps) {
const handleUIAction = (result: any) => {
console.log('UI Action received:', result);
// Handle different action types
switch (result.payload?.toolName) {
case 'nextStep':
console.log('User wants to continue tutorial');
// Trigger next step in your application
break;
default:
console.log('Unknown action:', result);
}
};
if (
mcpResource.type === 'resource' &&
mcpResource.resource.uri?.startsWith('ui://')
) {
return (
<UIResourceRenderer
resource={mcpResource.resource}
onUIAction={handleUIAction}
/>
);
}
return <p>Unsupported resource type</p>;
}
export default MCPUIApp;
Ruby Example: Simple Dashboard
require 'mcp_ui_server'
class DashboardServer
def create_dashboard_resource(data)
McpUiServer.create_ui_resource(
uri: "ui://dashboard/#{Time.now.to_i}",
content: {
type: :raw_html,
htmlString: build_dashboard_html(data)
},
encoding: :text
)
end
private
def build_dashboard_html(data)
<<~HTML
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #2c3e50; text-align: center;">System Dashboard</h1>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0;">
#{data.map { |item| create_card_html(item) }.join}
</div>
<button onclick="refreshDashboard()"
style="width: 100%; padding: 12px; background: #3498db; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer;">
Refresh Data
</button>
<script>
function refreshDashboard() {
window.parent.postMessage({
type: 'tool',
payload: {
toolName: 'refresh_dashboard',
params: { timestamp: new Date().toISOString() }
}
}, '*');
}
</script>
</div>
HTML
end
def create_card_html(item)
<<~HTML
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-left: 4px solid #3498db;">
<h3 style="margin: 0 0 10px 0; color: #2c3e50;">#{item[:title]}</h3>
<p style="margin: 0; font-size: 24px; font-weight: bold; color: #27ae60;">#{item[:value]}</p>
<small style="color: #7f8c8d;">#{item[:description]}</small>
</div>
HTML
end
end
# Usage example
dashboard_data = [
{ title: 'Active Users', value: '1,234', description: 'Currently online' },
{ title: 'Revenue', value: '$45,678', description: 'This month' },
{ title: 'Orders', value: '892', description: 'Pending processing' }
]
server = DashboardServer.new
resource = server.create_dashboard_resource(dashboard_data)
Advanced Features: Remote DOM Components
Remote DOM is MCP-UI’s most powerful feature, allowing you to create dynamic, framework-aware components that can interact seamlessly with the host application.
React Remote DOM Example
import { createUIResource } from '@mcp-ui/server';
const createInteractiveFormResource = () => {
return createUIResource({
uri: 'ui://forms/user-registration',
content: {
type: 'remoteDom',
script: `
// Create form container
const form = document.createElement('div');
form.style.cssText = 'max-width: 400px; margin: 0 auto; padding: 20px; background: #f8f9fa; border-radius: 8px;';
// Form title
const title = document.createElement('h2');
title.textContent = 'User Registration';
title.style.cssText = 'color: #495057; margin-bottom: 20px; text-align: center;';
form.appendChild(title);
// Name input
const nameGroup = createInputGroup('Name', 'text', 'name');
form.appendChild(nameGroup);
// Email input
const emailGroup = createInputGroup('Email', 'email', 'email');
form.appendChild(emailGroup);
// Role select
const roleGroup = createSelectGroup('Role', 'role', [
{ value: 'user', label: 'User' },
{ value: 'admin', label: 'Administrator' },
{ value: 'moderator', label: 'Moderator' }
]);
form.appendChild(roleGroup);
// Submit button
const submitBtn = document.createElement('button');
submitBtn.textContent = 'Register User';
submitBtn.style.cssText = 'width: 100%; padding: 12px; background: #007bff; color: white; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; margin-top: 20px;';
submitBtn.addEventListener('click', (e) => {
e.preventDefault();
const formData = {
name: form.querySelector('[name="name"]').value,
email: form.querySelector('[name="email"]').value,
role: form.querySelector('[name="role"]').value
};
// Validate form
if (!formData.name || !formData.email) {
alert('Please fill in all required fields');
return;
}
// Send data to parent
window.parent.postMessage({
type: 'tool',
payload: {
toolName: 'register_user',
params: formData
}
}, '*');
});
form.appendChild(submitBtn);
// Helper functions
function createInputGroup(label, type, name) {
const group = document.createElement('div');
group.style.cssText = 'margin-bottom: 15px;';
const labelEl = document.createElement('label');
labelEl.textContent = label;
labelEl.style.cssText = 'display: block; margin-bottom: 5px; font-weight: 500; color: #495057;';
const input = document.createElement('input');
input.type = type;
input.name = name;
input.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #ced4da; border-radius: 4px; font-size: 14px;';
input.required = true;
group.appendChild(labelEl);
group.appendChild(input);
return group;
}
function createSelectGroup(label, name, options) {
const group = document.createElement('div');
group.style.cssText = 'margin-bottom: 15px;';
const labelEl = document.createElement('label');
labelEl.textContent = label;
labelEl.style.cssText = 'display: block; margin-bottom: 5px; font-weight: 500; color: #495057;';
const select = document.createElement('select');
select.name = name;
select.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #ced4da; border-radius: 4px; font-size: 14px;';
options.forEach(option => {
const optionEl = document.createElement('option');
optionEl.value = option.value;
optionEl.textContent = option.label;
select.appendChild(optionEl);
});
group.appendChild(labelEl);
group.appendChild(select);
return group;
}
// Add form to root
root.appendChild(form);
`,
framework: 'react'
},
encoding: 'text'
});
};
Testing Your MCP-UI Implementation
Using UI Inspector
MCP-UI provides a built-in UI inspector for testing your implementations locally:
# Install the UI inspector
npm install -g @mcp-ui/ui-inspector
# Run the inspector
ui-inspector --server your-mcp-server-config.json
Local Testing Setup
Create a test environment to verify your MCP-UI components:
// test-mcp-ui.ts
import { createUIResource } from '@mcp-ui/server';
import { UIResourceRenderer } from '@mcp-ui/client';
// Test your UI resources
const testResource = createUIResource({
uri: 'ui://test/component',
content: {
type: 'rawHtml',
htmlString: '<div>Test Component</div>'
},
encoding: 'text'
});
console.log('Generated resource:', JSON.stringify(testResource, null, 2));
Integration Testing
# Test with a real MCP server
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{"method": "tools/call", "params": {"name": "create_greeting", "arguments": {"name": "World"}}}'
Best Practices and Security
Security Considerations
- Input Sanitization: Always sanitize user inputs before rendering in HTML
- Content Security Policy: Implement proper CSP headers for iframe content
- Sandbox Restrictions: Leverage iframe sandboxing for untrusted content
- Event Validation: Validate all UI action events before processing
Performance Optimization
- Resource Caching: Cache frequently used UI resources
- Lazy Loading: Load UI components only when needed
- Bundle Size: Keep JavaScript bundles small for faster loading
- Event Debouncing: Debounce frequent UI events to prevent spam
Code Organization
// Organize your UI resources
export class UIResourceFactory {
static createDashboard(data: DashboardData): UIResource {
// Implementation
}
static createForm(schema: FormSchema): UIResource {
// Implementation
}
static createChart(chartData: ChartData): UIResource {
// Implementation
}
}
// Use consistent naming conventions
const UI_NAMESPACES = {
DASHBOARD: 'ui://dashboard',
FORMS: 'ui://forms',
CHARTS: 'ui://charts'
} as const;
Supported MCP Hosts
MCP-UI is compatible with several MCP hosts, each with varying levels of feature support:
Host | Rendering | UI Actions | Notes |
---|---|---|---|
Postman | ✅ | ⚠️ | Full rendering, partial action support |
Goose | ✅ | ⚠️ | Good integration, some action limitations |
Smithery | ✅ | ❌ | Display only, no interactive features |
MCPJam | ✅ | ❌ | Playground environment |
VSCode | 🔄 | 🔄 | Coming soon |
Troubleshooting Common Issues
UI Not Rendering
// Check resource format
const resource = createUIResource({
uri: 'ui://test/1', // Must start with 'ui://'
content: {
type: 'rawHtml', // Correct content type
htmlString: '<div>Content</div>' // Valid HTML
},
encoding: 'text' // Correct encoding
});
Actions Not Working
// Ensure proper event format
window.parent.postMessage({
type: 'tool', // Must be 'tool'
payload: {
toolName: 'your_tool_name', // Valid tool name
params: { /* valid parameters */ }
}
}, '*');
Styling Issues
<!-- Use inline styles for better compatibility -->
<div style="padding: 20px; background: #f0f0f0;">
Content with inline styles
</div>
Conclusion
MCP-UI represents a significant advancement in AI agent interfaces, enabling rich, interactive experiences that go far beyond traditional text-based interactions. By following this tutorial, you’ve learned how to:
- Set up MCP-UI in both TypeScript and Ruby environments
- Create interactive UI components using different content types
- Implement advanced features like Remote DOM components
- Handle UI actions and events properly
- Follow security best practices and optimization techniques
The future of AI agent interactions lies in seamless, intuitive user interfaces, and MCP-UI provides the foundation for building these next-generation experiences. Whether you’re creating simple dashboards or complex interactive applications, MCP-UI offers the flexibility and power needed to bring your vision to life.
Additional Resources
- Official Documentation: mcpui.dev
- GitHub Repository: github.com/idosal/mcp-ui
- Live Examples: Try the hosted demo servers
- Community: Join the MCP-UI community for support and discussions
Start building your own MCP-UI components today and transform how users interact with your AI agents!