├── .gitignore ├── Dockerfile ├── README.md ├── jest.config.js ├── llms-install.md ├── package-lock.json ├── package.json ├── previewer ├── 404.html ├── _next │ └── static │ │ ├── 19Epkb2wCFZOEirwyJuiX │ │ ├── _buildManifest.js │ │ └── _ssgManifest.js │ │ ├── chunks │ │ ├── 035175d8.4ae9d736c866d3fe.js │ │ ├── 0e5ce63c-8a85f02350003f0b.js │ │ ├── 112.17b6b6b44ef0a435.js │ │ ├── 363642f4-0e8294c1151c8893.js │ │ ├── 486-e7a7389743eec8ee.js │ │ ├── 4bd1b696-320a07282111b80e.js │ │ ├── 508.181819ea40016616.js │ │ ├── 637-75db706f6fc6beb9.js │ │ ├── 684-9f1e563a3a38870c.js │ │ ├── 833.bf49ecf884d9739c.js │ │ ├── 870.3764a5ed5edda507.js │ │ ├── app │ │ │ ├── _not-found │ │ │ │ └── page-5b626a49d0e6fd28.js │ │ │ ├── layout-11d39c362485f7b4.js │ │ │ └── page-0bc7818b3176dbab.js │ │ ├── e58a7f8f-06fd1ba423172212.js │ │ ├── framework-f593a28cde54158e.js │ │ ├── main-28754d9b5e5c9c91.js │ │ ├── main-app-bec55ba40a50c333.js │ │ ├── pages │ │ │ ├── _app-da15c11dea942c36.js │ │ │ └── _error-cc3f077a18ea1793.js │ │ ├── polyfills-42372ed130431b0a.js │ │ └── webpack-0833cf18f9d7ddef.js │ │ ├── css │ │ └── 4c3fb5a1c12ae476.css │ │ └── media │ │ └── e11418ac562b8ac1-s.p.woff2 ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── index.txt ├── placeholder-logo.png ├── placeholder-logo.svg ├── placeholder-user.jpg ├── placeholder.jpg └── placeholder.svg ├── smithery.yaml ├── src ├── index.ts ├── tools │ ├── create-ui.ts │ ├── fetch-ui.ts │ ├── logo-search.ts │ └── refine-ui.ts └── utils │ ├── base-tool.ts │ ├── callback-server.ts │ ├── config.ts │ ├── console.ts │ ├── get-content-of-file.ts │ ├── http-client.test.ts │ └── http-client.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | node_modules/** 4 | /node_modules 5 | **/node_modules 6 | npm-debug.log 7 | yarn-debug.log 8 | yarn-error.log 9 | .pnpm-debug.log 10 | 11 | # Build output 12 | dist/ 13 | build/ 14 | 15 | # Environment variables 16 | .env 17 | .env.local 18 | .env.*.local 19 | 20 | # IDE and editor files 21 | .idea/ 22 | .vscode/ 23 | *.swp 24 | *.swo 25 | .DS_Store 26 | Thumbs.db 27 | 28 | # Testing 29 | coverage/ 30 | 31 | # Logs 32 | logs/ 33 | *.log 34 | 35 | # Temporary files 36 | *.tmp 37 | *.temp 38 | .cache/ 39 | 40 | # TypeScript 41 | *.tsbuildinfo 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.14.0-alpine 2 | 3 | WORKDIR /app 4 | 5 | # Copy package files 6 | COPY package.json package-lock.json ./ 7 | 8 | # Install pnpm and dependencies 9 | RUN npm install 10 | 11 | # Copy application code 12 | COPY . . 13 | 14 | # Build TypeScript 15 | RUN npm run build 16 | 17 | # Command will be provided by smithery.yaml 18 | CMD ["node", "dist/index.js"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 21st.dev Magic AI Agent 2 | 3 | ![MCP Banner](https://21st.dev/magic-agent-og-image.png) 4 | 5 | Magic Component Platform (MCP) is a powerful AI-driven tool that helps developers create beautiful, modern UI components instantly through natural language descriptions. It integrates seamlessly with popular IDEs and provides a streamlined workflow for UI development. 6 | 7 | ## 🌟 Features 8 | 9 | - **AI-Powered UI Generation**: Create UI components by describing them in natural language 10 | - **Multi-IDE Support**: 11 | - [Cursor](https://cursor.com) IDE integration 12 | - [Windsurf](https://windsurf.ai) support 13 | - [VSCode](https://code.visualstudio.com/) support 14 | - [VSCode + Cline](https://cline.bot) integration (Beta) 15 | - **Modern Component Library**: Access to a vast collection of pre-built, customizable components inspired by [21st.dev](https://21st.dev) 16 | - **Real-time Preview**: Instantly see your components as you create them 17 | - **TypeScript Support**: Full TypeScript support for type-safe development 18 | - **SVGL Integration**: Access to a vast collection of professional brand assets and logos 19 | - **Component Enhancement**: Improve existing components with advanced features and animations (Coming Soon) 20 | 21 | ## 🎯 How It Works 22 | 23 | 1. **Tell Agent What You Need** 24 | 25 | - In your AI Agent's chat, just type `/ui` and describe the component you're looking for 26 | - Example: `/ui create a modern navigation bar with responsive design` 27 | 28 | 2. **Let Magic Create It** 29 | 30 | - Your IDE prompts you to use Magic 31 | - Magic instantly builds a polished UI component 32 | - Components are inspired by 21st.dev's library 33 | 34 | 3. **Seamless Integration** 35 | - Components are automatically added to your project 36 | - Start using your new UI components right away 37 | - All components are fully customizable 38 | 39 | ## 🚀 Getting Started 40 | 41 | ### Prerequisites 42 | 43 | - Node.js (Latest LTS version recommended) 44 | - One of the supported IDEs: 45 | - Cursor 46 | - Windsurf 47 | - VSCode (with Cline extension) 48 | 49 | ### Installation 50 | 51 | 1. **Generate API Key** 52 | 53 | - Visit [21st.dev Magic Console](https://21st.dev/magic/console) 54 | - Generate a new API key 55 | 56 | 2. **Choose Installation Method** 57 | 58 | #### Method 1: CLI Installation (Recommended) 59 | 60 | One command to install and configure MCP for your IDE: 61 | 62 | ```bash 63 | npx @21st-dev/cli@latest install --api-key 64 | ``` 65 | 66 | Supported clients: cursor, windsurf, cline, claude 67 | 68 | #### Method 2: Manual Configuration 69 | 70 | If you prefer manual setup, add this to your IDE's MCP config file: 71 | 72 | ```json 73 | { 74 | "mcpServers": { 75 | "@21st-dev/magic": { 76 | "command": "npx", 77 | "args": ["-y", "@21st-dev/magic@latest", "API_KEY=\"your-api-key\""] 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | Config file locations: 84 | 85 | - Cursor: `~/.cursor/mcp.json` 86 | - Windsurf: `~/.codeium/windsurf/mcp_config.json` 87 | - Cline: `~/.cline/mcp_config.json` 88 | - Claude: `~/.claude/mcp_config.json` 89 | 90 | #### Method 3: VS Code Installation 91 | 92 | For one-click installation, click one of the install buttons below: 93 | 94 | [![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-NPM-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=%4021st-dev%2Fmagic&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%4021st-dev%2Fmagic%40latest%22%5D%2C%22env%22%3A%7B%22API_KEY%22%3A%22%24%7Binput%3AapiKey%7D%22%7D%7D&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22apiKey%22%2C%22description%22%3A%2221st.dev+Magic+API+Key%22%2C%22password%22%3Atrue%7D%5D) [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-NPM-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=%4021st-dev%2Fmagic&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%4021st-dev%2Fmagic%40latest%22%5D%2C%22env%22%3A%7B%22API_KEY%22%3A%22%24%7Binput%3AapiKey%7D%22%7D%7D&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22apiKey%22%2C%22description%22%3A%2221st.dev+Magic+API+Key%22%2C%22password%22%3Atrue%7D%5D&quality=insiders) 95 | 96 | ##### Manual VS Code Setup 97 | 98 | First, check the install buttons above for one-click installation. For manual setup: 99 | 100 | Add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`: 101 | 102 | ```json 103 | { 104 | "mcp": { 105 | "inputs": [ 106 | { 107 | "type": "promptString", 108 | "id": "apiKey", 109 | "description": "21st.dev Magic API Key", 110 | "password": true 111 | } 112 | ], 113 | "servers": { 114 | "@21st-dev/magic": { 115 | "command": "npx", 116 | "args": ["-y", "@21st-dev/magic@latest"], 117 | "env": { 118 | "API_KEY": "${input:apiKey}" 119 | } 120 | } 121 | } 122 | } 123 | } 124 | ``` 125 | 126 | Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace: 127 | 128 | ```json 129 | { 130 | "inputs": [ 131 | { 132 | "type": "promptString", 133 | "id": "apiKey", 134 | "description": "21st.dev Magic API Key", 135 | "password": true 136 | } 137 | ], 138 | "servers": { 139 | "@21st-dev/magic": { 140 | "command": "npx", 141 | "args": ["-y", "@21st-dev/magic@latest"], 142 | "env": { 143 | "API_KEY": "${input:apiKey}" 144 | } 145 | } 146 | } 147 | } 148 | ``` 149 | 150 | ## ❓ FAQ 151 | 152 | ### How does Magic AI Agent handle my codebase? 153 | 154 | Magic AI Agent only writes or modifies files related to the components it generates. It follows your project's code style and structure, and integrates seamlessly with your existing codebase without affecting other parts of your application. 155 | 156 | ### Can I customize the generated components? 157 | 158 | Yes! All generated components are fully editable and come with well-structured code. You can modify the styling, functionality, and behavior just like any other React component in your codebase. 159 | 160 | ### What happens if I run out of generations? 161 | 162 | If you exceed your monthly generation limit, you'll be prompted to upgrade your plan. You can upgrade at any time to continue generating components. Your existing components will remain fully functional. 163 | 164 | ### How soon do new components get added to 21st.dev's library? 165 | 166 | Authors can publish components to 21st.dev at any time, and Magic Agent will have immediate access to them. This means you'll always have access to the latest components and design patterns from the community. 167 | 168 | ### Is there a limit to component complexity? 169 | 170 | Magic AI Agent can handle components of varying complexity, from simple buttons to complex interactive forms. However, for best results, we recommend breaking down very complex UIs into smaller, manageable components. 171 | 172 | ## 🛠️ Development 173 | 174 | ### Project Structure 175 | 176 | ``` 177 | mcp/ 178 | ├── app/ 179 | │ └── components/ # Core UI components 180 | ├── types/ # TypeScript type definitions 181 | ├── lib/ # Utility functions 182 | └── public/ # Static assets 183 | ``` 184 | 185 | ### Key Components 186 | 187 | - `IdeInstructions`: Setup instructions for different IDEs 188 | - `ApiKeySection`: API key management interface 189 | - `WelcomeOnboarding`: Onboarding flow for new users 190 | 191 | ## 🤝 Contributing 192 | 193 | We welcome contributions! Please join our [Discord community](https://discord.gg/Qx4rFunHfm) and provide feedback to help improve Magic Agent. The source code is available on [GitHub](https://github.com/serafimcloud/21st). 194 | 195 | ## 👥 Community & Support 196 | 197 | - [Discord Community](https://discord.gg/Qx4rFunHfm) - Join our active community 198 | - [Twitter](https://x.com/serafimcloud) - Follow us for updates 199 | 200 | ## ⚠️ Beta Notice 201 | 202 | Magic Agent is currently in beta. All features are free during this period. We appreciate your feedback and patience as we continue to improve the platform. 203 | 204 | ## 📝 License 205 | 206 | MIT License 207 | 208 | ## 🙏 Acknowledgments 209 | 210 | - Thanks to our beta testers and community members 211 | - Special thanks to the Cursor, Windsurf, and Cline teams for their collaboration 212 | - Integration with [21st.dev](https://21st.dev) for component inspiration 213 | - [SVGL](https://svgl.app) for logo and brand asset integration 214 | 215 | --- 216 | 217 | For more information, join our [Discord community](https://discord.gg/Qx4rFunHfm) or visit [21st.dev/magic](https://21st.dev/magic). 218 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | extensionsToTreatAsEsm: [".ts"], 5 | moduleNameMapper: { 6 | "^(\\.{1,2}/.*)\\.js$": "$1", 7 | }, 8 | transform: { 9 | "^.+\\.tsx?$": [ 10 | "ts-jest", 11 | { 12 | useESM: true, 13 | }, 14 | ], 15 | }, 16 | testPathIgnorePatterns: ["/node_modules/", "/dist/"], 17 | }; 18 | -------------------------------------------------------------------------------- /llms-install.md: -------------------------------------------------------------------------------- 1 | # Magic MCP Server Installation Guide 2 | 3 | This guide is specifically designed for AI agents like Cline to install and configure the Magic MCP server. 4 | 5 | ## Prerequisites 6 | 7 | 1. Node.js (Latest LTS version) 8 | 2. A valid TWENTY_FIRST_API_KEY from [21st.dev/magic](https://21st.dev/magic) 9 | 10 | ## Installation Steps 11 | 12 | ### 1. Configuration Setup 13 | 14 | The Magic MCP server requires configuration based on the client type. Below are the setup instructions for each supported client: 15 | 16 | #### For Windsurf 17 | 18 | 1. Create or modify the configuration file at `~/.codeium/windsurf/mcp_config.json`: 19 | 20 | ```json 21 | { 22 | "mcpServers": { 23 | "magic": { 24 | "command": "npx", 25 | "args": [ 26 | "-y", 27 | "@smithery/cli@latest", 28 | "install", 29 | "@21st-dev/magic-mcp", 30 | "--client", 31 | "windsurf" 32 | ], 33 | "env": { 34 | "TWENTY_FIRST_API_KEY": "" 35 | } 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | 2. Replace `` with the actual TWENTY_FIRST_API_KEY value. 42 | 43 | #### For Cline 44 | 45 | 1. Create or modify the MCP configuration file: 46 | 47 | ```json 48 | { 49 | "mcpServers": { 50 | "@21st-dev-magic-mcp": { 51 | "command": "npx", 52 | "args": [ 53 | "-y", 54 | "@smithery/cli@latest", 55 | "run", 56 | "@21st-dev/magic-mcp", 57 | "--config", 58 | "\"{\\\"TWENTY_FIRST_API_KEY\\\":\\\"\\\"}\"" 59 | ] 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | 2. Replace `` with the actual TWENTY_FIRST_API_KEY value. 66 | 67 | ### 2. Verification 68 | 69 | To verify the installation: 70 | 71 | 1. The MCP server should appear in the list of available servers 72 | 2. Test the connection by sending a simple command: 73 | ``` 74 | /ui create a basic button 75 | ``` 76 | 77 | ## Troubleshooting 78 | 79 | Common issues and solutions: 80 | 81 | 1. If the server fails to start: 82 | 83 | - Verify the API key is valid 84 | - Check Node.js version (should be LTS) 85 | - Ensure all dependencies are installed 86 | 87 | 2. If components fail to generate: 88 | - Verify network connectivity 89 | - Check API key permissions 90 | - Ensure the command format is correct 91 | 92 | ## Environment Variables 93 | 94 | Required environment variables: 95 | 96 | - `TWENTY_FIRST_API_KEY`: Your Magic API key from 21st.dev 97 | 98 | ## Additional Notes 99 | 100 | - The server automatically handles TypeScript and React components 101 | - No additional configuration is needed for basic usage 102 | - The server supports hot reloading for development 103 | 104 | ## Support 105 | 106 | If you encounter any issues: 107 | 108 | 1. Check the [FAQ section](https://21st.dev/magic/docs/faq) 109 | 2. Join our [Discord community](https://discord.gg/Qx4rFunHfm) 110 | 3. Submit an issue on [GitHub](https://github.com/serafimcloud/21st) 111 | 112 | --- 113 | 114 | This installation guide is maintained by the Magic team. For updates and more information, visit [21st.dev/magic](https://21st.dev/magic). 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@21st-dev/magic", 3 | "version": "0.0.46", 4 | "type": "module", 5 | "description": "Magic MCP UI builder by 21st.dev", 6 | "homepage": "https://21st.dev/magic", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/21st-dev/magic-mcp.git" 10 | }, 11 | "bin": { 12 | "magic": "dist/index.js" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "keywords": [ 18 | "mcp", 19 | "model-context-protocol", 20 | "ai", 21 | "UI", 22 | "frontend", 23 | "21st.dev", 24 | "21", 25 | "magic" 26 | ], 27 | "main": "dist/index.js", 28 | "scripts": { 29 | "build": "tsc && shx cp -r previewer dist/ && shx chmod +x dist/*.js", 30 | "build:prod": "npm run build && npm run test", 31 | "debug": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js DEBUG=true", 32 | "start": "node dist/index.js", 33 | "prepare": "npm run build:prod", 34 | "dev": "nodemon --watch src --ext ts,json --exec \"npm run build\"", 35 | "publish-patch": "npm version patch && npm run build:prod && npm publish --access public", 36 | "test": "jest" 37 | }, 38 | "author": "serafim@21st.dev", 39 | "license": "ISC", 40 | "dependencies": { 41 | "@modelcontextprotocol/sdk": "^1.8.0", 42 | "@types/cors": "^2.8.17", 43 | "@types/express": "^5.0.0", 44 | "cors": "^2.8.5", 45 | "open": "^10.1.0", 46 | "zod": "^3.24.2" 47 | }, 48 | "devDependencies": { 49 | "@types/axios": "^0.9.36", 50 | "@types/jest": "^29.5.14", 51 | "@types/node": "^22.13.4", 52 | "@types/react": "^19.0.12", 53 | "jest": "^29.7.0", 54 | "nodemon": "^3.1.9", 55 | "shx": "^0.3.4", 56 | "ts-jest": "^29.1.2", 57 | "typescript": "^5.8.2" 58 | }, 59 | "engines": { 60 | "node": ">=18.0.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /previewer/404.html: -------------------------------------------------------------------------------- 1 | 404: This page could not be found.Magic MCP - Previewer

