#
tokens: 6058/50000 9/9 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── Dockerfile
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   ├── handlers.ts
│   ├── helpers.ts
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
node_modules
dist
.idea


```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Shadcn UI MCP Server

A powerful and flexible MCP (Model Control Protocol) server designed to enhance the development experience with Shadcn UI components. This server provides a robust foundation for building and managing UI components with advanced tooling and functionality.

<a href="https://glama.ai/mcp/servers/@heilgar/shadcn-ui-mcp-server">
  <img width="380" height="200" src="https://glama.ai/mcp/servers/@heilgar/shadcn-ui-mcp-server/badge" alt="Shadcn UI Server MCP server" />
</a>

## Features

### Tools 
The MCP server provides a set of tools that can be used through the Model Control Protocol:

- `list-components`: Get the list of available shadcn/ui components
- `get-component-docs`: Get documentation for a specific component
- `install-component`: Install a shadcn/ui component
- `list-blocks`: Get the list of available shadcn/ui blocks
- `get-block-docs`: Get documentation for a specific block
- `install-blocks`: Install a shadcn/ui block

### Functionality
- **Component Management**
  - List available shadcn/ui components
  - Get detailed documentation for specific components
  - Install components with support for multiple package managers (npm, pnpm, yarn, bun)

- **Block Management**
  - List available shadcn/ui blocks
  - Get documentation and code for specific blocks
  - Install blocks with support for multiple package managers

- **Package Manager Support**
  - Flexible runtime support for npm, pnpm, yarn, and bun
  - Automatic detection of user's preferred package manager

## Installation

### Prerequisites
- Node.js (v18 or higher)
- npm or yarn package manager

### Claude Desktop Configuration
To use with Claude Desktop, add the server config:

On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`

```json
{
  "mcpServers": {
    "shadcn-ui-server": {
      "command": "npx",
      "args": ["@heilgar/shadcn-ui-mcp-server"]
    }
  }
}
```

### Windsurf Configuration
Add this to your `./codeium/windsurf/model_config.json`:

```json
{
  "mcpServers": {
    "shadcn-ui-server": {
      "command": "npx",
      "args": ["@heilgar/shadcn-ui-mcp-server"]
    }
  }
}
```

### Cursor Configuration
Add this to your `.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "shadcn-ui-server": {
      "command": "npx",
      "args": ["@heilgar/shadcn-ui-mcp-server"]
    }
  }
}
```

## Development and Debugging

### Local Development
1. Install dependencies:
```bash
npm install
```

2. Build the server:
```bash
npm run build
```

### Debugging
Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) for debugging:

```bash
npm run inspector
```

The Inspector will provide a URL to access debugging tools in your browser, allowing you to:
- Monitor MCP communication
- Inspect tool calls and responses
- Debug server behavior
- View real-time logs

## Related Projects and Dependencies

This project is built using the following tools and libraries:

- [Model Context Protocol TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) - The official TypeScript SDK for MCP servers and clients
- [MCP Inspector](https://github.com/modelcontextprotocol/inspector) - A debugging tool for MCP servers
- [Cheerio](https://github.com/cheeriojs/cheerio) - Fast, flexible, and lean implementation of core jQuery designed specifically for the server

## License

MIT License - feel free to use this project for your own purposes.

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request. 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config
FROM node:lts-alpine

# Create app directory
WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm install --ignore-scripts --no-audit --no-fund

# Copy source
COPY . .

# Build TypeScript
RUN npm run build

# Default command
CMD ["node", "dist/index.js"]

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/build/project-config

startCommand:
  type: stdio
  commandFunction:
    # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
    |-
    (config) => ({ command: 'node', args: ['dist/index.js'] })
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
  exampleConfig: {}

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@heilgar/shadcn-ui-mcp-server",
  "version": "1.0.6",
  "description": "MCP server for shadcn/ui component references",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "shadcn-ui-mcp-server": "dist/index.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
    "start": "node dist/index.js",
    "dev": "tsc && node dist/index.js",
    "inspector": "mcp-inspector"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/heilgar/shadcn-ui-mcp-server.git"
  },
  "keywords": [
    "mcp",
    "model",
    "context",
    "protocol",
    "shadcn-ui",
    "mcp server"
  ],
  "author": "heilgar",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/heilgar/shadcn-ui-mcp-server/issues"
  },
  "homepage": "https://github.com/heilgar/shadcn-ui-mcp-server#readme",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.11.3",
    "cheerio": "^1.0.0"
  },
  "devDependencies": {
    "@types/cheerio": "^0.22.35",
    "@types/node": "^22.15.18",
    "typescript": "^5.8.3"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "volta": {
    "node": "22.15.1",
    "npm": "11.4.0"
  }
}


