diff --git a/README.md b/README.md index cbf9161..b177f39 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A Model Context Protocol (MCP) server that provides AI assistants with comprehen - **Metadata Access**: Get component dependencies, descriptions, and configuration details - **Directory Browsing**: Explore the shadcn/ui repository structure - **GitHub API Integration**: Efficient caching and intelligent rate limit handling +- **Framework Support**: Switch between React (shadcn/ui) and Svelte (shadcn-svelte) implementations ## 📦 Quick Start @@ -33,10 +34,22 @@ npx @jpisnice/shadcn-ui-mcp-server -g ghp_your_token_here # Using environment variable export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_your_token_here npx @jpisnice/shadcn-ui-mcp-server + +# Switch to Svelte framework (default is react) +npx @jpisnice/shadcn-ui-mcp-server --framework svelte + +# Use Svelte with GitHub token +npx @jpisnice/shadcn-ui-mcp-server --framework svelte --github-api-key ghp_your_token_here + +# Using environment variable for framework +export FRAMEWORK=svelte +npx @jpisnice/shadcn-ui-mcp-server ``` **🎯 Try it now**: Run `npx @jpisnice/shadcn-ui-mcp-server --help` to see all options! +**🔄 Framework Selection**: The server supports both React (shadcn/ui) and Svelte (shadcn-svelte) implementations. Use `--framework svelte` to switch to Svelte components. + ### 🔧 Command Line Options ```bash @@ -44,17 +57,22 @@ shadcn-ui-mcp-server [options] Options: --github-api-key, -g GitHub Personal Access Token + --framework, -f Framework to use: 'react' or 'svelte' (default: react) --help, -h Show help message --version, -v Show version information Environment Variables: GITHUB_PERSONAL_ACCESS_TOKEN Alternative way to provide GitHub token + FRAMEWORK Framework to use: 'react' or 'svelte' (default: react) Examples: npx @jpisnice/shadcn-ui-mcp-server --help npx @jpisnice/shadcn-ui-mcp-server --version npx @jpisnice/shadcn-ui-mcp-server -g ghp_1234567890abcdef GITHUB_PERSONAL_ACCESS_TOKEN=ghp_token npx @jpisnice/shadcn-ui-mcp-server + npx @jpisnice/shadcn-ui-mcp-server --framework svelte + npx @jpisnice/shadcn-ui-mcp-server -f react + export FRAMEWORK=svelte && npx @jpisnice/shadcn-ui-mcp-server ``` ## 🔑 GitHub API Token Setup @@ -96,6 +114,78 @@ export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_your_token_here npx @jpisnice/shadcn-ui-mcp-server ``` +## 🔄 Framework Selection + +The MCP server supports both **React** (shadcn/ui) and **Svelte** (shadcn-svelte) implementations. You can switch between them based on your project needs. + +### 📋 Framework Comparison + +| Framework | Repository | File Extension | Description | +|-----------|------------|----------------|-------------| +| **React** (default) | `shadcn-ui/ui` | `.tsx` | React components from shadcn/ui v4 | +| **Svelte** | `huntabyte/shadcn-svelte` | `.svelte` | Svelte components from shadcn-svelte | + +### 🎯 How to Switch Frameworks + +**Method 1: Command Line Argument (Recommended)** +```bash +# Use React (default) +npx @jpisnice/shadcn-ui-mcp-server + +# Switch to Svelte +npx @jpisnice/shadcn-ui-mcp-server --framework svelte +npx @jpisnice/shadcn-ui-mcp-server -f svelte + +# Switch back to React +npx @jpisnice/shadcn-ui-mcp-server --framework react +npx @jpisnice/shadcn-ui-mcp-server -f react +``` + +**Method 2: Environment Variable** +```bash +# Use Svelte +export FRAMEWORK=svelte +npx @jpisnice/shadcn-ui-mcp-server + +# Use React +export FRAMEWORK=react +npx @jpisnice/shadcn-ui-mcp-server + +# Or set for single command +FRAMEWORK=svelte npx @jpisnice/shadcn-ui-mcp-server +``` + +**Method 3: Combined with GitHub Token** +```bash +# Svelte with GitHub token +npx @jpisnice/shadcn-ui-mcp-server --framework svelte --github-api-key ghp_your_token_here + +# React with GitHub token (default) +npx @jpisnice/shadcn-ui-mcp-server --github-api-key ghp_your_token_here +``` + +### 🔍 Framework Detection + +The server will log which framework is being used: +```bash +INFO: Framework set to 'svelte' via command line argument +INFO: MCP Server configured for SVELTE framework +INFO: Repository: huntabyte/shadcn-svelte +INFO: File extension: .svelte +``` + +**⚠️ Important**: When using environment variables, make sure to use the correct syntax: +- ✅ Correct: `export FRAMEWORK=svelte && npx @jpisnice/shadcn-ui-mcp-server` +- ✅ Correct: `FRAMEWORK=svelte npx @jpisnice/shadcn-ui-mcp-server` +- ❌ Incorrect: `FRAMEWORK=svelte npx @jpisnice/shadcn-ui-mcp-server` (without proper spacing) + +### 💡 Use Cases + +- **React Projects**: Use default or `--framework react` for React/Next.js applications +- **Svelte Projects**: Use `--framework svelte` for Svelte/SvelteKit applications +- **Multi-Framework Development**: Switch between frameworks to compare implementations +- **Learning**: Explore both React and Svelte versions of the same components + ## 🛠️ Editor Integration ### VS Code Integration @@ -119,6 +209,11 @@ npx @jpisnice/shadcn-ui-mcp-server "shadcn-ui": { "command": "npx", "args": ["@jpisnice/shadcn-ui-mcp-server", "--github-api-key", "ghp_your_token_here"] + }, + // If using Svelte, do this instead: + "shadcn-ui-svelte": { + "command": "npx", + "args": ["@jpisnice/shadcn-ui-mcp-server", "--framework", "svelte", "--github-api-key", "ghp_your_token_here"] } } } @@ -143,6 +238,14 @@ npx @jpisnice/shadcn-ui-mcp-server "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here" } + }, + // If using Svelte, do this instead: + "shadcn-ui-svelte": { + "command": "npx", + "args": ["@jpisnice/shadcn-ui-mcp-server", "--framework", "svelte"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here" + } } } } @@ -163,6 +266,11 @@ npx @jpisnice/shadcn-ui-mcp-server "shadcn-ui": { "command": "npx", "args": ["@jpisnice/shadcn-ui-mcp-server", "--github-api-key", "ghp_your_token_here"] + }, + // If using Svelte, do this instead: + "shadcn-ui-svelte": { + "command": "npx", + "args": ["@jpisnice/shadcn-ui-mcp-server", "--framework", "svelte", "--github-api-key", "ghp_your_token_here"] } } } @@ -181,6 +289,14 @@ Create a `.cursorrules` file in your project root: "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here" } + }, + // If using Svelte, do this instead: + "shadcn-ui-svelte": { + "command": "npx", + "args": ["@jpisnice/shadcn-ui-mcp-server", "--framework", "svelte"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here" + } } } } @@ -196,6 +312,11 @@ Add to your Claude Desktop configuration (`~/.config/Claude/claude_desktop_confi "shadcn-ui": { "command": "npx", "args": ["@jpisnice/shadcn-ui-mcp-server", "--github-api-key", "ghp_your_token_here"] + }, + // If using Svelte, do this instead: + "shadcn-ui-svelte": { + "command": "npx", + "args": ["@jpisnice/shadcn-ui-mcp-server", "--framework", "svelte", "--github-api-key", "ghp_your_token_here"] } } } @@ -212,6 +333,14 @@ Or with environment variable: "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here" } + }, + // If using Svelte, do this instead: + "shadcn-ui-svelte": { + "command": "npx", + "args": ["@jpisnice/shadcn-ui-mcp-server", "--framework", "svelte"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here" + } } } } @@ -236,6 +365,16 @@ Or with environment variable: } ``` +Or for Svelte: + +```json +{ + "name": "shadcn-ui-svelte", + "command": "npx", + "args": ["@jpisnice/shadcn-ui-mcp-server", "--framework", "svelte", "--github-api-key", "ghp_your_token_here"] +} +``` + ## 🎯 Usage Examples ### Getting Component Source Code @@ -380,7 +519,8 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 🔗 Related Projects -- [shadcn/ui](https://ui.shadcn.com/) - The component library this server provides access to +- [shadcn/ui](https://ui.shadcn.com/) - React component library (default framework) +- [shadcn-svelte](https://www.shadcn-svelte.com/) - Svelte component library (use `--framework svelte`) - [Model Context Protocol](https://modelcontextprotocol.io/) - The protocol specification - [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) - Official MCP SDK diff --git a/src/index.ts b/src/index.ts index d0edd86..f400fa9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { setupHandlers } from './handler.js'; -import { axios } from './utils/axios.js'; +import { validateFrameworkSelection, getAxiosImplementation } from './utils/framework.js'; import { z } from 'zod'; import { toolHandlers, @@ -38,6 +38,7 @@ Usage: Options: --github-api-key, -g GitHub Personal Access Token for API access + --framework, -f Framework to use: 'react' or 'svelte' (default: react) --help, -h Show this help message --version, -v Show version information @@ -45,9 +46,12 @@ Examples: npx shadcn-ui-mcp-server npx shadcn-ui-mcp-server --github-api-key ghp_your_token_here npx shadcn-ui-mcp-server -g ghp_your_token_here + npx shadcn-ui-mcp-server --framework svelte + npx shadcn-ui-mcp-server -f react Environment Variables: GITHUB_PERSONAL_ACCESS_TOKEN Alternative way to provide GitHub token + FRAMEWORK Framework to use: 'react' or 'svelte' (default: react) LOG_LEVEL Log level (debug, info, warn, error) - default: info For more information, visit: https://github.com/Jpisnice/shadcn-ui-mcp-server @@ -98,6 +102,12 @@ async function main() { const { githubApiKey } = await parseArgs(); + // Validate and log framework selection + validateFrameworkSelection(); + + // Get the appropriate axios implementation based on framework + const axios = await getAxiosImplementation(); + // Configure GitHub API key if provided if (githubApiKey) { axios.setGitHubApiKey(githubApiKey); @@ -120,6 +130,16 @@ async function main() { description: "List of available shadcn/ui components that can be used in the project", uri: "resource:get_components", contentType: "text/plain" + }, + "get_install_script_for_component": { + description: "Generate installation script for a specific shadcn/ui component based on package manager", + uriTemplate: "resource-template:get_install_script_for_component?packageManager={packageManager}&component={component}", + contentType: "text/plain" + }, + "get_installation_guide": { + description: "Get the installation guide for shadcn/ui based on build tool and package manager", + uriTemplate: "resource-template:get_installation_guide?buildTool={buildTool}&packageManager={packageManager}", + contentType: "text/plain" } }, prompts: { diff --git a/src/prompts.ts b/src/prompts.ts index 66d3b27..e88d46b 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -5,6 +5,8 @@ * Prompts help to direct the model on how to process user requests. */ +import { getFramework } from './utils/framework.js'; + /** * List of prompts metadata available in this MCP server * Each prompt must have a name, description, and arguments if parameters are needed @@ -119,6 +121,9 @@ export const promptHandlers = { "build-shadcn-page": ({ pageType, features = "", layout = "sidebar", style = "modern" }: { pageType: string, features?: string, layout?: string, style?: string }) => { + const framework = getFramework(); + const isSvelte = framework === 'svelte'; + return { messages: [ { @@ -142,7 +147,7 @@ INSTRUCTIONS: - Use shadcn/ui v4 components and blocks as building blocks - Ensure responsive design with Tailwind CSS classes - Implement proper TypeScript types - - Follow React best practices and hooks patterns + - Follow ${isSvelte ? 'Svelte' : 'React'} best practices and ${isSvelte ? 'runes' : 'hooks'} patterns - Include proper accessibility attributes 3. For ${pageType} pages specifically: @@ -152,7 +157,7 @@ INSTRUCTIONS: - Create a main page component - Use sub-components for complex sections - Include proper imports from shadcn/ui registry - - Add necessary state management with React hooks + - Add necessary state management with ${isSvelte ? 'Svelte runes' : 'React hooks'} - Include proper error handling 5. Styling Guidelines: @@ -171,6 +176,9 @@ Please provide complete, production-ready code with proper imports and TypeScrip "create-dashboard": ({ dashboardType, widgets = "charts,tables,cards", navigation = "sidebar" }: { dashboardType: string, widgets?: string, navigation?: string }) => { + const framework = getFramework(); + const isSvelte = framework === 'svelte'; + return { messages: [ { @@ -208,7 +216,7 @@ INSTRUCTIONS: 5. Data Management: - Create mock data structures for ${dashboardType} - - Implement state management with React hooks + - Implement state management with ${isSvelte ? 'Svelte runes' : 'React hooks'} - Add loading states and error handling - Include data refresh functionality @@ -292,6 +300,9 @@ Provide complete authentication flow code with proper TypeScript types, validati "optimize-shadcn-component": ({ component, optimization = "performance", useCase = "general" }: { component: string, optimization?: string, useCase?: string }) => { + const framework = getFramework(); + const isSvelte = framework === 'svelte'; + return { messages: [ { @@ -312,7 +323,7 @@ INSTRUCTIONS: - Use 'get_component_metadata' to understand dependencies 2. Optimization Strategy for ${optimization}: - ${getOptimizationInstructions(optimization)} + ${getOptimizationInstructions(optimization, isSvelte)} 3. Use Case Specific Enhancements for ${useCase}: - Analyze how ${component} is typically used in ${useCase} scenarios @@ -326,8 +337,8 @@ INSTRUCTIONS: - Include usage examples demonstrating improvements 5. Best Practices: - - Follow React performance best practices - - Implement proper memoization where needed + - Follow ${isSvelte ? 'Svelte' : 'React'} performance best practices + - Implement ${isSvelte ? 'Svelte reactivity patterns' : 'proper memoization'} where needed - Ensure backward compatibility - Add comprehensive prop validation @@ -468,9 +479,16 @@ function getPageTypeSpecificInstructions(pageType: string): string { /** * Helper function to get optimization specific instructions */ -function getOptimizationInstructions(optimization: string): string { +function getOptimizationInstructions(optimization: string, isSvelte: boolean): string { const instructions = { - performance: ` + performance: isSvelte ? ` + - Use Svelte's built-in reactivity with runes for fine-grained updates + - Minimize the use of reactive statements that cause unnecessary updates + - Use derived state with $derived + - Consider using $effect only when necessary + - Implement lazy loading for heavy components + - Use the $state.raw for non-reactive data to avoid unnecessary reactivity overhead` + : ` - Implement React.memo for preventing unnecessary re-renders - Use useMemo and useCallback hooks appropriately - Optimize bundle size by code splitting @@ -503,6 +521,6 @@ function getOptimizationInstructions(optimization: string): string { - Ensure animations respect reduced motion preferences` }; - return instructions[optimization as keyof typeof instructions] || + return instructions[optimization as keyof typeof instructions] || 'Focus on general code quality improvements and best practices implementation.'; } \ No newline at end of file diff --git a/src/resource-templates.ts b/src/resource-templates.ts index 1a806f8..eeeb705 100644 --- a/src/resource-templates.ts +++ b/src/resource-templates.ts @@ -5,6 +5,8 @@ * resources based on parameters in the URI. */ +import { getFramework } from './utils/framework.js'; + /** * Resource template definitions exported to the MCP handler * Each template has a name, description, uriTemplate and contentType @@ -18,8 +20,8 @@ export const resourceTemplates = [ }, { name: 'get_installation_guide', - description: 'Get the installation guide for shadcn/ui based on framework and package manager', - uriTemplate: 'resource-template:get_installation_guide?framework={framework}&packageManager={packageManager}', + description: 'Get the installation guide for shadcn/ui based on build tool and package manager', + uriTemplate: 'resource-template:get_installation_guide?buildTool={buildTool}&packageManager={packageManager}', contentType: 'text/plain', }, ]; @@ -68,24 +70,28 @@ export const getResourceTemplate = (uri: string) => { }; } + // Get current framework and determine package name + const framework = getFramework(); + const packageName = framework === 'svelte' ? 'shadcn-svelte' : 'shadcn'; + // Generate installation script based on package manager let installCommand: string; switch (packageManager.toLowerCase()) { case 'npm': - installCommand = `npx shadcn@latest add ${component}`; + installCommand = `npx ${packageName}@latest add ${component} --yes --overwrite`; break; case 'pnpm': - installCommand = `pnpm dlx shadcn@latest add ${component}`; + installCommand = `pnpm dlx ${packageName}@latest add ${component} --yes --overwrite`; break; case 'yarn': - installCommand = `yarn dlx shadcn@latest add ${component}`; + installCommand = `yarn dlx ${packageName}@latest add ${component} --yes --overwrite`; break; case 'bun': - installCommand = `bunx --bun shadcn@latest add ${component}`; + installCommand = `bunx --bun ${packageName}@latest add ${component} --yes --overwrite`; break; default: - installCommand = `npx shadcn@latest add ${component}`; + installCommand = `npx ${packageName}@latest add ${component} --yes --overwrite`; } return { @@ -105,15 +111,28 @@ export const getResourceTemplate = (uri: string) => { if (uri.startsWith('resource-template:get_installation_guide')) { return async () => { try { - const framework = extractParam(uri, 'framework'); + const buildTool = extractParam(uri, 'buildTool'); const packageManager = extractParam(uri, 'packageManager'); - if (!framework) { + // Get current framework first since it's used in validation + const currentFramework = getFramework(); + + if (!buildTool) { return { - content: 'Missing framework parameter. Please specify next, vite, remix, etc.', + content: currentFramework === 'svelte' + ? 'Missing buildTool parameter. Available option: vite' + : 'Missing buildTool parameter. Please specify next, vite, remix, etc.', contentType: 'text/plain' }; } + + // Validate build tool for Svelte + if (currentFramework === 'svelte' && buildTool.toLowerCase() !== 'vite') { + return { + content: 'Invalid build tool for Svelte. Only "vite" is supported.', + contentType: 'text/plain' + }; + } if (!packageManager) { return { @@ -122,124 +141,204 @@ export const getResourceTemplate = (uri: string) => { }; } - // Generate installation guide based on framework and package manager - const guides = { - next: { - description: "Installation guide for Next.js project", - steps: [ - "Create a Next.js project if you don't have one already:", - `${packageManager} create next-app my-app`, - "", - "Navigate to your project directory:", - "cd my-app", - "", - "Add shadcn/ui to your project:", - packageManager === 'npm' ? 'npx shadcn-ui@latest init' : - packageManager === 'pnpm' ? 'pnpm dlx shadcn-ui@latest init' : - packageManager === 'yarn' ? 'yarn dlx shadcn-ui@latest init' : - packageManager === 'bun' ? 'bunx --bun shadcn-ui@latest init' : 'npx shadcn-ui@latest init', - "", - "Follow the prompts to select your preferences", - "", - "Once initialized, you can add components:", - packageManager === 'npm' ? 'npx shadcn-ui@latest add button' : - packageManager === 'pnpm' ? 'pnpm dlx shadcn-ui@latest add button' : - packageManager === 'yarn' ? 'yarn dlx shadcn-ui@latest add button' : - packageManager === 'bun' ? 'bunx --bun shadcn-ui@latest add button' : 'npx shadcn-ui@latest add button', - "", - "Now you can use the component in your project!" - ] - }, - vite: { - description: "Installation guide for Vite project", - steps: [ - "Create a Vite project if you don't have one already:", - `${packageManager}${packageManager === 'npm' ? ' create' : ''} vite my-app -- --template react-ts`, - "", - "Navigate to your project directory:", - "cd my-app", - "", - "Install dependencies:", - `${packageManager} ${packageManager === 'npm' ? 'install' : 'add'} -D tailwindcss postcss autoprefixer`, - "", - "Initialize Tailwind CSS:", - "npx tailwindcss init -p", - "", - "Add shadcn/ui to your project:", - packageManager === 'npm' ? 'npx shadcn-ui@latest init' : - packageManager === 'pnpm' ? 'pnpm dlx shadcn-ui@latest init' : - packageManager === 'yarn' ? 'yarn dlx shadcn-ui@latest init' : - packageManager === 'bun' ? 'bunx --bun shadcn-ui@latest init' : 'npx shadcn-ui@latest init', - "", - "Follow the prompts to select your preferences", - "", - "Once initialized, you can add components:", - packageManager === 'npm' ? 'npx shadcn-ui@latest add button' : - packageManager === 'pnpm' ? 'pnpm dlx shadcn-ui@latest add button' : - packageManager === 'yarn' ? 'yarn dlx shadcn-ui@latest add button' : - packageManager === 'bun' ? 'bunx --bun shadcn-ui@latest add button' : 'npx shadcn-ui@latest add button', - "", - "Now you can use the component in your project!" - ] - }, - remix: { - description: "Installation guide for Remix project", - steps: [ - "Create a Remix project if you don't have one already:", - `${packageManager === 'npm' ? 'npx' : packageManager === 'pnpm' ? 'pnpm dlx' : packageManager === 'yarn' ? 'yarn dlx' : 'bunx'} create-remix my-app`, - "", - "Navigate to your project directory:", - "cd my-app", - "", - "Install dependencies:", - `${packageManager} ${packageManager === 'npm' ? 'install' : 'add'} -D tailwindcss postcss autoprefixer`, - "", - "Initialize Tailwind CSS:", - "npx tailwindcss init -p", - "", - "Add shadcn/ui to your project:", - packageManager === 'npm' ? 'npx shadcn-ui@latest init' : - packageManager === 'pnpm' ? 'pnpm dlx shadcn-ui@latest init' : - packageManager === 'yarn' ? 'yarn dlx shadcn-ui@latest init' : - packageManager === 'bun' ? 'bunx --bun shadcn-ui@latest init' : 'npx shadcn-ui@latest init', - "", - "Follow the prompts to select your preferences", - "", - "Once initialized, you can add components:", - packageManager === 'npm' ? 'npx shadcn-ui@latest add button' : - packageManager === 'pnpm' ? 'pnpm dlx shadcn-ui@latest add button' : - packageManager === 'yarn' ? 'yarn dlx shadcn-ui@latest add button' : - packageManager === 'bun' ? 'bunx --bun shadcn-ui@latest add button' : 'npx shadcn-ui@latest add button', - "", - "Now you can use the component in your project!" - ] - }, - default: { - description: "Generic installation guide", - steps: [ - "Make sure you have a React project set up", - "", - "Add shadcn/ui to your project:", - packageManager === 'npm' ? 'npx shadcn-ui@latest init' : - packageManager === 'pnpm' ? 'pnpm dlx shadcn-ui@latest init' : - packageManager === 'yarn' ? 'yarn dlx shadcn-ui@latest init' : - packageManager === 'bun' ? 'bunx --bun shadcn-ui@latest init' : 'npx shadcn-ui@latest init', - "", - "Follow the prompts to select your preferences", - "", - "Once initialized, you can add components:", - packageManager === 'npm' ? 'npx shadcn-ui@latest add button' : - packageManager === 'pnpm' ? 'pnpm dlx shadcn-ui@latest add button' : - packageManager === 'yarn' ? 'yarn dlx shadcn-ui@latest add button' : - packageManager === 'bun' ? 'bunx --bun shadcn-ui@latest add button' : 'npx shadcn-ui@latest add button', - "", - "Now you can use the component in your project!" - ] - } - }; + // Determine package name + const packageName = currentFramework === 'svelte' ? 'shadcn-svelte' : 'shadcn-ui'; - // Select appropriate guide based on framework - const guide = guides[framework.toLowerCase() as keyof typeof guides] || guides.default; + // Generate installation guide based on build tool and package manager + const guides = currentFramework === 'svelte' + ? { + vite: { + description: "Installation guide for Svelte Vite project", + steps: [ + "Create a Vite project if you don't have one already:", + `${packageManager}${packageManager === 'npm' ? ' create' : ''} vite my-app -- --template svelte-ts`, + "", + "Navigate to your project directory:", + "cd my-app", + "", + "Install dependencies:", + packageManager === 'npm' ? `npm i && npm install -D tailwindcss @tailwindcss/vite` : + packageManager === 'pnpm' ? `pnpm i && pnpm install -D tailwindcss @tailwindcss/vite` : + packageManager === 'yarn' ? `yarn add tailwindcss @tailwindcss/vite` : + packageManager === 'bun' ? `bunx --bun install tailwindcss @tailwindcss/vite` : `npm install tailwindcss @tailwindcss/vite`, + "", + "The current version of Vite splits TypeScript configuration into three files, two of which need to be edited.", + "Add the baseUrl and paths properties to the compilerOptions section of the tsconfig.json and tsconfig.app.json files", + "\"compilerOptions\": { \"baseUrl\": \".\", \"paths\": { \"$lib\": [\"./src/lib\"], \"$lib/*\": [\"./src/lib/*\"] } }", + "", + "Add the following code to the tsconfig.app.json file to resolve paths, for your IDE:", + "\"baseUrl\": \".\", \"paths\": { \"$lib\": [\"./src/lib\"], \"$lib/*\": [\"./src/lib/*\"] }", + "", + "Add the following code to the vite.config.ts so your app can resolve paths without error", + "resolve: { alias: { $lib: path.resolve(\"./src/lib\"), }, },", + "Make sure, the following code is added to the vite.config.ts file:", + "import path from \"path\";", + "", + "Add the @tailwindcss/vite plugin to your Vite configuration (vite.config.ts).", + "import tailwindcss from '@tailwindcss/vite'", + "Make sure the following code is updated to the vite.config.ts file:", + "export default defineConfig({ plugins: [ tailwindcss(), ], })", + "", + "Add the following code to the app.css file:", + "@import \"tailwindcss\";", + "", + "Add shadcn/ui to your project (non-interactive):", + packageManager === 'npm' ? `npx ${packageName}@latest init --overwrite --base-color slate --css src/app.css --components-alias $lib/components --lib-alias $lib/ --utils-alias $lib/utils --hooks-alias $lib/hooks --ui-alias $lib/ui` : + packageManager === 'pnpm' ? `pnpm dlx ${packageName}@latest init --overwrite --base-color slate --css src/app.css --components-alias $lib/components --lib-alias $lib/ --utils-alias $lib/utils --hooks-alias $lib/hooks --ui-alias $lib/ui` : + packageManager === 'yarn' ? `yarn dlx ${packageName}@latest init --overwrite --base-color slate --css src/app.css --components-alias $lib/components --lib-alias $lib/ --utils-alias $lib/utils --hooks-alias $lib/hooks --ui-alias $lib/ui` : + packageManager === 'bun' ? `bunx --bun ${packageName}@latest init --overwrite --base-color slate --css src/app.css --components-alias $lib/components --lib-alias $lib/ --utils-alias $lib/utils --hooks-alias $lib/hooks --ui-alias $lib/ui` : `npx ${packageName}@latest init --overwrite --base-color slate --css src/app.css --components-alias $lib/components --lib-alias $lib --utils-alias $lib/utils --hooks-alias $lib/hooks --ui-alias $lib/ui`, + "", + "The command will automatically configure your project with sensible defaults.", + "", + "Once initialized, you can add components:", + packageManager === 'npm' ? `npx ${packageName}@latest add button --yes --overwrite` : + packageManager === 'pnpm' ? `pnpm dlx ${packageName}@latest add button --yes --overwrite` : + packageManager === 'yarn' ? `yarn dlx ${packageName}@latest add button --yes --overwrite` : + packageManager === 'bun' ? `bunx --bun ${packageName}@latest add button --yes --overwrite` : `npx ${packageName}@latest add button --yes --overwrite`, + "", + "Now you can use the component in your project!" + ] + }, + default: { + description: "Generic installation guide for Svelte", + steps: [ + "Make sure you have a Svelte project set up", + "", + "Add shadcn/ui to your project (non-interactive):", + packageManager === 'npm' ? `npx ${packageName}@latest init --overwrite --base-color slate --css src/app.css --components-alias $lib/components --lib-alias $lib/ --utils-alias $lib/utils --hooks-alias $lib/hooks --ui-alias $lib/ui` : + packageManager === 'pnpm' ? `pnpm dlx ${packageName}@latest init --overwrite --base-color slate --css src/app.css --components-alias $lib/components --lib-alias $lib/ --utils-alias $lib/utils --hooks-alias $lib/hooks --ui-alias $lib/ui` : + packageManager === 'yarn' ? `yarn dlx ${packageName}@latest init --overwrite --base-color slate --css src/app.css --components-alias $lib/components --lib-alias $lib/ --utils-alias $lib/utils --hooks-alias $lib/hooks --ui-alias $lib/ui` : + packageManager === 'bun' ? `bunx --bun ${packageName}@latest init --overwrite --base-color slate --css src/app.css --components-alias $lib/components --lib-alias $lib/ --utils-alias $lib/utils --hooks-alias $lib/hooks --ui-alias $lib/ui` : `npx ${packageName}@latest init --overwrite --base-color slate --css src/app.css --components-alias $lib/components --lib-alias $lib/ --utils-alias $lib/utils --hooks-alias $lib/hooks --ui-alias $lib/ui`, + "", + "The command will automatically configure your project with sensible defaults.", + "", + "Once initialized, you can add components (non-interactive):", + packageManager === 'npm' ? `npx ${packageName}@latest add button --yes --overwrite` : + packageManager === 'pnpm' ? `pnpm dlx ${packageName}@latest add button --yes --overwrite` : + packageManager === 'yarn' ? `yarn dlx ${packageName}@latest add button --yes --overwrite` : + packageManager === 'bun' ? `bunx --bun ${packageName}@latest add button --yes --overwrite` : `npx ${packageName}@latest add button --yes --overwrite`, + "", + "Now you can use the component in your project!" + ] + } + } + : { + next: { + description: "Installation guide for Next.js project", + steps: [ + "Create a Next.js project if you don't have one already:", + `${packageManager} create next-app my-app`, + "", + "Navigate to your project directory:", + "cd my-app", + "", + "Add shadcn/ui to your project:", + packageManager === 'npm' ? `npx ${packageName}@latest init` : + packageManager === 'pnpm' ? `pnpm dlx ${packageName}@latest init` : + packageManager === 'yarn' ? `yarn dlx ${packageName}@latest init` : + packageManager === 'bun' ? `bunx --bun ${packageName}@latest init` : `npx ${packageName}@latest init`, + "", + "Follow the prompts to select your preferences", + "", + "Once initialized, you can add components:", + packageManager === 'npm' ? `npx ${packageName}@latest add button` : + packageManager === 'pnpm' ? `pnpm dlx ${packageName}@latest add button` : + packageManager === 'yarn' ? `yarn dlx ${packageName}@latest add button` : + packageManager === 'bun' ? `bunx --bun ${packageName}@latest add button` : `npx ${packageName}@latest add button`, + "", + "Now you can use the component in your project!" + ] + }, + vite: { + description: "Installation guide for Vite project", + steps: [ + "Create a Vite project if you don't have one already:", + `${packageManager}${packageManager === 'npm' ? ' create' : ''} vite my-app -- --template react-ts`, + "", + "Navigate to your project directory:", + "cd my-app", + "", + "Install dependencies:", + `${packageManager} ${packageManager === 'npm' ? 'install' : 'add'} -D tailwindcss postcss autoprefixer`, + "", + "Initialize Tailwind CSS:", + "npx tailwindcss init -p", + "", + "Add shadcn/ui to your project:", + packageManager === 'npm' ? `npx ${packageName}@latest init` : + packageManager === 'pnpm' ? `pnpm dlx ${packageName}@latest init` : + packageManager === 'yarn' ? `yarn dlx ${packageName}@latest init` : + packageManager === 'bun' ? `bunx --bun ${packageName}@latest init` : `npx ${packageName}@latest init`, + "", + "Follow the prompts to select your preferences", + "", + "Once initialized, you can add components:", + packageManager === 'npm' ? `npx ${packageName}@latest add button` : + packageManager === 'pnpm' ? `pnpm dlx ${packageName}@latest add button` : + packageManager === 'yarn' ? `yarn dlx ${packageName}@latest add button` : + packageManager === 'bun' ? `bunx --bun ${packageName}@latest add button` : `npx ${packageName}@latest add button`, + "", + "Now you can use the component in your project!" + ] + }, + remix: { + description: "Installation guide for Remix project", + steps: [ + "Create a Remix project if you don't have one already:", + `${packageManager === 'npm' ? 'npx' : packageManager === 'pnpm' ? 'pnpm dlx' : packageManager === 'yarn' ? 'yarn dlx' : 'bunx'} create-remix my-app`, + "", + "Navigate to your project directory:", + "cd my-app", + "", + "Install dependencies:", + `${packageManager} ${packageManager === 'npm' ? 'install' : 'add'} -D tailwindcss postcss autoprefixer`, + "", + "Initialize Tailwind CSS:", + "npx tailwindcss init -p", + "", + "Add shadcn/ui to your project:", + packageManager === 'npm' ? `npx ${packageName}@latest init` : + packageManager === 'pnpm' ? `pnpm dlx ${packageName}@latest init` : + packageManager === 'yarn' ? `yarn dlx ${packageName}@latest init` : + packageManager === 'bun' ? `bunx --bun ${packageName}@latest init` : `npx ${packageName}@latest init`, + "", + "Follow the prompts to select your preferences", + "", + "Once initialized, you can add components:", + packageManager === 'npm' ? `npx ${packageName}@latest add button` : + packageManager === 'pnpm' ? `pnpm dlx ${packageName}@latest add button` : + packageManager === 'yarn' ? `yarn dlx ${packageName}@latest add button` : + packageManager === 'bun' ? `bunx --bun ${packageName}@latest add button` : `npx ${packageName}@latest add button`, + "", + "Now you can use the component in your project!" + ] + }, + default: { + description: "Generic installation guide for React", + steps: [ + "Make sure you have a React project set up", + "", + "Add shadcn/ui to your project:", + packageManager === 'npm' ? `npx ${packageName}@latest init` : + packageManager === 'pnpm' ? `pnpm dlx ${packageName}@latest init` : + packageManager === 'yarn' ? `yarn dlx ${packageName}@latest init` : + packageManager === 'bun' ? `bunx --bun ${packageName}@latest init` : `npx ${packageName}@latest init`, + "", + "Follow the prompts to select your preferences", + "", + "Once initialized, you can add components:", + packageManager === 'npm' ? `npx ${packageName}@latest add button` : + packageManager === 'pnpm' ? `pnpm dlx ${packageName}@latest add button` : + packageManager === 'yarn' ? `yarn dlx ${packageName}@latest add button` : + packageManager === 'bun' ? `bunx --bun ${packageName}@latest add button` : `npx ${packageName}@latest add button`, + "", + "Now you can use the component in your project!" + ] + } + }; + + // Select appropriate guide based on build tool + const guide = guides[buildTool.toLowerCase() as keyof typeof guides] || guides.default; return { content: `# ${guide.description} with ${packageManager}\n\n${guide.steps.join('\n')}`, @@ -255,4 +354,4 @@ export const getResourceTemplate = (uri: string) => { } return undefined; -}; \ No newline at end of file +}; diff --git a/src/tools/blocks/get-block.ts b/src/tools/blocks/get-block.ts index 48d3777..36c1863 100644 --- a/src/tools/blocks/get-block.ts +++ b/src/tools/blocks/get-block.ts @@ -1,4 +1,4 @@ -import { axios } from '../../utils/axios.js'; +import { getAxiosImplementation } from '../../utils/framework.js'; import { logError } from '../../utils/logger.js'; export async function handleGetBlock({ @@ -9,6 +9,7 @@ export async function handleGetBlock({ includeComponents?: boolean }) { try { + const axios = await getAxiosImplementation(); const blockData = await axios.getBlockCode(blockName, includeComponents); return { content: [{ type: "text", text: JSON.stringify(blockData, null, 2) }] diff --git a/src/tools/blocks/list-blocks.ts b/src/tools/blocks/list-blocks.ts index 7416bde..13b5d22 100644 --- a/src/tools/blocks/list-blocks.ts +++ b/src/tools/blocks/list-blocks.ts @@ -1,8 +1,9 @@ -import { axios } from '../../utils/axios.js'; +import { getAxiosImplementation } from '../../utils/framework.js'; import { logError } from '../../utils/logger.js'; export async function handleListBlocks({ category }: { category?: string }) { try { + const axios = await getAxiosImplementation(); const blocks = await axios.getAvailableBlocks(category); return { content: [{ diff --git a/src/tools/components/get-component-demo.ts b/src/tools/components/get-component-demo.ts index 3e4bb7e..33858f3 100644 --- a/src/tools/components/get-component-demo.ts +++ b/src/tools/components/get-component-demo.ts @@ -1,8 +1,9 @@ -import { axios } from '../../utils/axios.js'; +import { getAxiosImplementation } from '../../utils/framework.js'; import { logError } from '../../utils/logger.js'; export async function handleGetComponentDemo({ componentName }: { componentName: string }) { try { + const axios = await getAxiosImplementation(); const demoCode = await axios.getComponentDemo(componentName); return { content: [{ type: "text", text: demoCode }] diff --git a/src/tools/components/get-component-metadata.ts b/src/tools/components/get-component-metadata.ts index 670cfdb..2f0eacc 100644 --- a/src/tools/components/get-component-metadata.ts +++ b/src/tools/components/get-component-metadata.ts @@ -1,8 +1,9 @@ -import { axios } from '../../utils/axios.js'; +import { getAxiosImplementation } from '../../utils/framework.js'; import { logError } from '../../utils/logger.js'; export async function handleGetComponentMetadata({ componentName }: { componentName: string }) { try { + const axios = await getAxiosImplementation(); const metadata = await axios.getComponentMetadata(componentName); if (!metadata) { throw new Error(`Component metadata not found: ${componentName}`); diff --git a/src/tools/components/get-component.ts b/src/tools/components/get-component.ts index 6df2ea9..5f15bf2 100644 --- a/src/tools/components/get-component.ts +++ b/src/tools/components/get-component.ts @@ -1,8 +1,9 @@ -import { axios } from '../../utils/axios.js'; +import { getAxiosImplementation } from '../../utils/framework.js'; import { logError } from '../../utils/logger.js'; export async function handleGetComponent({ componentName }: { componentName: string }) { try { + const axios = await getAxiosImplementation(); const sourceCode = await axios.getComponentSource(componentName); return { content: [{ type: "text", text: sourceCode }] diff --git a/src/tools/components/list-components.ts b/src/tools/components/list-components.ts index ba91b26..f05dfe5 100644 --- a/src/tools/components/list-components.ts +++ b/src/tools/components/list-components.ts @@ -1,8 +1,9 @@ -import { axios } from '../../utils/axios.js'; +import { getAxiosImplementation } from '../../utils/framework.js'; import { logError } from '../../utils/logger.js'; export async function handleListComponents() { try { + const axios = await getAxiosImplementation(); const components = await axios.getAvailableComponents(); return { content: [{ diff --git a/src/tools/repository/get-directory-structure.ts b/src/tools/repository/get-directory-structure.ts index 8800d4e..e7d9dac 100644 --- a/src/tools/repository/get-directory-structure.ts +++ b/src/tools/repository/get-directory-structure.ts @@ -1,4 +1,4 @@ -import { axios } from '../../utils/axios.js'; +import { getAxiosImplementation } from '../../utils/framework.js'; import { logError } from '../../utils/logger.js'; export async function handleGetDirectoryStructure({ @@ -13,10 +13,14 @@ export async function handleGetDirectoryStructure({ branch?: string }) { try { + const axios = await getAxiosImplementation(); + // Get the default path based on available properties + const defaultPath = 'BLOCKS' in axios.paths ? axios.paths.BLOCKS : axios.paths.NEW_YORK_V4_PATH; + const directoryTree = await axios.buildDirectoryTree( owner || axios.paths.REPO_OWNER, repo || axios.paths.REPO_NAME, - path || axios.paths.NEW_YORK_V4_PATH, + path || defaultPath, branch || axios.paths.REPO_BRANCH ); return { diff --git a/src/utils/axios-svelte.ts b/src/utils/axios-svelte.ts new file mode 100644 index 0000000..578c152 --- /dev/null +++ b/src/utils/axios-svelte.ts @@ -0,0 +1,851 @@ +import { Axios } from "axios"; +import { logError, logWarning, logInfo } from './logger.js'; + +// Constants for the v4 repository structure +const REPO_OWNER = 'huntabyte'; +const REPO_NAME = 'shadcn-svelte'; +const REPO_BRANCH = 'main'; +const REGISTRY_PATH = `docs/src/lib/registry`; +const BLOCKS = `${REGISTRY_PATH}/blocks`; + +// GitHub API for accessing repository structure and metadata +const githubApi = new Axios({ + baseURL: "https://api.github.com", + headers: { + "Content-Type": "application/json", + "Accept": "application/vnd.github+json", + "User-Agent": "Mozilla/5.0 (compatible; ShadcnUiMcpServer/1.0.0)", + ...(process.env.GITHUB_PERSONAL_ACCESS_TOKEN && { + "Authorization": `Bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}` + }) + }, + timeout: 30000, // Increased from 15000 to 30000 (30 seconds) + transformResponse: [(data) => { + try { + return JSON.parse(data); + } catch { + return data; + } + }], +}); + +// GitHub Raw for directly fetching file contents +const githubRaw = new Axios({ + baseURL: `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${REPO_BRANCH}`, + headers: { + "User-Agent": "Mozilla/5.0 (compatible; ShadcnUiMcpServer/1.0.0)", + }, + timeout: 30000, // Increased from 15000 to 30000 (30 seconds) + transformResponse: [(data) => data], // Return raw data +}); + +/** + * Fetch component source code from the v4 registry + * @param componentName Name of the component + * @returns Promise with component source code + */ +async function getComponentSource(componentName: string): Promise { + const componentPath = `${REGISTRY_PATH}/ui/${componentName.toLowerCase()}/${componentName.toLowerCase()}.svelte`; + + try { + const response = await githubRaw.get(`/${componentPath}`); + return response.data; + } catch (error) { + throw new Error(`Component "${componentName}" not found in v4 registry`); + } +} + +/** + * Fetch component demo/example from the v4 registry + * @param componentName Name of the component + * @returns Promise with component demo code + */ +async function getComponentDemo(componentName: string): Promise { + const demoPath = `${REGISTRY_PATH}/examples/${componentName.toLowerCase()}-demo.svelte`; + + try { + const response = await githubRaw.get(`/${demoPath}`); + return response.data; + } catch (error) { + throw new Error(`Demo for component "${componentName}" not found in v4 registry`); + } +} + +/** + * Fetch all available components from the registry + * @returns Promise with list of component names + */ +async function getAvailableComponents(): Promise { + try { + // First try the GitHub API + const response = await githubApi.get(`/repos/${REPO_OWNER}/${REPO_NAME}/contents/${REGISTRY_PATH}/ui`); + + if (!response.data || !Array.isArray(response.data)) { + throw new Error('Invalid response from GitHub API'); + } + + const components = response.data.map((item: any) => item.name); + + if (components.length === 0) { + throw new Error('No components found in the registry'); + } + + return components; + } catch (error: any) { + logError('Error fetching components from GitHub API', error); + + // Check for specific error types + if (error.response) { + const status = error.response.status; + const message = error.response.data?.message || 'Unknown error'; + + if (status === 403 && message.includes('rate limit')) { + throw new Error(`GitHub API rate limit exceeded. Please set GITHUB_PERSONAL_ACCESS_TOKEN environment variable for higher limits. Error: ${message}`); + } else if (status === 404) { + throw new Error(`Components directory not found. The path ${BLOCKS}/ui may not exist in the repository.`); + } else if (status === 401) { + throw new Error(`Authentication failed. Please check your GITHUB_PERSONAL_ACCESS_TOKEN if provided.`); + } else { + throw new Error(`GitHub API error (${status}): ${message}`); + } + } + + // If it's a network error or other issue, provide a fallback + if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT') { + throw new Error(`Network error: ${error.message}. Please check your internet connection.`); + } + + // If all else fails, provide a fallback list of known components + logWarning('Using fallback component list due to API issues'); + return getFallbackComponents(); + } +} + +/** + * Fallback list of known shadcn/ui v4 components + * This is used when the GitHub API is unavailable + */ +function getFallbackComponents(): string[] { + return [ + 'accordion', + 'alert', + 'alert-dialog', + 'aspect-ratio', + 'avatar', + 'badge', + 'breadcrumb', + 'button', + 'calendar', + 'card', + 'carousel', + 'chart', + 'checkbox', + 'collapsible', + 'command', + 'context-menu', + 'dialog', + 'drawer', + 'dropdown-menu', + 'form', + 'hover-card', + 'input', + 'input-otp', + 'label', + 'menubar', + 'navigation-menu', + 'pagination', + 'popover', + 'progress', + 'radio-group', + 'resizable', + 'scroll-area', + 'select', + 'separator', + 'sheet', + 'sidebar', + 'skeleton', + 'slider', + 'sonner', + 'switch', + 'table', + 'tabs', + 'textarea', + 'toggle', + 'toggle-group', + 'tooltip' + ]; +} + +/** + * Fetch component metadata from the registry + * @param componentName Name of the component + * @returns Promise with component metadata + */ +async function getComponentMetadata(componentName: string): Promise { + try { + const response = await githubRaw.get(`/docs/registry.json`); + const registryContent = JSON.parse(response.data); + + const metadata = registryContent.items.map((item: any) => { + return { + name: item.name, + type: item.type, + dependencies: item.dependencies, + registryDependencies: item.registryDependencies + } + }); + const component = metadata.find((item: any) => item.name === componentName.toLowerCase()); + + if (!component) { + throw new Error(`Component "${componentName}" not found in registry`); + } + + return { + name: component.name, + type: component.type, + dependencies: component.dependencies, + registryDependencies: component.registryDependencies + }; + } catch (error) { + logError(`Error getting metadata for ${componentName}`, error); + return null; + } +} + +/** + * Recursively builds a directory tree structure from a GitHub repository + * @param owner Repository owner + * @param repo Repository name + * @param path Path within the repository to start building the tree from + * @param branch Branch name + * @returns Promise resolving to the directory tree structure + */ +async function buildDirectoryTree( + owner: string = REPO_OWNER, + repo: string = REPO_NAME, + path: string = BLOCKS, + branch: string = REPO_BRANCH +): Promise { + try { + const response = await githubApi.get(`/repos/${owner}/${repo}/contents/${path}?ref=${branch}`); + + if (!response.data) { + throw new Error('No data received from GitHub API'); + } + + const contents = response.data; + + // Handle different response types from GitHub API + if (!Array.isArray(contents)) { + // Check if it's an error response (like rate limit) + if (contents.message) { + const message: string = contents.message; + if (message.includes('rate limit exceeded')) { + throw new Error(`GitHub API rate limit exceeded. ${message} Consider setting GITHUB_PERSONAL_ACCESS_TOKEN environment variable for higher rate limits.`); + } else if (message.includes('Not Found')) { + throw new Error(`Path not found: ${path}. The path may not exist in the repository.`); + } else { + throw new Error(`GitHub API error: ${message}`); + } + } + + // If contents is not an array, it might be a single file + if (contents.type === 'file') { + return { + path: contents.path, + type: 'file', + name: contents.name, + url: contents.download_url, + sha: contents.sha, + }; + } else { + throw new Error(`Unexpected response type from GitHub API: ${JSON.stringify(contents)}`); + } + } + + // Build tree node for this level (directory with multiple items) + const result: Record = { + path, + type: 'directory', + children: {}, + }; + + // Process each item + for (const item of contents) { + if (item.type === 'file') { + // Add file to this directory's children + result.children[item.name] = { + path: item.path, + type: 'file', + name: item.name, + url: item.download_url, + sha: item.sha, + }; + } else if (item.type === 'dir') { + // Recursively process subdirectory (limit depth to avoid infinite recursion) + if (path.split('/').length < 8) { + try { + const subTree = await buildDirectoryTree(owner, repo, item.path, branch); + result.children[item.name] = subTree; + } catch (error) { + logWarning(`Failed to fetch subdirectory ${item.path}: ${error instanceof Error ? error.message : String(error)}`); + result.children[item.name] = { + path: item.path, + type: 'directory', + error: 'Failed to fetch contents' + }; + } + } + } + } + + return result; + } catch (error: any) { + logError(`Error building directory tree for ${path}`, error); + + // Check if it's already a well-formatted error from above + if (error.message && (error.message.includes('rate limit') || error.message.includes('GitHub API error'))) { + throw error; + } + + // Provide more specific error messages for HTTP errors + if (error.response) { + const status: number = error.response.status; + const responseData: any = error.response.data; + const message: string = responseData?.message || 'Unknown error'; + + if (status === 404) { + throw new Error(`Path not found: ${path}. The path may not exist in the repository.`); + } else if (status === 403) { + if (message.includes('rate limit')) { + throw new Error(`GitHub API rate limit exceeded: ${message} Consider setting GITHUB_PERSONAL_ACCESS_TOKEN environment variable for higher rate limits.`); + } else { + throw new Error(`Access forbidden: ${message}`); + } + } else if (status === 401) { + throw new Error(`Authentication failed. Please check your GITHUB_PERSONAL_ACCESS_TOKEN if provided.`); + } else { + throw new Error(`GitHub API error (${status}): ${message}`); + } + } + + throw error; + } +} + +/** + * Provides a basic directory structure for v4 registry without API calls + * This is used as a fallback when API rate limits are hit + */ +function getBasicV4Structure(): any { + return { + path: REGISTRY_PATH, + type: 'directory', + note: 'Basic structure provided due to API limitations', + children: { + 'ui': { + path: `${REGISTRY_PATH}/ui`, + type: 'directory', + description: 'Contains all v4 UI components', + note: 'Component files (.tsx) are located here' + }, + 'examples': { + path: `${REGISTRY_PATH}/examples`, + type: 'directory', + description: 'Contains component demo examples', + note: 'Demo files showing component usage' + }, + 'hooks': { + path: `${REGISTRY_PATH}/hooks`, + type: 'directory', + description: 'Contains custom React hooks' + }, + 'lib': { + path: `${REGISTRY_PATH}/lib`, + type: 'directory', + description: 'Contains utility libraries and functions' + } + } + }; +} + +/** + * Extract description from block code comments + * @param code The source code to analyze + * @returns Extracted description or null + */ +function extractBlockDescription(code: string): string | null { + // Look for JSDoc comments or description comments + const descriptionRegex = /\/\*\*[\s\S]*?\*\/|\/\/\s*(.+)/; + const match = code.match(descriptionRegex); + if (match) { + // Clean up the comment + const description = match[0] + .replace(/\/\*\*|\*\/|\*|\/\//g, '') + .trim() + .split('\n')[0] + .trim(); + return description.length > 0 ? description : null; + } + + // Look for component name as fallback + const componentRegex = /export\s+(?:default\s+)?function\s+(\w+)/; + const componentMatch = code.match(componentRegex); + if (componentMatch) { + return `${componentMatch[1]} - A reusable UI component`; + } + + return null; +} + +/** + * Extract dependencies from import statements + * @param code The source code to analyze + * @returns Array of dependency names + */ +function extractDependencies(code: string): string[] { + const dependencies: string[] = []; + + // Match import statements + const importRegex = /import\s+.*?\s+from\s+['"]([@\w\/\-\.]+)['"]/g; + let match: RegExpExecArray | null; + + match = importRegex.exec(code); + while (match !== null) { + const dep: string = match[1]; + if (!dep.startsWith('./') && !dep.startsWith('../') && !dep.startsWith('@/')) { + dependencies.push(dep); + } + match = importRegex.exec(code); + } + + return [...new Set(dependencies)]; // Remove duplicates +} + +/** + * Extract component usage from code + * @param code The source code to analyze + * @returns Array of component names used + */ +function extractComponentUsage(code: string): string[] { + const components: string[] = []; + + // Extract from imports of components (assuming they start with capital letters) + const importRegex = /import\s+\{([^}]+)\}\s+from/g; + let match: RegExpExecArray | null; + + match = importRegex.exec(code); + while (match !== null) { + const imports = match[1].split(',').map(imp => imp.trim()); + imports.forEach(imp => { + if (imp[0] && imp[0] === imp[0].toUpperCase()) { + components.push(imp); + } + }); + match = importRegex.exec(code); + } + + // Also look for JSX components in the code + const jsxRegex = /<([A-Z]\w+)/g; + match = jsxRegex.exec(code); + while (match !== null) { + components.push(match[1]); + match = jsxRegex.exec(code); + } + + return [...new Set(components)]; // Remove duplicates +} + +/** + * Generate usage instructions for complex blocks + * @param blockName Name of the block + * @param structure Structure information + * @returns Usage instructions string + */ +function generateComplexBlockUsage(blockName: string, structure: any[]): string { + const hasComponents = structure.some(item => item.name === 'components'); + + let usage = `To use the ${blockName} block:\n\n`; + usage += `1. Copy the main files to your project:\n`; + + structure.forEach(item => { + if (item.type === 'file') { + usage += ` - ${item.name}\n`; + } else if (item.type === 'directory' && item.name === 'components') { + usage += ` - components/ directory (${item.count} files)\n`; + } + }); + + if (hasComponents) { + usage += `\n2. Copy the components to your components directory\n`; + usage += `3. Update import paths as needed\n`; + usage += `4. Ensure all dependencies are installed\n`; + } else { + usage += `\n2. Update import paths as needed\n`; + usage += `3. Ensure all dependencies are installed\n`; + } + + return usage; +} + +/** + * Enhanced buildDirectoryTree with fallback for rate limits + */ +async function buildDirectoryTreeWithFallback( + owner: string = REPO_OWNER, + repo: string = REPO_NAME, + path: string = BLOCKS, + branch: string = REPO_BRANCH +): Promise { + try { + return await buildDirectoryTree(owner, repo, path, branch); + } catch (error: any) { + // If it's a rate limit error and we're asking for the default v4 path, provide fallback + if (error.message && error.message.includes('rate limit') && path === BLOCKS) { + logWarning('Using fallback directory structure due to rate limit'); + return getBasicV4Structure(); + } + // Re-throw other errors + throw error; + } +} + +/** + * Fetch block code from the v4 blocks directory + * @param blockName Name of the block (e.g., "calendar-01", "dashboard-01") + * @param includeComponents Whether to include component files for complex blocks + * @returns Promise with block code and structure + */ +async function getBlockCode(blockName: string, includeComponents: boolean = true): Promise { + const blocksPath = `${BLOCKS}`; + + try { + // First, check if it's a simple block file (.tsx) + try { + const simpleBlockResponse = await githubRaw.get(`/${blocksPath}/${blockName}.svelte`); + if (simpleBlockResponse.status === 200) { + const code = simpleBlockResponse.data; + + // Extract useful information from the code + const description = extractBlockDescription(code); + const dependencies = extractDependencies(code); + const components = extractComponentUsage(code); + + return { + name: blockName, + type: 'simple', + description: description || `Simple block: ${blockName}`, + code: code, + dependencies: dependencies, + componentsUsed: components, + size: code.length, + lines: code.split('\n').length, + usage: `Import and use directly in your application:\n\nimport { ${blockName.charAt(0).toUpperCase() + blockName.slice(1).replace(/-/g, '')} } from './blocks/${blockName}'` + }; + } + } catch (error) { + // Continue to check for complex block directory + } + + // Check if it's a complex block directory + const directoryResponse = await githubApi.get(`/repos/${REPO_OWNER}/${REPO_NAME}/contents/${blocksPath}/${blockName}?ref=${REPO_BRANCH}`); + + if (!directoryResponse.data) { + throw new Error(`Block "${blockName}" not found`); + } + + const blockStructure: any = { + name: blockName, + type: 'complex', + description: `Complex block: ${blockName}`, + files: {}, + structure: [], + totalFiles: 0, + dependencies: new Set(), + componentsUsed: new Set() + }; + + // Process the directory contents + if (Array.isArray(directoryResponse.data)) { + blockStructure.totalFiles = directoryResponse.data.length; + + for (const item of directoryResponse.data) { + if (item.type === 'file') { + // Get the main page file + const fileResponse = await githubRaw.get(`/${item.path}`); + const content = fileResponse.data; + + // Extract information from the file + const description = extractBlockDescription(content); + const dependencies = extractDependencies(content); + const components = extractComponentUsage(content); + + blockStructure.files[item.name] = { + path: item.name, + content: content, + size: content.length, + lines: content.split('\n').length, + description: description, + dependencies: dependencies, + componentsUsed: components + }; + + // Add to overall dependencies and components + dependencies.forEach((dep: string) => blockStructure.dependencies.add(dep)); + components.forEach((comp: string) => blockStructure.componentsUsed.add(comp)); + + blockStructure.structure.push({ + name: item.name, + type: 'file', + size: content.length, + description: description || `${item.name} - Main block file` + }); + + // Use the first file's description as the block description if available + if (description && blockStructure.description === `Complex block: ${blockName}`) { + blockStructure.description = description; + } + } else if (item.type === 'dir' && item.name === 'components' && includeComponents) { + // Get component files + const componentsResponse = await githubApi.get(`/repos/${REPO_OWNER}/${REPO_NAME}/contents/${item.path}?ref=${REPO_BRANCH}`); + + if (Array.isArray(componentsResponse.data)) { + blockStructure.files.components = {}; + const componentStructure: any[] = []; + + for (const componentItem of componentsResponse.data) { + if (componentItem.type === 'file') { + const componentResponse = await githubRaw.get(`/${componentItem.path}`); + const content = componentResponse.data; + + const dependencies = extractDependencies(content); + const components = extractComponentUsage(content); + + blockStructure.files.components[componentItem.name] = { + path: `components/${componentItem.name}`, + content: content, + size: content.length, + lines: content.split('\n').length, + dependencies: dependencies, + componentsUsed: components + }; + + // Add to overall dependencies and components + dependencies.forEach((dep: string) => blockStructure.dependencies.add(dep)); + components.forEach((comp: string) => blockStructure.componentsUsed.add(comp)); + + componentStructure.push({ + name: componentItem.name, + type: 'component', + size: content.length + }); + } + } + + blockStructure.structure.push({ + name: 'components', + type: 'directory', + files: componentStructure, + count: componentStructure.length + }); + } + } + } + } + + // Convert Sets to Arrays for JSON serialization + blockStructure.dependencies = Array.from(blockStructure.dependencies); + blockStructure.componentsUsed = Array.from(blockStructure.componentsUsed); + + // Add usage instructions + blockStructure.usage = generateComplexBlockUsage(blockName, blockStructure.structure); + + return blockStructure; + + } catch (error: any) { + if (error.response?.status === 404) { + throw new Error(`Block "${blockName}" not found. Available blocks can be found in the v4 blocks directory.`); + } + throw error; + } +} + +/** + * Get all available blocks with categorization + * @param category Optional category filter + * @returns Promise with categorized block list + */ +async function getAvailableBlocks(category?: string): Promise { + const blocksPath = `${BLOCKS}`; + + try { + const response = await githubApi.get(`/repos/${REPO_OWNER}/${REPO_NAME}/contents/${blocksPath}?ref=${REPO_BRANCH}`); + + if (!Array.isArray(response.data)) { + throw new Error('Unexpected response from GitHub API'); + } + + const blocks: any = { + calendar: [], + dashboard: [], + login: [], + sidebar: [], + products: [], + authentication: [], + charts: [], + mail: [], + music: [], + other: [] + }; + + for (const item of response.data) { + const blockInfo: any = { + name: item.name.replace('.svelte', ''), + type: item.type === 'file' ? 'simple' : 'complex', + path: item.path, + size: item.size || 0, + lastModified: item.download_url ? 'Available' : 'Directory' + }; + + // Add description based on name patterns + if (item.name.includes('calendar')) { + blockInfo.description = 'Calendar component for date selection and scheduling'; + blocks.calendar.push(blockInfo); + } else if (item.name.includes('dashboard')) { + blockInfo.description = 'Dashboard layout with charts, metrics, and data display'; + blocks.dashboard.push(blockInfo); + } else if (item.name.includes('login') || item.name.includes('signin')) { + blockInfo.description = 'Authentication and login interface'; + blocks.login.push(blockInfo); + } else if (item.name.includes('sidebar')) { + blockInfo.description = 'Navigation sidebar component'; + blocks.sidebar.push(blockInfo); + } else if (item.name.includes('products') || item.name.includes('ecommerce')) { + blockInfo.description = 'Product listing and e-commerce components'; + blocks.products.push(blockInfo); + } else if (item.name.includes('auth')) { + blockInfo.description = 'Authentication related components'; + blocks.authentication.push(blockInfo); + } else if (item.name.includes('chart') || item.name.includes('graph')) { + blockInfo.description = 'Data visualization and chart components'; + blocks.charts.push(blockInfo); + } else if (item.name.includes('mail') || item.name.includes('email')) { + blockInfo.description = 'Email and mail interface components'; + blocks.mail.push(blockInfo); + } else if (item.name.includes('music') || item.name.includes('player')) { + blockInfo.description = 'Music player and media components'; + blocks.music.push(blockInfo); + } else { + blockInfo.description = `${item.name} - Custom UI block`; + blocks.other.push(blockInfo); + } + } + + // Sort blocks within each category + Object.keys(blocks).forEach(key => { + blocks[key].sort((a: any, b: any) => a.name.localeCompare(b.name)); + }); + + // Filter by category if specified + if (category) { + const categoryLower = category.toLowerCase(); + if (blocks[categoryLower]) { + return { + category, + blocks: blocks[categoryLower], + total: blocks[categoryLower].length, + description: `${category.charAt(0).toUpperCase() + category.slice(1)} blocks available in shadcn/ui v4`, + usage: `Use 'get_block' tool with the block name to get the full source code and implementation details.` + }; + } else { + return { + category, + blocks: [], + total: 0, + availableCategories: Object.keys(blocks).filter(key => blocks[key].length > 0), + suggestion: `Category '${category}' not found. Available categories: ${Object.keys(blocks).filter(key => blocks[key].length > 0).join(', ')}` + }; + } + } + + // Calculate totals + const totalBlocks = Object.values(blocks).flat().length; + const nonEmptyCategories = Object.keys(blocks).filter(key => blocks[key].length > 0); + + return { + categories: blocks, + totalBlocks, + availableCategories: nonEmptyCategories, + summary: Object.keys(blocks).reduce((acc: any, key) => { + if (blocks[key].length > 0) { + acc[key] = blocks[key].length; + } + return acc; + }, {}), + usage: "Use 'get_block' tool with a specific block name to get full source code and implementation details.", + examples: nonEmptyCategories.slice(0, 3).map(cat => + blocks[cat][0] ? `${cat}: ${blocks[cat][0].name}` : '' + ).filter(Boolean) + }; + + } catch (error: any) { + if (error.response?.status === 404) { + throw new Error('Blocks directory not found in the v4 registry'); + } + throw error; + } +} + +/** + * Set or update GitHub API key for higher rate limits + * @param apiKey GitHub Personal Access Token + */ +function setGitHubApiKey(apiKey: string): void { + // Update the Authorization header for the GitHub API instance + if (apiKey && apiKey.trim()) { + (githubApi.defaults.headers as any)['Authorization'] = `Bearer ${apiKey.trim()}`; + logInfo('GitHub API key updated successfully'); + console.error('GitHub API key updated successfully'); + } else { + // Remove authorization header if empty key provided + delete (githubApi.defaults.headers as any)['Authorization']; + console.error('GitHub API key removed - using unauthenticated requests'); + console.error('For higher rate limits and reliability, provide a GitHub API token. See setup instructions: https://github.com/Jpisnice/shadcn-ui-mcp-server#readme'); + } +} + +/** + * Get current GitHub API rate limit status + * @returns Promise with rate limit information + */ +async function getGitHubRateLimit(): Promise { + try { + const response = await githubApi.get('/rate_limit'); + return response.data; + } catch (error: any) { + throw new Error(`Failed to get rate limit info: ${error.message}`); + } +} + +export const axios = { + githubRaw, + githubApi, + buildDirectoryTree: buildDirectoryTreeWithFallback, // Use fallback version by default + buildDirectoryTreeWithFallback, + getComponentSource, + getComponentDemo, + getAvailableComponents, + getComponentMetadata, + getBlockCode, + getAvailableBlocks, + setGitHubApiKey, + getGitHubRateLimit, + // Path constants for easy access + paths: { + REPO_OWNER, + REPO_NAME, + REPO_BRANCH, + REGISTRY_PATH, + BLOCKS + } +} \ No newline at end of file diff --git a/src/utils/framework.ts b/src/utils/framework.ts new file mode 100644 index 0000000..a040d35 --- /dev/null +++ b/src/utils/framework.ts @@ -0,0 +1,105 @@ +/** + * Framework selection utility for shadcn/ui MCP server + * + * This module handles switching between React and Svelte implementations + * based on environment variables or command line arguments. + * + * Usage: + * - Set FRAMEWORK environment variable to 'react' or 'svelte' + * - Or use --framework command line argument + * - Defaults to 'react' if not specified + */ + +import { logInfo, logWarning } from './logger.js'; + +// Framework types +export type Framework = 'react' | 'svelte'; + +// Default framework +const DEFAULT_FRAMEWORK: Framework = 'react'; + +/** + * Get the current framework from environment or command line arguments + * @returns The selected framework ('react' or 'svelte') + */ +export function getFramework(): Framework { + // Check command line arguments first + const args = process.argv.slice(2); + const frameworkIndex = args.findIndex(arg => arg === '--framework' || arg === '-f'); + + if (frameworkIndex !== -1 && args[frameworkIndex + 1]) { + const framework = args[frameworkIndex + 1].toLowerCase() as Framework; + if (framework === 'react' || framework === 'svelte') { + logInfo(`Framework set to '${framework}' via command line argument`); + return framework; + } else { + logWarning(`Invalid framework '${framework}' specified. Using default '${DEFAULT_FRAMEWORK}'`); + } + } + + // Check environment variable + const envFramework = process.env.FRAMEWORK?.toLowerCase() as Framework; + if (envFramework === 'react' || envFramework === 'svelte') { + logInfo(`Framework set to '${envFramework}' via environment variable`); + return envFramework; + } + + // Return default + logInfo(`Using default framework: '${DEFAULT_FRAMEWORK}'`); + return DEFAULT_FRAMEWORK; +} + +/** + * Get the axios implementation based on the current framework + * @returns The appropriate axios implementation + */ +export async function getAxiosImplementation() { + const framework = getFramework(); + + if (framework === 'svelte') { + // Dynamic import for Svelte implementation + return import('./axios-svelte.js').then(module => module.axios); + } else { + // Dynamic import for React implementation (default) + return import('./axios.js').then(module => module.axios); + } +} + +/** + * Get framework-specific information for help text + * @returns Framework information object + */ +export function getFrameworkInfo() { + const framework = getFramework(); + + return { + current: framework, + repository: framework === 'svelte' + ? 'huntabyte/shadcn-svelte' + : 'shadcn-ui/ui', + fileExtension: framework === 'svelte' ? '.svelte' : '.tsx', + description: framework === 'svelte' + ? 'Svelte components from shadcn-svelte' + : 'React components from shadcn/ui v4' + }; +} + +/** + * Validate framework selection and provide helpful feedback + */ +export function validateFrameworkSelection() { + const framework = getFramework(); + const info = getFrameworkInfo(); + + logInfo(`MCP Server configured for ${framework.toUpperCase()} framework`); + logInfo(`Repository: ${info.repository}`); + logInfo(`File extension: ${info.fileExtension}`); + logInfo(`Description: ${info.description}`); + + // Provide helpful information about switching frameworks + if (framework === 'react') { + logInfo('To switch to Svelte: set FRAMEWORK=svelte or use --framework svelte'); + } else { + logInfo('To switch to React: set FRAMEWORK=react or use --framework react'); + } +} \ No newline at end of file