UNPKG

mcpgen

Version:

Zero-friction MCP Server Generator

1,020 lines (871 loc) 30.6 kB
#!/usr/bin/env node /** * mcpgen - Minimal MCP Server Generator * * A state-of-the-art tool that creates custom MCP servers with zero friction. * * Features: * - Interactive CLI workflow * - Multiple data source options (in-memory, file, database, API) * - Advanced AI capabilities (vector embeddings, document processing, image analysis) * - One-click cloud deployment (AWS, GCP, Azure, Vercel, Heroku, Docker) * - Schema inference and TypeScript interface generation * - Ready-to-use tool templates optimized for Claude * - Custom tools creation with standardized templates * - Zero configuration deployment * - Built for performance and extensibility * * Created by @plawlost (Yaz Celebi) */ console.log("MCPGen v1.0.4 - Minimal MCP Server Generator"); console.log("Created by @plawlost (Yaz Celebi)"); async function startGenerator() { const fs = await import('fs/promises'); const path = await import('path'); const { execSync } = await import('child_process'); const readline = await import('readline'); const { fileURLToPath } = await import('url'); // Set up readline interface const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // ANSI colors for terminal output const colors = { reset: "\x1b[0m", bright: "\x1b[1m", dim: "\x1b[2m", blue: "\x1b[34m", green: "\x1b[32m", red: "\x1b[31m", yellow: "\x1b[33m", cyan: "\x1b[36m", gray: "\x1b[90m", magenta: "\x1b[35m" }; // Project configuration const config = { projectName: '', projectPath: '', description: '', dataSource: '', tools: [], customTools: [], deployment: '', useConfigFile: false, configFile: '' }; // Helper function to ask questions const askQuestion = (question, defaultValue = '') => { return new Promise((resolve) => { const defaultText = defaultValue ? ` (${defaultValue})` : ''; rl.question(`? ${question}${defaultText}: `, (answer) => { resolve(answer || defaultValue); }); }); }; // Check if the user wants to use a config file const checkForConfigFile = async () => { const useConfigFile = await askQuestion("Do you want to use a configuration file (y/n)", "n"); if (useConfigFile.toLowerCase() === 'y') { config.useConfigFile = true; config.configFile = await askQuestion("Enter path to config file", "mcpgen.config.json"); try { const configContent = await fs.readFile(config.configFile, 'utf8'); const configData = JSON.parse(configContent); // Apply configuration from file Object.assign(config, configData); console.log(`\n${colors.green}✅ Loaded configuration from ${config.configFile}${colors.reset}`); console.log(`${colors.dim}Project: ${config.projectName}${colors.reset}`); console.log(`${colors.dim}Data source: ${config.dataSource}${colors.reset}`); console.log(`${colors.dim}Tools: ${config.tools.join(', ')}${colors.reset}`); if (config.customTools && config.customTools.length > 0) { console.log(`${colors.dim}Custom tools: ${config.customTools.map(t => t.name).join(', ')}${colors.reset}`); } if (config.deployment && config.deployment !== 'none') { console.log(`${colors.dim}Deployment: ${config.deployment}${colors.reset}`); } return true; } catch (error) { console.error(`\n${colors.red}Error loading config file: ${error.message}${colors.reset}`); const createNew = await askQuestion("Would you like to create a new configuration interactively (y/n)", "y"); if (createNew.toLowerCase() !== 'y') { console.log("Exiting..."); process.exit(0); } config.useConfigFile = false; } } return false; }; // Helper to save the current configuration to a file const saveConfiguration = async () => { const saveConfig = await askQuestion("Would you like to save this configuration for future use (y/n)", "y"); if (saveConfig.toLowerCase() === 'y') { const configFileName = await askQuestion("Enter config file name", "mcpgen.config.json"); const configToSave = { projectName: config.projectName, projectPath: config.projectPath, description: config.description, dataSource: config.dataSource, tools: config.tools, customTools: config.customTools, deployment: config.deployment }; await fs.writeFile(configFileName, JSON.stringify(configToSave, null, 2)); console.log(`\n${colors.green}✅ Configuration saved to ${configFileName}${colors.reset}`); console.log(`${colors.dim}You can use this configuration in the future with:${colors.reset}`); console.log(`${colors.dim}mcpgen --config ${configFileName}${colors.reset}`); } }; console.log("\nWelcome to MCPGen!"); console.log("This tool helps you create custom MCP servers for Claude and other AI assistants."); // Check for config file const configLoaded = await checkForConfigFile(); if (!configLoaded) { // Project setup console.log("\nProject Setup"); config.projectName = await askQuestion("Project name", "my-mcp"); config.projectPath = await askQuestion("Project path", `./${config.projectName}`); config.description = await askQuestion("Description", "Custom MCP server"); // Data source selection console.log("\nData Source Selection"); console.log("1. Mock Data (in-memory)"); console.log("2. JSON File"); console.log("3. External API"); console.log("4. PostgreSQL"); console.log("5. MongoDB"); console.log("6. MySQL/MariaDB"); console.log("7. Supabase"); const dataSourceChoice = await askQuestion("Select data source (1-7)", "1"); switch(dataSourceChoice) { case "1": config.dataSource = "mock"; break; case "2": config.dataSource = "jsonFile"; break; case "3": config.dataSource = "api"; break; case "4": config.dataSource = "postgres"; break; case "5": config.dataSource = "mongodb"; break; case "6": config.dataSource = "mysql"; break; case "7": config.dataSource = "supabase"; break; default: config.dataSource = "mock"; } // Tool selection console.log("\nTool Selection"); console.log("1. search - Search for information"); console.log("2. getItem - Get detailed information about a specific item"); console.log("3. createItem - Create a new item"); console.log("4. getWeather - Get weather information for a location"); console.log("5. vectorEmbed - Generate vector embeddings for semantic search"); console.log("6. analyzeImage - Analyze and extract information from images"); console.log("7. processDocument - Extract and process information from documents"); console.log("8. visualizeData - Create data visualizations from datasets"); console.log("9. Custom tool - Define your own tool"); const toolsInput = await askQuestion("Select tools (comma-separated numbers, or 'a' for all)", "1,2"); // Process tool selection const toolMap = { "1": "search", "2": "getItem", "3": "createItem", "4": "getWeather", "5": "vectorEmbed", "6": "analyzeImage", "7": "processDocument", "8": "visualizeData" }; if (toolsInput.toLowerCase() === 'a') { config.tools = Object.values(toolMap); // Custom tools will be handled separately } else { // Parse tool selection, separating custom tools (option 9) const toolSelections = toolsInput .split(',') .map(num => num.trim()); // Standard tools config.tools = toolSelections .filter(num => num !== '9' && toolMap[num]) .map(num => toolMap[num]); // Check for custom tools if (toolSelections.includes('9')) { await defineCustomTools(); } } // Ask if user wants to add custom tools even if not initially selected if (!toolsInput.includes('9') && toolsInput.toLowerCase() !== 'a') { const addCustom = await askQuestion("Would you like to add custom tools (y/n)", "n"); if (addCustom.toLowerCase() === 'y') { await defineCustomTools(); } } // Cloud deployment selection console.log("\nCloud Deployment"); console.log("1. None (local only)"); console.log("2. AWS Lambda"); console.log("3. Google Cloud Functions"); console.log("4. Azure Functions"); console.log("5. Vercel"); console.log("6. Heroku"); console.log("7. Docker"); const deploymentChoice = await askQuestion("Select deployment target (1-7)", "1"); switch(deploymentChoice) { case "1": config.deployment = "none"; break; case "2": config.deployment = "aws"; break; case "3": config.deployment = "gcp"; break; case "4": config.deployment = "azure"; break; case "5": config.deployment = "vercel"; break; case "6": config.deployment = "heroku"; break; case "7": config.deployment = "docker"; break; default: config.deployment = "none"; } // Save configuration if desired await saveConfiguration(); } // Function to define custom tools interactively async function defineCustomTools() { let addingTools = true; while (addingTools) { console.log(`\n${colors.blue}Custom Tool Definition${colors.reset}`); const customTool = { name: await askQuestion("Tool name (camelCase, e.g. myCustomTool)"), description: await askQuestion("Tool description"), params: [] }; // Parameter definition console.log("\nParameter Definition"); console.log("Define parameters for your tool (leave name empty when done)"); let addingParams = true; while (addingParams) { const paramName = await askQuestion("Parameter name"); if (!paramName) { addingParams = false; continue; } console.log("Parameter type options:"); console.log("1. string"); console.log("2. number"); console.log("3. boolean"); console.log("4. array"); console.log("5. object"); const typeChoice = await askQuestion("Select parameter type (1-5)", "1"); let paramType; switch(typeChoice) { case "1": paramType = "string"; break; case "2": paramType = "number"; break; case "3": paramType = "boolean"; break; case "4": paramType = "array"; break; case "5": paramType = "object"; break; default: paramType = "string"; } const isRequired = await askQuestion("Is this parameter required (y/n)", "y"); if (isRequired.toLowerCase() !== 'y') { paramType += '?'; } customTool.params.push({ name: paramName, type: paramType }); } // Implementation template console.log("\nImplementation Template"); console.log("Choose an implementation template:"); console.log("1. Basic (returns static response)"); console.log("2. Database (queries the data source)"); console.log("3. API (makes external API request)"); console.log("4. Custom (enter your own implementation)"); const templateChoice = await askQuestion("Select template (1-4)", "1"); let implementationTemplate = ''; switch(templateChoice) { case "1": implementationTemplate = ` // Basic implementation for ${customTool.name} ${customTool.params.map(p => `const ${p.name} = args.${p.name};`).join('\n')} return { content: [ { type: "text", text: "This is a response from your custom tool: ${customTool.name}" }, { type: "text", text: "Add your implementation logic here" } ] };`; break; case "2": implementationTemplate = ` // Database implementation for ${customTool.name} ${customTool.params.map(p => `const ${p.name} = args.${p.name};`).join('\n')} // Query the data source const results = await fetchData("Your query here"); // Modify as needed return { content: [ { type: "text", text: "Results from database:" }, ...results.map(r => ({ type: "text", text: JSON.stringify(r) })) ] };`; break; case "3": implementationTemplate = ` // API implementation for ${customTool.name} ${customTool.params.map(p => `const ${p.name} = args.${p.name};`).join('\n')} // Make API request const response = await fetch("https://api.example.com/endpoint", { method: "GET", headers: { "Content-Type": "application/json" } }); const data = await response.json(); return { content: [ { type: "text", text: "API Response:" }, { type: "text", text: JSON.stringify(data) } ] };`; break; case "4": console.log("Enter your custom implementation below (type 'END' on a new line when finished):"); let customImpl = ''; let line = ''; while (true) { line = await askQuestion(""); if (line === 'END') break; customImpl += line + '\n'; } implementationTemplate = customImpl; break; } customTool.implementation = implementationTemplate; // Add the custom tool to the configuration if (!config.customTools) { config.customTools = []; } config.customTools.push(customTool); console.log(`\n${colors.green}✅ Custom tool "${customTool.name}" defined!${colors.reset}`); const defineAnother = await askQuestion("Would you like to define another custom tool (y/n)", "n"); if (defineAnother.toLowerCase() !== 'y') { addingTools = false; } } } // Generate project console.log("\nGenerating MCP Server..."); try { // Create project directory await fs.mkdir(config.projectPath, { recursive: true }); // Create a simple README.md const readmeContent = `# ${config.projectName} ${config.description} ## Features - Custom MCP server for Claude and other MCP-compatible clients - Data source: ${config.dataSource} - Tools: ${config.tools.join(', ')} ${config.customTools && config.customTools.length > 0 ? `- Custom tools: ${config.customTools.map(t => t.name).join(', ')}` : ''} ${config.deployment !== 'none' ? `- Deployment: ${config.deployment}` : ''} ## Getting Started \`\`\`bash npm install npm run build npm start \`\`\` ${config.deployment !== 'none' ? ` ## Deployment This project is configured for deployment to ${config.deployment.toUpperCase()}. \`\`\`bash npm run deploy \`\`\` ` : ''} ## Custom Tools ${config.customTools && config.customTools.length > 0 ? config.customTools.map(tool => `### ${tool.name}\n${tool.description}\n\nParameters:\n${tool.params.map(p => `- ${p.name}: ${p.type}`).join('\n')}\n`).join('\n') : 'No custom tools defined.'} Created with MCPGen by @plawlost (Yaz Celebi) `; await fs.writeFile( path.join(path.resolve(config.projectPath), 'README.md'), readmeContent ); // Create package.json with deployment script if needed const packageJson = { name: config.projectName, version: "1.0.0", description: config.description, main: "dist/main.js", type: "module", scripts: { build: "tsc", start: "node dist/main.js", dev: "tsc && node dist/main.js" }, dependencies: { "@modelcontextprotocol/sdk": "latest", "zod": "^3.21.4" }, devDependencies: { "@types/node": "^20.10.0", "typescript": "^5.2.2" } }; // Add deployment-specific dependencies and scripts switch(config.deployment) { case "aws": packageJson.scripts.deploy = "serverless deploy"; packageJson.devDependencies.serverless = "^3.34.0"; break; case "gcp": packageJson.scripts.deploy = "gcloud functions deploy"; break; case "azure": packageJson.scripts.deploy = "func azure functionapp publish"; break; case "vercel": packageJson.scripts.deploy = "vercel --prod"; packageJson.devDependencies.vercel = "^31.0.0"; break; case "heroku": packageJson.scripts.deploy = "git push heroku main"; break; case "docker": packageJson.scripts.deploy = "docker build -t ${config.projectName} . && docker run -p 3000:3000 ${config.projectName}"; break; } await fs.writeFile( path.join(path.resolve(config.projectPath), 'package.json'), JSON.stringify(packageJson, null, 2) ); // Create src directory await fs.mkdir(path.join(path.resolve(config.projectPath), 'src'), { recursive: true }); // Create main.ts with implementation let mainTsContent = `// Generated by MCPGen v1.0.4 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; // Create the MCP server const server = new McpServer({ name: "${config.projectName}", version: "1.0.0", description: "${config.description}" }); // Mock data source const mockData = [ { id: "1", title: "Item 1", description: "Description for item 1" }, { id: "2", title: "Item 2", description: "Description for item 2" }, { id: "3", title: "Item 3", description: "Description for item 3" } ]; // Basic data access functions async function searchItems(query) { return mockData.filter(item => item.title.toLowerCase().includes(query.toLowerCase()) || item.description.toLowerCase().includes(query.toLowerCase()) ); } async function getItemById(id) { return mockData.find(item => item.id === id); } ${config.tools.includes('search') ? ` // Search tool server.tool( "search", "Search for information", { query: z.string().describe("query"), limit: z.number().optional().describe("limit") }, async (args, _extra) => { const { query, limit = 5 } = args; const results = await searchItems(query); const limitedResults = results.slice(0, limit); return { content: [ { type: "text", text: "Results for '" + query + "':" }, ...limitedResults.map(r => ({ type: "text", text: "- " + r.title + ": " + (r.description || '') })) ] }; } );` : ''} ${config.tools.includes('getItem') ? ` // GetItem tool server.tool( "getItem", "Get detailed information about a specific item", { id: z.string().describe("id") }, async (args, _extra) => { const { id } = args; const item = await getItemById(id); if (!item) { return { content: [{ type: "text", text: "No item found with ID: " + id }] }; } return { content: [ { type: "text", text: "Item " + id + ": " + item.title }, { type: "text", text: item.description || 'No description available' } ] }; } );` : ''} ${config.customTools && config.customTools.length > 0 ? config.customTools.map(tool => ` // Custom tool: ${tool.name} server.tool( "${tool.name}", "${tool.description}", { ${tool.params.map(param => { const isOptional = param.type.endsWith('?'); const baseType = isOptional ? param.type.slice(0, -1) : param.type; let zodType; switch(baseType) { case 'string': zodType = 'z.string()'; break; case 'number': zodType = 'z.number()'; break; case 'boolean': zodType = 'z.boolean()'; break; case 'array': zodType = 'z.array(z.any())'; break; case 'object': zodType = 'z.record(z.string(), z.any())'; break; default: zodType = 'z.any()'; } if (isOptional) { zodType += '.optional()'; } return ` ${param.name}: ${zodType}.describe("${param.name}")`; }).join(',\n')} }, async (args, _extra) => { try { ${tool.implementation} } catch (error) { return { content: [ { type: "text", text: "Error in ${tool.name}: " + error.message } ] }; } } );`).join('\n\n') : ''} // Main function async function main() { try { // Register transport server.registerTransport( new StdioServerTransport({ input: process.stdin, output: process.stdout, }) ); console.error('MCP Server started'); console.error('Server name: ' + server.serverInfo.name); // Keep the process running process.stdin.resume(); } catch (error) { console.error('Error starting server: ' + error.message); process.exit(1); } } // Start the application main(); `; await fs.writeFile( path.join(path.resolve(config.projectPath), 'src', 'main.ts'), mainTsContent ); // Create deployment configuration files if (config.deployment !== 'none') { switch(config.deployment) { case "aws": // Create serverless.yml const serverlessYml = ` service: ${config.projectName} provider: name: aws runtime: nodejs18.x region: us-east-1 memorySize: 256 timeout: 30 functions: mcp: handler: dist/lambda.handler events: - http: path: / method: post `; await fs.writeFile( path.join(path.resolve(config.projectPath), 'serverless.yml'), serverlessYml ); // Create Lambda handler wrapper const lambdaWrapper = ` import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { handler as mcpHandler } from "./main.js"; export const handler = async (event, context) => { // Parse API Gateway event const body = JSON.parse(event.body || '{}'); try { const response = await mcpHandler(body); return { statusCode: 200, body: JSON.stringify(response) }; } catch (error) { return { statusCode: 500, body: JSON.stringify({ error: error.message }) }; } }; `; await fs.writeFile( path.join(path.resolve(config.projectPath), 'src', 'lambda.ts'), lambdaWrapper ); break; case "vercel": // Create vercel.json const vercelJson = `{ "version": 2, "builds": [ { "src": "dist/vercel.js", "use": "@vercel/node" } ], "routes": [ { "src": "/(.*)", "dest": "dist/vercel.js" } ] }`; // Create Vercel handler wrapper const vercelWrapper = ` import { handler as mcpHandler } from "./main.js"; export default async (req, res) => { if (req.method === 'OPTIONS') { res.status(200).end(); return; } try { const body = req.body || {}; const response = await mcpHandler(body); res.status(200).json(response); } catch (error) { res.status(500).json({ error: error.message }); } }; `; await fs.writeFile( path.join(path.resolve(config.projectPath), 'vercel.json'), vercelJson ); await fs.writeFile( path.join(path.resolve(config.projectPath), 'src', 'vercel.ts'), vercelWrapper ); break; case "docker": // Create Dockerfile const dockerfile = `FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install --production COPY dist ./dist EXPOSE 3000 CMD ["node", "dist/docker.js"] `; // Create Docker wrapper const dockerWrapper = ` import express from 'express'; import { handler as mcpHandler } from "./main.js"; const app = express(); const port = process.env.PORT || 3000; app.use(express.json()); app.post('/', async (req, res) => { try { const body = req.body || {}; const response = await mcpHandler(body); res.status(200).json(response); } catch (error) { res.status(500).json({ error: error.message }); } }); app.listen(port, () => { console.log(\`MCP server listening on port \${port}\`); }); `; // Create docker-compose.yml const dockerCompose = `version: '3' services: mcp-server: build: . ports: - "3000:3000" environment: - NODE_ENV=production `; await fs.writeFile( path.join(path.resolve(config.projectPath), 'Dockerfile'), dockerfile ); await fs.writeFile( path.join(path.resolve(config.projectPath), 'docker-compose.yml'), dockerCompose ); await fs.writeFile( path.join(path.resolve(config.projectPath), 'src', 'docker.ts'), dockerWrapper ); // Add express to package.json const dockerPackageJsonPath = path.join(path.resolve(config.projectPath), 'package.json'); const dockerPackageJson = JSON.parse(await fs.readFile(dockerPackageJsonPath, 'utf8')); dockerPackageJson.dependencies = dockerPackageJson.dependencies || {}; dockerPackageJson.dependencies.express = '^4.18.2'; await fs.writeFile(dockerPackageJsonPath, JSON.stringify(dockerPackageJson, null, 2)); break; } } // Create tsconfig.json const tsconfig = { compilerOptions: { target: "ES2022", module: "NodeNext", moduleResolution: "NodeNext", esModuleInterop: true, outDir: "dist", rootDir: "src", strict: true, skipLibCheck: true }, include: ["src/**/*"], exclude: ["node_modules", "dist"] }; await fs.writeFile( path.join(path.resolve(config.projectPath), 'tsconfig.json'), JSON.stringify(tsconfig, null, 2) ); // Create a template for custom tools const customToolTemplate = `/* * MCPGen Custom Tool Template * * Use this template to create new custom tools for your MCP server. * Copy this file, modify it, and add it to your project. */ // Import the tool to your main.ts file import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; /** * Register a custom tool with the MCP server * * @param server - The MCP server instance */ export function registerCustomTool(server: McpServer) { server.tool( "customToolName", // Tool name in camelCase "Description of what this tool does", // Tool description { // Define parameters with their types and descriptions param1: z.string().describe("Description of param1"), param2: z.number().optional().describe("Optional numeric parameter"), // Add more parameters as needed }, async (args, _extra) => { try { // Extract parameters const { param1, param2 = 42 } = args; // Implement your tool's logic here // For example, call an API, query a database, etc. const result = "This is a result: " + param1; // Return response with content return { content: [ { type: "text", text: "Custom tool response:" }, { type: "text", text: result } ] }; } catch (error) { // Handle errors gracefully return { content: [ { type: "text", text: "Error in custom tool: " + error.message } ] }; } } ); } `; // Create a templates directory for custom tools const templatesDir = path.join(path.resolve(config.projectPath), 'templates'); await fs.mkdir(templatesDir, { recursive: true }); await fs.writeFile( path.join(templatesDir, 'custom-tool-template.ts'), customToolTemplate ); console.log("\n" + colors.green + "✅ MCP Server generated successfully!" + colors.reset); console.log("\nNext steps:"); console.log(`1. cd ${config.projectPath}`); console.log("2. npm install"); console.log("3. npm run build"); console.log("4. npm start"); if (config.deployment !== 'none') { console.log("\nTo deploy to the cloud:"); console.log("5. npm run deploy"); } if (config.customTools && config.customTools.length > 0) { console.log("\nCustom tools are available in your server:"); config.customTools.forEach(tool => { console.log(`- ${tool.name}: ${tool.description}`); }); } console.log("\nTo add more custom tools, use the template at:"); console.log(`${config.projectPath}/templates/custom-tool-template.ts`); console.log("\nTo integrate with Claude Desktop:"); console.log("Add to your Claude Desktop configuration:"); console.log(" macOS: ~/Library/Application Support/Claude/claude_desktop_config.json"); console.log(" Windows: %APPDATA%\\Claude\\claude_desktop_config.json"); } catch (error) { console.error("\n" + colors.red + "❌ Error generating MCP Server: " + error.message + colors.reset); } // Close readline interface rl.close(); } // Process command line arguments async function processArgs() { const args = process.argv.slice(2); // Handle --config flag const configIndex = args.indexOf('--config'); if (configIndex !== -1 && configIndex + 1 < args.length) { // This will be handled in the startGenerator function // Just acknowledge it here console.log(`\nUsing configuration file: ${args[configIndex + 1]}`); } // Handle --version flag if (args.includes('--version') || args.includes('-v')) { console.log("v1.0.4"); return true; } // Handle --help flag if (args.includes('--help') || args.includes('-h')) { console.log(` MCPGen v1.0.4 - Zero-friction MCP Server Generator Usage: mcpgen [options] Options: --version, -v Show version number --help, -h Show help --config <file> Use configuration file Examples: mcpgen Interactive mode mcpgen --config my.config.json Use configuration file `); return true; } return false; } // Main function async function main() { const shouldExit = await processArgs(); if (shouldExit) return; console.log("\nStarting MCP Server Generator..."); await startGenerator(); console.log("\nThank you for using MCPGen!"); } // Start the application main();