```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { listComponents, listBlocks, getComponentDocs, installComponent, installBlock, getBlockDocs } from "./handlers.js";


const toolDefinitions = {
    "list-components": {
        description: "Get the list of available shadcn/ui components",
        parameters: {},
        toolSchema: {},
        handler: listComponents
    },
    "get-component-docs": {
        description: "Get documentation for a specific shadcn/ui component",
        parameters: {
            component: { type: "string", description: "Name of the component to get documentation for" }
        },
        toolSchema: {
            component: z.string().describe("Name of the component to get documentation for")
        },
        handler: getComponentDocs
    },
    "install-component": {
        description: "Install a shadcn/ui component",
        parameters: {
            component: { type: "string", description: "Name of the component to install" },
            runtime: { type: "string", description: "User runtime (npm, pnpm, yarn, bun)", optional: true }
        },
        toolSchema: {
            component: z.string().describe("Name of the component to install"),
            runtime: z.string().describe("User runtime (npm, pnpm, yarn, bun)").optional()
        },
        handler: installComponent   
    },
    "list-blocks": {
        description: "Get the list of available shadcn/ui blocks",
        parameters: {},
        toolSchema: {},
        handler: listBlocks
    },
    "get-block-docs": {
        description: "Get documentation (code) for a specific shadcn/ui block",
        parameters: {
            block: { type: "string", description: "Name of the block to get documentation for" }
        },
        toolSchema: {
            block: z.string().describe("Name of the block to get documentation for")
        },
        handler: getBlockDocs
    },
    "install-blocks": {
        description: "Install a shadcn/ui block",
        parameters: {
            block: { type: "string", description: "Name of the block to install" },
            runtime: { type: "string", description: "User runtime (npm, pnpm, yarn, bun)", optional: true }
        },
        toolSchema: {
            block: z.string().describe("Name of the block to install"),
            runtime: z.string().describe("User runtime (npm, pnpm, yarn, bun)").optional()
        },
        handler: installBlock
    },
};

const server = new McpServer({
    name: "shadcn-ui-mcp-server",
    version: "1.0.0",
    capabilities: {
       tools: toolDefinitions
    },
});

for (const [name, definition] of Object.entries(toolDefinitions)) {
    server.tool(
        name,
        definition.toolSchema,
        definition.handler
    );
}

async function main() {
    try {
        const transport = new StdioServerTransport();
        console.error("Starting shadcn/ui MCP server...");
        await server.connect(transport);
        console.error("Server connected and ready");
    } catch (error) {
        console.error("Failed to start server:", error);
        process.exit(1);
    }
}

process.on('SIGINT', () => process.exit(0));
process.on('SIGTERM', () => process.exit(0));

main().catch(error => {
    console.error("Unhandled error:", error);
    process.exit(1);
});
```

--------------------------------------------------------------------------------
/src/handlers.ts:
--------------------------------------------------------------------------------

