@dynemcp/create-dynemcp
Version:
Official CLI for creating DyneMCP projects. Generates production-ready MCP servers with secure and modern templates.
503 lines (491 loc) • 18.2 kB
JavaScript
var path = require('path');
var chalk = require('chalk');
var commander = require('commander');
var inquirer = require('inquirer');
var ora = require('ora');
var fs = require('fs-extra');
var fastGlob = require('fast-glob');
var execa = require('execa');
var fs$1 = require('fs');
var url = require('url');
var os = require('os');
var fs$2 = require('fs/promises');
var asyncSema = require('async-sema');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
const { glob } = fastGlob;
async function copy(source, destination, options = {}) {
const sources = Array.isArray(source) ? source : [source];
const { parents = true, cwd = process.cwd(), rename } = options;
try {
const files = await glob(sources, {
cwd,
dot: true,
absolute: false,
onlyFiles: true,
ignore: ['**/node_modules/**', '**/.git/**'],
});
for (const file of files) {
const src = path.resolve(cwd, file);
const filename = rename
? rename(path.basename(file))
: path.basename(file);
const relativeDir = path.dirname(file);
const dest = parents
? path.join(destination, relativeDir, filename)
: path.join(destination, filename);
// Ensure the directory exists
await fs.ensureDir(path.dirname(dest));
await fs.copy(src, dest);
}
}
catch (error) {
console.error('❌ Error copying files:', error);
throw error;
}
}
/**
* Create-DyneMCP Package Configuration
*
* Package-specific configuration for the create-dynemcp package.
* Imports from global config and adds create-dynemcp-specific settings.
*/
// =============================================================================
// GLOBAL CONFIGURATION (imported inline to avoid path issues)
// =============================================================================
const SDK_VERSION = '1.13.3';
const INSPECTOR_VERSION = '0.15.0';
// Paths Configuration
const PATHS = {
DEFAULT_CONFIG: 'dynemcp.config.json',
SOURCE_DIR: 'src',
};
// Template Configuration
const TEMPLATES = {
AVAILABLE_TEMPLATES: ['default-stdio', 'default-http']};
// Logging Configuration
const LOGGING = {
EMOJIS: {
SUCCESS: '✅',
ERROR: '❌',
WARNING: '⚠️',
INFO: 'ℹ️',
LOADING: '🔄',
},
};
// Package Manager Configuration
const PACKAGE_MANAGER = {
PREFERRED: 'pnpm',
ALTERNATIVES: [],
EXEC_COMMANDS: {
pnpm: 'pnpx',
},
};
async function installDependencies$1(projectPath) {
try {
await execa(PACKAGE_MANAGER.PREFERRED, ['install'], {
cwd: projectPath,
stdio: 'inherit',
});
}
catch (error) {
throw new Error(`Failed to install dependencies: ${error instanceof Error ? error.message : error}`);
}
}
/**
* Returns the absolute path to the templates directory
*/
function getTemplatesDir() {
const __filename = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli-wv-cflKS.js', document.baseURI).href)));
const __dirname = path.dirname(__filename);
// Always points to dist/templates relative to the bundle
const templatesPath = path.resolve(__dirname, 'templates');
if (!fs$1.existsSync(templatesPath)) {
throw new Error(`[getTemplatesDir] Templates folder not found at: ${templatesPath}`);
}
return templatesPath;
}
/**
* Returns a list of available templates in the templates directory
*/
async function getAvailableTemplates() {
return [...TEMPLATES.AVAILABLE_TEMPLATES];
}
const templatesDir = getTemplatesDir();
const pkgVersion = "0.1.0";
const getTemplateFile = ({ template, file, }) => {
return path.join(templatesDir, template, file);
};
const SRC_DIR_NAMES = [PATHS.SOURCE_DIR, 'prompts', 'resources', 'tools'];
const installTemplate = async (args) => {
try {
console.log(`Using ${args.packageManager}.`);
if (templatesDir === undefined) {
throw new Error('Templates directory not found');
}
console.log('\nInitializing project with template:', args.template, '\n');
const templatePath = path.join(getTemplatesDir(), args.template);
const copySource = ['**/*', '**/.*'];
if (!args.eslint)
copySource.push('!.eslintrc.js', '!.eslintignore');
if (!args.tailwind)
copySource.push('!tailwind.config.js', '!postcss.config.js');
await copy(copySource, args.root, {
parents: true,
cwd: templatePath,
rename(name) {
switch (name) {
case 'gitignore': {
return `.${name}`;
}
case 'README-template.md': {
return 'README.md';
}
default: {
return name;
}
}
},
});
const tsconfigFile = path.join(args.root, args.mode === 'js' ? 'jsconfig.json' : 'tsconfig.json');
if (await fs$2.stat(tsconfigFile).catch(() => false)) {
await fs$2.writeFile(tsconfigFile, (await fs$2.readFile(tsconfigFile, 'utf8'))
.replace('"@/*": ["./*"]', args.srcDir ? '"@/*": ["./src/*"]' : '"@/*": ["./*"]')
.replace('"@/*":', `"${args.importAlias}":`));
}
if (args.importAlias !== '@/*') {
const globFn = fastGlob.glob || fastGlob;
const files = await globFn('**/*', {
cwd: args.root,
dot: true,
stats: false,
ignore: [
'tsconfig.json',
'jsconfig.json',
'.git/**/*',
'**/node_modules/**',
],
});
const writeSema = new asyncSema.Sema(8, { capacity: files.length });
await Promise.all(files.map(async (file) => {
await writeSema.acquire();
const filePath = path.join(args.root, file);
if ((await fs$2.stat(filePath)).isFile()) {
await fs$2.writeFile(filePath, (await fs$2.readFile(filePath, 'utf8')).replace('@/', `${args.importAlias.replace(/\*/g, '')}`));
}
writeSema.release();
}));
}
if (args.srcDir) {
await fs$2.mkdir(path.join(args.root, PATHS.SOURCE_DIR), {
recursive: true,
});
const templateHasSrcStructure = await fs$2
.stat(path.join(args.root, PATHS.SOURCE_DIR))
.catch(() => false);
if (!templateHasSrcStructure) {
await Promise.all(SRC_DIR_NAMES.map(async (dir) => {
if (dir === PATHS.SOURCE_DIR) {
return;
}
const sourcePath = path.join(args.root, dir);
const targetPath = path.join(args.root, PATHS.SOURCE_DIR, dir);
if (await fs$2.stat(sourcePath).catch(() => false)) {
await fs$2.mkdir(path.dirname(targetPath), { recursive: true });
await fs$2
.rename(sourcePath, targetPath)
.catch((err) => {
if (err.code !== 'ENOENT') {
throw err;
}
});
}
}));
}
}
const version = process.env.DYNEMCP_TEST_VERSION ?? pkgVersion;
const generateScripts = () => {
return {
dev: 'dynemcp dev',
inspector: 'dynemcp dev inspector',
start: 'dynemcp start',
format: 'prettier --write .',
};
};
const packageJson = {
name: args.appName,
version: '0.1.0',
private: true,
scripts: generateScripts(),
type: 'module',
dependencies: {
'@dynemcp/dynemcp': `^${version}`,
'@modelcontextprotocol/sdk': `^${SDK_VERSION}`,
zod: '^3.25.71',
},
devDependencies: {
prettier: '^3.2.5',
},
};
if (args.mode === 'ts') {
packageJson.devDependencies = {
...packageJson.devDependencies,
typescript: '^5.4.2',
tsx: '^4.0.0',
'@modelcontextprotocol/inspector': `^${INSPECTOR_VERSION}`,
};
}
packageJson.engines = {
node: '>=20.0.0',
};
packageJson.packageManager = 'pnpm@10.9.0';
const devDeps = Object.keys(packageJson.devDependencies).length;
if (!devDeps) {
const tempJson = packageJson;
delete tempJson.devDependencies;
}
await fs$2.writeFile(path.join(args.root, 'package.json'), JSON.stringify(packageJson, null, 2) + os.EOL);
if (args.skipInstall)
return;
console.log('\nInstalling dependencies:');
Object.keys(packageJson.dependencies).forEach((dependency) => {
console.log(`- ${dependency}`);
});
if (devDeps) {
console.log('\nInstalling devDependencies:');
Object.keys(packageJson.devDependencies).forEach((dependency) => {
console.log(`- ${dependency}`);
});
}
console.log();
if (!args.skipInstall) {
console.log('\nInstalling dependencies. This may take a moment...');
try {
await installDependencies$1(args.root);
console.log(`${LOGGING.EMOJIS.SUCCESS} Dependencies installed successfully!`);
}
catch {
console.error(`${LOGGING.EMOJIS.ERROR} Failed to install dependencies. Please run the command below to install manually:`);
}
}
}
catch (error) {
console.error('[installTemplate] Error:', error);
throw error;
}
};
/**
* Creates a new project using the specified template and options
*/
async function createProject(projectPath, projectName, template) {
await fs.mkdir(projectPath, { recursive: true });
await installTemplate({
appName: projectName,
root: projectPath,
packageManager: PACKAGE_MANAGER.PREFERRED,
template,
mode: 'ts',
tailwind: false,
eslint: true,
srcDir: true,
importAlias: '@/*',
skipInstall: false,
});
}
/**
* Installs dependencies using pnpm
*/
async function installDependencies(projectPath) {
const { default: execa } = await import('execa');
const args = ['install'];
if (!fs.existsSync(`${projectPath}/package.json`)) {
return;
}
try {
await execa('pnpm', args, {
cwd: projectPath,
stdio: 'inherit',
});
}
catch (error) {
console.error(`Failed to install dependencies with pnpm.`);
throw error;
}
}
function validateProjectName(name) {
const problems = [];
if (!name || name.trim().length === 0) {
problems.push('Project name cannot be empty');
}
if (name.length > 214) {
problems.push('Project name cannot be longer than 214 characters');
}
const invalidChars = /[<>:"/\\|?*]/;
if (invalidChars.test(name)) {
problems.push('Project name contains invalid characters');
}
const reservedNames = [
'node_modules',
'package.json',
'package-lock.json',
'yarn.lock',
'pnpm-lock.yaml',
'.git',
'.gitignore',
'.env',
'.env.local',
'.env.development',
'.env.test',
'.env.production',
];
if (reservedNames.includes(name.toLowerCase())) {
problems.push(`Project name cannot be "${name}" (reserved name)`);
}
return {
valid: problems.length === 0,
problems: problems.length > 0 ? problems : undefined,
};
}
function validateProjectPath(projectPath) {
if (fs.existsSync(projectPath)) {
const files = fs.readdirSync(projectPath);
if (files.length > 0) {
return {
valid: false,
message: `The directory ${chalk.green(projectPath)} already exists and is not empty.`,
};
}
}
return { valid: true };
}
function validateTemplate(template, availableTemplates) {
if (!availableTemplates.includes(template)) {
return {
valid: false,
message: `Template ${chalk.red(template)} not found. Available templates: ${availableTemplates
.map((t) => chalk.green(t))
.join(', ')}`,
};
}
return { valid: true };
}
const version = "0.1.0";
const program = new commander.Command('create-dynemcp')
.version(version, '-v, --version', 'Output the current version of create-dynemcp')
.argument('[directory]', 'The directory to create the app in')
.usage('[directory] [options]')
.helpOption('-h, --help', 'Display this help message.')
.option('--template <name>', 'The template to use (default, calculator)', 'default')
.option('--skip-install', 'Skip installing dependencies')
.option('-y, --yes', 'Skip all prompts and use default values')
.allowUnknownOption()
.parse(process.argv);
async function promptForProjectName() {
const res = await inquirer.prompt({
type: 'input',
name: 'path',
message: 'What is your project named?',
default: 'my-mcp-project',
validate: (name) => {
const validation = validateProjectName(name);
if (validation.valid)
return true;
return ('Invalid project name: ' + (validation.problems?.[0] ?? 'Invalid name'));
},
});
return typeof res.path === 'string' ? res.path.trim() : 'my-mcp-project';
}
async function promptForTemplate() {
const res = await inquirer.prompt({
type: 'list',
name: 'template',
message: 'Select a project template:',
choices: [
{
name: 'Default - Studio - A minimal setup with basic examples Transport: STUDIO',
value: 'default-stdio',
},
{
name: 'Default - HTTP - A minimal setup with basic examples Transport: STREAMABLE HTTP',
value: 'default-http',
},
],
default: 'default-stdio',
});
return res.template;
}
async function run() {
try {
const options = program.opts();
const args = program.args;
let projectDirectory = args[0];
if (!projectDirectory) {
projectDirectory = await promptForProjectName();
}
let template = options.template;
if (!options.yes) {
template = await promptForTemplate();
}
const { valid, problems } = validateProjectName(projectDirectory);
if (!valid) {
console.error(chalk.red(`Invalid project name: ${problems?.join(', ')}`));
process.exit(1);
}
const projectPath = path.resolve(process.cwd(), projectDirectory);
const projectName = path.basename(projectPath);
const spinner = ora('Creating project...').start();
try {
await createProject(projectPath, projectName, template);
spinner.succeed('Project created successfully!');
if (!options.skipInstall) {
spinner.text = 'Installing dependencies...';
spinner.start();
try {
await installDependencies(projectPath);
spinner.succeed('Dependencies installed successfully!');
}
catch (error) {
console.error(error);
spinner.fail('Failed to install dependencies');
console.error(chalk.yellow('You can install dependencies manually by running:'));
console.error(chalk.cyan(` cd ${projectName}`));
console.error(chalk.cyan(' pnpm install'));
}
}
console.log();
console.log(chalk.green('✨ Project created successfully!'));
console.log();
console.log('Next steps:');
console.log(chalk.cyan(` cd ${projectName}`));
if (options.skipInstall) {
console.log(chalk.cyan(' pnpm install'));
}
console.log(chalk.cyan(' pnpm run dev'));
console.log();
console.log('📚 Documentation: https://github.com/DavidNazareno/dynemcp');
console.log();
}
catch (error) {
spinner.fail('Failed to create project');
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
process.exit(1);
}
}
catch (error) {
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
process.exit(1);
}
}
exports.createProject = createProject;
exports.getAvailableTemplates = getAvailableTemplates;
exports.getTemplateFile = getTemplateFile;
exports.getTemplatesDir = getTemplatesDir;
exports.installDependencies = installDependencies;
exports.installTemplate = installTemplate;
exports.promptForProjectName = promptForProjectName;
exports.promptForTemplate = promptForTemplate;
exports.run = run;
exports.validateProjectName = validateProjectName;
exports.validateProjectPath = validateProjectPath;
exports.validateTemplate = validateTemplate;
//# sourceMappingURL=cli-wv-cflKS.js.map
;