404

This page could not be found.

-------------------------------------------------------------------------------- /previewer/_next/static/19Epkb2wCFZOEirwyJuiX/_buildManifest.js: -------------------------------------------------------------------------------- 1 | self.__BUILD_MANIFEST=function(e,r,t){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},__routerFilterStatic:{numItems:2,errorRate:1e-4,numBits:39,numHashes:14,bitArray:[0,1,1,0,r,e,e,r,r,e,e,r,e,e,e,r,r,e,e,e,e,r,e,r,r,r,r,e,e,e,r,e,r,e,r,e,e,e,r]},__routerFilterDynamic:{numItems:r,errorRate:1e-4,numBits:r,numHashes:null,bitArray:[]},"/_error":["static/chunks/pages/_error-cc3f077a18ea1793.js"],sortedPages:["/_app","/_error"]}}(1,0,1e-4),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB(); -------------------------------------------------------------------------------- /previewer/_next/static/19Epkb2wCFZOEirwyJuiX/_ssgManifest.js: -------------------------------------------------------------------------------- 1 | self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB() -------------------------------------------------------------------------------- /previewer/_next/static/chunks/0e5ce63c-8a85f02350003f0b.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[105],{3096:(e,r,t)=>{t.d(r,{RiX:()=>v,Srz:()=>i,vKP:()=>f});var l=t(2115);function n(e,r){if(null==e)return{};var t,l,n={},o=Object.keys(e);for(l=0;l=0||(n[t]=e[t]);return n}var o=["color"],i=(0,l.forwardRef)(function(e,r){var t=e.color,i=n(e,o);return(0,l.createElement)("svg",Object.assign({width:"15",height:"15",viewBox:"0 0 15 15",fill:"none",xmlns:"http://www.w3.org/2000/svg"},i,{ref:r}),(0,l.createElement)("path",{d:"M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z",fill:void 0===t?"currentColor":t,fillRule:"evenodd",clipRule:"evenodd"}))}),c=["color"],f=(0,l.forwardRef)(function(e,r){var t=e.color,o=n(e,c);return(0,l.createElement)("svg",Object.assign({width:"15",height:"15",viewBox:"0 0 15 15",fill:"none",xmlns:"http://www.w3.org/2000/svg"},o,{ref:r}),(0,l.createElement)("path",{d:"M6.1584 3.13508C6.35985 2.94621 6.67627 2.95642 6.86514 3.15788L10.6151 7.15788C10.7954 7.3502 10.7954 7.64949 10.6151 7.84182L6.86514 11.8418C6.67627 12.0433 6.35985 12.0535 6.1584 11.8646C5.95694 11.6757 5.94673 11.3593 6.1356 11.1579L9.565 7.49985L6.1356 3.84182C5.94673 3.64036 5.95694 3.32394 6.1584 3.13508Z",fill:void 0===t?"currentColor":t,fillRule:"evenodd",clipRule:"evenodd"}))}),a=["color"],v=(0,l.forwardRef)(function(e,r){var t=e.color,o=n(e,a);return(0,l.createElement)("svg",Object.assign({width:"15",height:"15",viewBox:"0 0 15 15",fill:"none",xmlns:"http://www.w3.org/2000/svg"},o,{ref:r}),(0,l.createElement)("path",{d:"M9.875 7.5C9.875 8.81168 8.81168 9.875 7.5 9.875C6.18832 9.875 5.125 8.81168 5.125 7.5C5.125 6.18832 6.18832 5.125 7.5 5.125C8.81168 5.125 9.875 6.18832 9.875 7.5Z",fill:void 0===t?"currentColor":t}))})}}]); -------------------------------------------------------------------------------- /previewer/_next/static/chunks/112.17b6b6b44ef0a435.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[112],{7112:(e,t,n)=>{n.r(t),n.d(t,{SandpackStatic:()=>d});var r=n(7192),i=n(6757),o=n(2709),s=n(133);n(4272),n(8497);var a=function(e,t,n){var r=e.exec(t);if(r&&r.length>=1){var i=r.index+r[0].length;return t.substring(0,i)+n+t.substring(i)}},c=function(e){return"string"==typeof e?e:new TextDecoder().decode(e)},l=function(e){var t=c(e),n=new DOMParser().parseFromString(t,"text/html");n.documentElement.getAttribute("lang")||n.documentElement.setAttribute("lang","en");var r=n.documentElement.outerHTML;return"\n".concat(r)},d=function(e){function t(t,n,r){void 0===r&&(r={});var s,a=e.call(this,t,n,r)||this;return a.files=new Map,a.status="initializing",a.emitter=new o.E,a.previewController=new i.PreviewController({baseUrl:null!=(s=r.bundlerURL)?s:"https://preview.sandpack-static-server.codesandbox.io",getFileContent:function(e){var t=a.files.get(e);if(!t)throw Error("File not found");if(e.endsWith(".html")||e.endsWith(".htm"))try{t=l(t),t=a.injectProtocolScript(t),t=a.injectExternalResources(t,r.externalResources),t=a.injectScriptIntoHead(t,{script:o.c,scope:{channelId:(0,o.g)()}})}catch(e){console.error("Runtime injection failed",e)}return t}}),"string"==typeof t?(a.selector=t,a.element=document.querySelector(t),a.iframe=document.createElement("iframe")):(a.element=t,a.iframe=t),a.iframe.getAttribute("sandbox")||(a.iframe.setAttribute("sandbox","allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts allow-downloads allow-pointer-lock"),a.iframe.setAttribute("allow","accelerometer; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; clipboard-read; clipboard-write; xr-spatial-tracking;")),a.eventListener=a.eventListener.bind(a),"undefined"!=typeof window&&window.addEventListener("message",a.eventListener),a.updateSandbox(),a}return(0,r.g)(t,e),t.prototype.injectContentIntoHead=function(e,t){var n;return e=null!=(n=a(/]*>/g,e=c(e),"\n"+t))?n:t+"\n"+e},t.prototype.injectProtocolScript=function(e){return this.injectContentIntoHead(e,'Magic MCP - Previewer
-------------------------------------------------------------------------------- /previewer/index.txt: -------------------------------------------------------------------------------- 1 | 1:"$Sreact.fragment" 2 | 2:I[6671,["486","static/chunks/486-e7a7389743eec8ee.js","177","static/chunks/app/layout-11d39c362485f7b4.js"],"Toaster"] 3 | 3:I[9304,["486","static/chunks/486-e7a7389743eec8ee.js","177","static/chunks/app/layout-11d39c362485f7b4.js"],"ThemeProvider"] 4 | 4:I[7555,[],""] 5 | 5:I[1295,[],""] 6 | 6:I[3713,["193","static/chunks/e58a7f8f-06fd1ba423172212.js","79","static/chunks/363642f4-0e8294c1151c8893.js","105","static/chunks/0e5ce63c-8a85f02350003f0b.js","486","static/chunks/486-e7a7389743eec8ee.js","637","static/chunks/637-75db706f6fc6beb9.js","974","static/chunks/app/page-0bc7818b3176dbab.js"],"default"] 7 | 7:I[9665,[],"MetadataBoundary"] 8 | 9:I[9665,[],"OutletBoundary"] 9 | c:I[4911,[],"AsyncMetadataOutlet"] 10 | e:I[9665,[],"ViewportBoundary"] 11 | 10:I[6614,[],""] 12 | :HL["/_next/static/media/e11418ac562b8ac1-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}] 13 | :HL["/_next/static/css/4c3fb5a1c12ae476.css","style"] 14 | 0:{"P":null,"b":"19Epkb2wCFZOEirwyJuiX","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/4c3fb5a1c12ae476.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"__className_3a0388","children":["$","body",null,{"children":[["$","$L2",null,{}],["$","$L3",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":["$","div",null,{"className":"flex flex-col min-h-screen","children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","main",null,{"className":"flex flex-col h-screen","children":["$","$L6",null,{}]}],["$","$L7",null,{"children":"$L8"}],null,["$","$L9",null,{"children":["$La","$Lb",["$","$Lc",null,{"promise":"$@d"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,["$","$1","6c_rBtRs_v-8_Udt-eHy0",{"children":[["$","$Le",null,{"children":"$Lf"}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],null]}],false]],"m":"$undefined","G":["$10","$undefined"],"s":false,"S":true} 15 | 11:"$Sreact.suspense" 16 | 12:I[4911,[],"AsyncMetadata"] 17 | 8:["$","$11",null,{"fallback":null,"children":["$","$L12",null,{"promise":"$@13"}]}] 18 | b:null 19 | f:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]] 20 | a:null 21 | 13:{"metadata":[["$","title","0",{"children":"Magic MCP - Previewer"}],["$","meta","1",{"name":"description","content":"Choose the best variant of the component"}],["$","link","2",{"rel":"icon","href":"/favicon.ico"}],["$","link","3",{"rel":"icon","href":"/favicon-16x16.png","sizes":"16x16","type":"image/png"}],["$","link","4",{"rel":"icon","href":"/favicon-32x32.png","sizes":"32x32","type":"image/png"}],["$","link","5",{"rel":"apple-touch-icon","href":"/apple-touch-icon.png","sizes":"180x180","type":"image/png"}]],"error":null,"digest":"$undefined"} 22 | d:{"metadata":"$13:metadata","error":null,"digest":"$undefined"} 23 | -------------------------------------------------------------------------------- /previewer/placeholder-logo.png: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR�M��0PLTEZ? tRNS� �@��`P0p���w �IDATx��ؽJ3Q�7'��%�|?� ���E�l�7���(X�D������w`����[�*t����D���mD�}��4; ;�DDDDDDDDDDDD_�_İ��!�y�`�_�:�� ;Ļ�'|� ��;.I"����3*5����J�1�� �T��FI�� ��=��3܃�2~�b���0��U9\��]�4�#w0��Gt\&1 �?21,���o!e�m��ĻR�����5�� ؽAJ�9��R)�5�0.FFASaǃ�T�#|�K���I�������1� 4 | M������N"��$����G�V�T� ��T^^��A�$S��h(�������G]co"J׸^^�'�=���%� �W�6Ы�W��w�a�߇*�^^�YG�c���`'F����������������^5_�,�S�%IEND�B`� -------------------------------------------------------------------------------- /previewer/placeholder-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /previewer/placeholder-user.jpg: -------------------------------------------------------------------------------- 1 | ����JFIF��C 2 |   3 |  4 | $ &%# #"(-90(*6+"#2D26;=@@@&0FKE>J9?@=��C  =)#)==================================================���������� ـ|�r4�-�̈"x�'�0�Í��8�H�N�q�����Q�������V�`=�($q"_�� 5 | �S8�P��0VFbP��! 6 | Io40��[?p#�|� @!.E�3��4pBq �Z s���C  AQR�!1@Ua��02Tq���56cps�� "#$PS�����?��R���,�� ��� 7 | �n�k��n8rZ�����9Vv��V��ms$9zWʏh�-+@Z�2�PGE������EY9��i�Ͻ�S ��O��Ȕ��_I��W髵�}�����B�ՎT��>%r �[e/,W�D}��D�>b�e>�v�Z�p&�*VS��V�sV�c�:��~K�������C��:��k�'An|ʶ�}\��C� �f����a�;�h��J����q!i=���"�q�NF�IZ�`�wĝ5hAj� 8 | �RXl䎉�lk���@I�%l��Ն���FDY-����Eq�i����O�I�_�2b�lNj�Yu��k���AO����٣��ܭ�n��cam�jN�j���VL�}� ;��oކ6��շs��,���ք���l�i����l{I�O��(!%J $ ���n�-@G����n��ܮi!�괁G�:�^��n�g3l�F%���9]�Pq��)�:��� @�*ɍmׅ�VLY'�s+�z ���V�m�J9��S�_���#��;�����NJ!5�#�q\�M@��@�]yz�����A;e�k��@�s�^���G����\�5F��(��S��Ly���c�i8�����o�8T��i�N��7D����t-�p�3`r�q r�;|�.��bTG��[i H��͚-� 9 | ��Oj�H����M�ؒFE�{�3X�n���e� �R3/�~����� 10 | ��a����!�j&@^r�����Y�������l�Z? �7땵��)ki�w��\.�u�����X��\.�u�����X��\.�u�����X��\.�u��p�M����o(N��3�Vg�����Z�s��%�\�]q}d�k\_Y5���MG�����Q��q}d�k\_Y5���MG�����Q��q}d�kV|5���8���//�������?�����?�� -------------------------------------------------------------------------------- /previewer/placeholder.jpg: -------------------------------------------------------------------------------- 1 | ����JFIFHH���ExifMM*JR(�iZHH�����8Photoshop 3.08BIM8BIM%��ُ�� ���B~���� 2 | ���s!1"AQ2aq#� �B�R3�$b0�r�C�4��S@%c5�s�PD���&T6d�t�`҄�p�'E7e�Uu��Å��Fv��GVf� 3 | ()*89:HIJWXYZghijwxyz����������������������������������������������������������� 4 | ����! 1A0"2Q@3#aBqR4�P$��C�b5S��%`�D�r��c6p&ET�'�� 5 | ()*789:FGHIJUVWXYZdefghijstuvwxyz�����������������������������������������������������������������������������C  6 |  7 | 8 | ")$+*($''-2@7-0=0''8L9=CEHIH+6OUNFT@GHE��C !!E.'.EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE�� �k����?��?��?��3 !1AQaq��������� 0@P`p���������?!��� ��3 !1AQa q𑁡�����0@P`p���������?���?���?��� -------------------------------------------------------------------------------- /previewer/placeholder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - TWENTY_FIRST_API_KEY 10 | properties: 11 | TWENTY_FIRST_API_KEY: 12 | type: string 13 | description: "The API key from https://21st.dev/magic/console" 14 | commandFunction: 15 | # A function that produces the CLI command to start the MCP on stdio. 16 | |- 17 | (config) => ({ command: 'node', args: ['dist/index.js'], env: { TWENTY_FIRST_API_KEY: config.TWENTY_FIRST_API_KEY } }) 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | 6 | import { setupJsonConsole } from "./utils/console.js"; 7 | 8 | import { CreateUiTool } from "./tools/create-ui.js"; 9 | import { FetchUiTool } from "./tools/fetch-ui.js"; 10 | import { LogoSearchTool } from "./tools/logo-search.js"; 11 | import { RefineUiTool } from "./tools/refine-ui.js"; 12 | 13 | setupJsonConsole(); 14 | 15 | const VERSION = "0.0.46"; 16 | const server = new McpServer({ 17 | name: "21st-magic", 18 | version: VERSION, 19 | }); 20 | 21 | // Register tools 22 | new CreateUiTool().register(server); 23 | new LogoSearchTool().register(server); 24 | new FetchUiTool().register(server); 25 | new RefineUiTool().register(server); 26 | 27 | async function runServer() { 28 | const transport = new StdioServerTransport(); 29 | console.log(`Starting server v${VERSION} (PID: ${process.pid})`); 30 | 31 | let isShuttingDown = false; 32 | 33 | const cleanup = () => { 34 | if (isShuttingDown) return; 35 | isShuttingDown = true; 36 | 37 | console.log(`Shutting down server (PID: ${process.pid})...`); 38 | try { 39 | transport.close(); 40 | } catch (error) { 41 | console.error(`Error closing transport (PID: ${process.pid}):`, error); 42 | } 43 | console.log(`Server closed (PID: ${process.pid})`); 44 | process.exit(0); 45 | }; 46 | 47 | transport.onerror = (error: Error) => { 48 | console.error(`Transport error (PID: ${process.pid}):`, error); 49 | cleanup(); 50 | }; 51 | 52 | transport.onclose = () => { 53 | console.log(`Transport closed unexpectedly (PID: ${process.pid})`); 54 | cleanup(); 55 | }; 56 | 57 | process.on("SIGTERM", () => { 58 | console.log(`Received SIGTERM (PID: ${process.pid})`); 59 | cleanup(); 60 | }); 61 | 62 | process.on("SIGINT", () => { 63 | console.log(`Received SIGINT (PID: ${process.pid})`); 64 | cleanup(); 65 | }); 66 | 67 | process.on("beforeExit", () => { 68 | console.log(`Received beforeExit (PID: ${process.pid})`); 69 | cleanup(); 70 | }); 71 | 72 | await server.connect(transport); 73 | console.log(`Server started (PID: ${process.pid})`); 74 | } 75 | 76 | runServer().catch((error) => { 77 | console.error(`Fatal error running server (PID: ${process.pid}):`, error); 78 | if (!process.exitCode) { 79 | process.exit(1); 80 | } 81 | }); 82 | -------------------------------------------------------------------------------- /src/tools/create-ui.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { BaseTool } from "../utils/base-tool.js"; 3 | import { twentyFirstClient } from "../utils/http-client.js"; 4 | import { CallbackServer } from "../utils/callback-server.js"; 5 | import open from "open"; 6 | import { getContentOfFile } from "../utils/get-content-of-file.js"; 7 | 8 | const UI_TOOL_NAME = "21st_magic_component_builder"; 9 | const UI_TOOL_DESCRIPTION = ` 10 | "Use this tool when the user requests a new UI component—e.g., mentions /ui, /21 /21st, or asks for a button, input, dialog, table, form, banner, card, or other React component. 11 | This tool ONLY returns the text snippet for that UI component. 12 | After calling this tool, you must edit or add files to integrate the snippet into the codebase." 13 | `; 14 | 15 | interface CreateUiResponse { 16 | text: string; 17 | } 18 | 19 | export class CreateUiTool extends BaseTool { 20 | name = UI_TOOL_NAME; 21 | description = UI_TOOL_DESCRIPTION; 22 | 23 | schema = z.object({ 24 | message: z.string().describe("Full users message"), 25 | searchQuery: z 26 | .string() 27 | .describe( 28 | "Generate a search query for 21st.dev (library for searching UI components) to find a UI component that matches the user's message. Must be a two-four words max or phrase" 29 | ), 30 | absolutePathToCurrentFile: z 31 | .string() 32 | .describe( 33 | "Absolute path to the current file to which we want to apply changes" 34 | ), 35 | absolutePathToProjectDirectory: z 36 | .string() 37 | .describe("Absolute path to the project root directory"), 38 | context: z 39 | .string() 40 | .describe( 41 | "Extract additional context about what should be done to create a ui component/page based on the user's message, search query, and conversation history, files. Don't halucinate and be on point." 42 | ), 43 | }); 44 | 45 | async execute({ 46 | message, 47 | searchQuery, 48 | absolutePathToCurrentFile, 49 | context, 50 | }: z.infer): Promise<{ 51 | content: Array<{ type: "text"; text: string }>; 52 | }> { 53 | try { 54 | const response = await twentyFirstClient.post<{ 55 | data1: { text: string }; 56 | data2: { text: string }; 57 | data3: { text: string }; 58 | }>("/api/create-ui-variation", { 59 | message, 60 | searchQuery, 61 | fileContent: await getContentOfFile(absolutePathToCurrentFile), 62 | context, 63 | }); 64 | 65 | if (response.status !== 200) { 66 | open("https://21st.dev/settings/billing"); 67 | return { 68 | content: [ 69 | { 70 | type: "text" as const, 71 | // @ts-ignore 72 | text: response.data.text as string, 73 | }, 74 | ], 75 | }; 76 | } 77 | 78 | const server = new CallbackServer(); 79 | const { data } = await server.promptUser({ 80 | initialData: { 81 | data1: response.data.data1, 82 | data2: response.data.data2, 83 | data3: response.data.data3, 84 | }, 85 | }); 86 | 87 | const componentData = data || { 88 | text: "No component data received. Please try again.", 89 | }; 90 | 91 | const responseToUser = ` 92 | ${"```tsx"} 93 | ${componentData.code} 94 | ${"```"} 95 | 96 | 97 | You're provided with a code snippet for a UI component. Your task is to integrate it into user's codebase. 98 | Don't change the code of this component, just add it, integrate it, make sure that you add all imports, if you have missing ui components from this code -use shadcn/ui for imports. 99 | 100 | 101 | ### Styling instructions 102 | Check your globals.css and tailwind.config.js for brand color variables 103 | Replace hardcoded colors with your brand color variables where appropriate 104 | Example: replace 'bg-blue-500' with 'bg-brand' if you have brand colors defined 105 | 106 | ## Integration instructions 107 | Make sure all required dependencies are installed 108 | Check component's TypeScript types match your project's conventions 109 | Verify all imported components and utilities exist in your project 110 | 111 | ## Shadcn/ui instructions 112 | After you add the component, make sure to add the component to the project. If you can't resolve components from demo code, 113 | Make sure to install shadcn/ui components from the demo code missing imports 114 | 115 | Examples of importing shadcn/ui components: 116 | if these imports can't be resolved: 117 | ${"```tsx"} 118 | import { 119 | Table 120 | } from "@/components/ui/table" 121 | import { Textarea } from "@/components/ui/textarea" 122 | ${"```"} 123 | 124 | then run this command: 125 | ${"```bash"} 126 | npx shadcn@latest add table textarea 127 | ${"```"} 128 | 129 | 130 | `; 131 | 132 | return { 133 | content: [ 134 | { 135 | type: "text" as const, 136 | text: responseToUser, 137 | }, 138 | ], 139 | }; 140 | } catch (error) { 141 | console.error("Error executing tool", error); 142 | throw error; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/tools/fetch-ui.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { BaseTool } from "../utils/base-tool.js"; 3 | import { twentyFirstClient } from "../utils/http-client.js"; 4 | 5 | const FETCH_UI_TOOL_NAME = "21st_magic_component_inspiration"; 6 | const FETCH_UI_TOOL_DESCRIPTION = ` 7 | "Use this tool when the user wants to see component, get inspiration, or /21st fetch data and previews from 21st.dev. This tool returns the JSON data of matching components without generating new code. This tool ONLY returns the text snippet for that UI component. 8 | After calling this tool, you must edit or add files to integrate the snippet into the codebase." 9 | `; 10 | 11 | interface FetchUiResponse { 12 | text: string; 13 | } 14 | 15 | export class FetchUiTool extends BaseTool { 16 | name = FETCH_UI_TOOL_NAME; 17 | description = FETCH_UI_TOOL_DESCRIPTION; 18 | 19 | schema = z.object({ 20 | message: z.string().describe("Full users message"), 21 | searchQuery: z 22 | .string() 23 | .describe( 24 | "Search query for 21st.dev (library for searching UI components) to find a UI component that matches the user's message. Must be a two-four words max or phrase" 25 | ), 26 | }); 27 | 28 | async execute({ message, searchQuery }: z.infer) { 29 | try { 30 | const { data } = await twentyFirstClient.post( 31 | "/api/fetch-ui", 32 | { 33 | message, 34 | searchQuery, 35 | } 36 | ); 37 | 38 | return { 39 | content: [ 40 | { 41 | type: "text" as const, 42 | text: data.text, 43 | }, 44 | ], 45 | }; 46 | } catch (error) { 47 | console.error("Error executing tool", error); 48 | throw error; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/tools/logo-search.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { promises as fs } from "fs"; 3 | import { BaseTool } from "../utils/base-tool.js"; 4 | 5 | // Types for SVGL API responses 6 | interface ThemeOptions { 7 | dark: string; 8 | light: string; 9 | } 10 | 11 | interface SVGLogo { 12 | id?: number; 13 | title: string; 14 | category: string | string[]; 15 | route: string | ThemeOptions; 16 | wordmark?: string | ThemeOptions; 17 | brandUrl?: string; 18 | url: string; 19 | } 20 | 21 | const LOGO_TOOL_NAME = "logo_search"; 22 | const LOGO_TOOL_DESCRIPTION = ` 23 | Search and return logos in specified format (JSX, TSX, SVG). 24 | Supports single and multiple logo searches with category filtering. 25 | Can return logos in different themes (light/dark) if available. 26 | 27 | When to use this tool: 28 | 1. When user types "/logo" command (e.g., "/logo GitHub") 29 | 2. When user asks to add a company logo that's not in the local project 30 | 31 | Example queries: 32 | - Single company: ["discord"] 33 | - Multiple companies: ["discord", "github", "slack"] 34 | - Specific brand: ["microsoft office"] 35 | - Command style: "/logo GitHub" -> ["github"] 36 | - Request style: "Add Discord logo to the project" -> ["discord"] 37 | 38 | Format options: 39 | - TSX: Returns TypeScript React component 40 | - JSX: Returns JavaScript React component 41 | - SVG: Returns raw SVG markup 42 | 43 | Each result includes: 44 | - Component name (e.g., DiscordIcon) 45 | - Component code 46 | - Import instructions 47 | `; 48 | 49 | export class LogoSearchTool extends BaseTool { 50 | name = LOGO_TOOL_NAME; 51 | description = LOGO_TOOL_DESCRIPTION; 52 | 53 | schema = z.object({ 54 | queries: z 55 | .array(z.string()) 56 | .describe("List of company names to search for logos"), 57 | format: z.enum(["JSX", "TSX", "SVG"]).describe("Output format"), 58 | }); 59 | 60 | private async fetchLogos(query: string): Promise { 61 | const baseUrl = "https://api.svgl.app"; 62 | const url = `${baseUrl}?search=${encodeURIComponent(query)}`; 63 | 64 | try { 65 | const response = await fetch(url); 66 | if (!response.ok) { 67 | if (response.status === 404) { 68 | return []; // Return empty array for not found instead of throwing 69 | } 70 | throw new Error(`SVGL API error: ${response.statusText}`); 71 | } 72 | const data = await response.json(); 73 | return Array.isArray(data) ? data : []; 74 | } catch (error) { 75 | console.error( 76 | `[${LOGO_TOOL_NAME}] Error fetching logos for ${query}:`, 77 | error 78 | ); 79 | return []; // Return empty array on error 80 | } 81 | } 82 | 83 | private async fetchSVGContent(url: string): Promise { 84 | try { 85 | const response = await fetch(url); 86 | if (!response.ok) { 87 | throw new Error(`Failed to fetch SVG content: ${response.statusText}`); 88 | } 89 | return await response.text(); 90 | } catch (error) { 91 | console.error("Error fetching SVG content:", error); 92 | throw error; 93 | } 94 | } 95 | 96 | private async convertToFormat( 97 | svgContent: string, 98 | format: "JSX" | "TSX" | "SVG", 99 | componentName: string = "Icon" 100 | ): Promise { 101 | if (format === "SVG") { 102 | return svgContent; 103 | } 104 | 105 | // Convert to JSX/TSX 106 | const jsxContent = svgContent 107 | .replace(/class=/g, "className=") 108 | .replace(/style="([^"]*)"/g, (match: string, styles: string) => { 109 | const cssObject = styles 110 | .split(";") 111 | .filter(Boolean) 112 | .map((style: string) => { 113 | const [property, value] = style 114 | .split(":") 115 | .map((s: string) => s.trim()); 116 | const camelProperty = property.replace(/-([a-z])/g, (g: string) => 117 | g[1].toUpperCase() 118 | ); 119 | return `${camelProperty}: "${value}"`; 120 | }) 121 | .join(", "); 122 | return `style={{${cssObject}}}`; 123 | }); 124 | 125 | // Make sure we use the full component name (with Icon suffix) 126 | const finalComponentName = componentName.endsWith("Icon") 127 | ? componentName 128 | : `${componentName}Icon`; 129 | return format === "TSX" 130 | ? `const ${finalComponentName}: React.FC = () => (${jsxContent})` 131 | : `function ${finalComponentName}() { return (${jsxContent}) }`; 132 | } 133 | 134 | private async saveTestResult(data: { 135 | queries: string[]; 136 | format: string; 137 | successful: Array<{ query: string; content: string }>; 138 | failed: Array<{ query: string; message: string }>; 139 | }) { 140 | const timestamp = new Date().toISOString(); 141 | const filename = `test-results/logo-search-${timestamp.replace( 142 | /[:.]/g, 143 | "-" 144 | )}.json`; 145 | 146 | // Format response as component structure 147 | const foundIcons = data.successful.map((r) => { 148 | const title = r.content.split("\n")[0].replace("// ", "").split(" (")[0]; 149 | const componentName = 150 | title 151 | .split(" ") 152 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 153 | .join("") 154 | .replace(/[^a-zA-Z0-9]/g, "") + "Icon"; 155 | 156 | return { 157 | icon: componentName, 158 | code: r.content.split("\n").slice(1).join("\n"), 159 | }; 160 | }); 161 | 162 | const missingIcons = data.failed.map((f) => ({ 163 | icon: f.query, 164 | alternatives: [ 165 | "Search for SVG version on the official website", 166 | "Check other icon libraries (e.g., heroicons, lucide)", 167 | "Request SVG file from the user", 168 | ], 169 | })); 170 | 171 | const response = { 172 | icons: foundIcons, 173 | notFound: missingIcons, 174 | setup: [ 175 | "1. Add these icons to your project:", 176 | foundIcons 177 | .map((c) => ` ${c.icon}.${data.format.toLowerCase()}`) 178 | .join("\n"), 179 | "2. Import and use like this:", 180 | "```tsx", 181 | "import { " + 182 | foundIcons.map((c) => c.icon).join(", ") + 183 | " } from '@/icons';", 184 | "```", 185 | ].join("\n"), 186 | }; 187 | 188 | try { 189 | await fs.writeFile(filename, JSON.stringify(response, null, 2), "utf-8"); 190 | console.log(`[${LOGO_TOOL_NAME}] Test results saved to ${filename}`); 191 | } catch (error) { 192 | console.error(`[${LOGO_TOOL_NAME}] Error saving test results:`, error); 193 | } 194 | } 195 | 196 | async execute({ queries, format }: z.infer) { 197 | console.log( 198 | `[${LOGO_TOOL_NAME}] Starting logo search for: ${queries.join( 199 | ", " 200 | )} in ${format} format` 201 | ); 202 | try { 203 | // Process all queries 204 | const results = await Promise.all( 205 | queries.map(async (query) => { 206 | try { 207 | console.log(`[${LOGO_TOOL_NAME}] Fetching logos for ${query}...`); 208 | const logos = await this.fetchLogos(query); 209 | 210 | if (logos.length === 0) { 211 | console.log(`[${LOGO_TOOL_NAME}] No logo found for ${query}`); 212 | return { 213 | query, 214 | success: false, 215 | message: `No logo found for: "${query}"`, 216 | }; 217 | } 218 | 219 | const logo = logos[0]; 220 | console.log( 221 | `[${LOGO_TOOL_NAME}] Processing logo for: ${logo.title}` 222 | ); 223 | 224 | const svgUrl = 225 | typeof logo.route === "string" ? logo.route : logo.route.light; 226 | console.log(`[${LOGO_TOOL_NAME}] Fetching SVG from: ${svgUrl}`); 227 | const svgContent = await this.fetchSVGContent(svgUrl); 228 | 229 | console.log(`[${LOGO_TOOL_NAME}] Converting to ${format} format`); 230 | const formattedContent = await this.convertToFormat( 231 | svgContent, 232 | format, 233 | logo.title + "Icon" 234 | ); 235 | 236 | console.log(`[${LOGO_TOOL_NAME}] Successfully processed ${query}`); 237 | return { 238 | query, 239 | success: true, 240 | content: `// ${logo.title} (${logo.url})\n${formattedContent}`, 241 | }; 242 | } catch (error) { 243 | console.error( 244 | `[${LOGO_TOOL_NAME}] Error processing ${query}:`, 245 | error 246 | ); 247 | return { 248 | query, 249 | success: false, 250 | message: error instanceof Error ? error.message : "Unknown error", 251 | }; 252 | } 253 | }) 254 | ); 255 | 256 | // Prepare summary 257 | const successful = results.filter((r) => r.success); 258 | const failed = results.filter((r) => !r.success); 259 | 260 | console.log(`[${LOGO_TOOL_NAME}] Results summary:`); 261 | console.log( 262 | `[${LOGO_TOOL_NAME}] Successfully processed: ${successful.length}` 263 | ); 264 | console.log(`[${LOGO_TOOL_NAME}] Failed to process: ${failed.length}`); 265 | 266 | // Save test results 267 | await this.saveTestResult({ 268 | queries, 269 | format, 270 | successful: successful 271 | .filter( 272 | (r): r is typeof r & { content: string } => r.content !== undefined 273 | ) 274 | .map((r) => ({ 275 | query: r.query, 276 | content: r.content, 277 | })), 278 | failed: failed 279 | .filter( 280 | (r): r is typeof r & { message: string } => r.message !== undefined 281 | ) 282 | .map((r) => ({ 283 | query: r.query, 284 | message: r.message, 285 | })), 286 | }); 287 | 288 | // Format response as component structure 289 | const foundIcons = successful.map((r) => { 290 | const title = 291 | r.content?.split("\n")[0].replace("// ", "").split(" (")[0] || ""; 292 | const componentName = 293 | title 294 | .split(" ") 295 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 296 | .join("") 297 | .replace(/[^a-zA-Z0-9]/g, "") + "Icon"; 298 | 299 | return { 300 | icon: componentName, 301 | code: r.content?.split("\n").slice(1).join("\n") || "", 302 | }; 303 | }); 304 | 305 | const missingIcons = failed.map((f) => ({ 306 | icon: f.query, 307 | alternatives: [ 308 | "Search for SVG version on the official website", 309 | "Check other icon libraries (e.g., heroicons, lucide)", 310 | "Request SVG file from the user", 311 | ], 312 | })); 313 | 314 | const response = { 315 | icons: foundIcons, 316 | notFound: missingIcons, 317 | setup: [ 318 | "1. Add these icons to your project:", 319 | foundIcons 320 | .map((c) => ` ${c.icon}.${format.toLowerCase()}`) 321 | .join("\n"), 322 | "2. Import and use like this:", 323 | "```tsx", 324 | "import { " + 325 | foundIcons.map((c) => c.icon).join(", ") + 326 | " } from '@/icons';", 327 | "```", 328 | ].join("\n"), 329 | }; 330 | 331 | // Log results 332 | return { 333 | content: [ 334 | { 335 | type: "text" as const, 336 | text: JSON.stringify(response, null, 2), 337 | }, 338 | ], 339 | }; 340 | } catch (error) { 341 | // Log error 342 | console.error(`[${LOGO_TOOL_NAME}] Error:`, error); 343 | throw error; 344 | } 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/tools/refine-ui.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { BaseTool } from "../utils/base-tool.js"; 3 | import { twentyFirstClient } from "../utils/http-client.js"; 4 | import { getContentOfFile } from "../utils/get-content-of-file.js"; 5 | 6 | const REFINE_UI_TOOL_NAME = "21st_magic_component_refiner"; 7 | const REFINE_UI_TOOL_DESCRIPTION = ` 8 | "Use this tool when the user requests to re-design/refine/improve current UI component with /ui or /21 commands, 9 | or when context is about improving, or refining UI for a React component or molecule (NOT for big pages). 10 | This tool improves UI of components and returns redesigned version of the component and instructions on how to implement it." 11 | `; 12 | 13 | interface RefineUiResponse { 14 | text: string; 15 | } 16 | 17 | export class RefineUiTool extends BaseTool { 18 | name = REFINE_UI_TOOL_NAME; 19 | description = REFINE_UI_TOOL_DESCRIPTION; 20 | 21 | schema = z.object({ 22 | userMessage: z.string().describe("Full user's message about UI refinement"), 23 | absolutePathToRefiningFile: z 24 | .string() 25 | .describe("Absolute path to the file that needs to be refined"), 26 | context: z 27 | .string() 28 | .describe( 29 | "Extract the specific UI elements and aspects that need improvement based on user messages, code, and conversation history. Identify exactly which components (buttons, forms, modals, etc.) the user is referring to and what aspects (styling, layout, responsiveness, etc.) they want to enhance. Do not include generic improvements - focus only on what the user explicitly mentions or what can be reasonably inferred from the available context. If nothing specific is mentioned or you cannot determine what needs improvement, return an empty string." 30 | ), 31 | }); 32 | 33 | async execute({ 34 | userMessage, 35 | absolutePathToRefiningFile, 36 | context, 37 | }: z.infer) { 38 | try { 39 | const { data } = await twentyFirstClient.post( 40 | "/api/refine-ui", 41 | { 42 | userMessage, 43 | fileContent: await getContentOfFile(absolutePathToRefiningFile), 44 | context, 45 | } 46 | ); 47 | 48 | return { 49 | content: [ 50 | { 51 | type: "text" as const, 52 | text: data.text, 53 | }, 54 | ], 55 | }; 56 | } catch (error) { 57 | console.error("Error executing tool", error); 58 | throw error; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/base-tool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { z } from "zod"; 3 | 4 | export abstract class BaseTool { 5 | abstract name: string; 6 | abstract description: string; 7 | abstract schema: z.ZodObject; 8 | 9 | register(server: McpServer) { 10 | server.tool( 11 | this.name, 12 | this.description, 13 | this.schema.shape, 14 | this.execute.bind(this) 15 | ); 16 | } 17 | 18 | abstract execute(args: z.infer): Promise<{ 19 | content: Array<{ type: "text"; text: string }>; 20 | }>; 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/callback-server.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { createServer, IncomingMessage, Server, ServerResponse } from "http"; 3 | import net from "net"; 4 | import open from "open"; 5 | import path from "path"; 6 | import { fileURLToPath, parse as parseUrl } from "url"; 7 | import { twentyFirstClient } from "./http-client.js"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | 12 | export interface CallbackResponse { 13 | data?: any; 14 | } 15 | 16 | export interface CallbackServerConfig { 17 | initialData?: any; 18 | timeout?: number; 19 | } 20 | 21 | export class CallbackServer { 22 | private server: Server | null = null; 23 | private port: number; 24 | private sessionId = Math.random().toString(36).substring(7); 25 | private timeoutId?: NodeJS.Timeout; 26 | private mimeTypes: Record = { 27 | ".html": "text/html", 28 | ".js": "text/javascript", 29 | ".css": "text/css", 30 | ".json": "application/json", 31 | ".png": "image/png", 32 | ".jpg": "image/jpg", 33 | ".gif": "image/gif", 34 | ".ico": "image/x-icon", 35 | }; 36 | 37 | constructor(port = 3333) { 38 | this.port = port; 39 | } 40 | 41 | private parseBodyJson(req: IncomingMessage): Promise { 42 | return new Promise((resolve) => { 43 | let body = ""; 44 | req.on("data", (chunk) => { 45 | body += chunk.toString(); 46 | }); 47 | req.on("end", () => { 48 | try { 49 | const data = body ? JSON.parse(body) : {}; 50 | resolve(data); 51 | } catch (e) { 52 | resolve({}); 53 | } 54 | }); 55 | }); 56 | } 57 | 58 | private getRouteParams( 59 | url: string, 60 | pattern: string 61 | ): Record | null { 62 | const urlParts = url.split("/").filter(Boolean); 63 | const patternParts = pattern.split("/").filter(Boolean); 64 | 65 | if (urlParts.length !== patternParts.length) return null; 66 | 67 | const params: Record = {}; 68 | 69 | for (let i = 0; i < patternParts.length; i++) { 70 | if (patternParts[i].startsWith(":")) { 71 | const paramName = patternParts[i].substring(1); 72 | params[paramName] = urlParts[i]; 73 | } else if (patternParts[i] !== urlParts[i]) { 74 | return null; 75 | } 76 | } 77 | 78 | return params; 79 | } 80 | 81 | private async serveStatic(res: ServerResponse, filepath: string) { 82 | try { 83 | const stat = await fs.promises.stat(filepath); 84 | 85 | if (stat.isDirectory()) { 86 | filepath = path.join(filepath, "index.html"); 87 | } 88 | 89 | const ext = path.extname(filepath); 90 | const contentType = this.mimeTypes[ext] || "application/octet-stream"; 91 | 92 | const content = await fs.promises.readFile(filepath); 93 | 94 | res.writeHead(200, { "Content-Type": contentType }); 95 | res.end(content, "utf-8"); 96 | } catch (err) { 97 | if ((err as NodeJS.ErrnoException).code === "ENOENT") { 98 | const previewerPath = path.join(__dirname, "../previewer"); 99 | const indexPath = path.join(previewerPath, "index.html"); 100 | 101 | if (filepath !== indexPath) { 102 | await this.serveStatic(res, indexPath); 103 | } else { 104 | res.writeHead(404); 105 | res.end("Not found"); 106 | } 107 | } else { 108 | res.writeHead(500); 109 | res.end("Internal server error"); 110 | } 111 | } 112 | } 113 | 114 | private handleRequest = async (req: IncomingMessage, res: ServerResponse) => { 115 | const urlInfo = parseUrl(req.url || "/"); 116 | const pathname = urlInfo.pathname || "/"; 117 | 118 | // Handle callback route 119 | if (req.method === "GET" && pathname.startsWith("/callback/")) { 120 | const params = this.getRouteParams(pathname, "/callback/:id"); 121 | if (params && params.id === this.sessionId) { 122 | res.writeHead(200, { "Content-Type": "application/json" }); 123 | res.end( 124 | JSON.stringify({ status: "success", data: this.config?.initialData }) 125 | ); 126 | return; 127 | } else { 128 | res.writeHead(404, { "Content-Type": "application/json" }); 129 | res.end( 130 | JSON.stringify({ status: "error", message: "Session not found" }) 131 | ); 132 | return; 133 | } 134 | } 135 | 136 | // Handle callback post 137 | if (req.method === "POST" && pathname.startsWith("/callback/")) { 138 | const params = this.getRouteParams(pathname, "/callback/:id"); 139 | if (params && params.id === this.sessionId && this.promiseResolve) { 140 | if (this.timeoutId) clearTimeout(this.timeoutId); 141 | 142 | const body = await this.parseBodyJson(req); 143 | this.promiseResolve({ data: body || {} }); 144 | this.shutdown(); 145 | 146 | res.writeHead(200, { "Content-Type": "application/json" }); 147 | res.end(JSON.stringify({ status: "success" })); 148 | return; 149 | } else { 150 | res.writeHead(404, { "Content-Type": "application/json" }); 151 | res.end( 152 | JSON.stringify({ status: "error", message: "Session not found" }) 153 | ); 154 | return; 155 | } 156 | } 157 | 158 | // Handle fix-code-error route 159 | if (req.method === "POST" && pathname.startsWith("/fix-code-error/")) { 160 | const params = this.getRouteParams(pathname, "/fix-code-error/:id"); 161 | if (!params || params.id !== this.sessionId) { 162 | res.writeHead(404, { "Content-Type": "application/json" }); 163 | res.end( 164 | JSON.stringify({ status: "error", message: "Session not found" }) 165 | ); 166 | return; 167 | } 168 | 169 | const body = await this.parseBodyJson(req); 170 | const { code, errorMessage } = body; 171 | 172 | if (!code || !errorMessage) { 173 | res.writeHead(400, { "Content-Type": "application/json" }); 174 | res.end( 175 | JSON.stringify({ 176 | status: "error", 177 | message: "Missing code or errorMessage", 178 | }) 179 | ); 180 | return; 181 | } 182 | 183 | try { 184 | const response = await twentyFirstClient.post<{ fixedCode: string }>( 185 | "/api/fix-code-error", 186 | { code, errorMessage } 187 | ); 188 | 189 | if (response.status === 200) { 190 | res.writeHead(200, { "Content-Type": "application/json" }); 191 | res.end(JSON.stringify({ status: "success", data: response.data })); 192 | } else { 193 | res.writeHead(response.status, { 194 | "Content-Type": "application/json", 195 | }); 196 | res.end( 197 | JSON.stringify({ 198 | status: "error", 199 | message: response.data || "API Error", 200 | }) 201 | ); 202 | } 203 | } catch (error: any) { 204 | console.error("Error proxying /fix-code-error:", error); 205 | res.writeHead(500, { "Content-Type": "application/json" }); 206 | res.end( 207 | JSON.stringify({ 208 | status: "error", 209 | message: error.message || "Internal Server Error", 210 | }) 211 | ); 212 | } 213 | return; 214 | } 215 | 216 | // Handle host-component route 217 | if (req.method === "POST" && pathname.startsWith("/host-component")) { 218 | const body = await this.parseBodyJson(req); 219 | const { code } = body; 220 | 221 | if (!code) { 222 | res.writeHead(400, { "Content-Type": "application/json" }); 223 | res.end( 224 | JSON.stringify({ 225 | status: "error", 226 | message: "Missing code param in request body", 227 | }) 228 | ); 229 | return; 230 | } 231 | 232 | try { 233 | const response = await twentyFirstClient.post<{ url: string }>( 234 | "/api/host-component", 235 | { code } 236 | ); 237 | 238 | if (response.status === 200) { 239 | res.writeHead(200, { "Content-Type": "application/json" }); 240 | res.end(JSON.stringify({ status: "success", data: response.data })); 241 | } else { 242 | res.writeHead(response.status, { 243 | "Content-Type": "application/json", 244 | }); 245 | res.end( 246 | JSON.stringify({ 247 | status: "error", 248 | message: response.data || "API Error", 249 | }) 250 | ); 251 | } 252 | } catch (error: any) { 253 | console.error("Error proxying /host-component:", error); 254 | res.writeHead(500, { "Content-Type": "application/json" }); 255 | res.end( 256 | JSON.stringify({ 257 | status: "error", 258 | message: error.message || "Internal Server Error", 259 | }) 260 | ); 261 | } 262 | return; 263 | } 264 | 265 | // Serve static files or send index.html 266 | const previewerPath = path.join(__dirname, "../previewer"); 267 | const filePath = path.join( 268 | previewerPath, 269 | pathname === "/" ? "index.html" : pathname 270 | ); 271 | await this.serveStatic(res, filePath); 272 | }; 273 | 274 | private async shutdown(): Promise { 275 | if (this.server) { 276 | this.server.close(); 277 | this.server = null; 278 | } 279 | if (this.timeoutId) { 280 | clearTimeout(this.timeoutId); 281 | } 282 | } 283 | 284 | private isPortAvailable(port: number): Promise { 285 | return new Promise((resolve) => { 286 | const tester = net 287 | .createServer() 288 | .once("error", () => resolve(false)) 289 | .once("listening", () => { 290 | tester.close(); 291 | resolve(true); 292 | }) 293 | .listen(port, "127.0.0.1"); 294 | }); 295 | } 296 | 297 | private async findAvailablePort(): Promise { 298 | let port = this.port; 299 | for (let attempt = 0; attempt < 100; attempt++) { 300 | if (await this.isPortAvailable(port)) { 301 | return port; 302 | } 303 | port++; 304 | } 305 | throw new Error("Unable to find an available port after 100 attempts"); 306 | } 307 | 308 | private config?: CallbackServerConfig; 309 | private promiseResolve?: (value: CallbackResponse) => void; 310 | private promiseReject?: (reason: any) => void; 311 | 312 | async promptUser( 313 | config: CallbackServerConfig = {} 314 | ): Promise { 315 | const { initialData = null, timeout = 300000 } = config; 316 | this.config = config; 317 | 318 | try { 319 | const availablePort = await this.findAvailablePort(); 320 | this.server = createServer(this.handleRequest); 321 | this.server.listen(availablePort, "127.0.0.1"); 322 | 323 | return new Promise((resolve, reject) => { 324 | this.promiseResolve = resolve; 325 | this.promiseReject = reject; 326 | 327 | if (!this.server) { 328 | reject(new Error("Failed to start server")); 329 | return; 330 | } 331 | 332 | this.server.on("error", (error) => { 333 | if (this.promiseReject) this.promiseReject(error); 334 | }); 335 | 336 | this.timeoutId = setTimeout(() => { 337 | resolve({ data: { timedOut: true } }); 338 | this.shutdown(); 339 | }, timeout); 340 | 341 | const url = `http://127.0.0.1:${availablePort}?id=${this.sessionId}`; 342 | 343 | open(url).catch((error) => { 344 | console.warn("Failed to open browser:", error); 345 | resolve({ data: { browserOpenFailed: true } }); 346 | this.shutdown(); 347 | }); 348 | }); 349 | } catch (error) { 350 | await this.shutdown(); 351 | throw error; 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | apiKey?: string; 3 | } 4 | 5 | const parseArguments = (): Config => { 6 | const config: Config = {}; 7 | 8 | // Command line arguments override environment variables 9 | process.argv.forEach((arg) => { 10 | const keyValuePatterns = [ 11 | /^([A-Z_]+)=(.+)$/, // API_KEY=value format 12 | /^--([A-Z_]+)=(.+)$/, // --API_KEY=value format 13 | /^\/([A-Z_]+):(.+)$/, // /API_KEY:value format (Windows style) 14 | /^-([A-Z_]+)[ =](.+)$/, // -API_KEY value or -API_KEY=value format 15 | ]; 16 | 17 | for (const pattern of keyValuePatterns) { 18 | const match = arg.match(pattern); 19 | if (match) { 20 | const [, key, value] = match; 21 | if (key === "API_KEY") { 22 | // Strip surrounding quotes from the value 23 | const cleanValue = value.replaceAll('"', "").replaceAll("'", ""); 24 | config.apiKey = cleanValue; 25 | break; 26 | } 27 | } 28 | } 29 | }); 30 | 31 | return config; 32 | }; 33 | 34 | export const config = parseArguments(); 35 | -------------------------------------------------------------------------------- /src/utils/console.ts: -------------------------------------------------------------------------------- 1 | // Override console for Cursor's error handling 2 | 3 | const originalConsoleLog = console.log; 4 | const originalConsoleError = console.error; 5 | 6 | export function setupJsonConsole() { 7 | console.log = function (...args) { 8 | const message = args 9 | .map((arg) => { 10 | if (typeof arg === "object" || Array.isArray(arg)) { 11 | try { 12 | return JSON.stringify(arg); 13 | } catch (e) { 14 | return String(arg); 15 | } 16 | } 17 | return String(arg); 18 | }) 19 | .join(" "); 20 | 21 | originalConsoleLog( 22 | JSON.stringify({ 23 | jsonrpc: "2.0", 24 | method: "window/logMessage", 25 | params: { 26 | type: 3, 27 | message: message, 28 | }, 29 | }) 30 | ); 31 | }; 32 | 33 | console.error = function (...args) { 34 | const message = args 35 | .map((arg) => { 36 | if (typeof arg === "object" || Array.isArray(arg)) { 37 | try { 38 | return JSON.stringify(arg); 39 | } catch (e) { 40 | return String(arg); 41 | } 42 | } 43 | return String(arg); 44 | }) 45 | .join(" "); 46 | 47 | originalConsoleError( 48 | JSON.stringify({ 49 | jsonrpc: "2.0", 50 | method: "window/logMessage", 51 | params: { 52 | type: 1, 53 | message: message, 54 | }, 55 | }) 56 | ); 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/get-content-of-file.ts: -------------------------------------------------------------------------------- 1 | export async function getContentOfFile(path: string): Promise { 2 | try { 3 | const fs = await import("fs/promises"); 4 | return await fs.readFile(path, "utf-8"); 5 | } catch (error) { 6 | console.error(`Error reading file ${path}:`, error); 7 | return ""; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/http-client.test.ts: -------------------------------------------------------------------------------- 1 | import { BASE_URL } from "./http-client.js"; 2 | 3 | describe("http-client", () => { 4 | it("should use production URL in production environment", () => { 5 | expect(BASE_URL).toBe("https://magic.21st.dev"); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/utils/http-client.ts: -------------------------------------------------------------------------------- 1 | import { config } from "./config.js"; 2 | 3 | const TWENTY_FIRST_API_KEY = 4 | config.apiKey || process.env.TWENTY_FIRST_API_KEY || process.env.API_KEY; 5 | 6 | const isTesting = process.env.DEBUG === "true" ? true : false; 7 | export const BASE_URL = isTesting 8 | ? "http://localhost:3005" 9 | : "https://magic.21st.dev"; 10 | 11 | type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; 12 | 13 | interface HttpClient { 14 | get( 15 | endpoint: string, 16 | options?: RequestInit 17 | ): Promise<{ status: number; data: T }>; 18 | post( 19 | endpoint: string, 20 | data?: unknown, 21 | options?: RequestInit 22 | ): Promise<{ status: number; data: T }>; 23 | put( 24 | endpoint: string, 25 | data?: unknown, 26 | options?: RequestInit 27 | ): Promise<{ status: number; data: T }>; 28 | delete( 29 | endpoint: string, 30 | data?: unknown, 31 | options?: RequestInit 32 | ): Promise<{ status: number; data: T }>; 33 | patch( 34 | endpoint: string, 35 | data?: unknown, 36 | options?: RequestInit 37 | ): Promise<{ status: number; data: T }>; 38 | } 39 | 40 | const createMethod = (method: HttpMethod) => { 41 | return async ( 42 | endpoint: string, 43 | data?: unknown, 44 | options: RequestInit = {} 45 | ) => { 46 | const headers: HeadersInit = { 47 | "Content-Type": "application/json", 48 | ...(TWENTY_FIRST_API_KEY ? { "x-api-key": TWENTY_FIRST_API_KEY } : {}), 49 | ...options.headers, 50 | }; 51 | 52 | console.log("BASE_URL", BASE_URL); 53 | 54 | const response = await fetch(`${BASE_URL}${endpoint}`, { 55 | ...options, 56 | method, 57 | headers, 58 | ...(data ? { body: JSON.stringify(data) } : {}), 59 | }); 60 | 61 | console.log("response", response); 62 | return { status: response.status, data: (await response.json()) as T }; 63 | }; 64 | }; 65 | 66 | export const twentyFirstClient: HttpClient = { 67 | get: createMethod("GET"), 68 | post: createMethod("POST"), 69 | put: createMethod("PUT"), 70 | delete: createMethod("DELETE"), 71 | patch: createMethod("PATCH"), 72 | }; 73 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "types": ["node", "jest"] 14 | }, 15 | "include": ["src/**/*", "preview-build"], 16 | "exclude": ["node_modules", "dist"] 17 | } 18 | --------------------------------------------------------------------------------