UI Helloworld
MCP server: UI Helloworld
Installation
npx mcp-ui-helloworldAsk AI about UI Helloworld
Powered by Claude Β· Grounded in docs
I know everything about UI Helloworld. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
MCP-UI: Hello World Tutorial
Table of Contents
- What is MCP-UI?
- Architecture Overview
- Project Structure
- Step-by-Step Setup
- How It Works
- Code Walkthrough
- Troubleshooting
What is MCP-UI?
MCP-UI (Model Context Protocol - User Interface) is an extension of the Model Context Protocol that allows AI assistants and servers to generate and return interactive UI components directly to clients. Instead of just returning text responses, MCP-UI enables servers to create rich, interactive HTML interfaces that can be rendered in web dashboards.
Key Concepts
-
MCP (Model Context Protocol): A protocol that enables AI assistants to securely access external data sources and tools through standardized interfaces.
-
MCP-UI Extension: Allows MCP servers to return UI resources (HTML components) along with text responses, enabling rich visual interfaces.
-
SSE (Server-Sent Events): A web standard that allows a server to push data to a web browser over HTTP. Used for real-time communication between the dashboard and server.
-
JSON-RPC: A stateless, light-weight remote procedure call (RPC) protocol used for communication between the client and server.
Architecture Overview
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β β β β β β
β React ββββSSEβββΊβ Express.js ββββMCPβββΊβ MCP Server β
β Dashboard β β Server β β (Tools) β
β (Client) β β (SSE Transport) β β β
β β β β β β
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β β
β β
βββββββββββPOSTβββββββββββββββββ
(JSON-RPC)
Components:
-
MCP Server (
server/server.js):- Defines tools (like
say_hello) - Handles tool execution
- Returns UI resources using
@mcp-ui/server
- Defines tools (like
-
Express.js Server (
server/server.js):- Provides SSE endpoint (
/sse) for real-time communication - Handles POST requests (
/message) for JSON-RPC calls - Manages transport connections
- Provides SSE endpoint (
-
React Dashboard (
mcp-dashboard/src/Dashboard.tsx):- Connects to server via SSE
- Sends tool execution requests
- Receives and renders UI components
Project Structure
MCP-UI/
βββ server/ # MCP Server (Backend)
β βββ server.js # Main server file with MCP tools
β βββ package.json # Server dependencies
β βββ .gitignore
β
βββ mcp-dashboard/ # React Dashboard (Frontend)
β βββ src/
β β βββ Dashboard.tsx # Main dashboard component
β β βββ App.tsx # App entry point
β β βββ main.tsx # React entry point
β βββ package.json # Dashboard dependencies
β βββ vite.config.ts # Vite configuration
β
βββ README.md # This file
Step-by-Step Setup
Prerequisites
- Node.js (v18 or higher)
- npm or yarn
- Basic knowledge of JavaScript/TypeScript and React
Step 1: Create Project Structure
mkdir MCP-UI
cd MCP-UI
mkdir server mcp-dashboard
Step 2: Setup MCP Server
2.1 Initialize Server Project
cd server
npm init -y
2.2 Install Dependencies
npm install express cors @modelcontextprotocol/sdk @mcp-ui/server
Dependencies Explained:
express: Web server frameworkcors: Enable Cross-Origin Resource Sharing@modelcontextprotocol/sdk: MCP SDK for creating servers@mcp-ui/server: Utilities for creating UI resources
2.3 Create server.js
Create server/server.js with the following structure:
import express from "express";
import cors from "cors";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { createUIResource } from "@mcp-ui/server";
const app = express();
app.use(cors());
app.use(express.json());
// 1. Create MCP Server instance
const server = new Server(
{ name: "greeting-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// 2. Define available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [{
name: "say_hello",
description: "Generates a UI greeting card",
inputSchema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"]
},
}],
};
});
// 3. Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "say_hello") {
const name = request.params.arguments.name;
// Create UI resource with HTML content
const uiResource = createUIResource({
uri: `ui://greeting/${Date.now()}`,
content: {
type: "rawHtml",
htmlString: `<div>Hello, ${name}!</div>`
},
encoding: "text",
});
return {
content: [
{ type: "text", text: `Generated greeting for ${name}` },
uiResource
]
};
}
});
// 4. Setup SSE Transport
const transports = {};
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/message", res);
const sessionId = transport.sessionId;
transports[sessionId] = transport;
transport.onclose = () => {
delete transports[sessionId];
};
await server.connect(transport);
});
// 5. Handle POST messages
app.post("/message", async (req, res) => {
const sessionId = req.query.sessionId;
const transport = transports[sessionId];
if (!transport) {
res.status(404).send('Session not found');
return;
}
await transport.handlePostMessage(req, res, req.body);
});
app.listen(3000, () => console.log("Server running on http://localhost:3000"));
2.4 Update package.json
Add to server/package.json:
{
"type": "module",
"scripts": {
"start": "node server.js"
}
}
Step 3: Setup React Dashboard
3.1 Create React Project with Vite
cd ../mcp-dashboard
npm create vite@latest . -- --template react-ts
npm install
3.2 Create Dashboard.tsx
Create mcp-dashboard/src/Dashboard.tsx:
import React, { useState, useEffect, useRef } from 'react';
interface UiResource {
uri: string;
mimeType: string;
text: string;
}
interface McpContentItem {
type: string;
text?: string;
resource?: UiResource;
}
interface JsonRpcResponse {
jsonrpc: string;
id: number;
result: {
content: McpContentItem[];
};
}
const Dashboard: React.FC = () => {
const [name, setName] = useState<string>('Hamid');
const [htmlContent, setHtmlContent] = useState<string | null>(null);
const [isConnected, setIsConnected] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [sessionId, setSessionId] = useState<string | null>(null);
const [pendingRequestId, setPendingRequestId] = useState<number | null>(null);
const pendingRequestIdRef = useRef<number | null>(null);
useEffect(() => {
pendingRequestIdRef.current = pendingRequestId;
}, [pendingRequestId]);
// Establish SSE Connection
useEffect(() => {
const eventSource = new EventSource('http://localhost:3000/sse');
eventSource.onopen = () => {
setIsConnected(true);
};
eventSource.addEventListener('endpoint', (event: MessageEvent) => {
const url = new URL(event.data, 'http://localhost');
const sessionIdParam = url.searchParams.get('sessionId');
if (sessionIdParam) {
setSessionId(sessionIdParam);
}
});
eventSource.addEventListener('message', (event: MessageEvent) => {
try {
const data: JsonRpcResponse = JSON.parse(event.data);
if (pendingRequestIdRef.current && data.id === pendingRequestIdRef.current) {
const uiItem = data.result?.content?.find(
(item) => item.type === 'resource' && item.resource?.uri.startsWith('ui://')
);
if (uiItem && uiItem.resource) {
setHtmlContent(uiItem.resource.text);
}
setLoading(false);
setPendingRequestId(null);
}
} catch (e) {
console.error('Parse error:', e);
}
});
return () => {
eventSource.close();
};
}, []);
const handleGenerate = async () => {
if (!sessionId) return;
setLoading(true);
const requestId = Date.now();
setPendingRequestId(requestId);
const rpcRequest = {
jsonrpc: '2.0',
id: requestId,
method: 'tools/call',
params: {
name: 'say_hello',
arguments: { name },
},
};
try {
await fetch(`http://localhost:3000/message?sessionId=${sessionId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(rpcRequest),
});
} catch (error) {
console.error('Error:', error);
setLoading(false);
}
};
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<button onClick={handleGenerate} disabled={!sessionId || loading}>
Generate Greeting
</button>
{htmlContent && (
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />
)}
</div>
);
};
export default Dashboard;
3.3 Update App.tsx
import Dashboard from './Dashboard'
function App() {
return <Dashboard />
}
export default App
Step 4: Run the Application
Terminal 1 - Start Server:
cd server
npm start
Terminal 2 - Start Dashboard:
cd mcp-dashboard
npm run dev
Open Browser:
Navigate to http://localhost:5173 (or the port shown by Vite)
How It Works
1. Connection Flow
Client Server
β β
βββGET /sseβββββββββββββββΊβ
β β Create SSEServerTransport
β β Generate sessionId
β β Store transport
ββββSSE Streamβββββββββββββ
β event: endpoint β
β data: /message? β
β sessionId=xxx β
β β
β Extract sessionId β
β β
2. Tool Execution Flow
Client Server MCP Server
β β β
βββPOST /messageββββββββββΊβ β
β ?sessionId=xxx β β
β {method: tools/call} β β
β βββhandlePostMessageββββββΊβ
β β β Execute tool
β β β Create UI resource
β ββββResponseβββββββββββββββ
β β β
ββββ202 Acceptedβββββββββββ β
β β β
ββββSSE: messageβββββββββββ β
β {result: {...}} β β
β β β
β Parse & Render UI β β
3. Data Flow
- SSE Connection: Establishes persistent connection for server-to-client communication
- Session Management: Each SSE connection gets a unique
sessionIdfor routing requests - Tool Call: Client sends JSON-RPC request via POST with
sessionId - Tool Execution: Server executes tool and creates UI resource
- Response: Server sends JSON-RPC response through SSE stream
- Rendering: Client extracts HTML from resource and renders it
Code Walkthrough
Server Side
Creating UI Resources
const uiResource = createUIResource({
uri: `ui://greeting/${Date.now()}`, // Unique identifier
content: {
type: "rawHtml", // Content type
htmlString: `<div>...</div>` // HTML content
},
encoding: "text",
});
SSE Transport Setup
// Create transport for each SSE connection
const transport = new SSEServerTransport("/message", res);
const sessionId = transport.sessionId; // Unique session ID
// Store transport for later use
transports[sessionId] = transport;
// Connect MCP server to transport
await server.connect(transport);
Handling POST Requests
app.post("/message", async (req, res) => {
// Get sessionId from query parameter
const sessionId = req.query.sessionId;
// Find corresponding transport
const transport = transports[sessionId];
// Handle message (requires 3 args: req, res, parsedBody)
await transport.handlePostMessage(req, res, req.body);
});
Client Side
SSE Connection
const eventSource = new EventSource('http://localhost:3000/sse');
// Listen for endpoint event (contains sessionId)
eventSource.addEventListener('endpoint', (event) => {
const url = new URL(event.data, 'http://localhost');
const sessionId = url.searchParams.get('sessionId');
setSessionId(sessionId);
});
// Listen for message events (JSON-RPC responses)
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
// Process response...
});
Sending Tool Calls
const rpcRequest = {
jsonrpc: '2.0',
id: Date.now(), // Unique request ID
method: 'tools/call',
params: {
name: 'say_hello',
arguments: { name: 'Hamid' }
},
};
await fetch(`http://localhost:3000/message?sessionId=${sessionId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(rpcRequest),
});
Rendering UI
// Extract UI resource from response
const uiItem = data.result.content.find(
(item) => item.type === 'resource' &&
item.resource?.uri.startsWith('ui://')
);
// Get HTML content
const html = uiItem.resource.text;
// Render (use dangerouslySetInnerHTML with caution!)
<div dangerouslySetInnerHTML={{ __html: html }} />
Troubleshooting
Common Issues
1. "Session not found" Error
Problem: The sessionId in the POST request doesn't match any stored transport.
Solutions:
- Ensure SSE connection is established before sending POST requests
- Check that
sessionIdis correctly extracted from the endpoint event - Verify the server is storing transports correctly
2. No Response Received
Problem: POST request succeeds but no SSE message arrives.
Solutions:
- Check browser console for SSE connection errors
- Verify the server is sending responses through the correct transport
- Ensure
pendingRequestIdmatches the responseid
3. Multiple SSE Connections
Problem: Dashboard creates multiple SSE connections.
Solutions:
- Use empty dependency array
[]inuseEffectfor SSE setup - Use
useRefto access latest state values in event handlers - Clean up EventSource in
useEffectreturn function
4. HTML Not Rendering
Problem: UI resource received but not displayed.
Solutions:
- Check that you're accessing
resource.text(notresource.content.htmlString) - Verify HTML content is valid
- Check browser console for rendering errors
Debug Tips
-
Server Logging: Add
console.logstatements to track:- Transport creation and storage
- POST request handling
- Tool execution
-
Client Logging: Log in browser console:
- SSE connection events
- Received messages
- Request/response IDs
-
Network Tab: Use browser DevTools to inspect:
- SSE connection (EventStream)
- POST requests and responses
- Response headers and status codes
Next Steps
Enhancements You Can Add
- Multiple Tools: Add more tools to your MCP server
- Error Handling: Improve error messages and recovery
- UI Styling: Enhance the dashboard with better CSS
- Security: Add authentication and input validation
- Real-time Updates: Use SSE for server-initiated updates
- State Management: Add Redux or Zustand for complex state
- Type Safety: Expand TypeScript interfaces for better type checking
Resources
- MCP Documentation
- SSE Specification
- JSON-RPC 2.0 Specification
- React Documentation
- Express.js Documentation
Summary
This tutorial demonstrated how to:
- β Create an MCP server with UI capabilities
- β Set up SSE transport for real-time communication
- β Build a React dashboard to interact with the server
- β Generate and render dynamic UI components
- β Handle the complete request/response cycle
You now have a working MCP-UI application that can generate and display interactive UI components! π
Happy Coding! π
