1. Conduid
  2. Developer Tools
  3. UI Helloworld
MCP server · Developer Tools

UI Helloworld

MCP server: UI Helloworld

Unclaimed last commit 7 months ago devtools
41Fair

Scored 4 months ago · breakdown

About UI Helloworld

UI Helloworld is an MCP server published by hamid-hoseini in the Developer Tools category: mCP server: UI Helloworld. It has been installed 0 times through Conduid.

The repository has 1 stars and 0 forks, with the last commit 7 months ago. Six months or more without a commit doesn't mean the server is broken, but check the open issues (0) before depending on it in production.

Install

Install
npx mcp-ui-helloworld

This server has no ConduID identity, so agent calls to it are not receipted. Pin the version you install and review the source before granting it credentials.

Ask AI

Ask AI about UI Helloworld

Powered by Claude · Grounded in docs

I know everything about UI Helloworld. Ask me about installation, configuration, usage, or troubleshooting.

Security checks

  • ·README presentNot checked yet.
  • ·License declaredNot checked yet.
  • ·Tests presentNot checked yet.
  • ·Dependencies pinnedNot checked yet.
  • ·No dynamic code executionNot checked yet.
  • !Scoped permissionsDoesn't declare a permission scope. Assume it can do anything its process can.

README

MCP-UI: Hello World Tutorial

Table of Contents

  1. What is MCP-UI?
  2. Architecture Overview
  3. Project Structure
  4. Step-by-Step Setup
  5. How It Works
  6. Code Walkthrough
  7. 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

  1. MCP (Model Context Protocol): A protocol that enables AI assistants to securely access external data sources and tools through standardized interfaces.

  2. MCP-UI Extension: Allows MCP servers to return UI resources (HTML components) along with text responses, enabling rich visual interfaces.

  3. 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.

  4. 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:

  1. MCP Server (server/server.js):

    • Defines tools (like say_hello)
    • Handles tool execution
    • Returns UI resources using @mcp-ui/server
  2. 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
  3. 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 framework
  • cors: 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

  1. SSE Connection: Establishes persistent connection for server-to-client communication
  2. Session Management: Each SSE connection gets a unique sessionId for routing requests
  3. Tool Call: Client sends JSON-RPC request via POST with sessionId
  4. Tool Execution: Server executes tool and creates UI resource
  5. Response: Server sends JSON-RPC response through SSE stream
  6. 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 sessionId is 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 pendingRequestId matches the response id

3. Multiple SSE Connections

Problem: Dashboard creates multiple SSE connections.

Solutions:

  • Use empty dependency array [] in useEffect for SSE setup
  • Use useRef to access latest state values in event handlers
  • Clean up EventSource in useEffect return function

4. HTML Not Rendering

Problem: UI resource received but not displayed.

Solutions:

  • Check that you're accessing resource.text (not resource.content.htmlString)
  • Verify HTML content is valid
  • Check browser console for rendering errors

Debug Tips

  1. Server Logging: Add console.log statements to track:

    • Transport creation and storage
    • POST request handling
    • Tool execution
  2. Client Logging: Log in browser console:

    • SSE connection events
    • Received messages
    • Request/response IDs
  3. 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

  1. Multiple Tools: Add more tools to your MCP server
  2. Error Handling: Improve error messages and recovery
  3. UI Styling: Enhance the dashboard with better CSS
  4. Security: Add authentication and input validation
  5. Real-time Updates: Use SSE for server-initiated updates
  6. State Management: Add Redux or Zustand for complex state
  7. Type Safety: Expand TypeScript interfaces for better type checking

Resources


Summary

This tutorial demonstrated how to:

  1. ✅ Create an MCP server with UI capabilities
  2. ✅ Set up SSE transport for real-time communication
  3. ✅ Build a React dashboard to interact with the server
  4. ✅ Generate and render dynamic UI components
  5. ✅ Handle the complete request/response cycle

You now have a working MCP-UI application that can generate and display interactive UI components! 🎉


Happy Coding! 🚀

README mirrored from the source repository 4 months ago. The original is authoritative.

Questions

About UI Helloworld

How do I install UI Helloworld?

Run npx mcp-ui-helloworld, then add the server to your MCP client's configuration. Conduid has recorded 0 installs, so the command is known to work with current clients.

Is UI Helloworld safe to use with an AI agent?

Its trust score is 41 out of 100 (fair). It passes 0 of 1 static security checks; the failures are listed above. It has no ConduID identity yet, so agent calls to it are not receipted.

Is UI Helloworld still maintained?

The last commit was 7 months ago, with 0 open issues. That's long enough that you should check whether the maintainer is responding to issues before depending on it.