frontmcp 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +116 -60
  2. package/package.json +1 -1
  3. package/src/cli.ts +121 -73
package/dist/cli.js CHANGED
@@ -76,7 +76,7 @@ ${c('bold', 'Commands')}
76
76
  init Create or fix a tsconfig.json suitable for FrontMCP
77
77
  doctor Check Node/npm versions and tsconfig requirements
78
78
  inspector Launch MCP Inspector (npx @modelcontextprotocol/inspector)
79
- create Scaffold a new FrontMCP project in the current directory
79
+ create <name> Scaffold a new FrontMCP project in ./<name>
80
80
  help Show this help message
81
81
 
82
82
  ${c('bold', 'Options')}
@@ -89,7 +89,7 @@ ${c('bold', 'Examples')}
89
89
  frontmcp init
90
90
  frontmcp doctor
91
91
  frontmcp inspector
92
- npx frontmcp create
92
+ npx frontmcp create my-mcp
93
93
  `);
94
94
  }
95
95
  function parseArgs(argv) {
@@ -146,30 +146,24 @@ function resolveEntry(cwd, explicit) {
146
146
  return full;
147
147
  throw new Error(`Entry override not found: ${explicit}`);
148
148
  }
149
- // 1) package.json main
150
149
  const pkgPath = path.join(cwd, 'package.json');
151
150
  if (yield fileExists(pkgPath)) {
152
151
  const pkg = yield readJSON(pkgPath);
153
152
  if (pkg && typeof pkg.main === 'string' && pkg.main.trim()) {
154
153
  const mainCandidates = tryCandidates(path.resolve(cwd, pkg.main));
155
- for (const p of mainCandidates) {
154
+ for (const p of mainCandidates)
156
155
  if (yield fileExists(p))
157
156
  return p;
158
- }
159
- // If "main" is a directory-like path, try index.* within it
160
157
  const asDir = path.resolve(cwd, pkg.main);
161
158
  const idxCandidates = tryCandidates(path.join(asDir, 'index'));
162
- for (const p of idxCandidates) {
159
+ for (const p of idxCandidates)
163
160
  if (yield fileExists(p))
164
161
  return p;
165
- }
166
162
  }
167
163
  }
168
- // 2) src/main.ts
169
164
  const fallback = path.join(cwd, 'src', 'main.ts');
170
165
  if (yield fileExists(fallback))
171
166
  return fallback;
172
- // 3) Not found
173
167
  const msg = [
174
168
  c('red', 'No entry file found.'),
175
169
  '',
@@ -191,6 +185,55 @@ function runCmd(cmd, args, opts = {}) {
191
185
  child.on('error', reject);
192
186
  });
193
187
  }
188
+ function ensureDir(p) {
189
+ return __awaiter(this, void 0, void 0, function* () {
190
+ yield fs_1.promises.mkdir(p, { recursive: true });
191
+ });
192
+ }
193
+ function isDirEmpty(dir) {
194
+ return __awaiter(this, void 0, void 0, function* () {
195
+ try {
196
+ const items = yield fs_1.promises.readdir(dir);
197
+ return items.length === 0;
198
+ }
199
+ catch (e) {
200
+ if ((e === null || e === void 0 ? void 0 : e.code) === 'ENOENT')
201
+ return true;
202
+ throw e;
203
+ }
204
+ });
205
+ }
206
+ function sanitizeForFolder(name) {
207
+ const seg = name.startsWith('@') && name.includes('/') ? name.split('/')[1] : name;
208
+ return seg.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').toLowerCase() || 'frontmcp-app';
209
+ }
210
+ function sanitizeForNpm(name) {
211
+ if (name.startsWith('@') && name.includes('/')) {
212
+ const [scope, pkg] = name.split('/');
213
+ const s = scope.replace(/[^a-z0-9-]/gi, '').toLowerCase();
214
+ const p = pkg.replace(/[^a-z0-9._-]/gi, '-').toLowerCase();
215
+ return `@${s}/${p || 'frontmcp-app'}`;
216
+ }
217
+ return name.replace(/[^a-z0-9._-]/gi, '-').toLowerCase() || 'frontmcp-app';
218
+ }
219
+ /* ------------------------ Self-version detection -------------------------- */
220
+ function getSelfVersion() {
221
+ return __awaiter(this, void 0, void 0, function* () {
222
+ // Best-effort: find our own package.json near the executed bin.
223
+ const binPath = process.argv[1] || __filename;
224
+ const candidates = [
225
+ path.resolve(path.dirname(binPath), '../package.json'),
226
+ path.resolve(path.dirname(binPath), '../../package.json'),
227
+ ];
228
+ for (const p of candidates) {
229
+ const j = yield readJSON(p);
230
+ if (j === null || j === void 0 ? void 0 : j.version)
231
+ return j.version;
232
+ }
233
+ // Fallback if not found; still satisfies the "has field" requirement.
234
+ return '0.0.0';
235
+ });
236
+ }
194
237
  /* --------------------------------- Actions -------------------------------- */
195
238
  function runDev(opts) {
196
239
  return __awaiter(this, void 0, void 0, function* () {
@@ -201,11 +244,6 @@ function runDev(opts) {
201
244
  yield runCmd('npx', ['-y', 'tsx', '--watch', entry]);
202
245
  });
203
246
  }
204
- function ensureDir(p) {
205
- return __awaiter(this, void 0, void 0, function* () {
206
- yield fs_1.promises.mkdir(p, { recursive: true });
207
- });
208
- }
209
247
  function isTsLike(p) {
210
248
  return /\.tsx?$/i.test(p);
211
249
  }
@@ -229,7 +267,6 @@ function runBuild(opts) {
229
267
  if (yield fileExists(tsconfigPath)) {
230
268
  console.log(c('gray', `[build] tsconfig.json detected (project options will be respected where applicable)`));
231
269
  }
232
- // Compile the single entry file
233
270
  yield runCmd('npx', ['-y', 'tsc', entry, ...args]);
234
271
  console.log(c('green', '✅ Build completed.'));
235
272
  console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
@@ -276,7 +313,6 @@ function deepMerge(base, patch) {
276
313
  function ensureRequiredTsOptions(obj) {
277
314
  const next = Object.assign({}, obj);
278
315
  next.compilerOptions = Object.assign({}, (next.compilerOptions || {}));
279
- // Force the required values
280
316
  next.compilerOptions.target = REQUIRED_DECORATOR_FIELDS.target;
281
317
  next.compilerOptions.module = REQUIRED_DECORATOR_FIELDS.module;
282
318
  next.compilerOptions.emitDecoratorMetadata = REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata;
@@ -311,18 +347,17 @@ function checkRequiredTsOptions(compilerOptions) {
311
347
  issues.push(`compilerOptions.experimentalDecorators should be true`);
312
348
  return { ok, issues };
313
349
  }
314
- function runInit() {
350
+ function runInit(baseDir) {
315
351
  return __awaiter(this, void 0, void 0, function* () {
316
- const cwd = process.cwd();
352
+ const cwd = baseDir !== null && baseDir !== void 0 ? baseDir : process.cwd();
317
353
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
318
354
  const existing = yield readJSON(tsconfigPath);
319
355
  if (!existing) {
320
- console.log(c('yellow', 'tsconfig.json not found — creating one.'));
356
+ console.log(c('yellow', `tsconfig.json not found — creating one in ${path.relative(process.cwd(), cwd) || '.'}.`));
321
357
  yield writeJSON(tsconfigPath, RECOMMENDED_TSCONFIG);
322
358
  console.log(c('green', '✅ Created tsconfig.json with required decorator settings.'));
323
359
  return;
324
360
  }
325
- // Merge user config on top of recommended, then force required decorator options
326
361
  let merged = deepMerge(RECOMMENDED_TSCONFIG, existing);
327
362
  merged = ensureRequiredTsOptions(merged);
328
363
  yield writeJSON(tsconfigPath, merged);
@@ -347,7 +382,6 @@ function runDoctor() {
347
382
  const MIN_NPM = '10.0.0';
348
383
  const cwd = process.cwd();
349
384
  let ok = true;
350
- // Node
351
385
  const nodeVer = process.versions.node;
352
386
  if (cmpSemver(nodeVer, MIN_NODE) >= 0) {
353
387
  console.log(`✅ Node ${nodeVer} (min ${MIN_NODE})`);
@@ -356,7 +390,6 @@ function runDoctor() {
356
390
  ok = false;
357
391
  console.log(`❌ Node ${nodeVer} — please upgrade to >= ${MIN_NODE}`);
358
392
  }
359
- // npm
360
393
  let npmVer = 'unknown';
361
394
  try {
362
395
  npmVer = yield new Promise((resolve, reject) => {
@@ -379,7 +412,6 @@ function runDoctor() {
379
412
  ok = false;
380
413
  console.log('❌ npm not found in PATH');
381
414
  }
382
- // tsconfig.json presence + required fields
383
415
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
384
416
  if (yield fileExists(tsconfigPath)) {
385
417
  console.log(`✅ tsconfig.json found`);
@@ -398,7 +430,6 @@ function runDoctor() {
398
430
  ok = false;
399
431
  console.log(`❌ tsconfig.json not found — run ${c('cyan', 'frontmcp init')}`);
400
432
  }
401
- // Entry check (nice to have)
402
433
  try {
403
434
  const entry = yield resolveEntry(cwd);
404
435
  console.log(`✅ entry detected: ${path.relative(cwd, entry)}`);
@@ -407,12 +438,10 @@ function runDoctor() {
407
438
  const firstLine = (_c = (_b = (_a = e === null || e === void 0 ? void 0 : e.message) === null || _a === void 0 ? void 0 : _a.split('\n')) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : 'entry not found';
408
439
  console.log(`❌ entry not detected — ${firstLine}`);
409
440
  }
410
- if (ok) {
441
+ if (ok)
411
442
  console.log(c('green', '\nAll checks passed. You are ready to go!'));
412
- }
413
- else {
443
+ else
414
444
  console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
415
- }
416
445
  });
417
446
  }
418
447
  /* ------------------------------- Inspector -------------------------------- */
@@ -426,13 +455,13 @@ function runInspector() {
426
455
  function pkgNameFromCwd(cwd) {
427
456
  return path.basename(cwd).replace(/[^a-zA-Z0-9._-]/g, '-').toLowerCase() || 'frontmcp-app';
428
457
  }
429
- function upsertPackageJson(cwd) {
458
+ function upsertPackageJson(cwd, nameOverride, selfVersion) {
430
459
  return __awaiter(this, void 0, void 0, function* () {
431
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
460
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
432
461
  const pkgPath = path.join(cwd, 'package.json');
433
462
  const existing = yield readJSON(pkgPath);
434
463
  const base = {
435
- name: pkgNameFromCwd(cwd),
464
+ name: nameOverride !== null && nameOverride !== void 0 ? nameOverride : pkgNameFromCwd(cwd),
436
465
  version: '0.1.0',
437
466
  private: true,
438
467
  type: 'module',
@@ -449,39 +478,45 @@ function upsertPackageJson(cwd) {
449
478
  },
450
479
  dependencies: {
451
480
  '@frontmcp/sdk': 'latest',
452
- zod: 'latest',
481
+ zod: '^3.23.8',
482
+ 'reflect-metadata': '^0.2.2',
453
483
  },
454
484
  devDependencies: {
455
- typescript: 'latest',
456
- tsx: 'latest',
485
+ frontmcp: selfVersion, // exact version used by npx
486
+ tsx: '^4.20.6',
487
+ typescript: '^5.5.3',
457
488
  },
458
489
  };
459
490
  if (!existing) {
460
491
  yield writeJSON(pkgPath, base);
461
- console.log(c('green', '✅ Created package.json (with scripts: dev, build, inspect, doctor)'));
492
+ console.log(c('green', '✅ Created package.json (pinned tsx/typescript/zod/reflect-metadata, exact frontmcp)'));
462
493
  return;
463
494
  }
464
- // Merge, preserving user fields; ensure our requirements exist
465
495
  const merged = Object.assign(Object.assign({}, base), existing);
496
+ merged.name = existing.name || base.name;
466
497
  merged.main = existing.main || base.main;
467
498
  merged.type = existing.type || base.type;
468
- merged.scripts = Object.assign(Object.assign(Object.assign({}, base.scripts), (existing.scripts || {})), { dev: ((_b = (_a = existing.scripts) === null || _a === void 0 ? void 0 : _a.dev) !== null && _b !== void 0 ? _b : base.scripts.dev), build: ((_d = (_c = existing.scripts) === null || _c === void 0 ? void 0 : _c.build) !== null && _d !== void 0 ? _d : base.scripts.build), inspect: ((_f = (_e = existing.scripts) === null || _e === void 0 ? void 0 : _e.inspect) !== null && _f !== void 0 ? _f : base.scripts.inspect), doctor: ((_h = (_g = existing.scripts) === null || _g === void 0 ? void 0 : _g.doctor) !== null && _h !== void 0 ? _h : base.scripts.doctor) });
499
+ merged.scripts = Object.assign(Object.assign(Object.assign({}, base.scripts), (existing.scripts || {})), { dev: (_b = (_a = existing.scripts) === null || _a === void 0 ? void 0 : _a.dev) !== null && _b !== void 0 ? _b : base.scripts.dev, build: (_d = (_c = existing.scripts) === null || _c === void 0 ? void 0 : _c.build) !== null && _d !== void 0 ? _d : base.scripts.build, inspect: (_f = (_e = existing.scripts) === null || _e === void 0 ? void 0 : _e.inspect) !== null && _f !== void 0 ? _f : base.scripts.inspect, doctor: (_h = (_g = existing.scripts) === null || _g === void 0 ? void 0 : _g.doctor) !== null && _h !== void 0 ? _h : base.scripts.doctor });
469
500
  merged.engines = Object.assign(Object.assign({}, (existing.engines || {})), { node: ((_j = existing.engines) === null || _j === void 0 ? void 0 : _j.node) || base.engines.node, npm: ((_k = existing.engines) === null || _k === void 0 ? void 0 : _k.npm) || base.engines.npm });
470
- merged.dependencies = Object.assign(Object.assign({}, existing.dependencies), { '@frontmcp/sdk': ((_l = existing.dependencies) === null || _l === void 0 ? void 0 : _l['@frontmcp/sdk']) || base.dependencies['@frontmcp/sdk'], zod: ((_m = existing.dependencies) === null || _m === void 0 ? void 0 : _m.zod) || base.dependencies.zod });
471
- merged.devDependencies = Object.assign(Object.assign({}, existing.devDependencies), { typescript: ((_o = existing.devDependencies) === null || _o === void 0 ? void 0 : _o.typescript) || base.devDependencies.typescript, tsx: ((_p = existing.devDependencies) === null || _p === void 0 ? void 0 : _p.tsx) || base.devDependencies.tsx });
501
+ merged.dependencies = Object.assign(Object.assign(Object.assign({}, base.dependencies), (existing.dependencies || {})), {
502
+ // ensure pins
503
+ zod: '^3.23.8', 'reflect-metadata': '^0.2.2' });
504
+ merged.devDependencies = Object.assign(Object.assign(Object.assign({}, base.devDependencies), (existing.devDependencies || {})), {
505
+ // ensure pins
506
+ frontmcp: selfVersion, tsx: '^4.20.6', typescript: '^5.5.3' });
472
507
  yield writeJSON(pkgPath, merged);
473
- console.log(c('green', '✅ Updated package.json (ensured scripts, engines, deps)'));
508
+ console.log(c('green', '✅ Updated package.json (ensured exact frontmcp and pinned versions)'));
474
509
  });
475
510
  }
476
- function scaffoldFileIfMissing(p, content) {
511
+ function scaffoldFileIfMissing(baseDir, p, content) {
477
512
  return __awaiter(this, void 0, void 0, function* () {
478
513
  if (yield fileExists(p)) {
479
- console.log(c('gray', `skip: ${path.relative(process.cwd(), p)} already exists`));
514
+ console.log(c('gray', `skip: ${path.relative(baseDir, p)} already exists`));
480
515
  return;
481
516
  }
482
517
  yield ensureDir(path.dirname(p));
483
518
  yield fs_1.promises.writeFile(p, content.replace(/^\n/, ''), 'utf8');
484
- console.log(c('green', `✓ created ${path.relative(process.cwd(), p)}`));
519
+ console.log(c('green', `✓ created ${path.relative(baseDir, p)}`));
485
520
  });
486
521
  }
487
522
  const TEMPLATE_MAIN_TS = `
