# Directory Structure
```
├── .env.example
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── cli.ts
│ ├── index.ts
│ ├── server.ts
│ └── services
│ ├── component-parser.ts
│ └── github.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Dependency directories
node_modules/
# Build output
dist/
# Environment variables
.env
# OS specific
.DS_Store
Thumbs.db
# Cache files
cache/
# Log files
server.log
*.log
```
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
# GitHub API Token - Create one at https://github.com/settings/tokens
# Required for higher API rate limits when fetching components from GitHub
GITHUB_TOKEN=your_github_token_here
# Transport type for the MCP server - Options: stdio or http
# Default: stdio
TRANSPORT_TYPE=stdio
# Port for the HTTP server when TRANSPORT_TYPE is set to http
# Default: 3000
PORT=3000
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# MCP Magic UI
An MCP (Model Context Protocol) server for accessing and exploring Magic UI components from the [magicuidesign/magicui](https://github.com/magicuidesign/magicui) repository.
## What is MCP Magic UI?
MCP Magic UI is a server that implements the Model Context Protocol (MCP) to provide access to Magic UI components. It fetches component data from the Magic UI GitHub repository, categorizes them, and makes them available through an MCP API. This allows AI assistants and other MCP clients to easily discover and use Magic UI components in their applications.
## Features
- **Component Discovery**: Access all Magic UI components through MCP tools
- **Component Categorization**: Components are automatically categorized based on their names and dependencies
- **Caching System**: Local caching of component data to reduce GitHub API calls and work offline
- **Multiple Transport Options**: Support for both stdio and HTTP transport methods
- **Fallback Mechanism**: Mock data is provided when GitHub API is unavailable
## Installation
```bash
# Clone the repository
git clone https://github.com/idcdev/mcp-magic-ui.git
cd mcp-magic-ui
# Install dependencies
npm install
# Build the project
npm run build
```
## Configuration
To avoid GitHub API rate limits, it's recommended to set up a GitHub personal access token:
1. Create a token at https://github.com/settings/tokens
2. Create a `.env` file in the project root (or copy from `.env.example`)
3. Add your token to the `.env` file:
```
GITHUB_TOKEN=your_github_token_here
```
## Usage
### Starting the server
You can start the server using either stdio or HTTP transport:
```bash
# Using stdio transport (default)
npm start
# Using HTTP transport
TRANSPORT_TYPE=http npm start
```
### Connecting to the server
You can connect to the server using any MCP client. For example, using the MCP Inspector:
```bash
npx @modelcontextprotocol/inspector mcp-magic-ui
```
Or, if using HTTP transport:
```bash
npx @modelcontextprotocol/inspector http://localhost:3000
```
## Available Tools
The server provides the following MCP tools:
- `get_all_components` - Get a list of all available Magic UI components with their metadata
- `get_component_by_path` - Get the source code of a specific component by its file path
## Project Structure
- `src/` - Source code
- `index.ts` - Main entry point for the server
- `cli.ts` - Command-line interface
- `server.ts` - MCP server configuration and tool definitions
- `services/` - Service modules
- `github.ts` - GitHub API interaction and caching
- `component-parser.ts` - Component categorization and processing
- `cache/` - Local cache for component data
- `dist/` - Compiled JavaScript code
## How It Works
1. The server fetches component data from the Magic UI GitHub repository
2. Component data is cached locally to reduce API calls and enable offline usage
3. Components are categorized based on their names and dependencies
4. The server exposes MCP tools to access and search for components
5. Clients can connect to the server using stdio or HTTP transport
## Contributing
Contributions are welcome! Here are some ways you can contribute:
- Report bugs and suggest features by creating issues
- Improve documentation
- Submit pull requests with bug fixes or new features
## License
MIT
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"declaration": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { createServer } from './server.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
// Load environment variables from .env file
dotenv.config();
// Redirect console.log to console.error
const originalConsoleLog = console.log;
console.log = function(...args) {
console.error(...args);
};
async function main() {
// Create and start server with stdio transport
const server = await createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(error => {
console.error('Error starting MCP server:', error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-magic-ui",
"version": "1.0.0",
"description": "MCP server for Magic UI components",
"main": "dist/index.js",
"type": "module",
"bin": {
"mcp-magic-ui": "dist/cli.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc -w & nodemon dist/index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"postbuild": "chmod +x dist/cli.js"
},
"keywords": [
"mcp",
"magic-ui",
"components"
],
"author": "",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.1",
"@octokit/rest": "^19.0.13",
"@types/node-fetch": "^2.6.12",
"dotenv": "^16.4.7",
"express": "^4.18.2",
"node-fetch": "^3.3.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.10.5",
"nodemon": "^3.0.2",
"typescript": "^5.3.3"
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import { createServer } from './server.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import express from 'express';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { Request, Response } from 'express';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
// Load environment variables from .env file
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function main() {
try {
const server = await createServer();
// Determine transport method from environment variable or argument
const transportType = process.env.TRANSPORT_TYPE || 'stdio';
if (transportType === 'stdio') {
// Use stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
console.log('MCP server started with stdio transport');
} else if (transportType === 'http') {
// Use HTTP with SSE transport
const app = express();
const port = process.env.PORT || 3000;
let transport: SSEServerTransport;
app.get('/sse', async (req: Request, res: Response) => {
transport = new SSEServerTransport('/messages', res);
await server.connect(transport);
});
app.post('/messages', async (req: Request, res: Response) => {
if (transport) {
await transport.handlePostMessage(req, res);
} else {
res.status(400).send('No active transport connection');
}
});
app.listen(port, () => {
console.log(`MCP server started with HTTP transport on port ${port}`);
});
} else {
console.error(`Unknown transport type: ${transportType}`);
process.exit(1);
}
} catch (error) {
console.error('Error starting MCP server:', error);
process.exit(1);
}
}
main();
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { GitHubService } from "./services/github.js";
import { ComponentParser } from "./services/component-parser.js";
export async function createServer() {
// Initialize services
const githubService = new GitHubService();
const componentParser = new ComponentParser(githubService);
// Load all components
console.error("Loading components...");
await componentParser.loadAllComponents();
console.error("Components loaded successfully!");
// Create MCP server
const server = new McpServer({
name: "Magic UI Components",
version: "1.0.0",
description: "MCP server for accessing Magic UI components",
});
// Register all components tool
server.tool(
"get_all_components",
{},
async () => {
// Get the raw content of registry.json
const rawData = githubService.getRawRegistryData();
// If no data, return processed components as fallback
if (!rawData || rawData.length === 0) {
const components = componentParser.getAllComponents();
return {
content: [{
type: "text",
text: JSON.stringify(components, null, 2),
}],
};
}
// Return the raw content of registry.json
return {
content: [{
type: "text",
text: JSON.stringify(rawData, null, 2),
}],
};
}
);
// Adicionar ferramenta para obter um componente específico pelo caminho
server.tool(
"get_component_by_path",
{
path: z.string().describe("Path to the component file"),
},
async (params: any) => {
const path = params.path as string;
try {
// Obter o conteúdo do arquivo
const content = await githubService.getFileContent(path);
if (!content) {
return {
content: [{
type: "text",
text: `Component file at path '${path}' not found or empty`,
}],
isError: true,
};
}
return {
content: [{
type: "text",
text: content,
}],
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error fetching component at path '${path}': ${error}`,
}],
isError: true,
};
}
}
);
return server;
}
```
--------------------------------------------------------------------------------
/src/services/component-parser.ts:
--------------------------------------------------------------------------------
```typescript
import { GitHubService, RegistryComponent } from './github.js';
// Simplified interface for components
export interface Component {
name: string;
description: string;
code: string;
category: string;
dependencies?: string[];
files?: {
path: string;
type: string;
target: string;
}[];
title?: string;
}
export class ComponentParser {
private githubService: GitHubService;
private components: Map<string, Component> = new Map();
constructor(githubService: GitHubService) {
this.githubService = githubService;
}
// Load all components
async loadAllComponents(): Promise<void> {
// Load components from registry
await this.githubService.loadRegistryComponents();
// Convert registry components to Component format
const registryComponents = this.githubService.getAllRegistryComponents();
for (const registryComponent of registryComponents) {
const component: Component = {
name: registryComponent.name,
description: registryComponent.description,
code: '', // We don't need to load the code here
category: this.determineCategory(registryComponent),
dependencies: registryComponent.dependencies,
files: registryComponent.files,
title: registryComponent.title
};
this.components.set(component.name, component);
}
console.error(`Loaded ${this.components.size} components`);
}
// Determine category based on registry component
private determineCategory(registryComponent: RegistryComponent): string {
const name = registryComponent.name.toLowerCase();
// If the component has dependencies, use them to help determine the category
if (registryComponent.dependencies) {
if (registryComponent.dependencies.includes('motion')) return 'Animation';
}
// Determine category based on component name
if (name.includes('button')) return 'Button';
if (name.includes('card')) return 'Card';
if (name.includes('text')) return 'Typography';
if (name.includes('input')) return 'Form';
if (name.includes('form')) return 'Form';
if (name.includes('dialog')) return 'Dialog';
if (name.includes('modal')) return 'Dialog';
if (name.includes('menu')) return 'Navigation';
if (name.includes('nav')) return 'Navigation';
if (name.includes('table')) return 'Data Display';
if (name.includes('list')) return 'Data Display';
if (name.includes('grid')) return 'Layout';
if (name.includes('layout')) return 'Layout';
if (name.includes('animation')) return 'Animation';
if (name.includes('effect')) return 'Effect';
// Default category
return 'Other';
}
// Get component by name
getComponent(name: string): Component | undefined {
return this.components.get(name);
}
// Get all components
getAllComponents(): Component[] {
return Array.from(this.components.values());
}
}
```
--------------------------------------------------------------------------------
/src/services/github.ts:
--------------------------------------------------------------------------------
```typescript
import { Octokit } from '@octokit/rest';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// Get the current directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Path to cache file
const CACHE_DIR = path.join(__dirname, '../../cache');
const REGISTRY_CACHE_FILE = path.join(CACHE_DIR, 'registry.json');
const CACHE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
export interface RegistryComponent {
name: string;
type: string;
title: string;
description: string;
dependencies?: string[];
files: {
path: string;
type: string;
target: string;
}[];
tailwind?: any;
}
export class GitHubService {
private octokit: Octokit;
private owner = 'magicuidesign';
private repo = 'magicui';
private componentsPath = 'components';
private registryComponents: Map<string, RegistryComponent> = new Map();
private retryDelay = 1000; // ms
private maxRetries = 3;
private rawRegistryData: any[] = []; // Store raw content of registry.json
constructor(token?: string) {
// Use token from environment if not provided
const authToken = token || process.env.GITHUB_TOKEN;
this.octokit = new Octokit({
auth: authToken,
request: {
retries: this.maxRetries,
retryAfter: this.retryDelay
}
});
if (!authToken) {
console.warn('GitHub token not provided. API rate limits will be restricted.');
console.warn('Create a token at https://github.com/settings/tokens and set it as GITHUB_TOKEN in .env file.');
}
// Create cache directory if it doesn't exist
this.ensureCacheDirectory();
}
// Ensure cache directory exists
private ensureCacheDirectory(): void {
try {
if (!fs.existsSync(CACHE_DIR)) {
fs.mkdirSync(CACHE_DIR, { recursive: true });
console.error(`Cache directory created at ${CACHE_DIR}`);
}
} catch (error) {
console.error('Error creating cache directory:', error);
}
}
// Check if cache is valid (not expired)
private isCacheValid(): boolean {
try {
if (!fs.existsSync(REGISTRY_CACHE_FILE)) {
return false;
}
const stats = fs.statSync(REGISTRY_CACHE_FILE);
const cacheAge = Date.now() - stats.mtimeMs;
return cacheAge < CACHE_EXPIRY_MS;
} catch (error) {
console.error('Error checking cache validity:', error);
return false;
}
}
// Save data to cache
private saveToCache(data: string): void {
try {
fs.writeFileSync(REGISTRY_CACHE_FILE, data);
console.error(`Registry data cached to ${REGISTRY_CACHE_FILE}`);
} catch (error) {
console.error('Error saving to cache:', error);
}
}
// Load data from cache
private loadFromCache(): string | null {
try {
if (this.isCacheValid()) {
console.error('Loading registry from cache');
return fs.readFileSync(REGISTRY_CACHE_FILE, 'utf-8');
}
return null;
} catch (error) {
console.error('Error loading from cache:', error);
return null;
}
}
async getComponentsList(): Promise<string[]> {
try {
// Fetch list of components from GitHub
const { data } = await this.octokit.repos.getContent({
owner: this.owner,
repo: this.repo,
path: this.componentsPath,
});
// Filter directories (each directory is a component)
return Array.isArray(data)
? data
.filter((item: any) => item.type === 'dir')
.map((item: any) => item.name)
: [];
} catch (error) {
console.error('Error fetching components list:', error);
return [];
}
}
async getComponentFiles(componentName: string): Promise<any[]> {
try {
// Fetch all files for a specific component
const { data } = await this.octokit.repos.getContent({
owner: this.owner,
repo: this.repo,
path: `${this.componentsPath}/${componentName}`,
});
return Array.isArray(data) ? data : [];
} catch (error) {
console.error(`Error fetching files for component ${componentName}:`, error);
return [];
}
}
async getFileContent(path: string): Promise<string> {
try {
// Fetch content of a specific file
const { data } = await this.octokit.repos.getContent({
owner: this.owner,
repo: this.repo,
path,
});
if ('content' in data) {
// Decode content from base64
return Buffer.from(data.content, 'base64').toString('utf-8');
}
throw new Error(`Could not get content for path: ${path}`);
} catch (error: any) {
// Check if it's a rate limit error
if (error.status === 403 && error.message.includes('API rate limit exceeded')) {
console.error('GitHub API rate limit exceeded. Consider using a token.');
// Return empty string instead of throwing
return '';
}
console.error(`Error fetching file content for ${path}:`, error);
return '';
}
}
// Try to load from cache first
async loadRegistryComponents(): Promise<void> {
// Try to load from cache first
let content = this.loadFromCache();
// If no valid cache, fetch from GitHub
if (!content) {
try {
const response = await this.octokit.repos.getContent({
owner: this.owner,
repo: this.repo,
path: 'registry.json',
});
// Save to cache if successfully retrieved
if (response.data && 'content' in response.data) {
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
this.saveToCache(content);
}
} catch (error) {
console.error('Error fetching registry from GitHub:', error);
}
}
// If still no content, use mock data
if (!content) {
console.warn('Using mock data as fallback');
this.loadMockRegistryData();
return;
}
try {
const data = JSON.parse(content);
// Check file structure - registry.json has a structure with { name, homepage, items: [] }
if (data && data.items && Array.isArray(data.items)) {
// Store raw content of registry.json (the items)
this.rawRegistryData = data.items;
// Process each component in the registry
for (const component of data.items) {
if (component.name && component.type) {
this.registryComponents.set(component.name, component as RegistryComponent);
}
}
console.log(`Loaded ${this.registryComponents.size} components`);
} else {
// Old or unexpected structure
console.error('Unexpected registry.json structure');
}
} catch (error) {
console.error('Error parsing registry data:', error);
}
}
// Get raw registry.json content
getRawRegistryData(): any[] {
return this.rawRegistryData;
}
getRegistryComponent(name: string): RegistryComponent | undefined {
return this.registryComponents.get(name);
}
getAllRegistryComponents(): RegistryComponent[] {
return Array.from(this.registryComponents.values());
}
// Load some mock data for testing when GitHub API fails
private loadMockRegistryData() {
const mockComponents: RegistryComponent[] = [
{
name: 'animated-beam',
type: 'registry:ui',
title: 'Animated Beam',
description: 'An animated beam of light which travels along a path. Useful for showcasing the "integration" features of a website.',
dependencies: ['motion'],
files: [
{
path: 'registry/magicui/animated-beam.tsx',
type: 'registry:ui',
target: 'components/magicui/animated-beam.tsx'
}
]
},
{
name: 'border-beam',
type: 'registry:ui',
title: 'Border Beam',
description: 'An animated beam of light which travels along the border of its container.',
files: [
{
path: 'registry/magicui/border-beam.tsx',
type: 'registry:ui',
target: 'components/magicui/border-beam.tsx'
}
]
},
{
name: 'shimmer-button',
type: 'registry:ui',
title: 'Shimmer Button',
description: 'A button with a shimmer effect that moves across the button.',
files: [
{
path: 'registry/magicui/shimmer-button.tsx',
type: 'registry:ui',
target: 'components/magicui/shimmer-button.tsx'
}
]
}
];
mockComponents.forEach(component => {
this.registryComponents.set(component.name, component);
});
console.error(`Loaded ${this.registryComponents.size} mock components`);
}
}
```