# Directory Structure ``` ├── .gitignore ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── smithery.yaml ├── src │ ├── handlers.ts │ ├── helpers.ts │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` node_modules dist .idea ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Shadcn UI MCP Server A powerful and flexible MCP (Model Control Protocol) server designed to enhance the development experience with Shadcn UI components. This server provides a robust foundation for building and managing UI components with advanced tooling and functionality. <a href="https://glama.ai/mcp/servers/@heilgar/shadcn-ui-mcp-server"> <img width="380" height="200" src="https://glama.ai/mcp/servers/@heilgar/shadcn-ui-mcp-server/badge" alt="Shadcn UI Server MCP server" /> </a> ## Features ### Tools The MCP server provides a set of tools that can be used through the Model Control Protocol: - `list-components`: Get the list of available shadcn/ui components - `get-component-docs`: Get documentation for a specific component - `install-component`: Install a shadcn/ui component - `list-blocks`: Get the list of available shadcn/ui blocks - `get-block-docs`: Get documentation for a specific block - `install-blocks`: Install a shadcn/ui block ### Functionality - **Component Management** - List available shadcn/ui components - Get detailed documentation for specific components - Install components with support for multiple package managers (npm, pnpm, yarn, bun) - **Block Management** - List available shadcn/ui blocks - Get documentation and code for specific blocks - Install blocks with support for multiple package managers - **Package Manager Support** - Flexible runtime support for npm, pnpm, yarn, and bun - Automatic detection of user's preferred package manager ## Installation ### Prerequisites - Node.js (v18 or higher) - npm or yarn package manager ### Claude Desktop Configuration To use with Claude Desktop, add the server config: On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json` On Windows: `%APPDATA%/Claude/claude_desktop_config.json` ```json { "mcpServers": { "shadcn-ui-server": { "command": "npx", "args": ["@heilgar/shadcn-ui-mcp-server"] } } } ``` ### Windsurf Configuration Add this to your `./codeium/windsurf/model_config.json`: ```json { "mcpServers": { "shadcn-ui-server": { "command": "npx", "args": ["@heilgar/shadcn-ui-mcp-server"] } } } ``` ### Cursor Configuration Add this to your `.cursor/mcp.json`: ```json { "mcpServers": { "shadcn-ui-server": { "command": "npx", "args": ["@heilgar/shadcn-ui-mcp-server"] } } } ``` ## Development and Debugging ### Local Development 1. Install dependencies: ```bash npm install ``` 2. Build the server: ```bash npm run build ``` ### Debugging Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) for debugging: ```bash npm run inspector ``` The Inspector will provide a URL to access debugging tools in your browser, allowing you to: - Monitor MCP communication - Inspect tool calls and responses - Debug server behavior - View real-time logs ## Related Projects and Dependencies This project is built using the following tools and libraries: - [Model Context Protocol TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) - The official TypeScript SDK for MCP servers and clients - [MCP Inspector](https://github.com/modelcontextprotocol/inspector) - A debugging tool for MCP servers - [Cheerio](https://github.com/cheeriojs/cheerio) - Fast, flexible, and lean implementation of core jQuery designed specifically for the server ## License MIT License - feel free to use this project for your own purposes. ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules"] } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config FROM node:lts-alpine # Create app directory WORKDIR /app # Install dependencies COPY package*.json ./ RUN npm install --ignore-scripts --no-audit --no-fund # Copy source COPY . . # Build TypeScript RUN npm run build # Default command CMD ["node", "dist/index.js"] ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/build/project-config startCommand: type: stdio commandFunction: # A JS function that produces the CLI command based on the given config to start the MCP on stdio. |- (config) => ({ command: 'node', args: ['dist/index.js'] }) configSchema: # JSON Schema defining the configuration options for the MCP. type: object exampleConfig: {} ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "@heilgar/shadcn-ui-mcp-server", "version": "1.0.6", "description": "MCP server for shadcn/ui component references", "type": "module", "main": "dist/index.js", "bin": { "shadcn-ui-mcp-server": "dist/index.js" }, "files": [ "dist" ], "scripts": { "build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"", "start": "node dist/index.js", "dev": "tsc && node dist/index.js", "inspector": "mcp-inspector" }, "repository": { "type": "git", "url": "git+https://github.com/heilgar/shadcn-ui-mcp-server.git" }, "keywords": [ "mcp", "model", "context", "protocol", "shadcn-ui", "mcp server" ], "author": "heilgar", "license": "MIT", "bugs": { "url": "https://github.com/heilgar/shadcn-ui-mcp-server/issues" }, "homepage": "https://github.com/heilgar/shadcn-ui-mcp-server#readme", "dependencies": { "@modelcontextprotocol/sdk": "^1.11.3", "cheerio": "^1.0.0" }, "devDependencies": { "@types/cheerio": "^0.22.35", "@types/node": "^22.15.18", "typescript": "^5.8.3" }, "engines": { "node": ">=18.0.0" }, "volta": { "node": "22.15.1", "npm": "11.4.0" } } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { listComponents, listBlocks, getComponentDocs, installComponent, installBlock, getBlockDocs } from "./handlers.js"; const toolDefinitions = { "list-components": { description: "Get the list of available shadcn/ui components", parameters: {}, toolSchema: {}, handler: listComponents }, "get-component-docs": { description: "Get documentation for a specific shadcn/ui component", parameters: { component: { type: "string", description: "Name of the component to get documentation for" } }, toolSchema: { component: z.string().describe("Name of the component to get documentation for") }, handler: getComponentDocs }, "install-component": { description: "Install a shadcn/ui component", parameters: { component: { type: "string", description: "Name of the component to install" }, runtime: { type: "string", description: "User runtime (npm, pnpm, yarn, bun)", optional: true } }, toolSchema: { component: z.string().describe("Name of the component to install"), runtime: z.string().describe("User runtime (npm, pnpm, yarn, bun)").optional() }, handler: installComponent }, "list-blocks": { description: "Get the list of available shadcn/ui blocks", parameters: {}, toolSchema: {}, handler: listBlocks }, "get-block-docs": { description: "Get documentation (code) for a specific shadcn/ui block", parameters: { block: { type: "string", description: "Name of the block to get documentation for" } }, toolSchema: { block: z.string().describe("Name of the block to get documentation for") }, handler: getBlockDocs }, "install-blocks": { description: "Install a shadcn/ui block", parameters: { block: { type: "string", description: "Name of the block to install" }, runtime: { type: "string", description: "User runtime (npm, pnpm, yarn, bun)", optional: true } }, toolSchema: { block: z.string().describe("Name of the block to install"), runtime: z.string().describe("User runtime (npm, pnpm, yarn, bun)").optional() }, handler: installBlock }, }; const server = new McpServer({ name: "shadcn-ui-mcp-server", version: "1.0.0", capabilities: { tools: toolDefinitions }, }); for (const [name, definition] of Object.entries(toolDefinitions)) { server.tool( name, definition.toolSchema, definition.handler ); } async function main() { try { const transport = new StdioServerTransport(); console.error("Starting shadcn/ui MCP server..."); await server.connect(transport); console.error("Server connected and ready"); } catch (error) { console.error("Failed to start server:", error); process.exit(1); } } process.on('SIGINT', () => process.exit(0)); process.on('SIGTERM', () => process.exit(0)); main().catch(error => { console.error("Unhandled error:", error); process.exit(1); }); ``` -------------------------------------------------------------------------------- /src/handlers.ts: -------------------------------------------------------------------------------- ```typescript import { parseComponentsFromHtml, fetchWithRetry, validateRuntime, createResponse, handleError, fetchAndCacheComponentData, fetchAndCacheBlocks, resourceCache, PackageManager, BASE_URL } from "./helpers.js"; export const listComponents = async () => { try { const response = await fetchWithRetry(`${BASE_URL}/components`); const html = await response.text(); const components = parseComponentsFromHtml(html); return createResponse(JSON.stringify(components, null, 2)); } catch (error) { return handleError(error, "Error fetching components list"); } } export const getComponentDocs = async ({component}: {component: string}) => { try { if (!component) { return createResponse("Component name is required", true); } const componentData = await fetchAndCacheComponentData(component); if (!componentData.description && !componentData.doc) { return createResponse(`No documentation found for component '${component}'`, true); } return createResponse( `${componentData.doc}`, false, "text/markdown" ); } catch (error) { return handleError(error, "Error fetching component documentation"); } } export const installComponent = async ({component, runtime}: {component: string, runtime?: string}) => { try { if (!component) { return createResponse("Component name is required", true); } if (runtime && !validateRuntime(runtime)) { return createResponse(`Invalid runtime: ${runtime}. Must be one of: npm, pnpm, yarn, bun`, true); } const componentData = await fetchAndCacheComponentData(component); if (!componentData.commands?.[0]) { return createResponse(`No installation command found for component '${component}'`, true); } const commands = componentData.commands[0]; const selectedRuntime = runtime as PackageManager | undefined; const command = selectedRuntime ? commands[selectedRuntime] : commands.npm; if (!command) { return createResponse(`No installation command found for runtime '${runtime}'`, true); } return createResponse(command); } catch (error) { return handleError(error, "Error generating installation command"); } } export const listBlocks = async () => { try { const blocks = await fetchAndCacheBlocks(); const blockNames = blocks.map(block => block.name); return createResponse(JSON.stringify(blockNames, null, 2)); } catch (error) { return handleError(error, "Error fetching blocks"); } } async function getBlockData(block: string) { if (!block) { return { error: "Block name is required" }; } let blockData = resourceCache.get(block); if (!blockData) { await fetchAndCacheBlocks(); blockData = resourceCache.get(block); } if (!blockData) { return { error: `Block '${block}' not found. Use list-blocks to see available blocks.` }; } return { blockData }; } export const getBlockDocs = async ({block}: {block: string}) => { try { const { blockData, error } = await getBlockData(block); if (error) return createResponse(error, true); if (!blockData) return createResponse("Unexpected error: block data missing", true); if (!blockData.doc) { return createResponse(`No documentation found for block '${block}'`, true); } return createResponse( `${JSON.stringify(blockData, null, 2)}`, false, "application/json" ); } catch (error) { return handleError(error, "Error fetching block documentation"); } } export const installBlock = async ({block, runtime}: {block: string, runtime?: string}) => { try { if (runtime && !validateRuntime(runtime)) { return createResponse(`Invalid runtime: ${runtime}. Must be one of: npm, pnpm, yarn, bun`, true); } const { blockData, error } = await getBlockData(block); if (error) return createResponse(error, true); if (!blockData) return createResponse("Unexpected error: block data missing", true); if (!blockData.commands?.[0]) { return createResponse(`No installation command found for block '${block}'`, true); } const commands = blockData.commands[0]; const selectedRuntime = runtime as PackageManager | undefined; const command = selectedRuntime ? commands[selectedRuntime] : commands.npm; if (!command) { return createResponse(`No installation command found for runtime '${runtime}'`, true); } return createResponse(command); } catch (error) { return handleError(error, "Error generating installation command"); } } ``` -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- ```typescript import { load } from "cheerio"; export const RETRY_ATTEMPTS = 3; export const RETRY_DELAY_MS = 500; export const BASE_URL = "https://ui.shadcn.com"; export const RAW_GITHUB_URL = "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps"; export const BLOCK_PAGES = [ `${BASE_URL}/blocks/sidebar`, `${BASE_URL}/blocks/authentication`, ]; export type PackageManager = "pnpm" | "npm" | "yarn" | "bun"; export interface CommandSet { npm: string; pnpm: string; yarn: string; bun: string; } export interface ComponentDocResource { name: string; description?: string; doc?: string; commands?: CommandSet[]; links?: string[]; isBlock?: boolean; } export const RUNTIME_REPLACEMENTS: Record<Exclude<PackageManager, 'npm'>, string> = { pnpm: 'pnpm dlx', yarn: 'yarn dlx', bun: 'bunx' }; export const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/; export const DESCRIPTION_PATTERNS = [ /description:\s*["']([^"']+)["']/, /description:\s*([^\n]+)/, /description\s*:\s*["']([^"']+)["']/ ]; export const FIRST_PARAGRAPH_REGEX = /---\n[\s\S]*?\n---\n\n([^\n]+)/; export const LINKS_REGEX = /links:\n([\s\S]*?)(?=\n\w|$)/; export const CLI_COMMAND_REGEX = /```bash\nnpx shadcn@latest add [^\n]+\n```/; export const USAGE_REGEX = /## Usage\n\n([\s\S]*?)(?=\n## |$)/; export const CODE_BLOCKS_REGEX = /```(?:tsx|ts|jsx|js)([\s\S]*?)```/g; export const CODE_BLOCK_CLEANUP_REGEX = /```(?:tsx|ts|jsx|js)\n|```$/g; export const resourceCache = new Map<string, ComponentDocResource>(); const loadCheerio = (html: string) => load(html, { decodeEntities: true }); export const validateRuntime = (runtime?: string): runtime is PackageManager => !runtime || ['npm', 'pnpm', 'yarn', 'bun'].includes(runtime); export const extractDescription = (frontmatter: string, mdxContent: string): string => { for (const pattern of DESCRIPTION_PATTERNS) { const match = frontmatter.match(pattern); if (match) return match[1].trim(); } const firstParagraphMatch = mdxContent.match(FIRST_PARAGRAPH_REGEX); return firstParagraphMatch ? firstParagraphMatch[1].trim() : ''; }; export const extractLinks = (frontmatter: string): string[] => { const links: string[] = []; const linksMatch = frontmatter.match(LINKS_REGEX); if (linksMatch) { const linksContent = linksMatch[1]; const docLinkMatch = linksContent.match(/doc:\s*([^\n]+)/); const apiLinkMatch = linksContent.match(/api:\s*([^\n]+)/); if (docLinkMatch) links.push(docLinkMatch[1].trim()); if (apiLinkMatch) links.push(apiLinkMatch[1].trim()); } return links; }; export const getCliCommand = (cliCommand: string, runtime?: PackageManager): string => { if (!runtime || runtime === 'npm') return cliCommand; return cliCommand.replace('npx', RUNTIME_REPLACEMENTS[runtime]); }; export const createResponse = (text: string, isError = false, mimeType?: string) => ({ content: [{ type: "text" as const, text, ...(mimeType && { mimeType }) }], ...(isError && { isError }) }); export const handleError = (error: unknown, prefix: string) => createResponse(`${prefix}: ${error instanceof Error ? error.message : String(error)}`, true); export async function fetchWithRetry(url: string, retries = RETRY_ATTEMPTS, delay = RETRY_DELAY_MS): Promise<Response> { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error ${response.status}: ${response.statusText}`); } return response; } catch (error) { if (retries <= 1) throw error; await new Promise(resolve => setTimeout(resolve, delay)); return fetchWithRetry(url, retries - 1, delay * 2); // Exponential backoff } } async function cacheResource<T>( key: string, fetchFn: () => Promise<T>, cache: Map<string, T> ): Promise<T> { // Check cache first if (cache.has(key)) { return cache.get(key)!; } try { const data = await fetchFn(); cache.set(key, data); return data; } catch (error) { throw new Error(`Failed to fetch data for key '${key}': ${error instanceof Error ? error.message : String(error)}`); } } export async function fetchAndCache( key: string, fetchFn: () => Promise<any>, transformFn: (data: any) => ComponentDocResource[] ): Promise<ComponentDocResource[]> { try { const rawData = await fetchFn(); const transformedData = transformFn(rawData); transformedData.forEach(data => resourceCache.set(data.name, data)); return transformedData; } catch (error) { throw new Error( `Failed to fetch and transform data for key '${key}': ${ error instanceof Error ? error.message : String(error) }` ); } } export async function fetchAndCacheComponentData(component: string): Promise<ComponentDocResource> { if (!component || typeof component !== 'string') { throw new Error('Invalid component name'); } // Sanitize component name const sanitizedComponent = component.replace(/[^a-zA-Z0-9-_]/g, ''); if (sanitizedComponent !== component) { throw new Error(`Invalid component name: ${component}`); } // Check cache if (resourceCache.has(component)) { return resourceCache.get(component)!; } const docSubPath = `www/content/docs/components`; const url = `${RAW_GITHUB_URL}/${docSubPath}/${component}.mdx`; const transformComponentData = (mdxContent: string): ComponentDocResource[] => { const frontmatterMatch = mdxContent.match(FRONTMATTER_REGEX); const frontmatter = frontmatterMatch ? frontmatterMatch[1] : ''; const description = extractDescription(frontmatter, mdxContent); const links = extractLinks(frontmatter); const cliCommandMatch = mdxContent.match(CLI_COMMAND_REGEX); const cliCommand = cliCommandMatch ? cliCommandMatch[0].replace(/```bash\n|\n```/g, '').trim() : undefined; let commands: CommandSet[] | undefined = undefined; if (cliCommand) { commands = [{ npm: cliCommand, pnpm: getCliCommand(cliCommand, 'pnpm'), yarn: getCliCommand(cliCommand, 'yarn'), bun: getCliCommand(cliCommand, 'bun') }]; } return [{ name: component, description, doc: mdxContent, commands, links: links.length > 0 ? links : undefined, isBlock: false }]; }; const [componentData] = await fetchAndCache( component, async () => { const response = await fetchWithRetry(url); return response.text(); }, transformComponentData ); return componentData; } export type Block = { name: string; command: string; doc: string; description?: string; }; export async function fetchAndCacheBlocks(): Promise<ComponentDocResource[]> { const transformBlocks = (blockPages: Block[][]): ComponentDocResource[] => { const allBlocks = blockPages.flat(); return allBlocks.map((block: Block) => ({ name: block.name, description: block.description, doc: block.doc, commands: [{ npm: block.command, pnpm: getCliCommand(block.command, 'pnpm'), yarn: getCliCommand(block.command, 'yarn'), bun: getCliCommand(block.command, 'bun') }], isBlock: true })); }; return fetchAndCache( 'blocks', async () => Promise.all(BLOCK_PAGES.map(parseBlocksFromPage)), transformBlocks ); } export async function parseBlocksFromPage(url: string): Promise<Block[]> { if (!url || !url.startsWith('https://')) { throw new Error(`Invalid URL: ${url}`); } try { const response = await fetchWithRetry(url); const html = await response.text(); const $ = loadCheerio(html); const blocks: Block[] = []; $('.container-wrapper.flex-1 div[id]').each((_, el) => { const $block = $(el); const id = $block.attr('id'); if (id && !id.startsWith('radix-')) { const anchor = $block.find('div.flex.w-full.items-center.gap-2.md\\:pr-\\[14px\\] > a'); const description = anchor.text().trim(); const command = $block.find('div.flex.w-full.items-center.gap-2.md\\:pr-\\[14px\\] > div.ml-auto.hidden.items-center.gap-2.md\\:flex > div.flex.h-7.items-center.gap-1.rounded-md.border.p-\\[2px\\] > button > span').text().trim(); const doc = $block.find('code').first().text().trim(); blocks.push({ name: id, description, command, doc }); } }); if (blocks.length === 0) { console.error(`Warning: No blocks found at ${url}`); } return blocks; } catch (error) { throw new Error(`Failed to parse blocks from ${url}: ${error instanceof Error ? error.message : String(error)}`); } } export function parseComponentsFromHtml(html: string): string[] { if (!html || typeof html !== 'string') { throw new Error('Invalid HTML content'); } try { const $ = loadCheerio(html); const components = $('a[href^="/docs/components/"]') .map((_, el) => { const href = $(el).attr('href'); return href?.split('/').pop(); }) .get() .filter((name): name is string => Boolean(name)) .sort(); if (components.length === 0) { console.error('Warning: No components found in HTML'); } return components; } catch (error) { throw new Error(`Failed to parse components: ${error instanceof Error ? error.message : String(error)}`); } } ```