frontmcp 0.1.2 → 0.1.3

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 +217 -14
  2. package/package.json +7 -6
  3. package/src/cli.ts +257 -15
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 Scaffold a new FrontMCP project in the current directory
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
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);
@@ -226,11 +235,20 @@ function runBuild(opts) {
226
235
  console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
227
236
  });
228
237
  }
238
+ /* --------------------------- tsconfig management --------------------------- */
239
+ const REQUIRED_DECORATOR_FIELDS = {
240
+ target: 'es2021',
241
+ module: 'esnext',
242
+ emitDecoratorMetadata: true,
243
+ experimentalDecorators: true,
244
+ };
229
245
  const RECOMMENDED_TSCONFIG = {
230
246
  compilerOptions: {
231
- target: 'ES2020',
232
- module: 'CommonJS',
233
- moduleResolution: 'Node',
247
+ target: REQUIRED_DECORATOR_FIELDS.target,
248
+ module: REQUIRED_DECORATOR_FIELDS.module,
249
+ emitDecoratorMetadata: REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata,
250
+ experimentalDecorators: REQUIRED_DECORATOR_FIELDS.experimentalDecorators,
251
+ moduleResolution: 'NodeNext',
234
252
  strict: true,
235
253
  esModuleInterop: true,
236
254
  resolveJsonModule: true,
@@ -255,6 +273,44 @@ function deepMerge(base, patch) {
255
273
  }
256
274
  return out;
257
275
  }
276
+ function ensureRequiredTsOptions(obj) {
277
+ const next = Object.assign({}, obj);
278
+ next.compilerOptions = Object.assign({}, (next.compilerOptions || {}));
279
+ // Force the required values
280
+ next.compilerOptions.target = REQUIRED_DECORATOR_FIELDS.target;
281
+ next.compilerOptions.module = REQUIRED_DECORATOR_FIELDS.module;
282
+ next.compilerOptions.emitDecoratorMetadata = REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata;
283
+ next.compilerOptions.experimentalDecorators = REQUIRED_DECORATOR_FIELDS.experimentalDecorators;
284
+ return next;
285
+ }
286
+ function normalizeStr(x) {
287
+ return typeof x === 'string' ? x.toLowerCase() : undefined;
288
+ }
289
+ function checkRequiredTsOptions(compilerOptions) {
290
+ const issues = [];
291
+ const ok = [];
292
+ const target = normalizeStr(compilerOptions === null || compilerOptions === void 0 ? void 0 : compilerOptions.target);
293
+ const moduleVal = normalizeStr(compilerOptions === null || compilerOptions === void 0 ? void 0 : compilerOptions.module);
294
+ const edm = compilerOptions === null || compilerOptions === void 0 ? void 0 : compilerOptions.emitDecoratorMetadata;
295
+ const ed = compilerOptions === null || compilerOptions === void 0 ? void 0 : compilerOptions.experimentalDecorators;
296
+ if (target === REQUIRED_DECORATOR_FIELDS.target)
297
+ ok.push(`compilerOptions.target = "${REQUIRED_DECORATOR_FIELDS.target}"`);
298
+ else
299
+ issues.push(`compilerOptions.target should be "${REQUIRED_DECORATOR_FIELDS.target}"`);
300
+ if (moduleVal === REQUIRED_DECORATOR_FIELDS.module)
301
+ ok.push(`compilerOptions.module = "${REQUIRED_DECORATOR_FIELDS.module}"`);
302
+ else
303
+ issues.push(`compilerOptions.module should be "${REQUIRED_DECORATOR_FIELDS.module}"`);
304
+ if (edm === REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata)
305
+ ok.push(`compilerOptions.emitDecoratorMetadata = true`);
306
+ else
307
+ issues.push(`compilerOptions.emitDecoratorMetadata should be true`);
308
+ if (ed === REQUIRED_DECORATOR_FIELDS.experimentalDecorators)
309
+ ok.push(`compilerOptions.experimentalDecorators = true`);
310
+ else
311
+ issues.push(`compilerOptions.experimentalDecorators should be true`);
312
+ return { ok, issues };
313
+ }
258
314
  function runInit() {
259
315
  return __awaiter(this, void 0, void 0, function* () {
260
316
  const cwd = process.cwd();
@@ -262,15 +318,15 @@ function runInit() {
262
318
  const existing = yield readJSON(tsconfigPath);
263
319
  if (!existing) {
264
320
  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'));
321
+ yield writeJSON(tsconfigPath, RECOMMENDED_TSCONFIG);
322
+ console.log(c('green', '✅ Created tsconfig.json with required decorator settings.'));
267
323
  return;
268
324
  }
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'));
325
+ // Merge user config on top of recommended, then force required decorator options
326
+ let merged = deepMerge(RECOMMENDED_TSCONFIG, existing);
327
+ merged = ensureRequiredTsOptions(merged);
328
+ yield writeJSON(tsconfigPath, merged);
329
+ console.log(c('green', '✅ tsconfig.json verified and updated (required decorator settings enforced).'));
274
330
  });
275
331
  }
276
332
  function cmpSemver(a, b) {
@@ -287,8 +343,8 @@ function cmpSemver(a, b) {
287
343
  function runDoctor() {
288
344
  return __awaiter(this, void 0, void 0, function* () {
289
345
  var _a, _b, _c;
290
- const MIN_NODE = '18.0.0';
291
- const MIN_NPM = '8.0.0';
346
+ const MIN_NODE = '22.0.0';
347
+ const MIN_NPM = '10.0.0';
292
348
  const cwd = process.cwd();
293
349
  let ok = true;
294
350
  // Node
@@ -323,10 +379,20 @@ function runDoctor() {
323
379
  ok = false;
324
380
  console.log('❌ npm not found in PATH');
325
381
  }
326
- // tsconfig.json presence
382
+ // tsconfig.json presence + required fields
327
383
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
328
384
  if (yield fileExists(tsconfigPath)) {
329
385
  console.log(`✅ tsconfig.json found`);
386
+ const tsconfig = yield readJSON(tsconfigPath);
387
+ const { ok: oks, issues } = checkRequiredTsOptions(tsconfig === null || tsconfig === void 0 ? void 0 : tsconfig.compilerOptions);
388
+ for (const line of oks)
389
+ console.log(c('green', ` ✓ ${line}`));
390
+ if (issues.length) {
391
+ ok = false;
392
+ for (const line of issues)
393
+ console.log(c('yellow', ` • ${line}`));
394
+ console.log(c('cyan', ` -> Run "frontmcp init" to apply the required settings.`));
395
+ }
330
396
  }
331
397
  else {
332
398
  ok = false;
@@ -349,6 +415,137 @@ function runDoctor() {
349
415
  }
350
416
  });
351
417
  }
418
+ /* ------------------------------- Inspector -------------------------------- */
419
+ function runInspector() {
420
+ return __awaiter(this, void 0, void 0, function* () {
421
+ console.log(`${c('cyan', '[inspector]')} launching MCP Inspector...`);
422
+ yield runCmd('npx', ['-y', '@modelcontextprotocol/inspector']);
423
+ });
424
+ }
425
+ /* --------------------------------- Create --------------------------------- */
426
+ function pkgNameFromCwd(cwd) {
427
+ return path.basename(cwd).replace(/[^a-zA-Z0-9._-]/g, '-').toLowerCase() || 'frontmcp-app';
428
+ }
429
+ function upsertPackageJson(cwd) {
430
+ return __awaiter(this, void 0, void 0, function* () {
431
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
432
+ const pkgPath = path.join(cwd, 'package.json');
433
+ const existing = yield readJSON(pkgPath);
434
+ const base = {
435
+ name: pkgNameFromCwd(cwd),
436
+ version: '0.1.0',
437
+ private: true,
438
+ type: 'module',
439
+ main: 'src/main.ts',
440
+ scripts: {
441
+ dev: 'frontmcp dev',
442
+ build: 'frontmcp build',
443
+ inspect: 'frontmcp inspector',
444
+ doctor: 'frontmcp doctor',
445
+ },
446
+ engines: {
447
+ node: '>=22',
448
+ npm: '>=10',
449
+ },
450
+ dependencies: {
451
+ '@frontmcp/sdk': 'latest',
452
+ zod: 'latest',
453
+ },
454
+ devDependencies: {
455
+ typescript: 'latest',
456
+ tsx: 'latest',
457
+ },
458
+ };
459
+ if (!existing) {
460
+ yield writeJSON(pkgPath, base);
461
+ console.log(c('green', '✅ Created package.json (with scripts: dev, build, inspect, doctor)'));
462
+ return;
463
+ }
464
+ // Merge, preserving user fields; ensure our requirements exist
465
+ const merged = Object.assign(Object.assign({}, base), existing);
466
+ merged.main = existing.main || base.main;
467
+ 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) });
469
+ 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 });
472
+ yield writeJSON(pkgPath, merged);
473
+ console.log(c('green', '✅ Updated package.json (ensured scripts, engines, deps)'));
474
+ });
475
+ }
476
+ function scaffoldFileIfMissing(p, content) {
477
+ return __awaiter(this, void 0, void 0, function* () {
478
+ if (yield fileExists(p)) {
479
+ console.log(c('gray', `skip: ${path.relative(process.cwd(), p)} already exists`));
480
+ return;
481
+ }
482
+ yield ensureDir(path.dirname(p));
483
+ yield fs_1.promises.writeFile(p, content.replace(/^\n/, ''), 'utf8');
484
+ console.log(c('green', `✓ created ${path.relative(process.cwd(), p)}`));
485
+ });
486
+ }
487
+ const TEMPLATE_MAIN_TS = `
488
+ import 'reflect-metadata';
489
+ import { FrontMcp } from '@frontmcp/sdk';
490
+ import { CalcApp } from './calc.app';
491
+
492
+ @FrontMcp({
493
+ info: { name: 'Demo 🚀', version: '0.1.0' },
494
+ apps: [CalcApp],
495
+ auth: {
496
+ type: 'remote',
497
+ name: 'my-remote-auth',
498
+ baseUrl: 'https://idp.example.com',
499
+ },
500
+ })
501
+ export default class Server {}
502
+ `;
503
+ const TEMPLATE_CALC_APP_TS = `
504
+ import { App } from '@frontmcp/sdk';
505
+ import AddTool from './tools/add.tool';
506
+
507
+ @App({
508
+ id: 'calc',
509
+ name: 'Calculator',
510
+ tools: [AddTool],
511
+ })
512
+ export class CalcApp {}
513
+ `;
514
+ const TEMPLATE_ADD_TOOL_TS = `
515
+ import { tool } from '@frontmcp/sdk';
516
+ import { z } from 'zod';
517
+
518
+ const AddTool = tool({
519
+ name: 'add',
520
+ description: 'Add two numbers',
521
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
522
+ outputSchema: z.object({ result: z.number() }),
523
+ })((input, _ctx) => {
524
+ return { result: input.a + input.b };
525
+ });
526
+
527
+ export default AddTool;
528
+ `;
529
+ function runCreate() {
530
+ 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));
533
+ // 1) tsconfig
534
+ yield runInit();
535
+ // 2) package.json
536
+ yield upsertPackageJson(cwd);
537
+ // 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
542
+ 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'));
547
+ });
548
+ }
352
549
  /* --------------------------------- Main ----------------------------------- */
