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
JavaScript
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()