@@ -526,24 +561,43 @@ const AddTool = tool({
526
561
 
527
562
  export default AddTool;
528
563
  `;
529
- function runCreate() {
564
+ function runCreate(projectArg) {
530
565
  return __awaiter(this, void 0, void 0, function* () {
531
- const cwd = process.cwd();
532
- console.log(c('cyan', '[create]'), 'Scaffolding FrontMCP project in', c('bold', cwd));
566
+ if (!projectArg) {
567
+ console.error(c('red', 'Error: project name is required.\n'));
568
+ console.log(`Usage: ${c('bold', 'npx frontmcp create <project-name>')}`);
569
+ process.exit(1);
570
+ }
571
+ const folder = sanitizeForFolder(projectArg);
572
+ const pkgName = sanitizeForNpm(projectArg);
573
+ const targetDir = path.resolve(process.cwd(), folder);
574
+ if (yield fileExists(targetDir)) {
575
+ if (!(yield isDirEmpty(targetDir))) {
576
+ console.error(c('red', `Refusing to scaffold into non-empty directory: ${path.relative(process.cwd(), targetDir)}`));
577
+ console.log(c('gray', 'Pick a different name or start with an empty folder.'));
578
+ process.exit(1);
579
+ }
580
+ }
581
+ else {
582
+ yield ensureDir(targetDir);
583
+ }
584
+ console.log(`${c('cyan', '[create]')} Creating project in ${c('bold', './' + path.relative(process.cwd(), targetDir))}`);
585
+ process.chdir(targetDir);
533
586
  // 1) tsconfig
534
- yield runInit();
535
- // 2) package.json
536
- yield upsertPackageJson(cwd);
587
+ yield runInit(targetDir);
588
+ // 2) package.json (with pinned deps and exact frontmcp version)
589
+ const selfVersion = yield getSelfVersion();
590
+ yield upsertPackageJson(targetDir, pkgName, selfVersion);
537
591
  // 3) files
538
- yield scaffoldFileIfMissing(path.join(cwd, 'src', 'main.ts'), TEMPLATE_MAIN_TS);
539
- yield scaffoldFileIfMissing(path.join(cwd, 'src', 'calc.app.ts'), TEMPLATE_CALC_APP_TS);
540
- yield scaffoldFileIfMissing(path.join(cwd, 'src', 'tools', 'add.tool.ts'), TEMPLATE_ADD_TOOL_TS);
541
- // 4) final tips
592
+ yield scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'main.ts'), TEMPLATE_MAIN_TS);
593
+ yield scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'calc.app.ts'), TEMPLATE_CALC_APP_TS);
594
+ yield scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'tools', 'add.tool.ts'), TEMPLATE_ADD_TOOL_TS);
542
595
  console.log('\nNext steps:');
543
- console.log(' 1) npm install');
544
- console.log(' 2) npm run dev ', c('gray', '# starts tsx watcher via frontmcp dev'));
545
- console.log(' 3) npm run inspect', c('gray', '# launch MCP Inspector'));
546
- console.log(' 4) npm run build ', c('gray', '# compile with tsc via frontmcp build'));
596
+ console.log(` 1) cd ${folder}`);
597
+ console.log(' 2) npm install');
598
+ console.log(' 3) npm run dev ', c('gray', '# starts tsx watcher via frontmcp dev'));
599
+ console.log(' 4) npm run inspect ', c('gray', '# launch MCP Inspector'));
600
+ console.log(' 5) npm run build ', c('gray', '# compile with tsc via frontmcp build'));
547
601
  });
548
602
  }
549
603
  /* --------------------------------- Main ----------------------------------- */
@@ -574,9 +628,11 @@ function main() {
574
628
  case 'inspector':
575
629
  yield runInspector();
576
630
  break;
577
- case 'create':
578
- yield runCreate();
631
+ case 'create': {
632
+ const projectName = parsed._[1]; // require a name
633
+ yield runCreate(projectName);
579
634
  break;
635
+ }
580
636
  case 'help':
581
637
  showHelp();
582
638
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontmcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "FrontMCP command line interface",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -47,7 +47,7 @@ ${c('bold', 'Commands')}
47
47
  init Create or fix a tsconfig.json suitable for FrontMCP
48
48
  doctor Check Node/npm versions and tsconfig requirements
49
49
  inspector Launch MCP Inspector (npx @modelcontextprotocol/inspector)
50
- create Scaffold a new FrontMCP project in the current directory
50
+ create <name> Scaffold a new FrontMCP project in ./<name>
51
51
  help Show this help message
52
52
 
53
53
  ${c('bold', 'Options')}
@@ -60,7 +60,7 @@ ${c('bold', 'Examples')}
60
60
  frontmcp init
61
61
  frontmcp doctor
62
62
  frontmcp inspector
63
- npx frontmcp create
63
+ npx frontmcp create my-mcp
64
64
  `);
