#
tokens: 8202/50000 9/9 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | }
```