frontmcp 0.1.2 → 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 +295 -36
  2. package/package.json +7 -6
  3. package/src/cli.ts +331 -41
package/dist/cli.js CHANGED
@@ -74,7 +74,9 @@ ${c('bold', 'Commands')}
74
74
  dev Start in development mode (tsx --watch <entry>)
75
75
  build Compile entry with TypeScript (tsc)
76
76
  init Create or fix a tsconfig.json suitable for FrontMCP
77
- doctor Check Node/npm versions and tsconfig presence
77
+ doctor Check Node/npm versions and tsconfig requirements
78
+ inspector Launch MCP Inspector (npx @modelcontextprotocol/inspector)
79
+ create <name> Scaffold a new FrontMCP project in ./<name>
78
80
  help Show this help message
79
81
 
80
82
  ${c('bold', 'Options')}
@@ -86,6 +88,8 @@ ${c('bold', 'Examples')}
86
88
  frontmcp build --out-dir build
87
89
  frontmcp init
88
90
  frontmcp doctor
91
+ frontmcp inspector
92
+ npx frontmcp create my-mcp
89
93
  `);
90
94
  }
91
95
  function parseArgs(argv) {
@@ -125,6 +129,11 @@ function readJSON(jsonPath) {
125
129
  }
126
130
  });
127
131
  }
132
+ function writeJSON(p, obj) {
133
+ return __awaiter(this, void 0, void 0, function* () {
134
+ yield fs_1.promises.writeFile(p, JSON.stringify(obj, null, 2) + '\n', 'utf8');
135
+ });
136
+ }
128
137
  function tryCandidates(base) {
129
138
  const exts = ['', '.ts', '.tsx', '.js', '.mjs', '.cjs'];
130
139
  return exts.map((ext) => base + ext);
@@ -137,30 +146,24 @@ function resolveEntry(cwd, explicit) {
137
146
  return full;
138
147
  throw new Error(`Entry override not found: ${explicit}`);
139
148
  }
140
- // 1) package.json main
141
149
  const pkgPath = path.join(cwd, 'package.json');
142
150
  if (yield fileExists(pkgPath)) {
143
151
  const pkg = yield readJSON(pkgPath);
144
152
  if (pkg && typeof pkg.main === 'string' && pkg.main.trim()) {
145
153
  const mainCandidates = tryCandidates(path.resolve(cwd, pkg.main));
146
- for (const p of mainCandidates) {
154
+ for (const p of mainCandidates)
147
155
  if (yield fileExists(p))
148
156
  return p;
149
- }
150
- // If "main" is a directory-like path, try index.* within it
151
157
  const asDir = path.resolve(cwd, pkg.main);
152
158
  const idxCandidates = tryCandidates(path.join(asDir, 'index'));
153
- for (const p of idxCandidates) {
159
+ for (const p of idxCandidates)
154
160
  if (yield fileExists(p))
155
161
  return p;
156
- }
157
162
  }
158
163
  }
159
- // 2) src/main.ts
160
164
  const fallback = path.join(cwd, 'src', 'main.ts');
161
165
  if (yield fileExists(fallback))
162
166
  return fallback;
163
- // 3) Not found
164
167
  const msg = [
165
168
  c('red', 'No entry file found.'),
166
169
  '',
@@ -182,6 +185,55 @@ function runCmd(cmd, args, opts = {}) {
182
185
  child.on('error', reject);
183
186
  });
184
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
+ }
185
237
  /* --------------------------------- Actions -------------------------------- */
186
238
  function runDev(opts) {
187
239
  return __awaiter(this, void 0, void 0, function* () {
@@ -192,11 +244,6 @@ function runDev(opts) {
192
244
  yield runCmd('npx', ['-y', 'tsx', '--watch', entry]);
193
245
  });
194
246
  }
195
- function ensureDir(p) {
196
- return __awaiter(this, void 0, void 0, function* () {
197
- yield fs_1.promises.mkdir(p, { recursive: true });
198
- });
199
- }
200
247
  function isTsLike(p) {
201
248
  return /\.tsx?$/i.test(p);
202
249
  }
@@ -220,17 +267,25 @@ function runBuild(opts) {
220
267
  if (yield fileExists(tsconfigPath)) {
221
268
  console.log(c('gray', `[build] tsconfig.json detected (project options will be respected where applicable)`));
222
269
  }
223
- // Compile the single entry file
224
270
  yield runCmd('npx', ['-y', 'tsc', entry, ...args]);
225
271
  console.log(c('green', '✅ Build completed.'));
226
272
  console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
227
273
  });
228
274
  }
275
+ /* --------------------------- tsconfig management --------------------------- */
276
+ const REQUIRED_DECORATOR_FIELDS = {
277
+ target: 'es2021',
278
+ module: 'esnext',
279
+ emitDecoratorMetadata: true,
280
+ experimentalDecorators: true,
281
+ };
229
282
  const RECOMMENDED_TSCONFIG = {
230
283
  compilerOptions: {
231
- target: 'ES2020',
232
- module: 'CommonJS',
233
- moduleResolution: 'Node',
284
+ target: REQUIRED_DECORATOR_FIELDS.target,
285
+ module: REQUIRED_DECORATOR_FIELDS.module,
286
+ emitDecoratorMetadata: REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata,
287
+ experimentalDecorators: REQUIRED_DECORATOR_FIELDS.experimentalDecorators,
288
+ moduleResolution: 'NodeNext',
234
289
  strict: true,
235
290
  esModuleInterop: true,
236
291
  resolveJsonModule: true,
@@ -255,22 +310,58 @@ function deepMerge(base, patch) {
255
310
  }
256
311
  return out;
257
312
  }
258
- function runInit() {
313
+ function ensureRequiredTsOptions(obj) {
314
+ const next = Object.assign({}, obj);
315
+ next.compilerOptions = Object.assign({}, (next.compilerOptions || {}));
316
+ next.compilerOptions.target = REQUIRED_DECORATOR_FIELDS.target;
317
+ next.compilerOptions.module = REQUIRED_DECORATOR_FIELDS.module;
318
+ next.compilerOptions.emitDecoratorMetadata = REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata;
319
+ next.compilerOptions.experimentalDecorators = REQUIRED_DECORATOR_FIELDS.experimentalDecorators;
320
+ return next;
321
+ }
322
+ function normalizeStr(x) {
323
+ return typeof x === 'string' ? x.toLowerCase() : undefined;
324
+ }
325
+ function checkRequiredTsOptions(compilerOptions) {
326
+ const issues = [];
327
+ const ok = [];
328
+ const target = normalizeStr(compilerOptions === null || compilerOptions === void 0 ? void 0 : compilerOptions.target);
329
+ const moduleVal = normalizeStr(compilerOptions === null || compilerOptions === void 0 ? void 0 : compilerOptions.module);
330
+ const edm = compilerOptions === null || compilerOptions === void 0 ? void 0 : compilerOptions.emitDecoratorMetadata;
331
+ const ed = compilerOptions === null || compilerOptions === void 0 ? void 0 : compilerOptions.experimentalDecorators;
332
+ if (target === REQUIRED_DECORATOR_FIELDS.target)
333
+ ok.push(`compilerOptions.target = "${REQUIRED_DECORATOR_FIELDS.target}"`);
334
+ else
335
+ issues.push(`compilerOptions.target should be "${REQUIRED_DECORATOR_FIELDS.target}"`);
336
+ if (moduleVal === REQUIRED_DECORATOR_FIELDS.module)
337
+ ok.push(`compilerOptions.module = "${REQUIRED_DECORATOR_FIELDS.module}"`);
338
+ else
339
+ issues.push(`compilerOptions.module should be "${REQUIRED_DECORATOR_FIELDS.module}"`);
340
+ if (edm === REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata)
341
+ ok.push(`compilerOptions.emitDecoratorMetadata = true`);
342
+ else
343
+ issues.push(`compilerOptions.emitDecoratorMetadata should be true`);
344
+ if (ed === REQUIRED_DECORATOR_FIELDS.experimentalDecorators)
345
+ ok.push(`compilerOptions.experimentalDecorators = true`);
346
+ else
347
+ issues.push(`compilerOptions.experimentalDecorators should be true`);
348
+ return { ok, issues };
349
+ }
350
+ function runInit(baseDir) {
259
351
  return __awaiter(this, void 0, void 0, function* () {
260
- const cwd = process.cwd();
352
+ const cwd = baseDir !== null && baseDir !== void 0 ? baseDir : process.cwd();
261
353
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
262
354
  const existing = yield readJSON(tsconfigPath);
263
355
  if (!existing) {
264
- console.log(c('yellow', 'tsconfig.json not found — creating one.'));
265
- yield fs_1.promises.writeFile(tsconfigPath, JSON.stringify(RECOMMENDED_TSCONFIG, null, 2) + '\n', 'utf8');
266
- console.log(c('green', '✅ Created tsconfig.json'));
356
+ console.log(c('yellow', `tsconfig.json not found — creating one in ${path.relative(process.cwd(), cwd) || '.'}.`));
357
+ yield writeJSON(tsconfigPath, RECOMMENDED_TSCONFIG);
358
+ console.log(c('green', '✅ Created tsconfig.json with required decorator settings.'));
267
359
  return;
268
360
  }
269
- // We want to ADD missing recommended fields but NOT override user's existing choices.
270
- // So recommended (base) merged with existing (patch) -> existing wins.
271
- const fixed = deepMerge(RECOMMENDED_TSCONFIG, existing);
272
- yield fs_1.promises.writeFile(tsconfigPath, JSON.stringify(fixed, null, 2) + '\n', 'utf8');
273
- console.log(c('green', '✅ tsconfig.json verified/updated'));
361
+ let merged = deepMerge(RECOMMENDED_TSCONFIG, existing);
362
+ merged = ensureRequiredTsOptions(merged);
363
+ yield writeJSON(tsconfigPath, merged);
364
+ console.log(c('green', '✅ tsconfig.json verified and updated (required decorator settings enforced).'));
274
365
  });
275
366
  }
276
367
  function cmpSemver(a, b) {
@@ -287,11 +378,10 @@ function cmpSemver(a, b) {
287
378
  function runDoctor() {
288
379
  return __awaiter(this, void 0, void 0, function* () {
289
380
  var _a, _b, _c;
290
- const MIN_NODE = '18.0.0';
291
- const MIN_NPM = '8.0.0';
381
+ const MIN_NODE = '22.0.0';
382
+ const MIN_NPM = '10.0.0';
292
383
  const cwd = process.cwd();
293
384
  let ok = true;
294
- // Node
295
385
  const nodeVer = process.versions.node;
296
386
  if (cmpSemver(nodeVer, MIN_NODE) >= 0) {
297
387
  console.log(`✅ Node ${nodeVer} (min ${MIN_NODE})`);
@@ -300,7 +390,6 @@ function runDoctor() {
300
390
  ok = false;
301
391
  console.log(`❌ Node ${nodeVer} — please upgrade to >= ${MIN_NODE}`);
302
392
  }
303
- // npm
304
393
  let npmVer = 'unknown';
305
394
  try {
306
395
  npmVer = yield new Promise((resolve, reject) => {
@@ -323,16 +412,24 @@ function runDoctor() {
323
412
  ok = false;
324
413
  console.log('❌ npm not found in PATH');
325
414
  }
326
- // tsconfig.json presence
327
415
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
328
416
  if (yield fileExists(tsconfigPath)) {
329
417
  console.log(`✅ tsconfig.json found`);
418
+ const tsconfig = yield readJSON(tsconfigPath);
419
+ const { ok: oks, issues } = checkRequiredTsOptions(tsconfig === null || tsconfig === void 0 ? void 0 : tsconfig.compilerOptions);
420
+ for (const line of oks)
421
+ console.log(c('green', ` ✓ ${line}`));
422
+ if (issues.length) {
423
+ ok = false;
424
+ for (const line of issues)
425
+ console.log(c('yellow', ` • ${line}`));
426
+ console.log(c('cyan', ` -> Run "frontmcp init" to apply the required settings.`));
427
+ }
330
428
  }
331
429
  else {
332
430
  ok = false;
333
431
  console.log(`❌ tsconfig.json not found — run ${c('cyan', 'frontmcp init')}`);
334
432
  }
335
- // Entry check (nice to have)
336
433
  try {
337
434
  const entry = yield resolveEntry(cwd);
338
435
  console.log(`✅ entry detected: ${path.relative(cwd, entry)}`);
@@ -341,12 +438,166 @@ function runDoctor() {
341
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';
342
439
  console.log(`❌ entry not detected — ${firstLine}`);
343
440
  }
344
- if (ok) {
441
+ if (ok)
345
442
  console.log(c('green', '\nAll checks passed. You are ready to go!'));
443
+ else
444
+ console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
445
+ });
446
+ }
447
+ /* ------------------------------- Inspector -------------------------------- */
448
+ function runInspector() {
449
+ return __awaiter(this, void 0, void 0, function* () {
450
+ console.log(`${c('cyan', '[inspector]')} launching MCP Inspector...`);
451
+ yield runCmd('npx', ['-y', '@modelcontextprotocol/inspector']);
452
+ });
453
+ }
454
+ /* --------------------------------- Create --------------------------------- */
455
+ function pkgNameFromCwd(cwd) {
456
+ return path.basename(cwd).replace(/[^a-zA-Z0-9._-]/g, '-').toLowerCase() || 'frontmcp-app';
457
+ }
458
+ function upsertPackageJson(cwd, nameOverride, selfVersion) {
459
+ return __awaiter(this, void 0, void 0, function* () {
460
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
461
+ const pkgPath = path.join(cwd, 'package.json');
462
+ const existing = yield readJSON(pkgPath);
463
+ const base = {
464
+ name: nameOverride !== null && nameOverride !== void 0 ? nameOverride : pkgNameFromCwd(cwd),
465
+ version: '0.1.0',
466
+ private: true,
467
+ type: 'module',
468
+ main: 'src/main.ts',
469
+ scripts: {
470
+ dev: 'frontmcp dev',
471
+ build: 'frontmcp build',
472
+ inspect: 'frontmcp inspector',
473
+ doctor: 'frontmcp doctor',
474
+ },
475
+ engines: {
476
+ node: '>=22',
477
+ npm: '>=10',
478
+ },
479
+ dependencies: {
480
+ '@frontmcp/sdk': 'latest',
481
+ zod: '^3.23.8',
482
+ 'reflect-metadata': '^0.2.2',
483
+ },
484
+ devDependencies: {
485
+ frontmcp: selfVersion, // exact version used by npx
486
+ tsx: '^4.20.6',
487
+ typescript: '^5.5.3',
488
+ },
489
+ };
490
+ if (!existing) {
491
+ yield writeJSON(pkgPath, base);
492
+ console.log(c('green', '✅ Created package.json (pinned tsx/typescript/zod/reflect-metadata, exact frontmcp)'));
493
+ return;
494
+ }
495
+ const merged = Object.assign(Object.assign({}, base), existing);
496
+ merged.name = existing.name || base.name;
497
+ merged.main = existing.main || base.main;
498
+ merged.type = existing.type || base.type;
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 });
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 });
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' });
507
+ yield writeJSON(pkgPath, merged);
508
+ console.log(c('green', '✅ Updated package.json (ensured exact frontmcp and pinned versions)'));
509
+ });
510
+ }
511
+ function scaffoldFileIfMissing(baseDir, p, content) {
512
+ return __awaiter(this, void 0, void 0, function* () {
513
+ if (yield fileExists(p)) {
514
+ console.log(c('gray', `skip: ${path.relative(baseDir, p)} already exists`));
515
+ return;
516
+ }
517
+ yield ensureDir(path.dirname(p));
518
+ yield fs_1.promises.writeFile(p, content.replace(/^\n/, ''), 'utf8');
519
+ console.log(c('green', `✓ created ${path.relative(baseDir, p)}`));
520
+ });
521
+ }
522
+ const TEMPLATE_MAIN_TS = `
523
+ import 'reflect-metadata';
524
+ import { FrontMcp } from '@frontmcp/sdk';
525
+ import { CalcApp } from './calc.app';
526
+
527
+ @FrontMcp({
528
+ info: { name: 'Demo 🚀', version: '0.1.0' },
529
+ apps: [CalcApp],
530
+ auth: {
531
+ type: 'remote',
532
+ name: 'my-remote-auth',
533
+ baseUrl: 'https://idp.example.com',
534
+ },
535
+ })
536
+ export default class Server {}
537
+ `;
538
+ const TEMPLATE_CALC_APP_TS = `
539
+ import { App } from '@frontmcp/sdk';
540
+ import AddTool from './tools/add.tool';
541
+
542
+ @App({
543
+ id: 'calc',
544
+ name: 'Calculator',
545
+ tools: [AddTool],
546
+ })
547
+ export class CalcApp {}
548
+ `;
549
+ const TEMPLATE_ADD_TOOL_TS = `
550
+ import { tool } from '@frontmcp/sdk';
551
+ import { z } from 'zod';
552
+
553
+ const AddTool = tool({
554
+ name: 'add',
555
+ description: 'Add two numbers',
556
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
557
+ outputSchema: z.object({ result: z.number() }),
558
+ })((input, _ctx) => {
559
+ return { result: input.a + input.b };
560
+ });
561
+
562
+ export default AddTool;
563
+ `;
564
+ function runCreate(projectArg) {
565
+ return __awaiter(this, void 0, void 0, function* () {
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
+ }
346
580
  }
347
581
  else {
348
- console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
582
+ yield ensureDir(targetDir);
349
583
  }
584
+ console.log(`${c('cyan', '[create]')} Creating project in ${c('bold', './' + path.relative(process.cwd(), targetDir))}`);
585
+ process.chdir(targetDir);
586
+ // 1) tsconfig
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);
591
+ // 3) files
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);
595
+ console.log('\nNext steps:');
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'));
350
601
  });
351
602
  }
352
603
  /* --------------------------------- Main ----------------------------------- */
@@ -374,6 +625,14 @@ function main() {
374
625
  case 'doctor':
375
626
  yield runDoctor();
376
627
  break;
628
+ case 'inspector':
629
+ yield runInspector();
630
+ break;
631
+ case 'create': {
632
+ const projectName = parsed._[1]; // require a name
633
+ yield runCreate(projectName);
634
+ break;
635
+ }
377
636
  case 'help':
378
637
  showHelp();
379
638
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontmcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "FrontMCP command line interface",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -12,14 +12,15 @@
12
12
  "prepare": "npm run build"
13
13
  },
14
14
  "dependencies": {
15
- "@frontmcp/sdk": "0.1.2",
16
- "@frontmcp/core": "0.1.2",
17
- "@frontmcp/plugins": "0.1.2",
18
- "@frontmcp/adapters": "0.1.2",
15
+ "@frontmcp/sdk": "0.1.3",
16
+ "@frontmcp/core": "0.1.3",
17
+ "@frontmcp/plugins": "0.1.3",
18
+ "@frontmcp/adapters": "0.1.3",
19
19
  "tsx": "^4.20.6",
20
20
  "typescript": "^5.5.3"
21
21
  },
22
22
  "devDependencies": {
23
- "@types/node": "20.19.9"
23
+ "@types/node": "20.19.9",
24
+ "@modelcontextprotocol/inspector": "^0.17.2"
24
25
  }
25
26
  }
package/src/cli.ts CHANGED
@@ -11,7 +11,7 @@ import {spawn} from 'child_process';
11
11
 
12
12
  /* ----------------------------- Types & Helpers ---------------------------- */
13
13
 
14
- type Command = 'dev' | 'build' | 'init' | 'doctor' | 'help';
14
+ type Command = 'dev' | 'build' | 'init' | 'doctor' | 'inspector' | 'create' | 'help';
15
15
 
16
16
  interface ParsedArgs {
17
17
  _: string[];
@@ -45,7 +45,9 @@ ${c('bold', 'Commands')}
45
45
  dev Start in development mode (tsx --watch <entry>)
46
46
  build Compile entry with TypeScript (tsc)
47
47
  init Create or fix a tsconfig.json suitable for FrontMCP
48
- doctor Check Node/npm versions and tsconfig presence
48
+ doctor Check Node/npm versions and tsconfig requirements
49
+ inspector Launch MCP Inspector (npx @modelcontextprotocol/inspector)
50
+ create <name> Scaffold a new FrontMCP project in ./<name>
49
51
  help Show this help message
50
52
 
51
53
  ${c('bold', 'Options')}
@@ -57,6 +59,8 @@ ${c('bold', 'Examples')}
57
59
  frontmcp build --out-dir build
58
60
  frontmcp init
59
61
  frontmcp doctor
62
+ frontmcp inspector
63
+ npx frontmcp create my-mcp
60
64
  `);
