Skip to main content
Glama

shadcn-ui MCP Server

by ymadd
index.ts19 kB
#!/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); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ymadd/shadcn-ui-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server