create-modelfetch
Version:
CLI for scaffolding a new MCP server with ModelFetch
354 lines • 12.4 kB
JavaScript
import * as p from "@clack/prompts";
import { capitalCase, pascalCase } from "change-case";
import ejs from "ejs";
import { exec } from "node:child_process";
import { promises as fs } from "node:fs";
import path from "node:path";
import { promisify } from "node:util";
import pc from "picocolors";
import validatePackageName from "validate-npm-package-name";
import packageJson from "../package.json" with { type: "json" };
const execAsync = promisify(exec);
// ASCII art logo
const asciiLogo = ` __ __ _ _ _____ _ _
| \\/ | | | | | ___| | | | |
| . . | ___ __| | ___| | |_ ___| |_ ___| |__
| |\\/| |/ _ \\ / _\` |/ _ \\ | _/ _ \\ __/ __| '_ \\
| | | | (_) | (_| | __/ | || __/ || (__| | | |
\\_| |_/\\___/ \\__,_|\\___|_\\_| \\___|\\__\\___|_| |_|`;
// Package versions
const packageVersions = {
"@modelcontextprotocol/sdk": "1.17.4",
zod: "3.25.76",
tslib: "2.8.1",
typescript: "5.9.2",
modelfetch: packageJson.version,
"@modelfetch/node": packageJson.version,
"@types/node": "22.18.0",
tsx: "4.20.5",
"@modelfetch/next": packageJson.version,
"@modelfetch/bun": packageJson.version,
"@types/bun": "1.2.21",
"@modelfetch/deno": packageJson.version,
"@modelfetch/aws-lambda": packageJson.version,
"@types/aws-lambda": "8.10.152",
"aws-cdk-lib": "2.213.0",
"aws-cdk": "2.1027.0",
esbuild: "0.25.9",
"@modelfetch/vercel": packageJson.version,
next: "15.5.2",
react: "19.1.1",
"@types/react": "19.1.12",
"react-dom": "19.1.1",
"@types/react-dom": "19.1.9",
"@modelfetch/cloudflare": packageJson.version,
wrangler: "4.33.1",
"@modelfetch/netlify": packageJson.version,
"@modelfetch/fastly": packageJson.version,
"@fastly/js-compute": "3.34.0",
"@modelfetch/gcore": packageJson.version,
"@gcoredev/fastedge-sdk-js": "1.2.2",
"@modelfetch/azure-functions": packageJson.version,
"@azure/functions": "4.7.2-preview",
"@modelfetch/supabase": packageJson.version,
};
// Cloudflare compatibility date
const cloudflareCompatibilityDate = "2025-06-17";
function detectPackageManager() {
const userAgent = process.env.npm_config_user_agent;
if (userAgent) {
if (userAgent.includes("pnpm"))
return "pnpm";
if (userAgent.includes("yarn"))
return "yarn";
if (userAgent.includes("bun"))
return "bun";
}
return "npm";
}
function detectRuntime() {
const userAgent = process.env.npm_config_user_agent;
if (userAgent) {
if (userAgent.includes("bun"))
return "bun";
if (userAgent.includes("deno"))
return "deno";
}
return "node";
}
function validateProjectName(name) {
if (name) {
const validation = validatePackageName(name);
if (!validation.validForNewPackages) {
const issue = [
...(validation.errors ?? []),
...(validation.warnings ?? []),
].find(Boolean);
return (issue ?? "") || "name is invalid";
}
}
}
function getProjectTitle(projectName) {
return capitalCase(projectName).replaceAll("Mcp", "MCP");
}
function getStartCommand(runtime, packageManager) {
switch (runtime) {
case "deno": {
return "deno task start";
}
case "aws-lambda":
case "gcore": {
return `${packageManager} run deploy`;
}
case "next":
case "vercel":
case "cloudflare": {
return `${packageManager} run dev`;
}
case "netlify": {
return "deno task dev";
}
case "supabase": {
return "supabase start";
}
default: {
return `${packageManager} start`;
}
}
}
async function copyTemplate(templatePath, targetPath, options) {
await fs.mkdir(targetPath, { recursive: true });
const files = await fs.readdir(templatePath);
// Prepare EJS data
const ejsData = {
projectName: options.name,
projectTitle: options.title,
awsCdkStack: pascalCase(options.name) + "Stack",
runtime: options.runtime,
language: options.language,
packageManager: options.packageManager,
versions: packageVersions,
cloudflareCompatibilityDate,
};
for (const file of files) {
const srcPath = path.join(templatePath, file);
const stat = await fs.stat(srcPath);
if (stat.isDirectory()) {
const destPath = path.join(targetPath, file);
await copyTemplate(srcPath, destPath, options);
}
else if (file.endsWith(".template")) {
// Process template files with EJS
const content = await fs.readFile(srcPath, "utf8");
const rendered = ejs.render(content, ejsData);
// Remove .template extension from destination filename
const destFileName = path.basename(file, ".template");
const destPath = path.join(targetPath, destFileName);
await fs.writeFile(destPath, rendered);
}
else {
// Copy non-template files as-is
const destPath = path.join(targetPath, file);
await fs.copyFile(srcPath, destPath);
}
}
}
async function main() {
// Detect package manager running the CLI
const detectedPM = detectPackageManager();
// Wait for yarn to finish outputting if detected
if (detectedPM === "yarn")
await new Promise((resolve) => setTimeout(resolve, 500));
// Display ASCII art logo
console.log(pc.greenBright(asciiLogo));
// Display hero title
console.log();
console.log(pc.gray("A delightful TypeScript/JavaScript SDK for MCP servers"));
console.log();
p.intro(pc.bold("Let's scaffold your MCP server!"));
const projectName = await p.text({
message: "What is your MCP server name?",
placeholder: "my-mcp-server",
defaultValue: "my-mcp-server",
validate: validateProjectName,
});
if (p.isCancel(projectName)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
// Use placeholder value if user enters empty string
const finalProjectName = projectName || "my-mcp-server";
// Generate default title from project name
const defaultTitle = getProjectTitle(finalProjectName);
const projectTitle = await p.text({
message: "What is your MCP server title?",
placeholder: defaultTitle,
defaultValue: defaultTitle,
});
if (p.isCancel(projectTitle)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
// Use placeholder value if user enters empty string
const finalProjectTitle = projectTitle || defaultTitle;
const detectedRuntime = detectRuntime();
const runtime = (await p.select({
message: "Which runtime would you like to use?",
options: [
{ value: "node", label: "Node.js" },
{ value: "next", label: "Next.js" },
{ value: "bun", label: "Bun" },
{ value: "deno", label: "Deno" },
{ value: "vercel", label: "Vercel" },
{ value: "cloudflare", label: "Cloudflare" },
{ value: "netlify", label: "Netlify" },
{ value: "fastly", label: "Fastly" },
{ value: "supabase", label: "Supabase" },
{ value: "gcore", label: "Gcore" },
{ value: "aws-lambda", label: "AWS Lambda" },
{ value: "azure-functions", label: "Azure Functions" },
],
initialValue: detectedRuntime,
}));
if (p.isCancel(runtime)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
const language = (await p.select({
message: "Which language would you like to use?",
options: [
{ value: "typescript", label: "TypeScript" },
{ value: "javascript", label: "JavaScript" },
],
}));
if (p.isCancel(language)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
let packageManager;
let installDeps;
// Handle package manager selection based on runtime
switch (runtime) {
case "bun": {
packageManager = "bun";
break;
}
case "deno":
case "netlify":
case "supabase": {
packageManager = "npm";
installDeps = false; // Deno doesn't need to install dependencies
break;
}
default: {
packageManager = (await p.select({
message: "Which package manager would you like to use?",
options: [
{ value: "npm", label: "npm" },
{ value: "pnpm", label: "pnpm" },
{ value: "bun", label: "bun" },
{ value: "yarn", label: "yarn" },
],
initialValue: detectedPM,
}));
if (p.isCancel(packageManager)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
break;
}
}
// Ask about installing dependencies
if (typeof installDeps !== "boolean") {
const value = await p.confirm({
message: "Would you like to install dependencies?",
initialValue: true,
});
if (p.isCancel(value)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
installDeps = value;
}
const options = {
name: finalProjectName,
title: finalProjectTitle,
runtime,
language,
packageManager,
installDeps,
};
const targetDir = path.resolve(finalProjectName);
const templateName = `${runtime}-${language === "javascript" ? "js" : "ts"}`;
const templateDir = new URL(`../templates/${templateName}`, import.meta.url)
.pathname;
// Check if directory exists
let shouldRemoveExisting = false;
try {
await fs.access(targetDir);
const overwrite = await p.confirm({
message: `Directory ${finalProjectName} already exists. Overwrite?`,
initialValue: false,
});
if (!overwrite || p.isCancel(overwrite)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
shouldRemoveExisting = true;
}
catch {
// Directory doesn't exist, proceed with creation
}
const s = p.spinner();
try {
s.start("Scaffolding project");
// Remove existing directory if needed
if (shouldRemoveExisting)
await fs.rm(targetDir, { recursive: true });
// Copy template
await copyTemplate(templateDir, targetDir, options);
s.stop("Project scaffolded!");
// Install dependencies
if (installDeps) {
s.start("Installing dependencies");
const installCommand = {
npm: "npm install",
pnpm: "pnpm install",
yarn: "yarn install",
bun: "bun install",
}[packageManager];
await execAsync(installCommand, { cwd: targetDir });
s.stop("Dependencies installed!");
}
p.outro(pc.green("Your MCP server is ready!"));
console.log();
console.log(pc.bold("Next steps:"));
console.log();
console.log(pc.gray(" Navigate to the project directory:"));
console.log(` ${pc.cyan(`cd ${finalProjectName}`)}`);
console.log();
console.log(pc.gray(" Start the MCP server:"));
console.log(` ${pc.cyan(getStartCommand(runtime, packageManager))}`);
console.log();
console.log(pc.gray(" Test with the MCP Inspector:"));
console.log(` ${pc.cyan("npx -y @modelcontextprotocol/inspector@latest")}`);
console.log();
console.log(pc.gray(" Read the documentation:"));
console.log(` ${pc.cyan("https://www.modelfetch.com/docs")}`);
console.log();
}
catch (error) {
s.stop("create-modelfetch CLI failed!");
console.error(error);
process.exit(1);
}
}
try {
await main();
}
catch (error) {
console.log(pc.red("create-modelfetch CLI failed!"));
console.error(error);
process.exit(1);
}
//# sourceMappingURL=index.js.map