65
65
  }
66
66
 
@@ -109,30 +109,20 @@ async function resolveEntry(cwd: string, explicit?: string): Promise<string> {
109
109
  if (await fileExists(full)) return full;
110
110
  throw new Error(`Entry override not found: ${explicit}`);
111
111
  }
112
-
113
- // 1) package.json main
114
112
  const pkgPath = path.join(cwd, 'package.json');
115
113
  if (await fileExists(pkgPath)) {
116
114
  const pkg = await readJSON<any>(pkgPath);
117
115
  if (pkg && typeof pkg.main === 'string' && pkg.main.trim()) {
118
116
  const mainCandidates = tryCandidates(path.resolve(cwd, pkg.main));
119
- for (const p of mainCandidates) {
120
- if (await fileExists(p)) return p;
121
- }
122
- // If "main" is a directory-like path, try index.* within it
117
+ for (const p of mainCandidates) if (await fileExists(p)) return p;
123
118
  const asDir = path.resolve(cwd, pkg.main);
124
119
  const idxCandidates = tryCandidates(path.join(asDir, 'index'));
125
- for (const p of idxCandidates) {
126
- if (await fileExists(p)) return p;
127
- }
120
+ for (const p of idxCandidates) if (await fileExists(p)) return p;
128
121
  }
129
122
  }