61
65
  }
62
66
 
@@ -90,6 +94,10 @@ async function readJSON<T = any>(jsonPath: string): Promise<T | null> {
90
94
  }
91
95
  }
92
96
 
97
+ async function writeJSON(p: string, obj: any) {
98
+ await fsp.writeFile(p, JSON.stringify(obj, null, 2) + '\n', 'utf8');
99
+ }
100
+
93
101
  function tryCandidates(base: string): string[] {
94
102
  const exts = ['', '.ts', '.tsx', '.js', '.mjs', '.cjs'];
95
103
  return exts.map((ext) => base + ext);
@@ -101,30 +109,20 @@ async function resolveEntry(cwd: string, explicit?: string): Promise<string> {
101
109
  if (await fileExists(full)) return full;
102
110
  throw new Error(`Entry override not found: ${explicit}`);
103
111
  }
104
-
105
- // 1) package.json main
106
112
  const pkgPath = path.join(cwd, 'package.json');
107
113
  if (await fileExists(pkgPath)) {
108
114
  const pkg = await readJSON<any>(pkgPath);
109
115
  if (pkg && typeof pkg.main === 'string' && pkg.main.trim()) {
110
116
  const mainCandidates = tryCandidates(path.resolve(cwd, pkg.main));
111
- for (const p of mainCandidates) {
112
- if (await fileExists(p)) return p;
113
- }
114
- // If "main" is a directory-like path, try index.* within it
117
+ for (const p of mainCandidates) if (await fileExists(p)) return p;
115
118
  const asDir = path.resolve(cwd, pkg.main);
116
119
  const idxCandidates = tryCandidates(path.join(asDir, 'index'));
117
- for (const p of idxCandidates) {
118
- if (await fileExists(p)) return p;
119
- }
120
+ for (const p of idxCandidates) if (await fileExists(p)) return p;
120
121
  }
121
122
  }
122
-
123
- // 2) src/main.ts
124
123
  const fallback = path.join(cwd, 'src', 'main.ts');
125
124
  if (await fileExists(fallback)) return fallback;
126
125
 
127
- // 3) Not found
128
126
  const msg = [
129
127
  c('red', 'No entry file found.'),
130
128
  '',
@@ -147,6 +145,52 @@ function runCmd(cmd: string, args: string[], opts: { cwd?: string } = {}): Promi
147
145
  });
