UNPKG

create-mcp-craft

Version:

Create MCP TypeScript servers with Bun and Hono - the fastest way to scaffold Model Context Protocol servers

296 lines (243 loc) • 8.89 kB
#!/usr/bin/env node import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, copyFileSync } from 'fs' import { join, dirname, basename } from 'path' import { fileURLToPath } from 'url' import { execSync } from 'child_process' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) function log(message) { console.log(`šŸš€ ${message}`) } function error(message) { console.error(`āŒ ${message}`) } function success(message) { console.log(`āœ… ${message}`) } function copyDir(src, dest) { try { mkdirSync(dest, { recursive: true }) } catch (err) { if (err.code !== 'EEXIST') throw err } const entries = readdirSync(src, { withFileTypes: true }) for (const entry of entries) { const srcPath = join(src, entry.name) const destPath = join(dest, entry.name) if (entry.isDirectory()) { // Skip node_modules and .git directories if (entry.name === 'node_modules' || entry.name === '.git') { continue } copyDir(srcPath, destPath) } else { copyFileSync(srcPath, destPath) } } } function replaceVariables(content, variables) { let result = content for (const [key, value] of Object.entries(variables)) { const placeholder = `{{${key}}}` result = result.replaceAll(placeholder, value) } return result } function processTemplateFiles(templateDir, targetDir, variables) { const entries = readdirSync(templateDir, { withFileTypes: true }) for (const entry of entries) { const srcPath = join(templateDir, entry.name) // Handle special file naming for gitignore let destName = entry.name if (entry.name === 'gitignore') { destName = '.gitignore' } const destPath = join(targetDir, destName) if (entry.isDirectory()) { // Skip node_modules and .git directories if (entry.name === 'node_modules' || entry.name === '.git') { continue } mkdirSync(destPath, { recursive: true }) processTemplateFiles(srcPath, destPath, variables) } else { // Read file content const content = readFileSync(srcPath, 'utf8') // Replace variables in content const processedContent = replaceVariables(content, variables) // Write processed content to destination writeFileSync(destPath, processedContent) } } } function promptForInput(question, defaultValue = '') { // For now, return default values since we can't easily prompt in this context // In a real implementation, you'd use a library like 'prompts' for interactive input return defaultValue } function getGitUser() { try { const name = execSync('git config user.name', { encoding: 'utf8' }).trim() const email = execSync('git config user.email', { encoding: 'utf8' }).trim() return { name, email } } catch { return { name: 'Your Name', email: 'your.email@example.com' } } } function getBunPath() { try { return execSync('which bun', { encoding: 'utf8' }).trim() } catch { // Fallback paths for common bun installations const possiblePaths = [ '/Users/' + (process.env.USER || 'user') + '/.bun/bin/bun', '/usr/local/bin/bun', '/opt/homebrew/bin/bun', 'bun' // Last resort ] return possiblePaths[0] } } function runBunInstall(projectPath, bunPath) { try { log('šŸ“¦ Installing dependencies...') // Run bun install in the project directory execSync(`"${bunPath}" install`, { cwd: projectPath, stdio: 'inherit' }) success('Dependencies installed successfully') return true } catch (error) { console.warn('āš ļø Failed to install dependencies automatically') console.warn(' Please run manually:') console.warn(` cd ${basename(projectPath)} && bun install`) return false } } function initializeGit(projectPath, projectName) { try { log('šŸ”§ Initializing git repository...') // Initialize git repository execSync('git init', { cwd: projectPath, stdio: 'pipe' }) // Add all files execSync('git add .', { cwd: projectPath, stdio: 'pipe' }) // Create initial commit const commitSummary = `Initial commit: ${projectName}` const commitDescription = `Generated with create-mcp-craft - MCP TypeScript server template - Bun + Hono + TypeScript setup - Complete MCP protocol implementation` const commitMessage = `${commitSummary}\n\n${commitDescription}` execSync('git commit -F -', { cwd: projectPath, input: commitMessage, stdio: 'pipe' }) success('Git repository initialized with initial commit') return true } catch (error) { console.warn('āš ļø Git initialization failed (this is optional)') console.warn(' You can manually initialize git later with:') console.warn(' git init && git add . && git commit -m "Initial commit"') return false } } function kebabToPascal(str) { return str .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join('') } function main() { const args = process.argv.slice(2) let projectName = args[0] if (!projectName) { error('Please provide a project name') console.log('Usage: bunx create-mcp-craft@latest my-project-name') process.exit(1) } // Clean up project name projectName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-') log(`Creating MCP server: ${projectName}`) const templateDir = join(__dirname, 'template') const targetDir = join(process.cwd(), projectName) // Check if target directory already exists try { if (statSync(targetDir).isDirectory()) { error(`Directory ${projectName} already exists`) process.exit(1) } } catch { // Directory doesn't exist, which is what we want } // Get git user info and bun path for defaults const gitUser = getGitUser() const bunPath = getBunPath() // Define template variables const variables = { projectName: projectName, projectNamePascal: kebabToPascal(projectName), authorName: gitUser.name, authorEmail: gitUser.email, description: `MCP TypeScript server: ${projectName}`, repositoryUrl: `https://github.com/${gitUser.name}/${projectName}`, year: new Date().getFullYear(), } try { // Create target directory mkdirSync(targetDir, { recursive: true }) log('šŸ“‚ Copying template files...') // Process template files with variable replacement processTemplateFiles(templateDir, targetDir, variables) success(`Project ${projectName} created successfully!`) // Initialize git repository initializeGit(targetDir, projectName) // Install dependencies const installSuccess = runBunInstall(targetDir, bunPath) console.log('\nšŸŽÆ Next steps:') console.log(` cd ${projectName}`) if (!installSuccess) { console.log(' bun install') } console.log(' bun run dev:stdio') console.log('') console.log('šŸ”„ Git repository ready:') console.log(' git remote add origin <your-repo-url>') console.log(' git push -u origin main') console.log('\nšŸ“š Development commands:') console.log(' bun run dev:stdio # Run with stdio transport') console.log(' bun run dev:http # Run with HTTP transport') console.log(' bun run build # Build the project') console.log(' bun run lint # Run linting') console.log(' bun run typecheck # Run type checking') // Show Claude Desktop configuration with absolute paths const absoluteProjectPath = join(process.cwd(), projectName) console.log('\nšŸ¤– Claude Desktop Configuration:') console.log('Add this to your claude_desktop_config.json:') console.log('\n```json') console.log(JSON.stringify({ mcpServers: { [projectName]: { command: bunPath, args: [`${absoluteProjectPath}/src/index.ts`] } } }, null, 2)) console.log('```') console.log('\nšŸ“ Config file locations:') console.log(' macOS: ~/Library/Application Support/Claude/claude_desktop_config.json') console.log(' Windows: %APPDATA%\\Claude\\claude_desktop_config.json') console.log(' Linux: ~/.config/Claude/claude_desktop_config.json') console.log('\nšŸ’” Tips:') console.log(` • Bun path detected: ${bunPath}`) if (installSuccess) { console.log(' • Dependencies installed automatically') } console.log(' • Using absolute paths for reliable execution') console.log(' • Restart Claude Desktop after config changes') console.log(' • Test with: bun run dev:stdio (should show server startup)') console.log('\nšŸ“– Check the README.md for more information!') } catch (err) { error(`Failed to create project: ${err.message}`) process.exit(1) } } main()