frontmcp 0.1.3 → 0.1.5

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 +105 -61
  2. package/package.json +1 -1
  3. package/src/cli.ts +111 -74
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,43 @@ 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
+ return '0.1.5';
223
+ });
224
+ }
194
225
  /* --------------------------------- Actions -------------------------------- */
195
226
  function runDev(opts) {
196
227
  return __awaiter(this, void 0, void 0, function* () {
@@ -201,11 +232,6 @@ function runDev(opts) {
201
232
  yield runCmd('npx', ['-y', 'tsx', '--watch', entry]);
202
233
  });
203
234
  }
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
235
  function isTsLike(p) {
210
236
  return /\.tsx?$/i.test(p);
211
237
  }
@@ -229,7 +255,6 @@ function runBuild(opts) {
229
255
  if (yield fileExists(tsconfigPath)) {
230
256
  console.log(c('gray', `[build] tsconfig.json detected (project options will be respected where applicable)`));
231
257
  }
232
- // Compile the single entry file
233
258
  yield runCmd('npx', ['-y', 'tsc', entry, ...args]);
234
259
  console.log(c('green', '✅ Build completed.'));
235
260
  console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
@@ -248,7 +273,7 @@ const RECOMMENDED_TSCONFIG = {
248
273
  module: REQUIRED_DECORATOR_FIELDS.module,
249
274
  emitDecoratorMetadata: REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata,
250
275
  experimentalDecorators: REQUIRED_DECORATOR_FIELDS.experimentalDecorators,
251
- moduleResolution: 'NodeNext',
276
+ moduleResolution: 'node',
252
277
  strict: true,
253
278
  esModuleInterop: true,
254
279
  resolveJsonModule: true,
@@ -276,7 +301,6 @@ function deepMerge(base, patch) {
276
301
  function ensureRequiredTsOptions(obj) {
277
302
  const next = Object.assign({}, obj);
278
303
  next.compilerOptions = Object.assign({}, (next.compilerOptions || {}));
279
- // Force the required values
280
304
  next.compilerOptions.target = REQUIRED_DECORATOR_FIELDS.target;
281
305
  next.compilerOptions.module = REQUIRED_DECORATOR_FIELDS.module;
282
306
  next.compilerOptions.emitDecoratorMetadata = REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata;
@@ -311,18 +335,17 @@ function checkRequiredTsOptions(compilerOptions) {
311
335
  issues.push(`compilerOptions.experimentalDecorators should be true`);
312
336
  return { ok, issues };
313
337
  }
314
- function runInit() {
338
+ function runInit(baseDir) {
315
339
  return __awaiter(this, void 0, void 0, function* () {
316
- const cwd = process.cwd();
340
+ const cwd = baseDir !== null && baseDir !== void 0 ? baseDir : process.cwd();
317
341
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
318
342
  const existing = yield readJSON(tsconfigPath);
319
343
  if (!existing) {
320
- console.log(c('yellow', 'tsconfig.json not found — creating one.'));
344
+ console.log(c('yellow', `tsconfig.json not found — creating one in ${path.relative(process.cwd(), cwd) || '.'}.`));
321
345
  yield writeJSON(tsconfigPath, RECOMMENDED_TSCONFIG);
322
346
  console.log(c('green', '✅ Created tsconfig.json with required decorator settings.'));
323
347
  return;
324
348
  }
325
- // Merge user config on top of recommended, then force required decorator options
326
349
  let merged = deepMerge(RECOMMENDED_TSCONFIG, existing);
327
350
  merged = ensureRequiredTsOptions(merged);
328
351
  yield writeJSON(tsconfigPath, merged);
@@ -347,7 +370,6 @@ function runDoctor() {
347
370
  const MIN_NPM = '10.0.0';
348
371
  const cwd = process.cwd();
349
372
  let ok = true;
350
- // Node
351
373
  const nodeVer = process.versions.node;
352
374
  if (cmpSemver(nodeVer, MIN_NODE) >= 0) {
353
375
  console.log(`✅ Node ${nodeVer} (min ${MIN_NODE})`);
@@ -356,7 +378,6 @@ function runDoctor() {
356
378
  ok = false;
357
379
  console.log(`❌ Node ${nodeVer} — please upgrade to >= ${MIN_NODE}`);
358
380
  }
359
- // npm
360
381
  let npmVer = 'unknown';
361
382
  try {
362
383
  npmVer = yield new Promise((resolve, reject) => {
@@ -379,7 +400,6 @@ function runDoctor() {
379
400
  ok = false;
380
401
  console.log('❌ npm not found in PATH');
381
402
  }
382
- // tsconfig.json presence + required fields
383
403
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
384
404
  if (yield fileExists(tsconfigPath)) {
385
405
  console.log(`✅ tsconfig.json found`);
@@ -398,7 +418,6 @@ function runDoctor() {
398
418
  ok = false;
399
419
  console.log(`❌ tsconfig.json not found — run ${c('cyan', 'frontmcp init')}`);
400
420
  }
401
- // Entry check (nice to have)
402
421
  try {
403
422
  const entry = yield resolveEntry(cwd);
404
423
  console.log(`✅ entry detected: ${path.relative(cwd, entry)}`);
@@ -407,12 +426,10 @@ function runDoctor() {
407
426
  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
427
  console.log(`❌ entry not detected — ${firstLine}`);
409
428
  }
410
- if (ok) {
429
+ if (ok)
411
430
  console.log(c('green', '\nAll checks passed. You are ready to go!'));
412
- }
413
- else {
431
+ else
414
432
  console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
415
- }
416
433
  });
417
434
  }
418
435
  /* ------------------------------- Inspector -------------------------------- */
@@ -426,13 +443,13 @@ function runInspector() {
426
443
  function pkgNameFromCwd(cwd) {
427
444
  return path.basename(cwd).replace(/[^a-zA-Z0-9._-]/g, '-').toLowerCase() || 'frontmcp-app';
428
445
  }
429
- function upsertPackageJson(cwd) {
446
+ function upsertPackageJson(cwd, nameOverride, selfVersion) {
430
447
  return __awaiter(this, void 0, void 0, function* () {
431
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
448
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
432
449
  const pkgPath = path.join(cwd, 'package.json');
433
450
  const existing = yield readJSON(pkgPath);
434
451
  const base = {
435
- name: pkgNameFromCwd(cwd),
452
+ name: nameOverride !== null && nameOverride !== void 0 ? nameOverride : pkgNameFromCwd(cwd),
436
453
  version: '0.1.0',
437
454
  private: true,
438
455
  type: 'module',
@@ -449,39 +466,45 @@ function upsertPackageJson(cwd) {
449
466
  },
450
467
  dependencies: {
451
468
  '@frontmcp/sdk': 'latest',
452
- zod: 'latest',
469
+ zod: '^3.23.8',
470
+ 'reflect-metadata': '^0.2.2',
453
471
  },
454
472
  devDependencies: {
455
- typescript: 'latest',
456
- tsx: 'latest',
473
+ frontmcp: selfVersion, // exact version used by npx
474
+ tsx: '^4.20.6',
475
+ typescript: '^5.5.3',
457
476
  },
458
477
  };
459
478
  if (!existing) {
460
479
  yield writeJSON(pkgPath, base);
461
- console.log(c('green', '✅ Created package.json (with scripts: dev, build, inspect, doctor)'));
480
+ console.log(c('green', '✅ Created package.json (pinned tsx/typescript/zod/reflect-metadata, exact frontmcp)'));
462
481
  return;
463
482
  }
464
- // Merge, preserving user fields; ensure our requirements exist
465
483
  const merged = Object.assign(Object.assign({}, base), existing);
484
+ merged.name = existing.name || base.name;
466
485
  merged.main = existing.main || base.main;
467
486
  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) });
487
+ 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
488
  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 });
489
+ merged.dependencies = Object.assign(Object.assign(Object.assign({}, base.dependencies), (existing.dependencies || {})), {
490
+ // ensure pins
491
+ zod: '^3.23.8', 'reflect-metadata': '^0.2.2' });
492
+ merged.devDependencies = Object.assign(Object.assign(Object.assign({}, base.devDependencies), (existing.devDependencies || {})), {
493
+ // ensure pins
494
+ frontmcp: selfVersion, tsx: '^4.20.6', typescript: '^5.5.3' });
472
495
  yield writeJSON(pkgPath, merged);
473
- console.log(c('green', '✅ Updated package.json (ensured scripts, engines, deps)'));
496
+ console.log(c('green', '✅ Updated package.json (ensured exact frontmcp and pinned versions)'));
474
497
  });
475
498
  }
476
- function scaffoldFileIfMissing(p, content) {
499
+ function scaffoldFileIfMissing(baseDir, p, content) {
477
500
  return __awaiter(this, void 0, void 0, function* () {
478
501
  if (yield fileExists(p)) {
479
- console.log(c('gray', `skip: ${path.relative(process.cwd(), p)} already exists`));
502
+ console.log(c('gray', `skip: ${path.relative(baseDir, p)} already exists`));
480
503
  return;
481
504
  }
482
505
  yield ensureDir(path.dirname(p));
483
506
  yield fs_1.promises.writeFile(p, content.replace(/^\n/, ''), 'utf8');
484
- console.log(c('green', `✓ created ${path.relative(process.cwd(), p)}`));
507
+ console.log(c('green', `✓ created ${path.relative(baseDir, p)}`));
485
508
  });
486
509
  }
487
510
  const TEMPLATE_MAIN_TS = `
@@ -526,24 +549,43 @@ const AddTool = tool({
526
549
 
527
550
  export default AddTool;
528
551
  `;
529
- function runCreate() {
552
+ function runCreate(projectArg) {
530
553
  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));
554
+ if (!projectArg) {
555
+ console.error(c('red', 'Error: project name is required.\n'));
556
+ console.log(`Usage: ${c('bold', 'npx frontmcp create <project-name>')}`);
557
+ process.exit(1);
558
+ }
559
+ const folder = sanitizeForFolder(projectArg);
560
+ const pkgName = sanitizeForNpm(projectArg);
561
+ const targetDir = path.resolve(process.cwd(), folder);
562
+ if (yield fileExists(targetDir)) {
563
+ if (!(yield isDirEmpty(targetDir))) {
564
+ console.error(c('red', `Refusing to scaffold into non-empty directory: ${path.relative(process.cwd(), targetDir)}`));
565
+ console.log(c('gray', 'Pick a different name or start with an empty folder.'));
566
+ process.exit(1);
567
+ }
568
+ }
569
+ else {
570
+ yield ensureDir(targetDir);
571
+ }
572
+ console.log(`${c('cyan', '[create]')} Creating project in ${c('bold', './' + path.relative(process.cwd(), targetDir))}`);
573
+ process.chdir(targetDir);
533
574
  // 1) tsconfig
534
- yield runInit();
535
- // 2) package.json
536
- yield upsertPackageJson(cwd);
575
+ yield runInit(targetDir);
576
+ // 2) package.json (with pinned deps and exact frontmcp version)
577
+ const selfVersion = yield getSelfVersion();
578
+ yield upsertPackageJson(targetDir, pkgName, selfVersion);
537
579
  // 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
580
+ yield scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'main.ts'), TEMPLATE_MAIN_TS);
581
+ yield scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'calc.app.ts'), TEMPLATE_CALC_APP_TS);
582
+ yield scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'tools', 'add.tool.ts'), TEMPLATE_ADD_TOOL_TS);
542
583
  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'));
584
+ console.log(` 1) cd ${folder}`);
585
+ console.log(' 2) npm install');
586
+ console.log(' 3) npm run dev ', c('gray', '# starts tsx watcher via frontmcp dev'));
587
+ console.log(' 4) npm run inspect ', c('gray', '# launch MCP Inspector'));
588
+ console.log(' 5) npm run build ', c('gray', '# compile with tsc via frontmcp build'));
547
589
  });
548
590
  }
549
591
  /* --------------------------------- Main ----------------------------------- */
@@ -574,9 +616,11 @@ function main() {
574
616
  case 'inspector':
575
617
  yield runInspector();
576
618
  break;
577
- case 'create':
578
- yield runCreate();
619
+ case 'create': {
620
+ const projectName = parsed._[1]; // require a name
621
+ yield runCreate(projectName);
579
622
  break;
623
+ }
580
624
  case 'help':
581
625
  showHelp();
582
626
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontmcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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,41 @@ 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
+ return '0.1.5';
181
+ }
182
+
158
183
  /* --------------------------------- Actions -------------------------------- */
159
184
 
160
185
  async function runDev(opts: ParsedArgs): Promise<void> {
@@ -165,10 +190,6 @@ async function runDev(opts: ParsedArgs): Promise<void> {
165
190
  await runCmd('npx', ['-y', 'tsx', '--watch', entry]);
166
191
  }
167
192
 
168
- async function ensureDir(p: string): Promise<void> {
169
- await fsp.mkdir(p, {recursive: true});
170
- }
171
-
172
193
  function isTsLike(p: string): boolean {
173
194
  return /\.tsx?$/i.test(p);
174
195
  }
@@ -197,7 +218,6 @@ async function runBuild(opts: ParsedArgs): Promise<void> {
197
218
  console.log(c('gray', `[build] tsconfig.json detected (project options will be respected where applicable)`));
198
219
  }
199
220
 
200
- // Compile the single entry file
201
221
  await runCmd('npx', ['-y', 'tsc', entry, ...args]);
202
222
  console.log(c('green', '✅ Build completed.'));
203
223
  console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
@@ -219,7 +239,7 @@ const RECOMMENDED_TSCONFIG = {
219
239
  emitDecoratorMetadata: REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata,
220
240
  experimentalDecorators: REQUIRED_DECORATOR_FIELDS.experimentalDecorators,
221
241
 
222
- moduleResolution: 'NodeNext',
242
+ moduleResolution: 'node',
223
243
  strict: true,
224
244
  esModuleInterop: true,
225
245
  resolveJsonModule: true,
@@ -247,13 +267,10 @@ function deepMerge<T extends Record<string, any>, U extends Record<string, any>>
247
267
  function ensureRequiredTsOptions(obj: Record<string, any>): Record<string, any> {
248
268
  const next = {...obj};
249
269
  next.compilerOptions = {...(next.compilerOptions || {})};
250
-
251
- // Force the required values
252
270
  next.compilerOptions.target = REQUIRED_DECORATOR_FIELDS.target;
253
271
  next.compilerOptions.module = REQUIRED_DECORATOR_FIELDS.module;
254
272
  next.compilerOptions.emitDecoratorMetadata = REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata;
255
273
  next.compilerOptions.experimentalDecorators = REQUIRED_DECORATOR_FIELDS.experimentalDecorators;
256
-
257
274
  return next;
258
275
  }
259
276
 
@@ -285,19 +302,18 @@ function checkRequiredTsOptions(compilerOptions: Record<string, any> | undefined
285
302
  return {ok, issues};
286
303
  }
287
304
 
288
- async function runInit(): Promise<void> {
289
- const cwd = process.cwd();
305
+ async function runInit(baseDir?: string): Promise<void> {
306
+ const cwd = baseDir ?? process.cwd();
290
307
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
291
308
  const existing = await readJSON<Record<string, any>>(tsconfigPath);
292
309
 
293
310
  if (!existing) {
294
- console.log(c('yellow', 'tsconfig.json not found — creating one.'));
311
+ console.log(c('yellow', `tsconfig.json not found — creating one in ${path.relative(process.cwd(), cwd) || '.'}.`));
295
312
  await writeJSON(tsconfigPath, RECOMMENDED_TSCONFIG);
296
313
  console.log(c('green', '✅ Created tsconfig.json with required decorator settings.'));
297
314
  return;
298
315
  }
299
316
 
300
- // Merge user config on top of recommended, then force required decorator options
301
317
  let merged = deepMerge(RECOMMENDED_TSCONFIG as any, existing);
302
318
  merged = ensureRequiredTsOptions(merged);
303
319
 
@@ -322,7 +338,6 @@ async function runDoctor(): Promise<void> {
322
338
 
323
339
  let ok = true;
324
340
 
325
- // Node
326
341
  const nodeVer = process.versions.node;
327
342
  if (cmpSemver(nodeVer, MIN_NODE) >= 0) {
328
343
  console.log(`✅ Node ${nodeVer} (min ${MIN_NODE})`);
@@ -331,7 +346,6 @@ async function runDoctor(): Promise<void> {
331
346
  console.log(`❌ Node ${nodeVer} — please upgrade to >= ${MIN_NODE}`);
332
347
  }
333
348
 
334
- // npm
335
349
  let npmVer = 'unknown';
336
350
  try {
337
351
  npmVer = await new Promise<string>((resolve, reject) => {
@@ -352,13 +366,11 @@ async function runDoctor(): Promise<void> {
352
366
  console.log('❌ npm not found in PATH');
353
367
  }
354
368
 
355
- // tsconfig.json presence + required fields
356
369
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
357
370
  if (await fileExists(tsconfigPath)) {
358
371
  console.log(`✅ tsconfig.json found`);
359
372
  const tsconfig = await readJSON<Record<string, any>>(tsconfigPath);
360
373
  const {ok: oks, issues} = checkRequiredTsOptions(tsconfig?.compilerOptions);
361
-
362
374
  for (const line of oks) console.log(c('green', ` ✓ ${line}`));
363
375
  if (issues.length) {
364
376
  ok = false;
@@ -370,7 +382,6 @@ async function runDoctor(): Promise<void> {
370
382
  console.log(`❌ tsconfig.json not found — run ${c('cyan', 'frontmcp init')}`);
371
383
  }
372
384
 
373
- // Entry check (nice to have)
374
385
  try {
375
386
  const entry = await resolveEntry(cwd);
376
387
  console.log(`✅ entry detected: ${path.relative(cwd, entry)}`);
@@ -379,11 +390,8 @@ async function runDoctor(): Promise<void> {
379
390
  console.log(`❌ entry not detected — ${firstLine}`);
380
391
  }
381
392
 
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
- }
393
+ if (ok) console.log(c('green', '\nAll checks passed. You are ready to go!'));
394
+ else console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
387
395
  }
388
396
 
389
397
  /* ------------------------------- Inspector -------------------------------- */
@@ -399,12 +407,12 @@ function pkgNameFromCwd(cwd: string) {
399
407
  return path.basename(cwd).replace(/[^a-zA-Z0-9._-]/g, '-').toLowerCase() || 'frontmcp-app';
400
408
  }
401
409
 
402
- async function upsertPackageJson(cwd: string) {
410
+ async function upsertPackageJson(cwd: string, nameOverride: string | undefined, selfVersion: string) {
403
411
  const pkgPath = path.join(cwd, 'package.json');
404
412
  const existing = await readJSON<Record<string, any>>(pkgPath);
405
413
 
406
414
  const base = {
407
- name: pkgNameFromCwd(cwd),
415
+ name: nameOverride ?? pkgNameFromCwd(cwd),
408
416
  version: '0.1.0',
409
417
  private: true,
410
418
  type: 'module',
@@ -421,33 +429,35 @@ async function upsertPackageJson(cwd: string) {
421
429
  },
422
430
  dependencies: {
423
431
  '@frontmcp/sdk': 'latest',
424
- zod: 'latest',
432
+ zod: '^3.23.8',
433
+ 'reflect-metadata': '^0.2.2',
425
434
  },
426
435
  devDependencies: {
427
- typescript: 'latest',
428
- tsx: 'latest',
436
+ frontmcp: selfVersion, // exact version used by npx
437
+ tsx: '^4.20.6',
438
+ typescript: '^5.5.3',
429
439
  },
430
440
  };
431
441
 
432
442
  if (!existing) {
433
443
  await writeJSON(pkgPath, base);
434
- console.log(c('green', '✅ Created package.json (with scripts: dev, build, inspect, doctor)'));
444
+ console.log(c('green', '✅ Created package.json (pinned tsx/typescript/zod/reflect-metadata, exact frontmcp)'));
435
445
  return;
436
446
  }
437
447
 
438
- // Merge, preserving user fields; ensure our requirements exist
439
448
  const merged = {...base, ...existing};
440
449
 
450
+ merged.name = existing.name || base.name;
441
451
  merged.main = existing.main || base.main;
442
452
  merged.type = existing.type || base.type;
443
453
 
444
454
  merged.scripts = {
445
455
  ...base.scripts,
446
456
  ...(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),
457
+ dev: existing.scripts?.dev ?? base.scripts.dev,
458
+ build: existing.scripts?.build ?? base.scripts.build,
459
+ inspect: existing.scripts?.inspect ?? base.scripts.inspect,
460
+ doctor: existing.scripts?.doctor ?? base.scripts.doctor,
451
461
  };
452
462
 
453
463
  merged.engines = {
@@ -457,29 +467,34 @@ async function upsertPackageJson(cwd: string) {
457
467
  };
458
468
 
459
469
  merged.dependencies = {
460
- ...existing.dependencies,
461
- '@frontmcp/sdk': existing.dependencies?.['@frontmcp/sdk'] || base.dependencies['@frontmcp/sdk'],
462
- zod: existing.dependencies?.zod || base.dependencies.zod,
470
+ ...base.dependencies,
471
+ ...(existing.dependencies || {}),
472
+ // ensure pins
473
+ zod: '^3.23.8',
474
+ 'reflect-metadata': '^0.2.2',
463
475
  };
464
476
 
465
477
  merged.devDependencies = {
466
- ...existing.devDependencies,
467
- typescript: existing.devDependencies?.typescript || base.devDependencies.typescript,
468
- tsx: existing.devDependencies?.tsx || base.devDependencies.tsx,
478
+ ...base.devDependencies,
479
+ ...(existing.devDependencies || {}),
480
+ // ensure pins
481
+ frontmcp: selfVersion,
482
+ tsx: '^4.20.6',
483
+ typescript: '^5.5.3',
469
484
  };
470
485
 
471
486
  await writeJSON(pkgPath, merged);
472
- console.log(c('green', '✅ Updated package.json (ensured scripts, engines, deps)'));
487
+ console.log(c('green', '✅ Updated package.json (ensured exact frontmcp and pinned versions)'));
473
488
  }
474
489
 
475
- async function scaffoldFileIfMissing(p: string, content: string) {
490
+ async function scaffoldFileIfMissing(baseDir: string, p: string, content: string) {
476
491
  if (await fileExists(p)) {
477
- console.log(c('gray', `skip: ${path.relative(process.cwd(), p)} already exists`));
492
+ console.log(c('gray', `skip: ${path.relative(baseDir, p)} already exists`));
478
493
  return;
479
494
  }
480
495
  await ensureDir(path.dirname(p));
481
496
  await fsp.writeFile(p, content.replace(/^\n/, ''), 'utf8');
482
- console.log(c('green', `✓ created ${path.relative(process.cwd(), p)}`));
497
+ console.log(c('green', `✓ created ${path.relative(baseDir, p)}`));
483
498
  }
484
499
 
485
500
  const TEMPLATE_MAIN_TS = `
@@ -527,28 +542,48 @@ const AddTool = tool({
527
542
  export default AddTool;
528
543
  `;
529
544
 
530
- async function runCreate(): Promise<void> {
531
- const cwd = process.cwd();
545
+ async function runCreate(projectArg?: string): Promise<void> {
546
+ if (!projectArg) {
547
+ console.error(c('red', 'Error: project name is required.\n'));
548
+ console.log(`Usage: ${c('bold', 'npx frontmcp create <project-name>')}`);
549
+ process.exit(1);
550
+ }
551
+
552
+ const folder = sanitizeForFolder(projectArg);
553
+ const pkgName = sanitizeForNpm(projectArg);
554
+ const targetDir = path.resolve(process.cwd(), folder);
532
555
 
533
- console.log(c('cyan', '[create]'), 'Scaffolding FrontMCP project in', c('bold', cwd));
556
+ if (await fileExists(targetDir)) {
557
+ if (!(await isDirEmpty(targetDir))) {
558
+ console.error(c('red', `Refusing to scaffold into non-empty directory: ${path.relative(process.cwd(), targetDir)}`));
559
+ console.log(c('gray', 'Pick a different name or start with an empty folder.'));
560
+ process.exit(1);
561
+ }
562
+ } else {
563
+ await ensureDir(targetDir);
564
+ }
565
+
566
+ console.log(`${c('cyan', '[create]')} Creating project in ${c('bold', './' + path.relative(process.cwd(), targetDir))}`);
567
+ process.chdir(targetDir);
534
568
 
535
569
  // 1) tsconfig
536
- await runInit();
570
+ await runInit(targetDir);
537
571
 
538
- // 2) package.json
539
- await upsertPackageJson(cwd);
572
+ // 2) package.json (with pinned deps and exact frontmcp version)
573
+ const selfVersion = await getSelfVersion();
574
+ await upsertPackageJson(targetDir, pkgName, selfVersion);
540
575
 
541
576
  // 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);
577
+ await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'main.ts'), TEMPLATE_MAIN_TS);
578
+ await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'calc.app.ts'), TEMPLATE_CALC_APP_TS);
579
+ await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'tools', 'add.tool.ts'), TEMPLATE_ADD_TOOL_TS);
545
580
 
546
- // 4) final tips
547
581
  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'));
582
+ console.log(` 1) cd ${folder}`);
583
+ console.log(' 2) npm install');
584
+ console.log(' 3) npm run dev ', c('gray', '# starts tsx watcher via frontmcp dev'));
585
+ console.log(' 4) npm run inspect ', c('gray', '# launch MCP Inspector'));
586
+ console.log(' 5) npm run build ', c('gray', '# compile with tsc via frontmcp build'));
552
587
  }
553
588
 
554
589
  /* --------------------------------- Main ----------------------------------- */
@@ -581,9 +616,11 @@ async function main(): Promise<void> {
581
616
  case 'inspector':
582
617
  await runInspector();
583
618
  break;
584
- case 'create':
585
- await runCreate();
619
+ case 'create': {
620
+ const projectName = parsed._[1]; // require a name
621
+ await runCreate(projectName);
586
622
  break;
623
+ }
587
624
  case 'help':
588
625
  showHelp();
589
626
  break;