148
146
  }
149
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
+
150
194
  /* --------------------------------- Actions -------------------------------- */
151
195
 
152
196
  async function runDev(opts: ParsedArgs): Promise<void> {
@@ -157,10 +201,6 @@ async function runDev(opts: ParsedArgs): Promise<void> {
157
201
  await runCmd('npx', ['-y', 'tsx', '--watch', entry]);
158
202
  }
159
203
 
160
- async function ensureDir(p: string): Promise<void> {
161
- await fsp.mkdir(p, {recursive: true});
162
- }
163
-
164
204
  function isTsLike(p: string): boolean {
165
205
  return /\.tsx?$/i.test(p);
166
206
  }
@@ -189,17 +229,28 @@ async function runBuild(opts: ParsedArgs): Promise<void> {
189
229
  console.log(c('gray', `[build] tsconfig.json detected (project options will be respected where applicable)`));
190
230
  }
191
231
 
192
- // Compile the single entry file
193
232
  await runCmd('npx', ['-y', 'tsc', entry, ...args]);
194
233
  console.log(c('green', '✅ Build completed.'));
195
234
  console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
196
235
  }
197
236
 
237
+ /* --------------------------- tsconfig management --------------------------- */
238
+
239
+ const REQUIRED_DECORATOR_FIELDS = {
240
+ target: 'es2021',
241
+ module: 'esnext',
242
+ emitDecoratorMetadata: true,
243
+ experimentalDecorators: true,
244
+ } as const;
245
+
198
246
  const RECOMMENDED_TSCONFIG = {
199
247
  compilerOptions: {
200
- target: 'ES2020',
201
- module: 'CommonJS',
202
- moduleResolution: 'Node',
248
+ target: REQUIRED_DECORATOR_FIELDS.target,
249
+ module: REQUIRED_DECORATOR_FIELDS.module,
250
+ emitDecoratorMetadata: REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata,
251
+ experimentalDecorators: REQUIRED_DECORATOR_FIELDS.experimentalDecorators,
252
+
253
+ moduleResolution: 'NodeNext',
203
254
  strict: true,
204
255
  esModuleInterop: true,
205
256
  resolveJsonModule: true,
@@ -224,23 +275,61 @@ function deepMerge<T extends Record<string, any>, U extends Record<string, any>>
224
275
  return out as T & U;
225
276
  }
226
277
 
227
- async function runInit(): Promise<void> {
228
- const cwd = process.cwd();
278
+ function ensureRequiredTsOptions(obj: Record<string, any>): Record<string, any> {
279
+ const next = {...obj};
280
+ next.compilerOptions = {...(next.compilerOptions || {})};
281
+ next.compilerOptions.target = REQUIRED_DECORATOR_FIELDS.target;
282
+ next.compilerOptions.module = REQUIRED_DECORATOR_FIELDS.module;
283
+ next.compilerOptions.emitDecoratorMetadata = REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata;
284
+ next.compilerOptions.experimentalDecorators = REQUIRED_DECORATOR_FIELDS.experimentalDecorators;
285
+ return next;
286
+ }
287
+
288
+ function normalizeStr(x: unknown): string | undefined {
289
+ return typeof x === 'string' ? x.toLowerCase() : undefined;
290
+ }
291
+
292
+ function checkRequiredTsOptions(compilerOptions: Record<string, any> | undefined) {
293
+ const issues: string[] = [];
294
+ const ok: string[] = [];
295
+
296
+ const target = normalizeStr(compilerOptions?.target);
297
+ const moduleVal = normalizeStr(compilerOptions?.module);
298
+ const edm = compilerOptions?.emitDecoratorMetadata;
299
+ const ed = compilerOptions?.experimentalDecorators;
300
+
301
+ if (target === REQUIRED_DECORATOR_FIELDS.target) ok.push(`compilerOptions.target = "${REQUIRED_DECORATOR_FIELDS.target}"`);
302
+ else issues.push(`compilerOptions.target should be "${REQUIRED_DECORATOR_FIELDS.target}"`);
303
+
304
+ if (moduleVal === REQUIRED_DECORATOR_FIELDS.module) ok.push(`compilerOptions.module = "${REQUIRED_DECORATOR_FIELDS.module}"`);
305
+ else issues.push(`compilerOptions.module should be "${REQUIRED_DECORATOR_FIELDS.module}"`);
306
+
307
+ if (edm === REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata) ok.push(`compilerOptions.emitDecoratorMetadata = true`);
308
+ else issues.push(`compilerOptions.emitDecoratorMetadata should be true`);
309
+
310
+ if (ed === REQUIRED_DECORATOR_FIELDS.experimentalDecorators) ok.push(`compilerOptions.experimentalDecorators = true`);
311
+ else issues.push(`compilerOptions.experimentalDecorators should be true`);
312
+
313
+ return {ok, issues};
314
+ }
315
+
316
+ async function runInit(baseDir?: string): Promise<void> {
317
+ const cwd = baseDir ?? process.cwd();
229
318
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
230
319
  const existing = await readJSON<Record<string, any>>(tsconfigPath);
231
320
 
232
321
  if (!existing) {
233
- console.log(c('yellow', 'tsconfig.json not found — creating one.'));
234
- await fsp.writeFile(tsconfigPath, JSON.stringify(RECOMMENDED_TSCONFIG, null, 2) + '\n', 'utf8');
235
- console.log(c('green', '✅ Created tsconfig.json'));
322
+ console.log(c('yellow', `tsconfig.json not found — creating one in ${path.relative(process.cwd(), cwd) || '.'}.`));
323
+ await writeJSON(tsconfigPath, RECOMMENDED_TSCONFIG);
324
+ console.log(c('green', '✅ Created tsconfig.json with required decorator settings.'));
236
325
  return;
237
326
  }
238
327
 
239
- // We want to ADD missing recommended fields but NOT override user's existing choices.
240
- // So recommended (base) merged with existing (patch) -> existing wins.
241
- const fixed = deepMerge(RECOMMENDED_TSCONFIG as any, existing);
242
- await fsp.writeFile(tsconfigPath, JSON.stringify(fixed, null, 2) + '\n', 'utf8');
243
- console.log(c('green', '✅ tsconfig.json verified/updated'));
328
+ let merged = deepMerge(RECOMMENDED_TSCONFIG as any, existing);
329
+ merged = ensureRequiredTsOptions(merged);
330
+
331
+ await writeJSON(tsconfigPath, merged);
332
+ console.log(c('green', '✅ tsconfig.json verified and updated (required decorator settings enforced).'));
244
333
  }
245
334
 
246
335
  function cmpSemver(a: string, b: string): number {
@@ -254,13 +343,12 @@ function cmpSemver(a: string, b: string): number {
254
343
  }
255
344
 
256
345
  async function runDoctor(): Promise<void> {
257
- const MIN_NODE = '18.0.0';
258
- const MIN_NPM = '8.0.0';
346
+ const MIN_NODE = '22.0.0';
347
+ const MIN_NPM = '10.0.0';
259
348
  const cwd = process.cwd();
260
349
 
261
350
  let ok = true;
262
351
 
263
- // Node
264
352
  const nodeVer = process.versions.node;
265
353
  if (cmpSemver(nodeVer, MIN_NODE) >= 0) {
266
354
  console.log(`✅ Node ${nodeVer} (min ${MIN_NODE})`);
@@ -269,7 +357,6 @@ async function runDoctor(): Promise<void> {
269
357
  console.log(`❌ Node ${nodeVer} — please upgrade to >= ${MIN_NODE}`);
270
358
  }
271
359
 
272
- // npm
273
360
  let npmVer = 'unknown';
274
361
  try {
275
362
  npmVer = await new Promise<string>((resolve, reject) => {
@@ -290,16 +377,22 @@ async function runDoctor(): Promise<void> {
290
377
  console.log('❌ npm not found in PATH');
291
378
  }
292
379
 
293
- // tsconfig.json presence
294
380
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
295
381
  if (await fileExists(tsconfigPath)) {
296
382
  console.log(`✅ tsconfig.json found`);
383
+ const tsconfig = await readJSON<Record<string, any>>(tsconfigPath);
384
+ const {ok: oks, issues} = checkRequiredTsOptions(tsconfig?.compilerOptions);
385
+ for (const line of oks) console.log(c('green', ` ✓ ${line}`));
386
+ if (issues.length) {
387
+ ok = false;
388
+ for (const line of issues) console.log(c('yellow', ` • ${line}`));
389
+ console.log(c('cyan', ` -> Run "frontmcp init" to apply the required settings.`));
390
+ }
297
391
  } else {
298
392
  ok = false;
299
393
  console.log(`❌ tsconfig.json not found — run ${c('cyan', 'frontmcp init')}`);
300
394
  }
301
395
 
302
- // Entry check (nice to have)
303
396
  try {
304
397
  const entry = await resolveEntry(cwd);
305
398
  console.log(`✅ entry detected: ${path.relative(cwd, entry)}`);
@@ -308,11 +401,200 @@ async function runDoctor(): Promise<void> {
308
401
  console.log(`❌ entry not detected — ${firstLine}`);
309
402
  }
310
403
 
311
- if (ok) {
312
- console.log(c('green', '\nAll checks passed. You are ready to go!'));
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.'));
406
+ }
407
+
408
+ /* ------------------------------- Inspector -------------------------------- */
409
+
410
+ async function runInspector(): Promise<void> {
411
+ console.log(`${c('cyan', '[inspector]')} launching MCP Inspector...`);
412
+ await runCmd('npx', ['-y', '@modelcontextprotocol/inspector']);
413
+ }
414
+
415
+ /* --------------------------------- Create --------------------------------- */
416
+
417
+ function pkgNameFromCwd(cwd: string) {
418
+ return path.basename(cwd).replace(/[^a-zA-Z0-9._-]/g, '-').toLowerCase() || 'frontmcp-app';
419
+ }
420
+
421
+ async function upsertPackageJson(cwd: string, nameOverride: string | undefined, selfVersion: string) {
422
+ const pkgPath = path.join(cwd, 'package.json');
423
+ const existing = await readJSON<Record<string, any>>(pkgPath);
424
+
425
+ const base = {
426
+ name: nameOverride ?? pkgNameFromCwd(cwd),
427
+ version: '0.1.0',
428
+ private: true,
429
+ type: 'module',
430
+ main: 'src/main.ts',
431
+ scripts: {
432
+ dev: 'frontmcp dev',
433
+ build: 'frontmcp build',
434
+ inspect: 'frontmcp inspector',
435
+ doctor: 'frontmcp doctor',
436
+ },
437
+ engines: {
438
+ node: '>=22',
439
+ npm: '>=10',
440
+ },
441
+ dependencies: {
442
+ '@frontmcp/sdk': 'latest',
443
+ zod: '^3.23.8',
444
+ 'reflect-metadata': '^0.2.2',
445
+ },
446
+ devDependencies: {
447
+ frontmcp: selfVersion, // exact version used by npx
448
+ tsx: '^4.20.6',
449
+ typescript: '^5.5.3',
450
+ },
451
+ };
452
+
453
+ if (!existing) {
454
+ await writeJSON(pkgPath, base);
455
+ console.log(c('green', '✅ Created package.json (pinned tsx/typescript/zod/reflect-metadata, exact frontmcp)'));
456
+ return;
457
+ }
458
+
459
+ const merged = {...base, ...existing};
460
+
461
+ merged.name = existing.name || base.name;
462
+ merged.main = existing.main || base.main;
463
+ merged.type = existing.type || base.type;
464
+
465
+ merged.scripts = {
466
+ ...base.scripts,
467
+ ...(existing.scripts || {}),
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,
472
+ };
473
+
474
+ merged.engines = {
475
+ ...(existing.engines || {}),
476
+ node: existing.engines?.node || base.engines.node,
477
+ npm: existing.engines?.npm || base.engines.npm,
478
+ };
479
+
480
+ merged.dependencies = {
481
+ ...base.dependencies,
482
+ ...(existing.dependencies || {}),
483
+ // ensure pins
484
+ zod: '^3.23.8',
485
+ 'reflect-metadata': '^0.2.2',
486
+ };
487
+
488
+ merged.devDependencies = {
489
+ ...base.devDependencies,
490
+ ...(existing.devDependencies || {}),
491
+ // ensure pins
492
+ frontmcp: selfVersion,
493
+ tsx: '^4.20.6',
494
+ typescript: '^5.5.3',
495
+ };
496
+
497
+ await writeJSON(pkgPath, merged);
498
+ console.log(c('green', '✅ Updated package.json (ensured exact frontmcp and pinned versions)'));
499
+ }
500
+
501
+ async function scaffoldFileIfMissing(baseDir: string, p: string, content: string) {
502
+ if (await fileExists(p)) {
503
+ console.log(c('gray', `skip: ${path.relative(baseDir, p)} already exists`));
504
+ return;
505
+ }
506
+ await ensureDir(path.dirname(p));
507
+ await fsp.writeFile(p, content.replace(/^\n/, ''), 'utf8');
508
+ console.log(c('green', `✓ created ${path.relative(baseDir, p)}`));
509
+ }
510
+
511
+ const TEMPLATE_MAIN_TS = `
512
+ import 'reflect-metadata';
513
+ import { FrontMcp } from '@frontmcp/sdk';
514
+ import { CalcApp } from './calc.app';
515
+
516
+ @FrontMcp({
517
+ info: { name: 'Demo 🚀', version: '0.1.0' },
518
+ apps: [CalcApp],
519
+ auth: {
520
+ type: 'remote',
521
+ name: 'my-remote-auth',
522
+ baseUrl: 'https://idp.example.com',
523
+ },
524
+ })
525
+ export default class Server {}
526
+ `;
527
+
528
+ const TEMPLATE_CALC_APP_TS = `
529
+ import { App } from '@frontmcp/sdk';
530
+ import AddTool from './tools/add.tool';
531
+
532
+ @App({
533
+ id: 'calc',
534
+ name: 'Calculator',
535
+ tools: [AddTool],
536
+ })
537
+ export class CalcApp {}
538
+ `;
539
+
540
+ const TEMPLATE_ADD_TOOL_TS = `
541
+ import { tool } from '@frontmcp/sdk';
542
+ import { z } from 'zod';
543
+
544
+ const AddTool = tool({
545
+ name: 'add',
546
+ description: 'Add two numbers',
547
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
548
+ outputSchema: z.object({ result: z.number() }),
549
+ })((input, _ctx) => {
550
+ return { result: input.a + input.b };
551
+ });
552
+
553
+ export default AddTool;
554
+ `;
555
+
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);
566
+
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
+ }
313
573
  } else {
314
- console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
574
+ await ensureDir(targetDir);
315
575
  }
576
+
577
+ console.log(`${c('cyan', '[create]')} Creating project in ${c('bold', './' + path.relative(process.cwd(), targetDir))}`);
578
+ process.chdir(targetDir);
579
+
580
+ // 1) tsconfig
581
+ await runInit(targetDir);
582
+
583
+ // 2) package.json (with pinned deps and exact frontmcp version)
584
+ const selfVersion = await getSelfVersion();
585
+ await upsertPackageJson(targetDir, pkgName, selfVersion);
586
+
587
+ // 3) files
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);
591
+
592
+ console.log('\nNext steps:');
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'));
316
598
  }
317
599
 
318
600
  /* --------------------------------- Main ----------------------------------- */
@@ -342,6 +624,14 @@ async function main(): Promise<void> {
342
624
  case 'doctor':
343
625
  await runDoctor();
344
626
  break;
627
+ case 'inspector':
628
+ await runInspector();
629
+ break;
630
+ case 'create': {
631
+ const projectName = parsed._[1]; // require a name
632
+ await runCreate(projectName);
633
+ break;
634
+ }
345
635
  case 'help':
346
636
  showHelp();
347
637
  break;