UNPKG

@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
'use strict'; 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