```typescript
import {
    parseComponentsFromHtml, 
    fetchWithRetry, 
    validateRuntime,
    createResponse,
    handleError,
    fetchAndCacheComponentData,
    fetchAndCacheBlocks,
    resourceCache,
    PackageManager,
    BASE_URL
} from "./helpers.js";

export const listComponents = async () => {
    try {
        const response = await fetchWithRetry(`${BASE_URL}/components`);
        const html = await response.text();
        const components = parseComponentsFromHtml(html);
        return createResponse(JSON.stringify(components, null, 2));
    } catch (error) {
        return handleError(error, "Error fetching components list");
    }
}

export const getComponentDocs = async ({component}: {component: string}) => {
    try {
        if (!component) {
            return createResponse("Component name is required", true);
        }
        
        const componentData = await fetchAndCacheComponentData(component);
        
        if (!componentData.description && !componentData.doc) {
            return createResponse(`No documentation found for component '${component}'`, true);
        }
        
        return createResponse(
            `${componentData.doc}`,
            false,
            "text/markdown"
        );
    } catch (error) {
        return handleError(error, "Error fetching component documentation");
    }
}

export const installComponent = async ({component, runtime}: {component: string, runtime?: string}) => {
    try {
        if (!component) {
            return createResponse("Component name is required", true);
        }
        
        if (runtime && !validateRuntime(runtime)) {
            return createResponse(`Invalid runtime: ${runtime}. Must be one of: npm, pnpm, yarn, bun`, true);
        }
        
        const componentData = await fetchAndCacheComponentData(component);
        
        if (!componentData.commands?.[0]) {
            return createResponse(`No installation command found for component '${component}'`, true);
        }

        const commands = componentData.commands[0];
        const selectedRuntime = runtime as PackageManager | undefined;
        const command = selectedRuntime ? commands[selectedRuntime] : commands.npm;

        if (!command) {
            return createResponse(`No installation command found for runtime '${runtime}'`, true);
        }

        return createResponse(command);
    } catch (error) {
        return handleError(error, "Error generating installation command");
    }
}

export const listBlocks = async () => {
    try {
        const blocks = await fetchAndCacheBlocks();
        const blockNames = blocks.map(block => block.name);
        return createResponse(JSON.stringify(blockNames, null, 2));
    } catch (error) {
        return handleError(error, "Error fetching blocks");
    }
}

async function getBlockData(block: string) {
    if (!block) {
        return { error: "Block name is required" };
    }
    let blockData = resourceCache.get(block);
    if (!blockData) {
        await fetchAndCacheBlocks();
        blockData = resourceCache.get(block);
    }
    if (!blockData) {
        return { error: `Block '${block}' not found. Use list-blocks to see available blocks.` };
    }
    return { blockData };
}

export const getBlockDocs = async ({block}: {block: string}) => {
    try {
        const { blockData, error } = await getBlockData(block);
        if (error) return createResponse(error, true);
        if (!blockData) return createResponse("Unexpected error: block data missing", true);

        if (!blockData.doc) {
            return createResponse(`No documentation found for block '${block}'`, true);
        }

        return createResponse(
            `${JSON.stringify(blockData, null, 2)}`,
            false,
            "application/json"
        );
    } catch (error) {
        return handleError(error, "Error fetching block documentation");
    }
}

export const installBlock = async ({block, runtime}: {block: string, runtime?: string}) => {
    try {
        if (runtime && !validateRuntime(runtime)) {
            return createResponse(`Invalid runtime: ${runtime}. Must be one of: npm, pnpm, yarn, bun`, true);
        }
        const { blockData, error } = await getBlockData(block);
        if (error) return createResponse(error, true);
        if (!blockData) return createResponse("Unexpected error: block data missing", true);

        if (!blockData.commands?.[0]) {
            return createResponse(`No installation command found for block '${block}'`, true);
        }
        const commands = blockData.commands[0];
        const selectedRuntime = runtime as PackageManager | undefined;
        const command = selectedRuntime ? commands[selectedRuntime] : commands.npm;
        if (!command) {
            return createResponse(`No installation command found for runtime '${runtime}'`, true);
        }
        return createResponse(command);
    } catch (error) {
        return handleError(error, "Error generating installation command");
    }
}


```

--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------