353
550
  function main() {
354
551
  return __awaiter(this, void 0, void 0, function* () {
@@ -374,6 +571,12 @@ function main() {
374
571
  case 'doctor':
375
572
  yield runDoctor();
376
573
  break;
574
+ case 'inspector':
575
+ yield runInspector();
576
+ break;
577
+ case 'create':
578
+ yield runCreate();
579
+ break;
377
580
  case 'help':
378
581
  showHelp();
379
582
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontmcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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 Scaffold a new FrontMCP project in the current directory
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
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);
@@ -195,11 +203,23 @@ async function runBuild(opts: ParsedArgs): Promise<void> {
195
203
  console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
196
204
  }
197
205
 
206
+ /* --------------------------- tsconfig management --------------------------- */
207
+
208
+ const REQUIRED_DECORATOR_FIELDS = {
209
+ target: 'es2021',
210
+ module: 'esnext',
211
+ emitDecoratorMetadata: true,
212
+ experimentalDecorators: true,
213
+ } as const;
214
+
198
215
  const RECOMMENDED_TSCONFIG = {
199
216
  compilerOptions: {
200
- target: 'ES2020',
201
- module: 'CommonJS',
202
- moduleResolution: 'Node',
217
+ target: REQUIRED_DECORATOR_FIELDS.target,
218
+ module: REQUIRED_DECORATOR_FIELDS.module,
219
+ emitDecoratorMetadata: REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata,
220
+ experimentalDecorators: REQUIRED_DECORATOR_FIELDS.experimentalDecorators,
221
+
222
+ moduleResolution: 'NodeNext',
203
223
  strict: true,
204
224
  esModuleInterop: true,
205
225
  resolveJsonModule: true,
@@ -224,6 +244,47 @@ function deepMerge<T extends Record<string, any>, U extends Record<string, any>>
224
244
  return out as T & U;
225
245
  }
226
246
 
247
+ function ensureRequiredTsOptions(obj: Record<string, any>): Record<string, any> {
248
+ const next = {...obj};
249
+ next.compilerOptions = {...(next.compilerOptions || {})};
250
+
251
+ // Force the required values
252
+ next.compilerOptions.target = REQUIRED_DECORATOR_FIELDS.target;
253
+ next.compilerOptions.module = REQUIRED_DECORATOR_FIELDS.module;
254
+ next.compilerOptions.emitDecoratorMetadata = REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata;
255
+ next.compilerOptions.experimentalDecorators = REQUIRED_DECORATOR_FIELDS.experimentalDecorators;
256
+
257
+ return next;
258
+ }
259
+
260
+ function normalizeStr(x: unknown): string | undefined {
261
+ return typeof x === 'string' ? x.toLowerCase() : undefined;
262
+ }
263
+
264
+ function checkRequiredTsOptions(compilerOptions: Record<string, any> | undefined) {
265
+ const issues: string[] = [];
266
+ const ok: string[] = [];
267
+
268
+ const target = normalizeStr(compilerOptions?.target);
269
+ const moduleVal = normalizeStr(compilerOptions?.module);
270
+ const edm = compilerOptions?.emitDecoratorMetadata;
271
+ const ed = compilerOptions?.experimentalDecorators;
272
+
273
+ if (target === REQUIRED_DECORATOR_FIELDS.target) ok.push(`compilerOptions.target = "${REQUIRED_DECORATOR_FIELDS.target}"`);
274
+ else issues.push(`compilerOptions.target should be "${REQUIRED_DECORATOR_FIELDS.target}"`);
275
+
276
+ if (moduleVal === REQUIRED_DECORATOR_FIELDS.module) ok.push(`compilerOptions.module = "${REQUIRED_DECORATOR_FIELDS.module}"`);
277
+ else issues.push(`compilerOptions.module should be "${REQUIRED_DECORATOR_FIELDS.module}"`);
278
+
279
+ if (edm === REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata) ok.push(`compilerOptions.emitDecoratorMetadata = true`);
280
+ else issues.push(`compilerOptions.emitDecoratorMetadata should be true`);
281
+
282
+ if (ed === REQUIRED_DECORATOR_FIELDS.experimentalDecorators) ok.push(`compilerOptions.experimentalDecorators = true`);
283
+ else issues.push(`compilerOptions.experimentalDecorators should be true`);
284
+
285
+ return {ok, issues};
286
+ }
287
+
227
288
  async function runInit(): Promise<void> {
228
289
  const cwd = process.cwd();
229
290
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
@@ -231,16 +292,17 @@ async function runInit(): Promise<void> {
231
292
 
232
293
  if (!existing) {
233
294
  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'));
295
+ await writeJSON(tsconfigPath, RECOMMENDED_TSCONFIG);
296
+ console.log(c('green', '✅ Created tsconfig.json with required decorator settings.'));
236
297
  return;
237
298
  }
238
299
 
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'));
300
+ // Merge user config on top of recommended, then force required decorator options
301
+ let merged = deepMerge(RECOMMENDED_TSCONFIG as any, existing);
302
+ merged = ensureRequiredTsOptions(merged);
303
+
304
+ await writeJSON(tsconfigPath, merged);
305
+ console.log(c('green', '✅ tsconfig.json verified and updated (required decorator settings enforced).'));
244
306
  }
245
307
 
246
308
  function cmpSemver(a: string, b: string): number {
@@ -254,8 +316,8 @@ function cmpSemver(a: string, b: string): number {
254
316
  }
255
317
 
256
318
  async function runDoctor(): Promise<void> {
257
- const MIN_NODE = '18.0.0';
258
- const MIN_NPM = '8.0.0';
319
+ const MIN_NODE = '22.0.0';
320
+ const MIN_NPM = '10.0.0';
259
321
  const cwd = process.cwd();
260
322
 
261
323
  let ok = true;
@@ -290,10 +352,19 @@ async function runDoctor(): Promise<void> {
290
352
  console.log('❌ npm not found in PATH');
291
353
  }
292
354
 
293
- // tsconfig.json presence
355
+ // tsconfig.json presence + required fields
294
356
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
295
357
  if (await fileExists(tsconfigPath)) {
296
358
  console.log(`✅ tsconfig.json found`);
359
+ const tsconfig = await readJSON<Record<string, any>>(tsconfigPath);
360
+ const {ok: oks, issues} = checkRequiredTsOptions(tsconfig?.compilerOptions);
361
+
362
+ for (const line of oks) console.log(c('green', ` ✓ ${line}`));
363
+ if (issues.length) {
364
+ ok = false;
365
+ for (const line of issues) console.log(c('yellow', ` • ${line}`));
366
+ console.log(c('cyan', ` -> Run "frontmcp init" to apply the required settings.`));
367
+ }
297
368
  } else {
298
369
  ok = false;
299
370
  console.log(`❌ tsconfig.json not found — run ${c('cyan', 'frontmcp init')}`);
@@ -315,6 +386,171 @@ async function runDoctor(): Promise<void> {
315
386
  }
316
387
  }
317
388
 
389
+ /* ------------------------------- Inspector -------------------------------- */
390
+
391
+ async function runInspector(): Promise<void> {
392
+ console.log(`${c('cyan', '[inspector]')} launching MCP Inspector...`);
393
+ await runCmd('npx', ['-y', '@modelcontextprotocol/inspector']);
394
+ }
395
+
396
+ /* --------------------------------- Create --------------------------------- */
397
+
398
+ function pkgNameFromCwd(cwd: string) {
399
+ return path.basename(cwd).replace(/[^a-zA-Z0-9._-]/g, '-').toLowerCase() || 'frontmcp-app';
400
+ }
401
+
402
+ async function upsertPackageJson(cwd: string) {
403
+ const pkgPath = path.join(cwd, 'package.json');
404
+ const existing = await readJSON<Record<string, any>>(pkgPath);
405
+
406
+ const base = {
407
+ name: pkgNameFromCwd(cwd),
408
+ version: '0.1.0',
409
+ private: true,
410
+ type: 'module',
411
+ main: 'src/main.ts',
412
+ scripts: {
413
+ dev: 'frontmcp dev',
414
+ build: 'frontmcp build',
415
+ inspect: 'frontmcp inspector',
416
+ doctor: 'frontmcp doctor',
417
+ },
418
+ engines: {
419
+ node: '>=22',
420
+ npm: '>=10',
421
+ },
422
+ dependencies: {
423
+ '@frontmcp/sdk': 'latest',
424
+ zod: 'latest',
425
+ },
426
+ devDependencies: {
427
+ typescript: 'latest',
428
+ tsx: 'latest',
429
+ },
430
+ };
431
+
432
+ if (!existing) {
433
+ await writeJSON(pkgPath, base);
434
+ console.log(c('green', '✅ Created package.json (with scripts: dev, build, inspect, doctor)'));
435
+ return;
436
+ }
437
+
438
+ // Merge, preserving user fields; ensure our requirements exist
439
+ const merged = {...base, ...existing};
440
+
441
+ merged.main = existing.main || base.main;
442
+ merged.type = existing.type || base.type;
443
+
444
+ merged.scripts = {
445
+ ...base.scripts,
446
+ ...(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),
451
+ };
452
+
453
+ merged.engines = {
454
+ ...(existing.engines || {}),
455
+ node: existing.engines?.node || base.engines.node,
456
+ npm: existing.engines?.npm || base.engines.npm,
457
+ };
458
+
459
+ merged.dependencies = {
460
+ ...existing.dependencies,
461
+ '@frontmcp/sdk': existing.dependencies?.['@frontmcp/sdk'] || base.dependencies['@frontmcp/sdk'],
462
+ zod: existing.dependencies?.zod || base.dependencies.zod,
463
+ };
464
+
465
+ merged.devDependencies = {
466
+ ...existing.devDependencies,
467
+ typescript: existing.devDependencies?.typescript || base.devDependencies.typescript,
468
+ tsx: existing.devDependencies?.tsx || base.devDependencies.tsx,
469
+ };
470
+
471
+ await writeJSON(pkgPath, merged);
472
+ console.log(c('green', '✅ Updated package.json (ensured scripts, engines, deps)'));
473
+ }
474
+
475
+ async function scaffoldFileIfMissing(p: string, content: string) {
476
+ if (await fileExists(p)) {
477
+ console.log(c('gray', `skip: ${path.relative(process.cwd(), p)} already exists`));
478
+ return;
479
+ }
480
+ await ensureDir(path.dirname(p));
481
+ await fsp.writeFile(p, content.replace(/^\n/, ''), 'utf8');
482
+ console.log(c('green', `✓ created ${path.relative(process.cwd(), p)}`));
483
+ }
484
+
485
+ const TEMPLATE_MAIN_TS = `
486
+ import 'reflect-metadata';
487
+ import { FrontMcp } from '@frontmcp/sdk';
488
+ import { CalcApp } from './calc.app';
489
+
490
+ @FrontMcp({
491
+ info: { name: 'Demo 🚀', version: '0.1.0' },
492
+ apps: [CalcApp],
493
+ auth: {
494
+ type: 'remote',
495
+ name: 'my-remote-auth',
496
+ baseUrl: 'https://idp.example.com',
497
+ },
498
+ })
499
+ export default class Server {}
500
+ `;
501
+
502
+ const TEMPLATE_CALC_APP_TS = `
503
+ import { App } from '@frontmcp/sdk';
504
+ import AddTool from './tools/add.tool';
505
+
506
+ @App({
507
+ id: 'calc',
508
+ name: 'Calculator',
509
+ tools: [AddTool],
510
+ })
511
+ export class CalcApp {}
512
+ `;
513
+
514
+ const TEMPLATE_ADD_TOOL_TS = `
515
+ import { tool } from '@frontmcp/sdk';
516
+ import { z } from 'zod';
517
+
518
+ const AddTool = tool({
519
+ name: 'add',
520
+ description: 'Add two numbers',
521
+ inputSchema: z.object({ a: z.number(), b: z.number() }),
522
+ outputSchema: z.object({ result: z.number() }),
523
+ })((input, _ctx) => {
524
+ return { result: input.a + input.b };
525
+ });
526
+
527
+ export default AddTool;
528
+ `;
529
+
530
+ async function runCreate(): Promise<void> {
531
+ const cwd = process.cwd();
532
+
533
+ console.log(c('cyan', '[create]'), 'Scaffolding FrontMCP project in', c('bold', cwd));
534
+
535
+ // 1) tsconfig
536
+ await runInit();
537
+
538
+ // 2) package.json
539
+ await upsertPackageJson(cwd);
540
+
541
+ // 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);
545
+
546
+ // 4) final tips
547
+ 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'));
552
+ }
553
+
318
554
  /* --------------------------------- Main ----------------------------------- */
319
555
 
320
556
  async function main(): Promise<void> {
@@ -342,6 +578,12 @@ async function main(): Promise<void> {
342
578
  case 'doctor':
343
579
  await runDoctor();
344
580
  break;
581
+ case 'inspector':
582
+ await runInspector();
583
+ break;
584
+ case 'create':
585
+ await runCreate();
586
+ break;
345
587
  case 'help':
346
588
  showHelp();
347
589
  break;