# Directory Structure ``` ├── .gitignore ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` node_modules/ build/ *.log .env* ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # shadcn-ui MCP Server MCP server for shadcn/ui component references This is a TypeScript-based MCP server that provides reference information for shadcn/ui components. It implements a Model Context Protocol (MCP) server that helps AI assistants access shadcn/ui component documentation and examples. ## Features ### Tools - `list_shadcn_components` - Get a list of all available shadcn/ui components - `get_component_details` - Get detailed information about a specific component - `get_component_examples` - Get usage examples for a specific component - `search_components` - Search for components by keyword ### Functionality This server scrapes and caches information from: - The official shadcn/ui documentation site (https://ui.shadcn.com) - The shadcn/ui GitHub repository It provides structured data including: - Component descriptions - Installation instructions - Usage examples - Props and variants - Code samples ## Development Install dependencies: ```bash npm install ``` Build the server: ```bash npm run build ``` For development with auto-rebuild: ```bash npm run watch ``` ## Installation ### 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` #### Option 1: Using local build ```json { "mcpServers": { "shadcn-ui-server": { "command": "/path/to/shadcn-ui-server/build/index.js" } } } ``` #### Option 2: Using npx command ```json { "mcpServers": { "shadcn-ui-server": { "command": "npx", "args": ["-y", "shadcn-ui-mcp-server"] } } } ``` ### Windsurf Configuration Add this to your `./codeium/windsurf/model_config.json`: ```json { "mcpServers": { "shadcn-ui-server": { "command": "npx", "args": ["-y", "shadcn-ui-mcp-server"] } } } ``` ### Cursor Configuration Add this to your `.cursor/mcp.json`: ```json { "mcpServers": { "shadcn-ui-server": { "command": "npx", "args": ["-y", "shadcn-ui-mcp-server"] } } } ``` ### Debugging Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script: ```bash npm run inspector ``` The Inspector will provide a URL to access debugging tools in your browser. ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules"] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "shadcn-ui-mcp-server", "version": "0.1.2", "description": "MCP server for shadcn/ui component references", "type": "module", "license": "MIT", "bin": { "shadcn-ui-server": "build/index.js" }, "files": [ "build" ], "scripts": { "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", "prepare": "npm run build", "watch": "tsc --watch", "inspector": "npx @modelcontextprotocol/inspector build/index.js" }, "dependencies": { "@modelcontextprotocol/sdk": "0.6.0", "axios": "^1.7.9", "cheerio": "^1.0.0-rc.12" }, "devDependencies": { "@types/node": "^20.11.24", "typescript": "^5.3.3" }, "repository": { "type": "git", "url": "git+https://github.com/ymadd/shadcn-ui-mcp-server.git" }, "author": "ymadd", "bugs": { "url": "https://github.com/ymadd/shadcn-ui-mcp-server/issues" }, "homepage": "https://github.com/ymadd/shadcn-ui-mcp-server#readme", "keywords": [ "shadcn/ui", "MCP", "model context protocol", "component references" ] } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node /** * MCP server for shadcn/ui component references * This server provides tools to: * - List all available shadcn/ui components * - Get detailed information about specific components * - Get usage examples for components * - Search for components by keyword */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; import * as cheerio from "cheerio"; /** * Interface for component information */ interface ComponentInfo { name: string; description: string; url: string; sourceUrl?: string; apiReference?: string; installation?: string; usage?: string; props?: Record<string, ComponentProp>; examples?: ComponentExample[]; } /** * Interface for component property information */ interface ComponentProp { type: string; description: string; required: boolean; default?: string; example?: string; } /** * Interface for component example */ interface ComponentExample { title: string; code: string; description?: string; } /** * ShadcnUiServer class that handles all the component reference functionality */ class ShadcnUiServer { private server: Server; private axiosInstance; private componentCache: Map<string, ComponentInfo> = new Map(); private componentsListCache: ComponentInfo[] | null = null; private readonly SHADCN_DOCS_URL = "https://ui.shadcn.com"; private readonly SHADCN_GITHUB_URL = "https://github.com/shadcn-ui/ui"; private readonly SHADCN_RAW_GITHUB_URL = "https://raw.githubusercontent.com/shadcn-ui/ui/main"; constructor() { this.server = new Server( { name: "shadcn-ui-server", version: "0.1.0", }, { capabilities: { tools: {}, }, } ); this.axiosInstance = axios.create({ timeout: 10000, headers: { "User-Agent": "Mozilla/5.0 (compatible; ShadcnUiMcpServer/0.1.0)", }, }); this.setupToolHandlers(); this.server.onerror = (error) => console.error("[MCP Error]", error); process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } /** * Set up the tool handlers for the server */ private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "list_shadcn_components", description: "Get a list of all available shadcn/ui components", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "get_component_details", description: "Get detailed information about a specific shadcn/ui component", inputSchema: { type: "object", properties: { componentName: { type: "string", description: "Name of the shadcn/ui component (e.g., \"accordion\", \"button\")", }, }, required: ["componentName"], }, }, { name: "get_component_examples", description: "Get usage examples for a specific shadcn/ui component", inputSchema: { type: "object", properties: { componentName: { type: "string", description: "Name of the shadcn/ui component (e.g., \"accordion\", \"button\")", }, }, required: ["componentName"], }, }, { name: "search_components", description: "Search for shadcn/ui components by keyword", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query to find relevant components", }, }, required: ["query"], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case "list_shadcn_components": return await this.handleListComponents(); case "get_component_details": return await this.handleGetComponentDetails(request.params.arguments); case "get_component_examples": return await this.handleGetComponentExamples(request.params.arguments); case "search_components": return await this.handleSearchComponents(request.params.arguments); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } }); } /** * Handle the list_shadcn_components tool request */ private async handleListComponents() { try { if (!this.componentsListCache) { // Fetch the list of components const response = await this.axiosInstance.get(`${this.SHADCN_DOCS_URL}/docs/components`); const $ = cheerio.load(response.data); const components: ComponentInfo[] = []; // Extract component links $("a").each((_, element) => { const link = $(element); const url = link.attr("href"); if (url && url.startsWith("/docs/components/")) { const name = url.split("/").pop() || ""; components.push({ name, description: "", // Will be populated when fetching details url: `${this.SHADCN_DOCS_URL}${url}`, }); } }); this.componentsListCache = components; } return { content: [ { type: "text", text: JSON.stringify(this.componentsListCache, null, 2), }, ], }; } catch (error) { if (axios.isAxiosError(error)) { throw new McpError( ErrorCode.InternalError, `Failed to fetch shadcn/ui components: ${error.message}` ); } throw error; } } /** * Validates component name from arguments * @param args Arguments object * @returns Validated component name * @throws McpError if validation fails */ private validateComponentName(args: any): string { if (!args?.componentName || typeof args.componentName !== "string") { throw new McpError( ErrorCode.InvalidParams, "Component name is required and must be a string" ); } return args.componentName.toLowerCase(); } /** * Validates search query from arguments * @param args Arguments object * @returns Validated search query * @throws McpError if validation fails */ private validateSearchQuery(args: any): string { if (!args?.query || typeof args.query !== "string") { throw new McpError( ErrorCode.InvalidParams, "Search query is required and must be a string" ); } return args.query.toLowerCase(); } /** * Handles Axios errors consistently * @param error The caught error * @param context Context information for the error message * @throws McpError with appropriate error code and message */ private handleAxiosError(error: unknown, context: string): never { if (axios.isAxiosError(error)) { if (error.response?.status === 404) { throw new McpError( ErrorCode.InvalidParams, `${context} not found` ); } else { throw new McpError( ErrorCode.InternalError, `${context}: ${error.message}` ); } } throw error; } /** * Creates a standardized success response * @param data Data to include in the response * @returns Formatted response object */ private createSuccessResponse(data: any) { return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } /** * Handle the get_component_details tool request */ private async handleGetComponentDetails(args: any) { const componentName = this.validateComponentName(args); try { // Check cache first if (this.componentCache.has(componentName)) { return this.createSuccessResponse(this.componentCache.get(componentName)); } // Fetch component details const componentInfo = await this.fetchComponentDetails(componentName); // Save to cache this.componentCache.set(componentName, componentInfo); return this.createSuccessResponse(componentInfo); } catch (error) { this.handleAxiosError(error, `Component "${componentName}"`); } } /** * Fetches component details from the shadcn/ui documentation * @param componentName Name of the component to fetch * @returns Component information */ private async fetchComponentDetails(componentName: string): Promise<ComponentInfo> { const response = await this.axiosInstance.get(`${this.SHADCN_DOCS_URL}/docs/components/${componentName}`); const $ = cheerio.load(response.data); // Extract component information const title = $("h1").first().text().trim(); // Extract description properly const description = this.extractDescription($); // Extract GitHub source code link const sourceUrl = `${this.SHADCN_GITHUB_URL}/tree/main/apps/www/registry/default/ui/${componentName}`; // Extract installation instructions const installation = this.extractInstallation($); // Extract usage examples const usage = this.extractUsage($); // Extract variant information const props = this.extractVariants($, componentName); return { name: componentName, description, url: `${this.SHADCN_DOCS_URL}/docs/components/${componentName}`, sourceUrl, installation: installation.trim(), usage: usage.trim(), props: Object.keys(props).length > 0 ? props : undefined, }; } /** * Extracts component description from the page * @param $ Cheerio instance * @returns Extracted description */ private extractDescription($: cheerio.CheerioAPI): string { let description = ""; const descriptionElement = $("h1").first().next("p"); if (descriptionElement.length > 0) { // Get only text content, removing any JavaScript code const clonedElement = descriptionElement.clone(); clonedElement.find("script").remove(); description = clonedElement.text().trim(); } return description; } /** * Extracts installation instructions from the page * @param $ Cheerio instance * @returns Installation instructions */ private extractInstallation($: cheerio.CheerioAPI): string { let installation = ""; const installSection = $("h2").filter((_, el) => $(el).text().trim() === "Installation"); if (installSection.length > 0) { // Find installation command const codeBlock = installSection.nextAll("pre").first(); if (codeBlock.length > 0) { installation = codeBlock.text().trim(); } } return installation; } /** * Extracts usage examples from the page * @param $ Cheerio instance * @returns Usage examples */ private extractUsage($: cheerio.CheerioAPI): string { let usage = ""; const usageSection = $("h2").filter((_, el) => $(el).text().trim() === "Usage"); if (usageSection.length > 0) { const codeBlocks = usageSection.nextAll("pre"); if (codeBlocks.length > 0) { codeBlocks.each((_, el) => { usage += $(el).text().trim() + "\n\n"; }); } } return usage; } /** * Extracts variant information from the page * @param $ Cheerio instance * @param componentName Name of the component * @returns Object containing variant properties */ private extractVariants($: cheerio.CheerioAPI, componentName: string): Record<string, ComponentProp> { const props: Record<string, ComponentProp> = {}; // Extract variants from Examples section const examplesSection = $("h2").filter((_, el) => $(el).text().trim() === "Examples"); if (examplesSection.length > 0) { // Find each variant const variantHeadings = examplesSection.nextAll("h3"); variantHeadings.each((_, heading) => { const variantName = $(heading).text().trim(); // Get variant code example let codeExample = ""; // Find Code tab const codeTab = $(heading).nextAll(".tabs-content").first(); if (codeTab.length > 0) { const codeBlock = codeTab.find("pre"); if (codeBlock.length > 0) { codeExample = codeBlock.text().trim(); } } props[variantName] = { type: "variant", description: `${variantName} variant of the ${componentName} component`, required: false, example: codeExample }; }); } return props; } /** * Handle the get_component_examples tool request */ private async handleGetComponentExamples(args: any) { const componentName = this.validateComponentName(args); try { // Fetch component examples const examples = await this.fetchComponentExamples(componentName); return this.createSuccessResponse(examples); } catch (error) { this.handleAxiosError(error, `Component examples for "${componentName}"`); } } /** * Fetches component examples from documentation and GitHub * @param componentName Name of the component * @returns Array of component examples */ private async fetchComponentExamples(componentName: string): Promise<ComponentExample[]> { const response = await this.axiosInstance.get(`${this.SHADCN_DOCS_URL}/docs/components/${componentName}`); const $ = cheerio.load(response.data); const examples: ComponentExample[] = []; // Collect examples from different sources this.collectGeneralCodeExamples($, examples); this.collectSectionExamples($, "Usage", "Basic usage example", examples); this.collectSectionExamples($, "Link", "Link usage example", examples); await this.collectGitHubExamples(componentName, examples); return examples; } /** * Collects general code examples from the page * @param $ Cheerio instance * @param examples Array to add examples to */ private collectGeneralCodeExamples($: cheerio.CheerioAPI, examples: ComponentExample[]): void { const codeBlocks = $("pre"); codeBlocks.each((i, el) => { const code = $(el).text().trim(); if (code) { // Find heading before code block let title = "Code Example " + (i + 1); let description = "Code example"; // Look for headings let prevElement = $(el).prev(); while (prevElement.length && !prevElement.is("h1") && !prevElement.is("h2") && !prevElement.is("h3")) { prevElement = prevElement.prev(); } if (prevElement.is("h2") || prevElement.is("h3")) { title = prevElement.text().trim(); description = `${title} example`; } examples.push({ title, code, description }); } }); } /** * Collects examples from a specific section * @param $ Cheerio instance * @param sectionName Name of the section to collect from * @param descriptionPrefix Prefix for the description * @param examples Array to add examples to */ private collectSectionExamples( $: cheerio.CheerioAPI, sectionName: string, descriptionPrefix: string, examples: ComponentExample[] ): void { const section = $("h2").filter((_, el) => $(el).text().trim() === sectionName); if (section.length > 0) { const codeBlocks = section.nextAll("pre"); codeBlocks.each((i, el) => { const code = $(el).text().trim(); if (code) { examples.push({ title: `${sectionName} Example ${i + 1}`, code: code, description: descriptionPrefix }); } }); } } /** * Collects examples from GitHub repository * @param componentName Name of the component * @param examples Array to add examples to */ private async collectGitHubExamples(componentName: string, examples: ComponentExample[]): Promise<void> { try { const githubResponse = await this.axiosInstance.get( `${this.SHADCN_RAW_GITHUB_URL}/apps/www/registry/default/example/${componentName}-demo.tsx` ); if (githubResponse.status === 200) { examples.push({ title: "GitHub Demo Example", code: githubResponse.data, }); } } catch (error) { // Continue even if GitHub fetch fails console.error(`Failed to fetch GitHub example for ${componentName}:`, error); } } /** * Handle the search_components tool request */ private async handleSearchComponents(args: any) { const query = this.validateSearchQuery(args); try { // Ensure components list is loaded await this.ensureComponentsListLoaded(); // Filter components matching the search query const results = this.searchComponentsByQuery(query); return this.createSuccessResponse(results); } catch (error) { this.handleAxiosError(error, "Search failed"); } } /** * Ensures the components list is loaded in cache * @throws McpError if components list cannot be loaded */ private async ensureComponentsListLoaded(): Promise<void> { if (!this.componentsListCache) { await this.handleListComponents(); } if (!this.componentsListCache) { throw new McpError( ErrorCode.InternalError, "Failed to load components list" ); } } /** * Searches components by query string * @param query Search query * @returns Filtered components */ private searchComponentsByQuery(query: string): ComponentInfo[] { if (!this.componentsListCache) { return []; } return this.componentsListCache.filter(component => { return ( component.name.includes(query) || component.description.toLowerCase().includes(query) ); }); } /** * Run the server */ async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("shadcn/ui MCP server running on stdio"); } } // Create and run the server const server = new ShadcnUiServer(); server.run().catch((error) => { console.error("Server error:", error); process.exit(1); }); ```