```typescript
import { load } from "cheerio";

export const RETRY_ATTEMPTS = 3;
export const RETRY_DELAY_MS = 500;
export const BASE_URL = "https://ui.shadcn.com";
export const RAW_GITHUB_URL = "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps";

export const BLOCK_PAGES = [
    `${BASE_URL}/blocks/sidebar`,
    `${BASE_URL}/blocks/authentication`,
];

export type PackageManager = "pnpm" | "npm" | "yarn" | "bun";

export interface CommandSet {
    npm: string;
    pnpm: string;
    yarn: string;
    bun: string;
}

export interface ComponentDocResource {
    name: string;
    description?: string;
    doc?: string;
    commands?: CommandSet[];
    links?: string[];
    isBlock?: boolean;
}

export const RUNTIME_REPLACEMENTS: Record<Exclude<PackageManager, 'npm'>, string> = {
    pnpm: 'pnpm dlx',
    yarn: 'yarn dlx',
    bun: 'bunx'
};

export const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/;
export const DESCRIPTION_PATTERNS = [
    /description:\s*["']([^"']+)["']/,
    /description:\s*([^\n]+)/,
    /description\s*:\s*["']([^"']+)["']/
];
export const FIRST_PARAGRAPH_REGEX = /---\n[\s\S]*?\n---\n\n([^\n]+)/;
export const LINKS_REGEX = /links:\n([\s\S]*?)(?=\n\w|$)/;
export const CLI_COMMAND_REGEX = /```bash\nnpx shadcn@latest add [^\n]+\n```/;
export const USAGE_REGEX = /## Usage\n\n([\s\S]*?)(?=\n## |$)/;
export const CODE_BLOCKS_REGEX = /```(?:tsx|ts|jsx|js)([\s\S]*?)```/g;
export const CODE_BLOCK_CLEANUP_REGEX = /```(?:tsx|ts|jsx|js)\n|```$/g;

export const resourceCache = new Map<string, ComponentDocResource>();

const loadCheerio = (html: string) => load(html, {
    decodeEntities: true
});

export const validateRuntime = (runtime?: string): runtime is PackageManager => 
    !runtime || ['npm', 'pnpm', 'yarn', 'bun'].includes(runtime);

export const extractDescription = (frontmatter: string, mdxContent: string): string => {
    for (const pattern of DESCRIPTION_PATTERNS) {
        const match = frontmatter.match(pattern);
        if (match) return match[1].trim();
    }
    
    const firstParagraphMatch = mdxContent.match(FIRST_PARAGRAPH_REGEX);
    return firstParagraphMatch ? firstParagraphMatch[1].trim() : '';
};

export const extractLinks = (frontmatter: string): string[] => {
    const links: string[] = [];
    const linksMatch = frontmatter.match(LINKS_REGEX);
    
    if (linksMatch) {
        const linksContent = linksMatch[1];
        const docLinkMatch = linksContent.match(/doc:\s*([^\n]+)/);
        const apiLinkMatch = linksContent.match(/api:\s*([^\n]+)/);
        
        if (docLinkMatch) links.push(docLinkMatch[1].trim());
        if (apiLinkMatch) links.push(apiLinkMatch[1].trim());
    }
    
    return links;
};

export const getCliCommand = (cliCommand: string, runtime?: PackageManager): string => {
    if (!runtime || runtime === 'npm') return cliCommand;
    return cliCommand.replace('npx', RUNTIME_REPLACEMENTS[runtime]);
};

export const createResponse = (text: string, isError = false, mimeType?: string) => ({
    content: [{ type: "text" as const, text, ...(mimeType && { mimeType }) }],
    ...(isError && { isError })
});

export const handleError = (error: unknown, prefix: string) => 
    createResponse(`${prefix}: ${error instanceof Error ? error.message : String(error)}`, true);

export async function fetchWithRetry(url: string, retries = RETRY_ATTEMPTS, delay = RETRY_DELAY_MS): Promise<Response> {
    try {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
        }
        return response;
    } catch (error) {
        if (retries <= 1) throw error;
        
        await new Promise(resolve => setTimeout(resolve, delay));
        return fetchWithRetry(url, retries - 1, delay * 2); // Exponential backoff
    }
}

async function cacheResource<T>(
    key: string,
    fetchFn: () => Promise<T>,
    cache: Map<string, T>
): Promise<T> {
    // Check cache first
    if (cache.has(key)) {
        return cache.get(key)!;
    }

    try {
        const data = await fetchFn();
        cache.set(key, data);
        return data;
    } catch (error) {
        throw new Error(`Failed to fetch data for key '${key}': ${error instanceof Error ? error.message : String(error)}`);
    }
}

export async function fetchAndCache(
    key: string,
    fetchFn: () => Promise<any>,
    transformFn: (data: any) => ComponentDocResource[]
): Promise<ComponentDocResource[]> {
    try {
        const rawData = await fetchFn();
        const transformedData = transformFn(rawData);
        transformedData.forEach(data => resourceCache.set(data.name, data));
        return transformedData;
    } catch (error) {
        throw new Error(
            `Failed to fetch and transform data for key '${key}': ${
                error instanceof Error ? error.message : String(error)
            }`
        );
    }
}

export async function fetchAndCacheComponentData(component: string): Promise<ComponentDocResource> {
    if (!component || typeof component !== 'string') {
        throw new Error('Invalid component name');
    }
    
    // Sanitize component name
    const sanitizedComponent = component.replace(/[^a-zA-Z0-9-_]/g, '');
    if (sanitizedComponent !== component) {
        throw new Error(`Invalid component name: ${component}`);
    }
    
    // Check cache
    if (resourceCache.has(component)) {
        return resourceCache.get(component)!;
    }

    const docSubPath = `www/content/docs/components`;
    const url = `${RAW_GITHUB_URL}/${docSubPath}/${component}.mdx`;

    const transformComponentData = (mdxContent: string): ComponentDocResource[] => {
        const frontmatterMatch = mdxContent.match(FRONTMATTER_REGEX);
        const frontmatter = frontmatterMatch ? frontmatterMatch[1] : '';
        
        const description = extractDescription(frontmatter, mdxContent);
        const links = extractLinks(frontmatter);

        const cliCommandMatch = mdxContent.match(CLI_COMMAND_REGEX);
        const cliCommand = cliCommandMatch ? cliCommandMatch[0].replace(/```bash\n|\n```/g, '').trim() : undefined;

        let commands: CommandSet[] | undefined = undefined;
        
        if (cliCommand) {
            commands = [{
                npm: cliCommand,
                pnpm: getCliCommand(cliCommand, 'pnpm'),
                yarn: getCliCommand(cliCommand, 'yarn'),
                bun: getCliCommand(cliCommand, 'bun')
            }];
        }

        return [{
            name: component,
            description,
            doc: mdxContent,
            commands,
            links: links.length > 0 ? links : undefined,
            isBlock: false
        }];
    };

    const [componentData] = await fetchAndCache(
        component,
        async () => {
            const response = await fetchWithRetry(url);
            return response.text();
        },
        transformComponentData
    );

    return componentData;
}

export type Block = {
    name: string;
    command: string;
    doc: string;
    description?: string;
};

export async function fetchAndCacheBlocks(): Promise<ComponentDocResource[]> {
    const transformBlocks = (blockPages: Block[][]): ComponentDocResource[] => {
        const allBlocks = blockPages.flat();
        
        return allBlocks.map((block: Block) => ({
            name: block.name,
            description: block.description,
            doc: block.doc,
            commands: [{
                npm: block.command,
                pnpm: getCliCommand(block.command, 'pnpm'),
                yarn: getCliCommand(block.command, 'yarn'),
                bun: getCliCommand(block.command, 'bun')
            }],
            isBlock: true
        }));
    };

    return fetchAndCache(
        'blocks',
        async () => Promise.all(BLOCK_PAGES.map(parseBlocksFromPage)),
        transformBlocks
    );
}

export async function parseBlocksFromPage(url: string): Promise<Block[]> {
    if (!url || !url.startsWith('https://')) {
        throw new Error(`Invalid URL: ${url}`);
    }

    try {
        const response = await fetchWithRetry(url);
        const html = await response.text();
        const $ = loadCheerio(html);

        const blocks: Block[] = [];
        
        $('.container-wrapper.flex-1 div[id]').each((_, el) => {
            const $block = $(el);
            const id = $block.attr('id');

            if (id && !id.startsWith('radix-')) {
                const anchor = $block.find('div.flex.w-full.items-center.gap-2.md\\:pr-\\[14px\\] > a');
                const description = anchor.text().trim();
                const command = $block.find('div.flex.w-full.items-center.gap-2.md\\:pr-\\[14px\\] > div.ml-auto.hidden.items-center.gap-2.md\\:flex > div.flex.h-7.items-center.gap-1.rounded-md.border.p-\\[2px\\] > button > span').text().trim();
                const doc = $block.find('code').first().text().trim();
                
                blocks.push({ name: id, description, command, doc });
            }
        });

        if (blocks.length === 0) {
            console.error(`Warning: No blocks found at ${url}`);
        }

        return blocks;
    } catch (error) {
        throw new Error(`Failed to parse blocks from ${url}: ${error instanceof Error ? error.message : String(error)}`);
    }
}

export function parseComponentsFromHtml(html: string): string[] {
    if (!html || typeof html !== 'string') {
        throw new Error('Invalid HTML content');
    }
    
    try {
        const $ = loadCheerio(html);
        
        const components = $('a[href^="/docs/components/"]')
            .map((_, el) => {
                const href = $(el).attr('href');
                return href?.split('/').pop();
            })
            .get()
            .filter((name): name is string => Boolean(name))
            .sort();
        
        if (components.length === 0) {
            console.error('Warning: No components found in HTML');
        }
        
        return components;
    } catch (error) {
        throw new Error(`Failed to parse components: ${error instanceof Error ? error.message : String(error)}`);
    }
}
```