# 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: -------------------------------------------------------------------------------- ``` 1 | node_modules 2 | dist 3 | .idea 4 | 5 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Shadcn UI MCP Server 2 | 3 | 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. 4 | 5 | <a href="https://glama.ai/mcp/servers/@heilgar/shadcn-ui-mcp-server"> 6 | <img width="380" height="200" src="https://glama.ai/mcp/servers/@heilgar/shadcn-ui-mcp-server/badge" alt="Shadcn UI Server MCP server" /> 7 | </a> 8 | 9 | ## Features 10 | 11 | ### Tools 12 | The MCP server provides a set of tools that can be used through the Model Control Protocol: 13 | 14 | - `list-components`: Get the list of available shadcn/ui components 15 | - `get-component-docs`: Get documentation for a specific component 16 | - `install-component`: Install a shadcn/ui component 17 | - `list-blocks`: Get the list of available shadcn/ui blocks 18 | - `get-block-docs`: Get documentation for a specific block 19 | - `install-blocks`: Install a shadcn/ui block 20 | 21 | ### Functionality 22 | - **Component Management** 23 | - List available shadcn/ui components 24 | - Get detailed documentation for specific components 25 | - Install components with support for multiple package managers (npm, pnpm, yarn, bun) 26 | 27 | - **Block Management** 28 | - List available shadcn/ui blocks 29 | - Get documentation and code for specific blocks 30 | - Install blocks with support for multiple package managers 31 | 32 | - **Package Manager Support** 33 | - Flexible runtime support for npm, pnpm, yarn, and bun 34 | - Automatic detection of user's preferred package manager 35 | 36 | ## Installation 37 | 38 | ### Prerequisites 39 | - Node.js (v18 or higher) 40 | - npm or yarn package manager 41 | 42 | ### Claude Desktop Configuration 43 | To use with Claude Desktop, add the server config: 44 | 45 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 46 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json` 47 | 48 | ```json 49 | { 50 | "mcpServers": { 51 | "shadcn-ui-server": { 52 | "command": "npx", 53 | "args": ["@heilgar/shadcn-ui-mcp-server"] 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | ### Windsurf Configuration 60 | Add this to your `./codeium/windsurf/model_config.json`: 61 | 62 | ```json 63 | { 64 | "mcpServers": { 65 | "shadcn-ui-server": { 66 | "command": "npx", 67 | "args": ["@heilgar/shadcn-ui-mcp-server"] 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | ### Cursor Configuration 74 | Add this to your `.cursor/mcp.json`: 75 | 76 | ```json 77 | { 78 | "mcpServers": { 79 | "shadcn-ui-server": { 80 | "command": "npx", 81 | "args": ["@heilgar/shadcn-ui-mcp-server"] 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | ## Development and Debugging 88 | 89 | ### Local Development 90 | 1. Install dependencies: 91 | ```bash 92 | npm install 93 | ``` 94 | 95 | 2. Build the server: 96 | ```bash 97 | npm run build 98 | ``` 99 | 100 | ### Debugging 101 | Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) for debugging: 102 | 103 | ```bash 104 | npm run inspector 105 | ``` 106 | 107 | The Inspector will provide a URL to access debugging tools in your browser, allowing you to: 108 | - Monitor MCP communication 109 | - Inspect tool calls and responses 110 | - Debug server behavior 111 | - View real-time logs 112 | 113 | ## Related Projects and Dependencies 114 | 115 | This project is built using the following tools and libraries: 116 | 117 | - [Model Context Protocol TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) - The official TypeScript SDK for MCP servers and clients 118 | - [MCP Inspector](https://github.com/modelcontextprotocol/inspector) - A debugging tool for MCP servers 119 | - [Cheerio](https://github.com/cheeriojs/cheerio) - Fast, flexible, and lean implementation of core jQuery designed specifically for the server 120 | 121 | ## License 122 | 123 | MIT License - feel free to use this project for your own purposes. 124 | 125 | ## Contributing 126 | 127 | Contributions are welcome! Please feel free to submit a Pull Request. ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config 2 | FROM node:lts-alpine 3 | 4 | # Create app directory 5 | WORKDIR /app 6 | 7 | # Install dependencies 8 | COPY package*.json ./ 9 | RUN npm install --ignore-scripts --no-audit --no-fund 10 | 11 | # Copy source 12 | COPY . . 13 | 14 | # Build TypeScript 15 | RUN npm run build 16 | 17 | # Default command 18 | CMD ["node", "dist/index.js"] 19 | ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/build/project-config 2 | 3 | startCommand: 4 | type: stdio 5 | commandFunction: 6 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 7 | |- 8 | (config) => ({ command: 'node', args: ['dist/index.js'] }) 9 | configSchema: 10 | # JSON Schema defining the configuration options for the MCP. 11 | type: object 12 | exampleConfig: {} 13 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@heilgar/shadcn-ui-mcp-server", 3 | "version": "1.0.6", 4 | "description": "MCP server for shadcn/ui component references", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "bin": { 8 | "shadcn-ui-mcp-server": "dist/index.js" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"", 15 | "start": "node dist/index.js", 16 | "dev": "tsc && node dist/index.js", 17 | "inspector": "mcp-inspector" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/heilgar/shadcn-ui-mcp-server.git" 22 | }, 23 | "keywords": [ 24 | "mcp", 25 | "model", 26 | "context", 27 | "protocol", 28 | "shadcn-ui", 29 | "mcp server" 30 | ], 31 | "author": "heilgar", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/heilgar/shadcn-ui-mcp-server/issues" 35 | }, 36 | "homepage": "https://github.com/heilgar/shadcn-ui-mcp-server#readme", 37 | "dependencies": { 38 | "@modelcontextprotocol/sdk": "^1.11.3", 39 | "cheerio": "^1.0.0" 40 | }, 41 | "devDependencies": { 42 | "@types/cheerio": "^0.22.35", 43 | "@types/node": "^22.15.18", 44 | "typescript": "^5.8.3" 45 | }, 46 | "engines": { 47 | "node": ">=18.0.0" 48 | }, 49 | "volta": { 50 | "node": "22.15.1", 51 | "npm": "11.4.0" 52 | } 53 | } 54 | 55 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { z } from "zod"; 6 | import { listComponents, listBlocks, getComponentDocs, installComponent, installBlock, getBlockDocs } from "./handlers.js"; 7 | 8 | 9 | const toolDefinitions = { 10 | "list-components": { 11 | description: "Get the list of available shadcn/ui components", 12 | parameters: {}, 13 | toolSchema: {}, 14 | handler: listComponents 15 | }, 16 | "get-component-docs": { 17 | description: "Get documentation for a specific shadcn/ui component", 18 | parameters: { 19 | component: { type: "string", description: "Name of the component to get documentation for" } 20 | }, 21 | toolSchema: { 22 | component: z.string().describe("Name of the component to get documentation for") 23 | }, 24 | handler: getComponentDocs 25 | }, 26 | "install-component": { 27 | description: "Install a shadcn/ui component", 28 | parameters: { 29 | component: { type: "string", description: "Name of the component to install" }, 30 | runtime: { type: "string", description: "User runtime (npm, pnpm, yarn, bun)", optional: true } 31 | }, 32 | toolSchema: { 33 | component: z.string().describe("Name of the component to install"), 34 | runtime: z.string().describe("User runtime (npm, pnpm, yarn, bun)").optional() 35 | }, 36 | handler: installComponent 37 | }, 38 | "list-blocks": { 39 | description: "Get the list of available shadcn/ui blocks", 40 | parameters: {}, 41 | toolSchema: {}, 42 | handler: listBlocks 43 | }, 44 | "get-block-docs": { 45 | description: "Get documentation (code) for a specific shadcn/ui block", 46 | parameters: { 47 | block: { type: "string", description: "Name of the block to get documentation for" } 48 | }, 49 | toolSchema: { 50 | block: z.string().describe("Name of the block to get documentation for") 51 | }, 52 | handler: getBlockDocs 53 | }, 54 | "install-blocks": { 55 | description: "Install a shadcn/ui block", 56 | parameters: { 57 | block: { type: "string", description: "Name of the block to install" }, 58 | runtime: { type: "string", description: "User runtime (npm, pnpm, yarn, bun)", optional: true } 59 | }, 60 | toolSchema: { 61 | block: z.string().describe("Name of the block to install"), 62 | runtime: z.string().describe("User runtime (npm, pnpm, yarn, bun)").optional() 63 | }, 64 | handler: installBlock 65 | }, 66 | }; 67 | 68 | const server = new McpServer({ 69 | name: "shadcn-ui-mcp-server", 70 | version: "1.0.0", 71 | capabilities: { 72 | tools: toolDefinitions 73 | }, 74 | }); 75 | 76 | for (const [name, definition] of Object.entries(toolDefinitions)) { 77 | server.tool( 78 | name, 79 | definition.toolSchema, 80 | definition.handler 81 | ); 82 | } 83 | 84 | async function main() { 85 | try { 86 | const transport = new StdioServerTransport(); 87 | console.error("Starting shadcn/ui MCP server..."); 88 | await server.connect(transport); 89 | console.error("Server connected and ready"); 90 | } catch (error) { 91 | console.error("Failed to start server:", error); 92 | process.exit(1); 93 | } 94 | } 95 | 96 | process.on('SIGINT', () => process.exit(0)); 97 | process.on('SIGTERM', () => process.exit(0)); 98 | 99 | main().catch(error => { 100 | console.error("Unhandled error:", error); 101 | process.exit(1); 102 | }); ``` -------------------------------------------------------------------------------- /src/handlers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | parseComponentsFromHtml, 3 | fetchWithRetry, 4 | validateRuntime, 5 | createResponse, 6 | handleError, 7 | fetchAndCacheComponentData, 8 | fetchAndCacheBlocks, 9 | resourceCache, 10 | PackageManager, 11 | BASE_URL 12 | } from "./helpers.js"; 13 | 14 | export const listComponents = async () => { 15 | try { 16 | const response = await fetchWithRetry(`${BASE_URL}/components`); 17 | const html = await response.text(); 18 | const components = parseComponentsFromHtml(html); 19 | return createResponse(JSON.stringify(components, null, 2)); 20 | } catch (error) { 21 | return handleError(error, "Error fetching components list"); 22 | } 23 | } 24 | 25 | export const getComponentDocs = async ({component}: {component: string}) => { 26 | try { 27 | if (!component) { 28 | return createResponse("Component name is required", true); 29 | } 30 | 31 | const componentData = await fetchAndCacheComponentData(component); 32 | 33 | if (!componentData.description && !componentData.doc) { 34 | return createResponse(`No documentation found for component '${component}'`, true); 35 | } 36 | 37 | return createResponse( 38 | `${componentData.doc}`, 39 | false, 40 | "text/markdown" 41 | ); 42 | } catch (error) { 43 | return handleError(error, "Error fetching component documentation"); 44 | } 45 | } 46 | 47 | export const installComponent = async ({component, runtime}: {component: string, runtime?: string}) => { 48 | try { 49 | if (!component) { 50 | return createResponse("Component name is required", true); 51 | } 52 | 53 | if (runtime && !validateRuntime(runtime)) { 54 | return createResponse(`Invalid runtime: ${runtime}. Must be one of: npm, pnpm, yarn, bun`, true); 55 | } 56 | 57 | const componentData = await fetchAndCacheComponentData(component); 58 | 59 | if (!componentData.commands?.[0]) { 60 | return createResponse(`No installation command found for component '${component}'`, true); 61 | } 62 | 63 | const commands = componentData.commands[0]; 64 | const selectedRuntime = runtime as PackageManager | undefined; 65 | const command = selectedRuntime ? commands[selectedRuntime] : commands.npm; 66 | 67 | if (!command) { 68 | return createResponse(`No installation command found for runtime '${runtime}'`, true); 69 | } 70 | 71 | return createResponse(command); 72 | } catch (error) { 73 | return handleError(error, "Error generating installation command"); 74 | } 75 | } 76 | 77 | export const listBlocks = async () => { 78 | try { 79 | const blocks = await fetchAndCacheBlocks(); 80 | const blockNames = blocks.map(block => block.name); 81 | return createResponse(JSON.stringify(blockNames, null, 2)); 82 | } catch (error) { 83 | return handleError(error, "Error fetching blocks"); 84 | } 85 | } 86 | 87 | async function getBlockData(block: string) { 88 | if (!block) { 89 | return { error: "Block name is required" }; 90 | } 91 | let blockData = resourceCache.get(block); 92 | if (!blockData) { 93 | await fetchAndCacheBlocks(); 94 | blockData = resourceCache.get(block); 95 | } 96 | if (!blockData) { 97 | return { error: `Block '${block}' not found. Use list-blocks to see available blocks.` }; 98 | } 99 | return { blockData }; 100 | } 101 | 102 | export const getBlockDocs = async ({block}: {block: string}) => { 103 | try { 104 | const { blockData, error } = await getBlockData(block); 105 | if (error) return createResponse(error, true); 106 | if (!blockData) return createResponse("Unexpected error: block data missing", true); 107 | 108 | if (!blockData.doc) { 109 | return createResponse(`No documentation found for block '${block}'`, true); 110 | } 111 | 112 | return createResponse( 113 | `${JSON.stringify(blockData, null, 2)}`, 114 | false, 115 | "application/json" 116 | ); 117 | } catch (error) { 118 | return handleError(error, "Error fetching block documentation"); 119 | } 120 | } 121 | 122 | export const installBlock = async ({block, runtime}: {block: string, runtime?: string}) => { 123 | try { 124 | if (runtime && !validateRuntime(runtime)) { 125 | return createResponse(`Invalid runtime: ${runtime}. Must be one of: npm, pnpm, yarn, bun`, true); 126 | } 127 | const { blockData, error } = await getBlockData(block); 128 | if (error) return createResponse(error, true); 129 | if (!blockData) return createResponse("Unexpected error: block data missing", true); 130 | 131 | if (!blockData.commands?.[0]) { 132 | return createResponse(`No installation command found for block '${block}'`, true); 133 | } 134 | const commands = blockData.commands[0]; 135 | const selectedRuntime = runtime as PackageManager | undefined; 136 | const command = selectedRuntime ? commands[selectedRuntime] : commands.npm; 137 | if (!command) { 138 | return createResponse(`No installation command found for runtime '${runtime}'`, true); 139 | } 140 | return createResponse(command); 141 | } catch (error) { 142 | return handleError(error, "Error generating installation command"); 143 | } 144 | } 145 | 146 | ``` -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { load } from "cheerio"; 2 | 3 | export const RETRY_ATTEMPTS = 3; 4 | export const RETRY_DELAY_MS = 500; 5 | export const BASE_URL = "https://ui.shadcn.com"; 6 | export const RAW_GITHUB_URL = "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps"; 7 | 8 | export const BLOCK_PAGES = [ 9 | `${BASE_URL}/blocks/sidebar`, 10 | `${BASE_URL}/blocks/authentication`, 11 | ]; 12 | 13 | export type PackageManager = "pnpm" | "npm" | "yarn" | "bun"; 14 | 15 | export interface CommandSet { 16 | npm: string; 17 | pnpm: string; 18 | yarn: string; 19 | bun: string; 20 | } 21 | 22 | export interface ComponentDocResource { 23 | name: string; 24 | description?: string; 25 | doc?: string; 26 | commands?: CommandSet[]; 27 | links?: string[]; 28 | isBlock?: boolean; 29 | } 30 | 31 | export const RUNTIME_REPLACEMENTS: Record<Exclude<PackageManager, 'npm'>, string> = { 32 | pnpm: 'pnpm dlx', 33 | yarn: 'yarn dlx', 34 | bun: 'bunx' 35 | }; 36 | 37 | export const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/; 38 | export const DESCRIPTION_PATTERNS = [ 39 | /description:\s*["']([^"']+)["']/, 40 | /description:\s*([^\n]+)/, 41 | /description\s*:\s*["']([^"']+)["']/ 42 | ]; 43 | export const FIRST_PARAGRAPH_REGEX = /---\n[\s\S]*?\n---\n\n([^\n]+)/; 44 | export const LINKS_REGEX = /links:\n([\s\S]*?)(?=\n\w|$)/; 45 | export const CLI_COMMAND_REGEX = /```bash\nnpx shadcn@latest add [^\n]+\n```/; 46 | export const USAGE_REGEX = /## Usage\n\n([\s\S]*?)(?=\n## |$)/; 47 | export const CODE_BLOCKS_REGEX = /```(?:tsx|ts|jsx|js)([\s\S]*?)```/g; 48 | export const CODE_BLOCK_CLEANUP_REGEX = /```(?:tsx|ts|jsx|js)\n|```$/g; 49 | 50 | export const resourceCache = new Map<string, ComponentDocResource>(); 51 | 52 | const loadCheerio = (html: string) => load(html, { 53 | decodeEntities: true 54 | }); 55 | 56 | export const validateRuntime = (runtime?: string): runtime is PackageManager => 57 | !runtime || ['npm', 'pnpm', 'yarn', 'bun'].includes(runtime); 58 | 59 | export const extractDescription = (frontmatter: string, mdxContent: string): string => { 60 | for (const pattern of DESCRIPTION_PATTERNS) { 61 | const match = frontmatter.match(pattern); 62 | if (match) return match[1].trim(); 63 | } 64 | 65 | const firstParagraphMatch = mdxContent.match(FIRST_PARAGRAPH_REGEX); 66 | return firstParagraphMatch ? firstParagraphMatch[1].trim() : ''; 67 | }; 68 | 69 | export const extractLinks = (frontmatter: string): string[] => { 70 | const links: string[] = []; 71 | const linksMatch = frontmatter.match(LINKS_REGEX); 72 | 73 | if (linksMatch) { 74 | const linksContent = linksMatch[1]; 75 | const docLinkMatch = linksContent.match(/doc:\s*([^\n]+)/); 76 | const apiLinkMatch = linksContent.match(/api:\s*([^\n]+)/); 77 | 78 | if (docLinkMatch) links.push(docLinkMatch[1].trim()); 79 | if (apiLinkMatch) links.push(apiLinkMatch[1].trim()); 80 | } 81 | 82 | return links; 83 | }; 84 | 85 | export const getCliCommand = (cliCommand: string, runtime?: PackageManager): string => { 86 | if (!runtime || runtime === 'npm') return cliCommand; 87 | return cliCommand.replace('npx', RUNTIME_REPLACEMENTS[runtime]); 88 | }; 89 | 90 | export const createResponse = (text: string, isError = false, mimeType?: string) => ({ 91 | content: [{ type: "text" as const, text, ...(mimeType && { mimeType }) }], 92 | ...(isError && { isError }) 93 | }); 94 | 95 | export const handleError = (error: unknown, prefix: string) => 96 | createResponse(`${prefix}: ${error instanceof Error ? error.message : String(error)}`, true); 97 | 98 | export async function fetchWithRetry(url: string, retries = RETRY_ATTEMPTS, delay = RETRY_DELAY_MS): Promise<Response> { 99 | try { 100 | const response = await fetch(url); 101 | if (!response.ok) { 102 | throw new Error(`HTTP error ${response.status}: ${response.statusText}`); 103 | } 104 | return response; 105 | } catch (error) { 106 | if (retries <= 1) throw error; 107 | 108 | await new Promise(resolve => setTimeout(resolve, delay)); 109 | return fetchWithRetry(url, retries - 1, delay * 2); // Exponential backoff 110 | } 111 | } 112 | 113 | async function cacheResource<T>( 114 | key: string, 115 | fetchFn: () => Promise<T>, 116 | cache: Map<string, T> 117 | ): Promise<T> { 118 | // Check cache first 119 | if (cache.has(key)) { 120 | return cache.get(key)!; 121 | } 122 | 123 | try { 124 | const data = await fetchFn(); 125 | cache.set(key, data); 126 | return data; 127 | } catch (error) { 128 | throw new Error(`Failed to fetch data for key '${key}': ${error instanceof Error ? error.message : String(error)}`); 129 | } 130 | } 131 | 132 | export async function fetchAndCache( 133 | key: string, 134 | fetchFn: () => Promise<any>, 135 | transformFn: (data: any) => ComponentDocResource[] 136 | ): Promise<ComponentDocResource[]> { 137 | try { 138 | const rawData = await fetchFn(); 139 | const transformedData = transformFn(rawData); 140 | transformedData.forEach(data => resourceCache.set(data.name, data)); 141 | return transformedData; 142 | } catch (error) { 143 | throw new Error( 144 | `Failed to fetch and transform data for key '${key}': ${ 145 | error instanceof Error ? error.message : String(error) 146 | }` 147 | ); 148 | } 149 | } 150 | 151 | export async function fetchAndCacheComponentData(component: string): Promise<ComponentDocResource> { 152 | if (!component || typeof component !== 'string') { 153 | throw new Error('Invalid component name'); 154 | } 155 | 156 | // Sanitize component name 157 | const sanitizedComponent = component.replace(/[^a-zA-Z0-9-_]/g, ''); 158 | if (sanitizedComponent !== component) { 159 | throw new Error(`Invalid component name: ${component}`); 160 | } 161 | 162 | // Check cache 163 | if (resourceCache.has(component)) { 164 | return resourceCache.get(component)!; 165 | } 166 | 167 | const docSubPath = `www/content/docs/components`; 168 | const url = `${RAW_GITHUB_URL}/${docSubPath}/${component}.mdx`; 169 | 170 | const transformComponentData = (mdxContent: string): ComponentDocResource[] => { 171 | const frontmatterMatch = mdxContent.match(FRONTMATTER_REGEX); 172 | const frontmatter = frontmatterMatch ? frontmatterMatch[1] : ''; 173 | 174 | const description = extractDescription(frontmatter, mdxContent); 175 | const links = extractLinks(frontmatter); 176 | 177 | const cliCommandMatch = mdxContent.match(CLI_COMMAND_REGEX); 178 | const cliCommand = cliCommandMatch ? cliCommandMatch[0].replace(/```bash\n|\n```/g, '').trim() : undefined; 179 | 180 | let commands: CommandSet[] | undefined = undefined; 181 | 182 | if (cliCommand) { 183 | commands = [{ 184 | npm: cliCommand, 185 | pnpm: getCliCommand(cliCommand, 'pnpm'), 186 | yarn: getCliCommand(cliCommand, 'yarn'), 187 | bun: getCliCommand(cliCommand, 'bun') 188 | }]; 189 | } 190 | 191 | return [{ 192 | name: component, 193 | description, 194 | doc: mdxContent, 195 | commands, 196 | links: links.length > 0 ? links : undefined, 197 | isBlock: false 198 | }]; 199 | }; 200 | 201 | const [componentData] = await fetchAndCache( 202 | component, 203 | async () => { 204 | const response = await fetchWithRetry(url); 205 | return response.text(); 206 | }, 207 | transformComponentData 208 | ); 209 | 210 | return componentData; 211 | } 212 | 213 | export type Block = { 214 | name: string; 215 | command: string; 216 | doc: string; 217 | description?: string; 218 | }; 219 | 220 | export async function fetchAndCacheBlocks(): Promise<ComponentDocResource[]> { 221 | const transformBlocks = (blockPages: Block[][]): ComponentDocResource[] => { 222 | const allBlocks = blockPages.flat(); 223 | 224 | return allBlocks.map((block: Block) => ({ 225 | name: block.name, 226 | description: block.description, 227 | doc: block.doc, 228 | commands: [{ 229 | npm: block.command, 230 | pnpm: getCliCommand(block.command, 'pnpm'), 231 | yarn: getCliCommand(block.command, 'yarn'), 232 | bun: getCliCommand(block.command, 'bun') 233 | }], 234 | isBlock: true 235 | })); 236 | }; 237 | 238 | return fetchAndCache( 239 | 'blocks', 240 | async () => Promise.all(BLOCK_PAGES.map(parseBlocksFromPage)), 241 | transformBlocks 242 | ); 243 | } 244 | 245 | export async function parseBlocksFromPage(url: string): Promise<Block[]> { 246 | if (!url || !url.startsWith('https://')) { 247 | throw new Error(`Invalid URL: ${url}`); 248 | } 249 | 250 | try { 251 | const response = await fetchWithRetry(url); 252 | const html = await response.text(); 253 | const $ = loadCheerio(html); 254 | 255 | const blocks: Block[] = []; 256 | 257 | $('.container-wrapper.flex-1 div[id]').each((_, el) => { 258 | const $block = $(el); 259 | const id = $block.attr('id'); 260 | 261 | if (id && !id.startsWith('radix-')) { 262 | const anchor = $block.find('div.flex.w-full.items-center.gap-2.md\\:pr-\\[14px\\] > a'); 263 | const description = anchor.text().trim(); 264 | 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(); 265 | const doc = $block.find('code').first().text().trim(); 266 | 267 | blocks.push({ name: id, description, command, doc }); 268 | } 269 | }); 270 | 271 | if (blocks.length === 0) { 272 | console.error(`Warning: No blocks found at ${url}`); 273 | } 274 | 275 | return blocks; 276 | } catch (error) { 277 | throw new Error(`Failed to parse blocks from ${url}: ${error instanceof Error ? error.message : String(error)}`); 278 | } 279 | } 280 | 281 | export function parseComponentsFromHtml(html: string): string[] { 282 | if (!html || typeof html !== 'string') { 283 | throw new Error('Invalid HTML content'); 284 | } 285 | 286 | try { 287 | const $ = loadCheerio(html); 288 | 289 | const components = $('a[href^="/docs/components/"]') 290 | .map((_, el) => { 291 | const href = $(el).attr('href'); 292 | return href?.split('/').pop(); 293 | }) 294 | .get() 295 | .filter((name): name is string => Boolean(name)) 296 | .sort(); 297 | 298 | if (components.length === 0) { 299 | console.error('Warning: No components found in HTML'); 300 | } 301 | 302 | return components; 303 | } catch (error) { 304 | throw new Error(`Failed to parse components: ${error instanceof Error ? error.message : String(error)}`); 305 | } 306 | } ```