MCP-UI 완전 가이드: AI 에이전트를 위한 차세대 UI 경험 구축하기
⏱️ 예상 읽기 시간: 15분
MCP-UI 소개
MCP(Model Context Protocol)는 AI 에이전트가 외부 시스템과 상호작용하는 방식을 혁신적으로 변화시켰습니다. 하지만 기존의 텍스트 기반 상호작용은 복잡한 데이터 시각화나 인터랙티브 워크플로우를 다룰 때 한계가 있었습니다. 바로 이런 문제를 해결하기 위해 MCP-UI가 등장했습니다. MCP 서버 내에서 풍부하고 인터랙티브한 사용자 인터페이스를 구현할 수 있게 해주는 혁신적인 SDK입니다.
MCP-UI를 사용하면 개발자들이 MCP 호환 클라이언트에서 직접 렌더링할 수 있는 동적 UI 컴포넌트를 생성할 수 있어, 복잡한 작업에 대해 사용자에게 직관적인 시각적 인터페이스를 제공할 수 있습니다. 데이터 대시보드, 폼 인터페이스, 인터랙티브 시각화 등 무엇을 구축하든, MCP-UI는 AI 에이전트와 사용자 친화적 인터페이스 사이의 격차를 해소합니다.
MCP-UI란 무엇인가?
MCP-UI는 Model Context Protocol을 확장하여 풍부한 사용자 인터페이스 컴포넌트를 지원하는 오픈소스 SDK입니다. MCP 서버가 단순한 텍스트 응답뿐만 아니라 호환 클라이언트에서 렌더링할 수 있는 완전히 인터랙티브한 UI 요소를 반환할 수 있게 해줍니다.
주요 기능
- 다양한 콘텐츠 타입: 원시 HTML, 외부 URL, 원격 DOM 컴포넌트 지원
- 프레임워크 유연성: React, Web Components, 바닐라 JavaScript와 호환
- 보안 우선: 모든 UI 콘텐츠는 최대 보안을 위해 샌드박스 iframe에서 실행
- 인터랙티브 액션: UI 컴포넌트가 도구 호출을 트리거하고 에이전트와 상호작용 가능
- 크로스 플랫폼: Postman, Goose, Smithery 등 여러 MCP 호스트와 호환
아키텍처 개요
MCP-UI는 클라이언트-서버 아키텍처를 따릅니다:
- 서버 사이드: MCP-UI 서버 SDK를 사용하여 UI 리소스 생성
- 클라이언트 사이드: MCP-UI 클라이언트 렌더러를 사용하여 UI 리소스 렌더링
- 통신: UI 액션은 이벤트를 통해 서버로 다시 전달
설치 및 설정
사전 요구사항
시작하기 전에 다음이 필요합니다:
- Node.js 16+ (TypeScript 개발용)
- Ruby 3.0+ (Ruby 개발용)
- MCP 개념에 대한 기본 이해
- React 또는 Web Components에 대한 친숙함
TypeScript 설치
# npm 사용
npm install @mcp-ui/server @mcp-ui/client
# pnpm 사용
pnpm add @mcp-ui/server @mcp-ui/client
# yarn 사용
yarn add @mcp-ui/server @mcp-ui/client
Ruby 설치
gem install mcp_ui_server
첫 번째 MCP-UI 컴포넌트 구축하기
MCP-UI의 핵심 개념을 보여주는 간단한 예제부터 시작해보겠습니다.
TypeScript 예제: 인터랙티브 인사말
서버 사이드 구현
import { createUIResource } from '@mcp-ui/server';
// 간단한 HTML 인사말
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;">안녕하세요, ${name}님!</h2>
<p style="margin: 0; color: #333;">MCP-UI 튜토리얼에 오신 것을 환영합니다.</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;">
튜토리얼 계속하기
</button>
</div>
`
},
encoding: 'text'
});
};
// MCP 서버 도구에서 사용
export const greetingTool = {
name: 'create_greeting',
description: '인터랙티브 인사말 UI 생성',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: '인사할 이름' }
},
required: ['name']
},
handler: async (args: { name: string }) => {
const resource = createGreetingResource(args.name);
return {
content: [
{
type: 'resource',
resource: resource
}
]
};
}
};
클라이언트 사이드 렌더링
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 액션 수신:', result);
// 다양한 액션 타입 처리
switch (result.payload?.toolName) {
case 'nextStep':
console.log('사용자가 튜토리얼을 계속하려고 합니다');
// 애플리케이션에서 다음 단계 트리거
break;
default:
console.log('알 수 없는 액션:', result);
}
};
if (
mcpResource.type === 'resource' &&
mcpResource.resource.uri?.startsWith('ui://')
) {
return (
<UIResourceRenderer
resource={mcpResource.resource}
onUIAction={handleUIAction}
/>
);
}
return <p>지원되지 않는 리소스 타입입니다</p>;
}
export default MCPUIApp;
Ruby 예제: 간단한 대시보드
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;">시스템 대시보드</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;">
데이터 새로고침
</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
# 사용 예제
dashboard_data = [
{ title: '활성 사용자', value: '1,234', description: '현재 온라인' },
{ title: '수익', value: '₩45,678,000', description: '이번 달' },
{ title: '주문', value: '892', description: '처리 대기 중' }
]
server = DashboardServer.new
resource = server.create_dashboard_resource(dashboard_data)
고급 기능: 원격 DOM 컴포넌트
원격 DOM은 MCP-UI의 가장 강력한 기능으로, 호스트 애플리케이션과 원활하게 상호작용할 수 있는 동적이고 프레임워크 인식 컴포넌트를 생성할 수 있게 해줍니다.
React 원격 DOM 예제
import { createUIResource } from '@mcp-ui/server';
const createInteractiveFormResource = () => {
return createUIResource({
uri: 'ui://forms/user-registration',
content: {
type: 'remoteDom',
script: `
// 폼 컨테이너 생성
const form = document.createElement('div');
form.style.cssText = 'max-width: 400px; margin: 0 auto; padding: 20px; background: #f8f9fa; border-radius: 8px;';
// 폼 제목
const title = document.createElement('h2');
title.textContent = '사용자 등록';
title.style.cssText = 'color: #495057; margin-bottom: 20px; text-align: center;';
form.appendChild(title);
// 이름 입력
const nameGroup = createInputGroup('이름', 'text', 'name');
form.appendChild(nameGroup);
// 이메일 입력
const emailGroup = createInputGroup('이메일', 'email', 'email');
form.appendChild(emailGroup);
// 역할 선택
const roleGroup = createSelectGroup('역할', 'role', [
{ value: 'user', label: '사용자' },
{ value: 'admin', label: '관리자' },
{ value: 'moderator', label: '모더레이터' }
]);
form.appendChild(roleGroup);
// 제출 버튼
const submitBtn = document.createElement('button');
submitBtn.textContent = '사용자 등록';
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
};
// 폼 유효성 검사
if (!formData.name || !formData.email) {
alert('모든 필수 필드를 입력해주세요');
return;
}
// 부모에게 데이터 전송
window.parent.postMessage({
type: 'tool',
payload: {
toolName: 'register_user',
params: formData
}
}, '*');
});
form.appendChild(submitBtn);
// 헬퍼 함수들
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;
}
// 루트에 폼 추가
root.appendChild(form);
`,
framework: 'react'
},
encoding: 'text'
});
};
MCP-UI 구현 테스트하기
UI 인스펙터 사용하기
MCP-UI는 로컬에서 구현을 테스트할 수 있는 내장 UI 인스펙터를 제공합니다:
# UI 인스펙터 설치
npm install -g @mcp-ui/ui-inspector
# 인스펙터 실행
ui-inspector --server your-mcp-server-config.json
로컬 테스트 설정
MCP-UI 컴포넌트를 검증하기 위한 테스트 환경을 만들어보세요:
// test-mcp-ui.ts
import { createUIResource } from '@mcp-ui/server';
import { UIResourceRenderer } from '@mcp-ui/client';
// UI 리소스 테스트
const testResource = createUIResource({
uri: 'ui://test/component',
content: {
type: 'rawHtml',
htmlString: '<div>테스트 컴포넌트</div>'
},
encoding: 'text'
});
console.log('생성된 리소스:', JSON.stringify(testResource, null, 2));
통합 테스트
# 실제 MCP 서버로 테스트
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{"method": "tools/call", "params": {"name": "create_greeting", "arguments": {"name": "World"}}}'
모범 사례 및 보안
보안 고려사항
- 입력 살균: HTML에서 렌더링하기 전에 항상 사용자 입력을 살균하세요
- 콘텐츠 보안 정책: iframe 콘텐츠에 적절한 CSP 헤더를 구현하세요
- 샌드박스 제한: 신뢰할 수 없는 콘텐츠에 대해 iframe 샌드박싱을 활용하세요
- 이벤트 검증: 처리하기 전에 모든 UI 액션 이벤트를 검증하세요
성능 최적화
- 리소스 캐싱: 자주 사용되는 UI 리소스를 캐시하세요
- 지연 로딩: 필요할 때만 UI 컴포넌트를 로드하세요
- 번들 크기: 더 빠른 로딩을 위해 JavaScript 번들을 작게 유지하세요
- 이벤트 디바운싱: 스팸을 방지하기 위해 빈번한 UI 이벤트를 디바운스하세요
코드 구조화
// UI 리소스 구조화
export class UIResourceFactory {
static createDashboard(data: DashboardData): UIResource {
// 구현
}
static createForm(schema: FormSchema): UIResource {
// 구현
}
static createChart(chartData: ChartData): UIResource {
// 구현
}
}
// 일관된 명명 규칙 사용
const UI_NAMESPACES = {
DASHBOARD: 'ui://dashboard',
FORMS: 'ui://forms',
CHARTS: 'ui://charts'
} as const;
지원되는 MCP 호스트
MCP-UI는 여러 MCP 호스트와 호환되며, 각각 다양한 수준의 기능 지원을 제공합니다:
호스트 | 렌더링 | UI 액션 | 참고사항 |
---|---|---|---|
Postman | ✅ | ⚠️ | 완전한 렌더링, 부분적 액션 지원 |
Goose | ✅ | ⚠️ | 좋은 통합, 일부 액션 제한 |
Smithery | ✅ | ❌ | 표시만 가능, 인터랙티브 기능 없음 |
MCPJam | ✅ | ❌ | 플레이그라운드 환경 |
VSCode | 🔄 | 🔄 | 곧 출시 예정 |
일반적인 문제 해결
UI가 렌더링되지 않는 경우
// 리소스 형식 확인
const resource = createUIResource({
uri: 'ui://test/1', // 'ui://'로 시작해야 함
content: {
type: 'rawHtml', // 올바른 콘텐츠 타입
htmlString: '<div>콘텐츠</div>' // 유효한 HTML
},
encoding: 'text' // 올바른 인코딩
});
액션이 작동하지 않는 경우
// 적절한 이벤트 형식 확인
window.parent.postMessage({
type: 'tool', // 'tool'이어야 함
payload: {
toolName: 'your_tool_name', // 유효한 도구 이름
params: { /* 유효한 매개변수 */ }
}
}, '*');
스타일링 문제
<!-- 더 나은 호환성을 위해 인라인 스타일 사용 -->
<div style="padding: 20px; background: #f0f0f0;">
인라인 스타일이 적용된 콘텐츠
</div>
결론
MCP-UI는 AI 에이전트 인터페이스의 중대한 발전을 나타내며, 기존의 텍스트 기반 상호작용을 훨씬 뛰어넘는 풍부하고 인터랙티브한 경험을 가능하게 합니다. 이 튜토리얼을 통해 다음을 배웠습니다:
- TypeScript와 Ruby 환경 모두에서 MCP-UI 설정하기
- 다양한 콘텐츠 타입을 사용하여 인터랙티브 UI 컴포넌트 생성하기
- 원격 DOM 컴포넌트와 같은 고급 기능 구현하기
- UI 액션과 이벤트를 적절히 처리하기
- 보안 모범 사례와 최적화 기법 따르기
AI 에이전트 상호작용의 미래는 원활하고 직관적인 사용자 인터페이스에 있으며, MCP-UI는 이러한 차세대 경험을 구축하기 위한 기반을 제공합니다. 간단한 대시보드를 만들든 복잡한 인터랙티브 애플리케이션을 만들든, MCP-UI는 여러분의 비전을 실현하는 데 필요한 유연성과 강력함을 제공합니다.
추가 자료
- 공식 문서: mcpui.dev
- GitHub 저장소: github.com/idosal/mcp-ui
- 라이브 예제: 호스팅된 데모 서버를 체험해보세요
- 커뮤니티: 지원과 토론을 위해 MCP-UI 커뮤니티에 참여하세요
지금 바로 자신만의 MCP-UI 컴포넌트 구축을 시작하고 사용자가 AI 에이전트와 상호작용하는 방식을 혁신해보세요!