130
-
131
- // 2) src/main.ts
132
123
  const fallback = path.join(cwd, 'src', 'main.ts');
133
124
  if (await fileExists(fallback)) return fallback;
134
125
 
135
- // 3) Not found
136
126
  const msg = [
137
127
  c('red', 'No entry file found.'),
138
128
  '',
@@ -155,6 +145,52 @@ function runCmd(cmd: string, args: string[], opts: { cwd?: string } = {}): Promi
155
145
  });
156
146
  }
157
147
 
148
+ async function ensureDir(p: string): Promise<void> {
149
+ await fsp.mkdir(p, {recursive: true});
150
+ }
151
+
152
+ async function isDirEmpty(dir: string): Promise<boolean> {
153
+ try {
154
+ const items = await fsp.readdir(dir);
155
+ return items.length === 0;
156
+ } catch (e: any) {
157
+ if (e?.code === 'ENOENT') return true;
158
+ throw e;
159
+ }
160
+ }
161
+
162
+ function sanitizeForFolder(name: string): string {
163
+ const seg = name.startsWith('@') && name.includes('/') ? name.split('/')[1] : name;
164
+ return seg.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').toLowerCase() || 'frontmcp-app';
165
+ }
166
+
167
+ function sanitizeForNpm(name: string): string {
168
+ if (name.startsWith('@') && name.includes('/')) {
169
+ const [scope, pkg] = name.split('/');
170
+ const s = scope.replace(/[^a-z0-9-]/gi, '').toLowerCase();
171
+ const p = pkg.replace(/[^a-z0-9._-]/gi, '-').toLowerCase();
172
+ return `@${s}/${p || 'frontmcp-app'}`;
173
+ }
174
+ return name.replace(/[^a-z0-9._-]/gi, '-').toLowerCase() || 'frontmcp-app';
175
+ }
176
+
177
+ /* ------------------------ Self-version detection -------------------------- */
178
+
179
+ async function getSelfVersion(): Promise<string> {
180
+ // Best-effort: find our own package.json near the executed bin.
181
+ const binPath = process.argv[1] || __filename;
182
+ const candidates = [
183
+ path.resolve(path.dirname(binPath), '../package.json'),
184
+ path.resolve(path.dirname(binPath), '../../package.json'),
185
+ ];
186
+ for (const p of candidates) {
187
+ const j = await readJSON<any>(p);
188
+ if (j?.version) return j.version;
189
+ }
190
+ // Fallback if not found; still satisfies the "has field" requirement.
191
+ return '0.0.0';
192
+ }
193
+
158
194
  /* --------------------------------- Actions -------------------------------- */
