Model Control Protocol Server (MCP)
by zueai
#!/usr/bin/env bun
import { execSync } from "node:child_process"
import { cp, mkdir, readFile, writeFile } from "node:fs/promises"
import { join } from "node:path"
import { fileURLToPath } from "node:url"
import npmWhich from "npm-which"
import pc from "picocolors"
import prompts from "prompts"
import yargs from "yargs"
import { hideBin } from "yargs/helpers"
const __dirname = fileURLToPath(new URL(".", import.meta.url))
const PACKAGE_MANAGERS = {
bun: "bun install",
npm: "npm install",
pnpm: "pnpm install",
yarn: "yarn"
} as const
type PackageManager = keyof typeof PACKAGE_MANAGERS
interface Args {
name?: string
clone?: string
}
async function getProjectDetails() {
// Parse command line arguments
const argv = (await yargs(hideBin(process.argv))
.usage("Usage: $0 --name <name> [options]")
.option("name", {
type: "string",
describe: "Name of the MCP server"
})
.option("clone", {
type: "string",
describe: "GitHub URL of an existing MCP server to clone"
})
.example([
["$0 --name my-server", "Create a new MCP server"],
[
"$0 --name my-server --clone https://github.com/user/repo",
"Clone an existing MCP server"
]
])
.help().argv) as Args
const isCloning = !!argv.clone
const githubUrl = argv.clone || ""
let projectName = argv.name || ""
if (isCloning && !githubUrl) {
console.error(pc.red("GitHub URL is required when using --clone flag"))
process.exit(1)
}
if (isCloning && !projectName) {
// Extract repo name from GitHub URL
const repoName = githubUrl.split("/").pop()?.replace(".git", "") || ""
// Ask for project name with repo name as default
const response = await prompts({
type: "text",
name: "projectName",
message: "What is the name of your MCP server?",
initial: repoName,
validate: (value) =>
value.length > 0 ? true : "Project name is required"
})
projectName = response.projectName
} else if (!projectName) {
const response = await prompts({
type: "text",
name: "projectName",
message: "What is the name of your MCP server?",
validate: (value) =>
value.length > 0 ? true : "Project name is required"
})
projectName = response.projectName
}
if (!projectName) {
console.error(pc.red("Project name is required"))
process.exit(1)
}
// Ask for package manager preference
const { packageManager } = await prompts({
type: "select",
name: "packageManager",
message: "Which package manager do you want to use?",
choices: [
{ title: "bun", value: "bun" },
{ title: "npm", value: "npm" },
{ title: "pnpm", value: "pnpm" },
{ title: "yarn", value: "yarn" }
]
})
if (!packageManager) {
console.error(pc.red("Package manager selection is required"))
process.exit(1)
}
return { projectName, packageManager, githubUrl, isCloning }
}
async function setupProjectFiles(projectName: string) {
const targetDir = join(process.cwd(), projectName)
const templateDir = join(__dirname, "template")
// Create project directory
await mkdir(targetDir, { recursive: true })
// Copy template files
await cp(templateDir, targetDir, {
recursive: true
})
return targetDir
}
async function updateConfigurations(targetDir: string, projectName: string) {
// Update package.json with new name
const pkgPath = join(targetDir, "package.json")
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"))
pkg.name = projectName
await writeFile(pkgPath, JSON.stringify(pkg, null, 2))
// Update wrangler.jsonc with new name
const wranglerPath = join(targetDir, "wrangler.jsonc")
let wranglerContent = await readFile(wranglerPath, "utf-8")
wranglerContent = wranglerContent.replace(
/"name":\s*"[^"]*"/,
`"name": "${projectName}"`
)
await writeFile(wranglerPath, wranglerContent)
// Update README.md heading and clone command
const readmePath = join(targetDir, "README.md")
let readmeContent = await readFile(readmePath, "utf-8")
readmeContent = readmeContent.replace(/^# [^\n]+/, `# ${projectName}`)
readmeContent = readmeContent.replace(
/bun create mcp --clone https:\/\/github\.com\/[^/]+\/[^/\n]+/,
`bun create mcp --clone https://github.com/your-username/${projectName}`
)
await writeFile(readmePath, readmeContent)
}
function setupDependencies(targetDir: string, packageManager: PackageManager) {
// Initialize git repo with main branch
execSync("git init -b main", { cwd: targetDir })
// Install dependencies
console.log(pc.cyan("\n⚡️ Installing dependencies..."))
execSync(PACKAGE_MANAGERS[packageManager], {
cwd: targetDir,
stdio: "inherit"
})
}
function setupMCPAndWorkers(targetDir: string, packageManager: PackageManager) {
console.log(
pc.cyan("\n⚡️ Setting up MCP and deploying to Cloudflare Workers...")
)
const setupCommand =
packageManager === "npm"
? "npx"
: packageManager === "yarn"
? "yarn dlx"
: packageManager === "pnpm"
? "pnpm dlx"
: "bunx"
execSync(`${setupCommand} workers-mcp setup`, {
cwd: targetDir,
stdio: "inherit"
})
}
async function getMCPCommand(projectName: string) {
const homedir = process.env.HOME || process.env.USERPROFILE || ""
const claudeConfigPath = join(
homedir,
"Library/Application Support/Claude/claude_desktop_config.json"
)
const claudeConfig = JSON.parse(await readFile(claudeConfigPath, "utf-8"))
const mcpServer = claudeConfig.mcpServers[projectName]
if (!mcpServer) {
throw new Error("Could not find MCP server in Claude desktop config")
}
return [mcpServer.command, ...mcpServer.args].join(" ")
}
async function handleFinalSteps(targetDir: string, mcpCommand: string) {
// Copy command to clipboard
execSync(`echo "${mcpCommand}" | pbcopy`)
// Show command with nice spacing
console.log("\n")
console.log(
pc.white(
"Your MCP server command has been copied to clipboard. Here it is for reference:"
)
)
console.log("\n")
console.log(pc.cyan(mcpCommand))
console.log("\n")
console.log(
pc.white(
"Add it to Cursor by going to Settings -> Features -> MCP Servers"
)
)
console.log("\n")
// Ask if user wants to open in Cursor
const { openInCursor } = await prompts({
type: "confirm",
name: "openInCursor",
message: "Would you like to open the project in Cursor?",
initial: true
})
if (openInCursor) {
execSync(`cursor ${targetDir}`, { stdio: "inherit" })
}
console.log(pc.green("\n✨ MCP server created successfully!"))
if (!openInCursor) {
console.log(pc.white("\nYou can open the project later with:"))
console.log(pc.cyan(` cursor ${targetDir}\n`))
}
// Add closing message
console.log(pc.cyan("Happy hacking! 🚀\n"))
}
async function cloneExistingServer(
githubUrl: string,
projectName: string,
packageManager: PackageManager
) {
const targetDir = join(process.cwd(), projectName)
// Clone the repository
console.log(pc.cyan("\n⚡️ Cloning repository..."))
execSync(`git clone ${githubUrl} ${targetDir}`, { stdio: "inherit" })
// Remove the .git folder and reinitialize the repository
console.log(pc.cyan("\n⚡️ Initializing fresh git repository..."))
execSync(`rm -rf ${join(targetDir, ".git")}`)
execSync("git init -b main", { cwd: targetDir })
// Update configurations with new name
await updateConfigurations(targetDir, projectName)
// Install dependencies
console.log(pc.cyan("\n⚡️ Installing dependencies..."))
execSync(PACKAGE_MANAGERS[packageManager], {
cwd: targetDir,
stdio: "inherit"
})
// Generate and upload secret
console.log(pc.cyan("\n⚡️ Setting up MCP secret..."))
const setupCommand =
packageManager === "npm"
? "npx"
: packageManager === "yarn"
? "yarn dlx"
: packageManager === "pnpm"
? "pnpm dlx"
: "bunx"
execSync(`${setupCommand} workers-mcp secret generate`, {
cwd: targetDir,
stdio: "inherit"
})
execSync(`${setupCommand} workers-mcp secret upload`, {
cwd: targetDir,
stdio: "inherit"
})
// Deploy the worker
console.log(pc.cyan("\n⚡️ Deploying to Cloudflare Workers..."))
execSync("bun run deploy", {
cwd: targetDir,
stdio: "inherit"
})
// Get the worker URL
const { workerUrl } = await prompts({
type: "text",
name: "workerUrl",
message:
"Please enter the URL of your deployed worker (from the output above):",
validate: (value) =>
value.length > 0 ? true : "Worker URL is required"
})
if (!workerUrl) {
console.error(pc.red("Worker URL is required"))
process.exit(1)
}
// Get workers-mcp executable path
const execPath = npmWhich(targetDir).sync("workers-mcp")
// Construct MCP command
const mcpCommand = [
execPath,
"run",
projectName,
workerUrl,
targetDir
].join(" ")
return mcpCommand
}
async function main() {
// Display welcome message
console.log("\n")
console.log(pc.bgCyan(pc.black(" ⚡️ Welcome to create-mcp CLI ")))
try {
const { projectName, packageManager, githubUrl, isCloning } =
await getProjectDetails()
let mcpCommand: string
let targetDir: string
if (isCloning && githubUrl) {
mcpCommand = await cloneExistingServer(
githubUrl,
projectName,
packageManager as PackageManager
)
targetDir = join(process.cwd(), projectName)
} else {
targetDir = await setupProjectFiles(projectName)
await updateConfigurations(targetDir, projectName)
setupDependencies(targetDir, packageManager as PackageManager)
setupMCPAndWorkers(targetDir, packageManager as PackageManager)
mcpCommand = await getMCPCommand(projectName)
}
await handleFinalSteps(targetDir, mcpCommand)
} catch (error) {
console.error(pc.red("Error creating project:"), error)
process.exit(1)
}
}
main().catch(console.error)