159
195
 
160
196
  async function runDev(opts: ParsedArgs): Promise<void> {
@@ -165,10 +201,6 @@ async function runDev(opts: ParsedArgs): Promise<void> {
165
201
  await runCmd('npx', ['-y', 'tsx', '--watch', entry]);
166
202
  }
167
203
 
168
- async function ensureDir(p: string): Promise<void> {
169
- await fsp.mkdir(p, {recursive: true});
170
- }
171
-
172
204
  function isTsLike(p: string): boolean {
173
205
  return /\.tsx?$/i.test(p);
174
206
  }
@@ -197,7 +229,6 @@ async function runBuild(opts: ParsedArgs): Promise<void> {
197
229
  console.log(c('gray', `[build] tsconfig.json detected (project options will be respected where applicable)`));
198
230
  }
199
231
 
200
- // Compile the single entry file
201
232
  await runCmd('npx', ['-y', 'tsc', entry, ...args]);
202
233
  console.log(c('green', '✅ Build completed.'));
203
234
  console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
@@ -247,13 +278,10 @@ function deepMerge<T extends Record<string, any>, U extends Record<string, any>>
247
278
  function ensureRequiredTsOptions(obj: Record<string, any>): Record<string, any> {
248
279
  const next = {...obj};
249
280
  next.compilerOptions = {...(next.compilerOptions || {})};
250
-
251
- // Force the required values
252
281
  next.compilerOptions.target = REQUIRED_DECORATOR_FIELDS.target;
253
282
  next.compilerOptions.module = REQUIRED_DECORATOR_FIELDS.module;
254
283
  next.compilerOptions.emitDecoratorMetadata = REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata;
255
284
  next.compilerOptions.experimentalDecorators = REQUIRED_DECORATOR_FIELDS.experimentalDecorators;
256
-
257
285
  return next;
258
286
  }
259
287
 
@@ -285,19 +313,18 @@ function checkRequiredTsOptions(compilerOptions: Record<string, any> | undefined
285
313
  return {ok, issues};
286
314
  }
287
315
 
288
- async function runInit(): Promise<void> {
289
- const cwd = process.cwd();
316
+ async function runInit(baseDir?: string): Promise<void> {
317
+ const cwd = baseDir ?? process.cwd();
290
318
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
291
319
  const existing = await readJSON<Record<string, any>>(tsconfigPath);
292
320
 
293
321
  if (!existing) {
294
- console.log(c('yellow', 'tsconfig.json not found — creating one.'));
322
+ console.log(c('yellow', `tsconfig.json not found — creating one in ${path.relative(process.cwd(), cwd) || '.'}.`));
295
323
  await writeJSON(tsconfigPath, RECOMMENDED_TSCONFIG);
296
324
  console.log(c('green', '✅ Created tsconfig.json with required decorator settings.'));
297
325
  return;
298
326
  }
299
327
 
300
- // Merge user config on top of recommended, then force required decorator options
301
328
  let merged = deepMerge(RECOMMENDED_TSCONFIG as any, existing);
302
329
  merged = ensureRequiredTsOptions(merged);
303
330
 
@@ -322,7 +349,6 @@ async function runDoctor(): Promise<void> {
322
349
 
323
350
  let ok = true;
324
351
 
325
- // Node
326
352
  const nodeVer = process.versions.node;
327
353
  if (cmpSemver(nodeVer, MIN_NODE) >= 0) {
328
354
  console.log(`✅ Node ${nodeVer} (min ${MIN_NODE})`);
@@ -331,7 +357,6 @@ async function runDoctor(): Promise<void> {
331
357
  console.log(`❌ Node ${nodeVer} — please upgrade to >= ${MIN_NODE}`);
332
358
  }
333
359
 
334
- // npm
335
360
  let npmVer = 'unknown';
336
361
  try {
337
362
  npmVer = await new Promise<string>((resolve, reject) => {
@@ -352,13 +377,11 @@ async function runDoctor(): Promise<void> {
352
377
  console.log('❌ npm not found in PATH');
353
378
  }
354
379
 
355
- // tsconfig.json presence + required fields
356
380
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
357
381
  if (await fileExists(tsconfigPath)) {
358
382
  console.log(`✅ tsconfig.json found`);
359
383
  const tsconfig = await readJSON<Record<string, any>>(tsconfigPath);
360
384
  const {ok: oks, issues} = checkRequiredTsOptions(tsconfig?.compilerOptions);
361
-
362
385
  for (const line of oks) console.log(c('green', ` ✓ ${line}`));
363
386
  if (issues.length) {
364
387
  ok = false;
@@ -370,7 +393,6 @@ async function runDoctor(): Promise<void> {
370
393
  console.log(`❌ tsconfig.json not found — run ${c('cyan', 'frontmcp init')}`);
371
394
  }
372
395
 
373
- // Entry check (nice to have)
374
396
  try {
375
397
  const entry = await resolveEntry(cwd);
376
398
  console.log(`✅ entry detected: ${path.relative(cwd, entry)}`);
@@ -379,11 +401,8 @@ async function runDoctor(): Promise<void> {
379
401
  console.log(`❌ entry not detected — ${firstLine}`);
380
402
  }
381
403
 
382
- if (ok) {
383
- console.log(c('green', '\nAll checks passed. You are ready to go!'));
384
- } else {
385
- console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
386
- }
404
+ if (ok) console.log(c('green', '\nAll checks passed. You are ready to go!'));
405
+ else console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
387
406
  }
388
407
 
389
408
  /* ------------------------------- Inspector -------------------------------- */
@@ -399,12 +418,12 @@ function pkgNameFromCwd(cwd: string) {
399
418
  return path.basename(cwd).replace(/[^a-zA-Z0-9._-]/g, '-').toLowerCase() || 'frontmcp-app';
400
419
  }
401
420
 
402
- async function upsertPackageJson(cwd: string) {
421
+ async function upsertPackageJson(cwd: string, nameOverride: string | undefined, selfVersion: string) {
403
422
  const pkgPath = path.join(cwd, 'package.json');
404
423
  const existing = await readJSON<Record<string, any>>(pkgPath);
405
424
 
406
425
  const base = {
407
- name: pkgNameFromCwd(cwd),
426
+ name: nameOverride ?? pkgNameFromCwd(cwd),
408
427
  version: '0.1.0',
409
428
  private: true,
410
429
  type: 'module',
@@ -421,33 +440,35 @@ async function upsertPackageJson(cwd: string) {
421
440
  },
422
441
  dependencies: {
423
442
  '@frontmcp/sdk': 'latest',
424
- zod: 'latest',
443
+ zod: '^3.23.8',
444
+ 'reflect-metadata': '^0.2.2',
425
445
  },
426
446
  devDependencies: {
427
- typescript: 'latest',
428
- tsx: 'latest',
447
+ frontmcp: selfVersion, // exact version used by npx
448
+ tsx: '^4.20.6',
449
+ typescript: '^5.5.3',
429
450
  },
430
451
  };
431
452
 
432
453
  if (!existing) {
433
454
  await writeJSON(pkgPath, base);
434
- console.log(c('green', '✅ Created package.json (with scripts: dev, build, inspect, doctor)'));
455
+ console.log(c('green', '✅ Created package.json (pinned tsx/typescript/zod/reflect-metadata, exact frontmcp)'));
435
456
  return;
436
457
  }
437
458
 
438
- // Merge, preserving user fields; ensure our requirements exist
439
459
  const merged = {...base, ...existing};
440
460
 
461
+ merged.name = existing.name || base.name;
441
462
  merged.main = existing.main || base.main;
442
463
  merged.type = existing.type || base.type;
443
464
 
444
465
  merged.scripts = {
445
466
  ...base.scripts,
446
467
  ...(existing.scripts || {}),
447
- dev: (existing.scripts?.dev ?? base.scripts.dev),
448
- build: (existing.scripts?.build ?? base.scripts.build),
449
- inspect: (existing.scripts?.inspect ?? base.scripts.inspect),
450
- doctor: (existing.scripts?.doctor ?? base.scripts.doctor),
468
+ dev: existing.scripts?.dev ?? base.scripts.dev,
469
+ build: existing.scripts?.build ?? base.scripts.build,
470
+ inspect: existing.scripts?.inspect ?? base.scripts.inspect,
471
+ doctor: existing.scripts?.doctor ?? base.scripts.doctor,
451
472
  };
452
473
 
453
474
  merged.engines = {
@@ -457,29 +478,34 @@ async function upsertPackageJson(cwd: string) {
457
478
  };
458
479
 
459
480
  merged.dependencies = {
460
- ...existing.dependencies,
461
- '@frontmcp/sdk': existing.dependencies?.['@frontmcp/sdk'] || base.dependencies['@frontmcp/sdk'],
462
- zod: existing.dependencies?.zod || base.dependencies.zod,
481
+ ...base.dependencies,
482
+ ...(existing.dependencies || {}),
483
+ // ensure pins
484
+ zod: '^3.23.8',
485
+ 'reflect-metadata': '^0.2.2',
463
486
  };
464
487
 
465
488
  merged.devDependencies = {
466
- ...existing.devDependencies,
467
- typescript: existing.devDependencies?.typescript || base.devDependencies.typescript,
468
- tsx: existing.devDependencies?.tsx || base.devDependencies.tsx,
489
+ ...base.devDependencies,
490
+ ...(existing.devDependencies || {}),
491
+ // ensure pins
492
+ frontmcp: selfVersion,
493
+ tsx: '^4.20.6',
494
+ typescript: '^5.5.3',
469
495
  };
470
496
 
471
497
  await writeJSON(pkgPath, merged);
472
- console.log(c('green', '✅ Updated package.json (ensured scripts, engines, deps)'));
498
+ console.log(c('green', '✅ Updated package.json (ensured exact frontmcp and pinned versions)'));
473
499
  }
474
500
 
475
- async function scaffoldFileIfMissing(p: string, content: string) {
501
+ async function scaffoldFileIfMissing(baseDir: string, p: string, content: string) {
476
502
  if (await fileExists(p)) {
477
- console.log(c('gray', `skip: ${path.relative(process.cwd(), p)} already exists`));
503
+ console.log(c('gray', `skip: ${path.relative(baseDir, p)} already exists`));
478
504
  return;
479
505
  }
480
506
  await ensureDir(path.dirname(p));
481
507
  await fsp.writeFile(p, content.replace(/^\n/, ''), 'utf8');
482
- console.log(c('green', `✓ created ${path.relative(process.cwd(), p)}`));
508
+ console.log(c('green', `✓ created ${path.relative(baseDir, p)}`));
483
509
  }
484
510
 
485
511
  const TEMPLATE_MAIN_TS = `
@@ -527,28 +553,48 @@ const AddTool = tool({
527
553
  export default AddTool;
528
554
  `;
529
555
 
530
- async function runCreate(): Promise<void> {
531
- const cwd = process.cwd();
556
+ async function runCreate(projectArg?: string): Promise<void> {
557
+ if (!projectArg) {
558
+ console.error(c('red', 'Error: project name is required.\n'));
559
+ console.log(`Usage: ${c('bold', 'npx frontmcp create <project-name>')}`);
560
+ process.exit(1);
561
+ }
562
+
563
+ const folder = sanitizeForFolder(projectArg);
564
+ const pkgName = sanitizeForNpm(projectArg);
565
+ const targetDir = path.resolve(process.cwd(), folder);
532
566
 
533
- console.log(c('cyan', '[create]'), 'Scaffolding FrontMCP project in', c('bold', cwd));
567
+ if (await fileExists(targetDir)) {
568
+ if (!(await isDirEmpty(targetDir))) {
569
+ console.error(c('red', `Refusing to scaffold into non-empty directory: ${path.relative(process.cwd(), targetDir)}`));
570
+ console.log(c('gray', 'Pick a different name or start with an empty folder.'));
571
+ process.exit(1);
572
+ }
573
+ } else {
574
+ await ensureDir(targetDir);
575
+ }
576
+
577
+ console.log(`${c('cyan', '[create]')} Creating project in ${c('bold', './' + path.relative(process.cwd(), targetDir))}`);
578
+ process.chdir(targetDir);
534
579
 
535
580
  // 1) tsconfig
536
- await runInit();
581
+ await runInit(targetDir);
537
582
 
538
- // 2) package.json
539
- await upsertPackageJson(cwd);
583
+ // 2) package.json (with pinned deps and exact frontmcp version)
584
+ const selfVersion = await getSelfVersion();
585
+ await upsertPackageJson(targetDir, pkgName, selfVersion);
540
586
 
541
587
  // 3) files
542
- await scaffoldFileIfMissing(path.join(cwd, 'src', 'main.ts'), TEMPLATE_MAIN_TS);
543
- await scaffoldFileIfMissing(path.join(cwd, 'src', 'calc.app.ts'), TEMPLATE_CALC_APP_TS);
544
- await scaffoldFileIfMissing(path.join(cwd, 'src', 'tools', 'add.tool.ts'), TEMPLATE_ADD_TOOL_TS);
588
+ await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'main.ts'), TEMPLATE_MAIN_TS);
589
+ await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'calc.app.ts'), TEMPLATE_CALC_APP_TS);
590
+ await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'tools', 'add.tool.ts'), TEMPLATE_ADD_TOOL_TS);
545
591
 
546
- // 4) final tips
547
592
  console.log('\nNext steps:');
548
- console.log(' 1) npm install');
549
- console.log(' 2) npm run dev ', c('gray', '# starts tsx watcher via frontmcp dev'));
550
- console.log(' 3) npm run inspect', c('gray', '# launch MCP Inspector'));
551
- console.log(' 4) npm run build ', c('gray', '# compile with tsc via frontmcp build'));
593
+ console.log(` 1) cd ${folder}`);
594
+ console.log(' 2) npm install');
595
+ console.log(' 3) npm run dev ', c('gray', '# starts tsx watcher via frontmcp dev'));
596
+ console.log(' 4) npm run inspect ', c('gray', '# launch MCP Inspector'));
597
+ console.log(' 5) npm run build ', c('gray', '# compile with tsc via frontmcp build'));
552
598
  }
553
599
 
554
600
  /* --------------------------------- Main ----------------------------------- */
@@ -581,9 +627,11 @@ async function main(): Promise<void> {
581
627
  case 'inspector':
582
628
  await runInspector();
583
629
  break;
584
- case 'create':
585
- await runCreate();
630
+ case 'create': {
631
+ const projectName = parsed._[1]; // require a name
632
+ await runCreate(projectName);
586
633
  break;
634
+ }
587
635
  case 'help':
588
636
  showHelp();
589
637
  break;