frontmcp 0.0.1 → 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.
package/dist/cli.js ADDED
@@ -0,0 +1,595 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * frontmcp - FrontMCP command line interface
5
+ * Save as bin/frontmcp.ts (compile to JS with shebang preserved) or run with tsx.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
41
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
42
+ return new (P || (P = Promise))(function (resolve, reject) {
43
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
44
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
45
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
46
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
47
+ });
48
+ };
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ const fs = __importStar(require("fs"));
51
+ const fs_1 = require("fs");
52
+ const path = __importStar(require("path"));
53
+ const child_process_1 = require("child_process");
54
+ const COLORS = {
55
+ reset: '\x1b[0m',
56
+ bold: '\x1b[1m',
57
+ dim: '\x1b[2m',
58
+ red: '\x1b[31m',
59
+ green: '\x1b[32m',
60
+ yellow: '\x1b[33m',
61
+ blue: '\x1b[34m',
62
+ cyan: '\x1b[36m',
63
+ gray: '\x1b[90m',
64
+ };
65
+ const c = (color, s) => COLORS[color] + s + COLORS.reset;
66
+ function showHelp() {
67
+ console.log(`
68
+ ${c('bold', 'frontmcp')} — FrontMCP command line interface
69
+
70
+ ${c('bold', 'Usage')}
71
+ frontmcp <command> [options]
72
+
73
+ ${c('bold', 'Commands')}
74
+ dev Start in development mode (tsx --watch <entry>)
75
+ build Compile entry with TypeScript (tsc)
76
+ init Create or fix a tsconfig.json suitable for FrontMCP
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
80
+ help Show this help message
81
+
82
+ ${c('bold', 'Options')}
83
+ -o, --out-dir <dir> Output directory (default: ./dist)
84
+ -e, --entry <path> Manually specify entry file path
85
+
86
+ ${c('bold', 'Examples')}
87
+ frontmcp dev
88
+ frontmcp build --out-dir build
89
+ frontmcp init
90
+ frontmcp doctor
91
+ frontmcp inspector
92
+ npx frontmcp create
93
+ `);
94
+ }
95
+ function parseArgs(argv) {
96
+ const out = { _: [] };
97
+ for (let i = 0; i < argv.length; i++) {
98
+ const a = argv[i];
99
+ if (a === '--out-dir' || a === '-o')
100
+ out.outDir = argv[++i];
101
+ else if (a === '--entry' || a === '-e')
102
+ out.entry = argv[++i];
103
+ else if (a === '--help' || a === '-h')
104
+ out.help = true;
105
+ else
106
+ out._.push(a);
107
+ }
108
+ return out;
109
+ }
110
+ function fileExists(p) {
111
+ return __awaiter(this, void 0, void 0, function* () {
112
+ try {
113
+ yield fs_1.promises.access(p, fs.constants.F_OK);
114
+ return true;
115
+ }
116
+ catch (_a) {
117
+ return false;
118
+ }
119
+ });
120
+ }
121
+ function readJSON(jsonPath) {
122
+ return __awaiter(this, void 0, void 0, function* () {
123
+ try {
124
+ const buf = yield fs_1.promises.readFile(jsonPath, 'utf8');
125
+ return JSON.parse(buf);
126
+ }
127
+ catch (_a) {
128
+ return null;
129
+ }
130
+ });
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
+ }
137
+ function tryCandidates(base) {
138
+ const exts = ['', '.ts', '.tsx', '.js', '.mjs', '.cjs'];
139
+ return exts.map((ext) => base + ext);
140
+ }
141
+ function resolveEntry(cwd, explicit) {
142
+ return __awaiter(this, void 0, void 0, function* () {
143
+ if (explicit) {
144
+ const full = path.resolve(cwd, explicit);
145
+ if (yield fileExists(full))
146
+ return full;
147
+ throw new Error(`Entry override not found: ${explicit}`);
148
+ }
149
+ // 1) package.json main
150
+ const pkgPath = path.join(cwd, 'package.json');
151
+ if (yield fileExists(pkgPath)) {
152
+ const pkg = yield readJSON(pkgPath);
153
+ if (pkg && typeof pkg.main === 'string' && pkg.main.trim()) {
154
+ const mainCandidates = tryCandidates(path.resolve(cwd, pkg.main));
155
+ for (const p of mainCandidates) {
156
+ if (yield fileExists(p))
157
+ return p;
158
+ }
159
+ // If "main" is a directory-like path, try index.* within it
160
+ const asDir = path.resolve(cwd, pkg.main);
161
+ const idxCandidates = tryCandidates(path.join(asDir, 'index'));
162
+ for (const p of idxCandidates) {
163
+ if (yield fileExists(p))
164
+ return p;
165
+ }
166
+ }
167
+ }
168
+ // 2) src/main.ts
169
+ const fallback = path.join(cwd, 'src', 'main.ts');
170
+ if (yield fileExists(fallback))
171
+ return fallback;
172
+ // 3) Not found
173
+ const msg = [
174
+ c('red', 'No entry file found.'),
175
+ '',
176
+ 'I looked for:',
177
+ ` • ${pkgPath} with a valid "main" field`,
178
+ ` • ${path.relative(cwd, fallback)}`,
179
+ '',
180
+ 'Please create an entry file (e.g. src/main.ts) or set "main" in package.json,',
181
+ 'or run with an explicit path:',
182
+ ` frontmcp dev --entry src/main.ts`,
183
+ ].join('\n');
184
+ throw new Error(msg);
185
+ });
186
+ }
187
+ function runCmd(cmd, args, opts = {}) {
188
+ return new Promise((resolve, reject) => {
189
+ const child = (0, child_process_1.spawn)(cmd, args, Object.assign({ stdio: 'inherit', shell: true }, opts));
190
+ child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${code}`))));
191
+ child.on('error', reject);
192
+ });
193
+ }
194
+ /* --------------------------------- Actions -------------------------------- */
195
+ function runDev(opts) {
196
+ return __awaiter(this, void 0, void 0, function* () {
197
+ const cwd = process.cwd();
198
+ const entry = yield resolveEntry(cwd, opts.entry);
199
+ console.log(`${c('cyan', '[dev]')} using entry: ${path.relative(cwd, entry)}`);
200
+ console.log(`${c('gray', 'hint:')} press Ctrl+C to stop`);
201
+ yield runCmd('npx', ['-y', 'tsx', '--watch', entry]);
202
+ });
203
+ }
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
+ function isTsLike(p) {
210
+ return /\.tsx?$/i.test(p);
211
+ }
212
+ function runBuild(opts) {
213
+ return __awaiter(this, void 0, void 0, function* () {
214
+ const cwd = process.cwd();
215
+ const entry = yield resolveEntry(cwd, opts.entry);
216
+ const outDir = path.resolve(cwd, opts.outDir || 'dist');
217
+ yield ensureDir(outDir);
218
+ console.log(`${c('cyan', '[build]')} entry: ${path.relative(cwd, entry)}`);
219
+ console.log(`${c('cyan', '[build]')} outDir: ${path.relative(cwd, outDir)}`);
220
+ const args = [];
221
+ args.push('--outDir', outDir);
222
+ args.push('--skipLibCheck');
223
+ args.push('--rootDir', path.dirname(entry));
224
+ if (!isTsLike(entry)) {
225
+ args.push('--allowJs');
226
+ console.log(c('yellow', '[build] Entry is not TypeScript; enabling --allowJs'));
227
+ }
228
+ const tsconfigPath = path.join(cwd, 'tsconfig.json');
229
+ if (yield fileExists(tsconfigPath)) {
230
+ console.log(c('gray', `[build] tsconfig.json detected (project options will be respected where applicable)`));
231
+ }
232
+ // Compile the single entry file
233
+ yield runCmd('npx', ['-y', 'tsc', entry, ...args]);
234
+ console.log(c('green', '✅ Build completed.'));
235
+ console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
236
+ });
237
+ }
238
+ /* --------------------------- tsconfig management --------------------------- */
239
+ const REQUIRED_DECORATOR_FIELDS = {
240
+ target: 'es2021',
241
+ module: 'esnext',
242
+ emitDecoratorMetadata: true,
243
+ experimentalDecorators: true,
244
+ };
245
+ const RECOMMENDED_TSCONFIG = {
246
+ compilerOptions: {
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',
252
+ strict: true,
253
+ esModuleInterop: true,
254
+ resolveJsonModule: true,
255
+ skipLibCheck: true,
256
+ sourceMap: true,
257
+ outDir: 'dist',
258
+ rootDir: 'src',
259
+ types: ['node'],
260
+ },
261
+ include: ['src/**/*'],
262
+ };
263
+ function deepMerge(base, patch) {
264
+ var _a;
265
+ const out = Object.assign({}, base);
266
+ for (const [k, v] of Object.entries(patch)) {
267
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
268
+ out[k] = deepMerge((_a = base[k]) !== null && _a !== void 0 ? _a : {}, v);
269
+ }
270
+ else {
271
+ out[k] = v;
272
+ }
273
+ }
274
+ return out;
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
+ }
314
+ function runInit() {
315
+ return __awaiter(this, void 0, void 0, function* () {
316
+ const cwd = process.cwd();
317
+ const tsconfigPath = path.join(cwd, 'tsconfig.json');
318
+ const existing = yield readJSON(tsconfigPath);
319
+ if (!existing) {
320
+ console.log(c('yellow', 'tsconfig.json not found — creating one.'));
321
+ yield writeJSON(tsconfigPath, RECOMMENDED_TSCONFIG);
322
+ console.log(c('green', '✅ Created tsconfig.json with required decorator settings.'));
323
+ return;
324
+ }
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).'));
330
+ });
331
+ }
332
+ function cmpSemver(a, b) {
333
+ const pa = a.split('.').map((n) => parseInt(n, 10) || 0);
334
+ const pb = b.split('.').map((n) => parseInt(n, 10) || 0);
335
+ for (let i = 0; i < 3; i++) {
336
+ if ((pa[i] || 0) > (pb[i] || 0))
337
+ return 1;
338
+ if ((pa[i] || 0) < (pb[i] || 0))
339
+ return -1;
340
+ }
341
+ return 0;
342
+ }
343
+ function runDoctor() {
344
+ return __awaiter(this, void 0, void 0, function* () {
345
+ var _a, _b, _c;
346
+ const MIN_NODE = '22.0.0';
347
+ const MIN_NPM = '10.0.0';
348
+ const cwd = process.cwd();
349
+ let ok = true;
350
+ // Node
351
+ const nodeVer = process.versions.node;
352
+ if (cmpSemver(nodeVer, MIN_NODE) >= 0) {
353
+ console.log(`✅ Node ${nodeVer} (min ${MIN_NODE})`);
354
+ }
355
+ else {
356
+ ok = false;
357
+ console.log(`❌ Node ${nodeVer} — please upgrade to >= ${MIN_NODE}`);
358
+ }
359
+ // npm
360
+ let npmVer = 'unknown';
361
+ try {
362
+ npmVer = yield new Promise((resolve, reject) => {
363
+ var _a;
364
+ const child = (0, child_process_1.spawn)('npm', ['-v'], { shell: true });
365
+ let out = '';
366
+ (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (d) => (out += String(d)));
367
+ child.on('close', () => resolve(out.trim()));
368
+ child.on('error', reject);
369
+ });
370
+ if (cmpSemver(npmVer, MIN_NPM) >= 0) {
371
+ console.log(`✅ npm ${npmVer} (min ${MIN_NPM})`);
372
+ }
373
+ else {
374
+ ok = false;
375
+ console.log(`❌ npm ${npmVer} — please upgrade to >= ${MIN_NPM}`);
376
+ }
377
+ }
378
+ catch (_d) {
379
+ ok = false;
380
+ console.log('❌ npm not found in PATH');
381
+ }
382
+ // tsconfig.json presence + required fields
383
+ const tsconfigPath = path.join(cwd, 'tsconfig.json');
384
+ if (yield fileExists(tsconfigPath)) {
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
+ }
396
+ }
397
+ else {
398
+ ok = false;
399
+ console.log(`❌ tsconfig.json not found — run ${c('cyan', 'frontmcp init')}`);
400
+ }
401
+ // Entry check (nice to have)
402
+ try {
403
+ const entry = yield resolveEntry(cwd);
404
+ console.log(`✅ entry detected: ${path.relative(cwd, entry)}`);
405
+ }
406
+ catch (e) {
407
+ 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
+ console.log(`❌ entry not detected — ${firstLine}`);
409
+ }
410
+ if (ok) {
411
+ console.log(c('green', '\nAll checks passed. You are ready to go!'));
412
+ }
413
+ else {
414
+ console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
415
+ }
416
+ });
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
+ }
549
+ /* --------------------------------- Main ----------------------------------- */
550
+ function main() {
551
+ return __awaiter(this, void 0, void 0, function* () {
552
+ const argv = process.argv.slice(2);
553
+ const parsed = parseArgs(argv);
554
+ const cmd = parsed._[0];
555
+ if (parsed.help || !cmd) {
556
+ showHelp();
557
+ process.exit(0);
558
+ }
559
+ try {
560
+ switch (cmd) {
561
+ case 'dev':
562
+ yield runDev(parsed);
563
+ break;
564
+ case 'build':
565
+ parsed.outDir = parsed.outDir || 'dist';
566
+ yield runBuild(parsed);
567
+ break;
568
+ case 'init':
569
+ yield runInit();
570
+ break;
571
+ case 'doctor':
572
+ yield runDoctor();
573
+ break;
574
+ case 'inspector':
575
+ yield runInspector();
576
+ break;
577
+ case 'create':
578
+ yield runCreate();
579
+ break;
580
+ case 'help':
581
+ showHelp();
582
+ break;
583
+ default:
584
+ console.error(c('red', `Unknown command: ${cmd}`));
585
+ showHelp();
586
+ process.exitCode = 1;
587
+ }
588
+ }
589
+ catch (err) {
590
+ console.error('\n' + c('red', err instanceof Error ? err.stack || err.message : String(err)));
591
+ process.exit(1);
592
+ }
593
+ });
594
+ }
595
+ main();
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ console.log('Happy developing ✨');
package/package.json CHANGED
@@ -1,9 +1,26 @@
1
1
  {
2
2
  "name": "frontmcp",
3
- "version": "0.0.1",
4
- "description": "",
5
- "main": "index.js",
3
+ "version": "0.1.3",
4
+ "description": "FrontMCP command line interface",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "frontmcp": "dist/cli.js"
8
+ },
6
9
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
10
+ "build": "tsc",
11
+ "dev": "tsx src/cli.ts",
12
+ "prepare": "npm run build"
13
+ },
14
+ "dependencies": {
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
+ "tsx": "^4.20.6",
20
+ "typescript": "^5.5.3"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "20.19.9",
24
+ "@modelcontextprotocol/inspector": "^0.17.2"
8
25
  }
9
26
  }
package/src/cli.ts ADDED
@@ -0,0 +1,601 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * frontmcp - FrontMCP command line interface
4
+ * Save as bin/frontmcp.ts (compile to JS with shebang preserved) or run with tsx.
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import {promises as fsp} from 'fs';
9
+ import * as path from 'path';
10
+ import {spawn} from 'child_process';
11
+
12
+ /* ----------------------------- Types & Helpers ---------------------------- */
13
+
14
+ type Command = 'dev' | 'build' | 'init' | 'doctor' | 'inspector' | 'create' | 'help';
15
+
16
+ interface ParsedArgs {
17
+ _: string[];
18
+ outDir?: string;
19
+ entry?: string;
20
+ help?: boolean;
21
+ }
22
+
23
+ const COLORS = {
24
+ reset: '\x1b[0m',
25
+ bold: '\x1b[1m',
26
+ dim: '\x1b[2m',
27
+ red: '\x1b[31m',
28
+ green: '\x1b[32m',
29
+ yellow: '\x1b[33m',
30
+ blue: '\x1b[34m',
31
+ cyan: '\x1b[36m',
32
+ gray: '\x1b[90m',
33
+ } as const;
34
+
35
+ const c = (color: keyof typeof COLORS, s: string) => COLORS[color] + s + COLORS.reset;
36
+
37
+ function showHelp(): void {
38
+ console.log(`
39
+ ${c('bold', 'frontmcp')} — FrontMCP command line interface
40
+
41
+ ${c('bold', 'Usage')}
42
+ frontmcp <command> [options]
43
+
44
+ ${c('bold', 'Commands')}
45
+ dev Start in development mode (tsx --watch <entry>)
46
+ build Compile entry with TypeScript (tsc)
47
+ init Create or fix a tsconfig.json suitable for FrontMCP
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
51
+ help Show this help message
52
+
53
+ ${c('bold', 'Options')}
54
+ -o, --out-dir <dir> Output directory (default: ./dist)
55
+ -e, --entry <path> Manually specify entry file path
56
+
57
+ ${c('bold', 'Examples')}
58
+ frontmcp dev
59
+ frontmcp build --out-dir build
60
+ frontmcp init
61
+ frontmcp doctor
62
+ frontmcp inspector
63
+ npx frontmcp create
64
+ `);
65
+ }
66
+
67
+ function parseArgs(argv: string[]): ParsedArgs {
68
+ const out: ParsedArgs = {_: []};
69
+ for (let i = 0; i < argv.length; i++) {
70
+ const a = argv[i];
71
+ if (a === '--out-dir' || a === '-o') out.outDir = argv[++i];
72
+ else if (a === '--entry' || a === '-e') out.entry = argv[++i];
73
+ else if (a === '--help' || a === '-h') out.help = true;
74
+ else out._.push(a);
75
+ }
76
+ return out;
77
+ }
78
+
79
+ async function fileExists(p: string): Promise<boolean> {
80
+ try {
81
+ await fsp.access(p, fs.constants.F_OK);
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ async function readJSON<T = any>(jsonPath: string): Promise<T | null> {
89
+ try {
90
+ const buf = await fsp.readFile(jsonPath, 'utf8');
91
+ return JSON.parse(buf) as T;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ async function writeJSON(p: string, obj: any) {
98
+ await fsp.writeFile(p, JSON.stringify(obj, null, 2) + '\n', 'utf8');
99
+ }
100
+
101
+ function tryCandidates(base: string): string[] {
102
+ const exts = ['', '.ts', '.tsx', '.js', '.mjs', '.cjs'];
103
+ return exts.map((ext) => base + ext);
104
+ }
105
+
106
+ async function resolveEntry(cwd: string, explicit?: string): Promise<string> {
107
+ if (explicit) {
108
+ const full = path.resolve(cwd, explicit);
109
+ if (await fileExists(full)) return full;
110
+ throw new Error(`Entry override not found: ${explicit}`);
111
+ }
112
+
113
+ // 1) package.json main
114
+ const pkgPath = path.join(cwd, 'package.json');
115
+ if (await fileExists(pkgPath)) {
116
+ const pkg = await readJSON<any>(pkgPath);
117
+ if (pkg && typeof pkg.main === 'string' && pkg.main.trim()) {
118
+ 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
123
+ const asDir = path.resolve(cwd, pkg.main);
124
+ const idxCandidates = tryCandidates(path.join(asDir, 'index'));
125
+ for (const p of idxCandidates) {
126
+ if (await fileExists(p)) return p;
127
+ }
128
+ }
129
+ }
130
+
131
+ // 2) src/main.ts
132
+ const fallback = path.join(cwd, 'src', 'main.ts');
133
+ if (await fileExists(fallback)) return fallback;
134
+
135
+ // 3) Not found
136
+ const msg = [
137
+ c('red', 'No entry file found.'),
138
+ '',
139
+ 'I looked for:',
140
+ ` • ${pkgPath} with a valid "main" field`,
141
+ ` • ${path.relative(cwd, fallback)}`,
142
+ '',
143
+ 'Please create an entry file (e.g. src/main.ts) or set "main" in package.json,',
144
+ 'or run with an explicit path:',
145
+ ` frontmcp dev --entry src/main.ts`,
146
+ ].join('\n');
147
+ throw new Error(msg);
148
+ }
149
+
150
+ function runCmd(cmd: string, args: string[], opts: { cwd?: string } = {}): Promise<void> {
151
+ return new Promise((resolve, reject) => {
152
+ const child = spawn(cmd, args, {stdio: 'inherit', shell: true, ...opts});
153
+ child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${code}`))));
154
+ child.on('error', reject);
155
+ });
156
+ }
157
+
158
+ /* --------------------------------- Actions -------------------------------- */
159
+
160
+ async function runDev(opts: ParsedArgs): Promise<void> {
161
+ const cwd = process.cwd();
162
+ const entry = await resolveEntry(cwd, opts.entry);
163
+ console.log(`${c('cyan', '[dev]')} using entry: ${path.relative(cwd, entry)}`);
164
+ console.log(`${c('gray', 'hint:')} press Ctrl+C to stop`);
165
+ await runCmd('npx', ['-y', 'tsx', '--watch', entry]);
166
+ }
167
+
168
+ async function ensureDir(p: string): Promise<void> {
169
+ await fsp.mkdir(p, {recursive: true});
170
+ }
171
+
172
+ function isTsLike(p: string): boolean {
173
+ return /\.tsx?$/i.test(p);
174
+ }
175
+
176
+ async function runBuild(opts: ParsedArgs): Promise<void> {
177
+ const cwd = process.cwd();
178
+ const entry = await resolveEntry(cwd, opts.entry);
179
+ const outDir = path.resolve(cwd, opts.outDir || 'dist');
180
+ await ensureDir(outDir);
181
+
182
+ console.log(`${c('cyan', '[build]')} entry: ${path.relative(cwd, entry)}`);
183
+ console.log(`${c('cyan', '[build]')} outDir: ${path.relative(cwd, outDir)}`);
184
+
185
+ const args: string[] = [];
186
+ args.push('--outDir', outDir);
187
+ args.push('--skipLibCheck');
188
+ args.push('--rootDir', path.dirname(entry));
189
+
190
+ if (!isTsLike(entry)) {
191
+ args.push('--allowJs');
192
+ console.log(c('yellow', '[build] Entry is not TypeScript; enabling --allowJs'));
193
+ }
194
+
195
+ const tsconfigPath = path.join(cwd, 'tsconfig.json');
196
+ if (await fileExists(tsconfigPath)) {
197
+ console.log(c('gray', `[build] tsconfig.json detected (project options will be respected where applicable)`));
198
+ }
199
+
200
+ // Compile the single entry file
201
+ await runCmd('npx', ['-y', 'tsc', entry, ...args]);
202
+ console.log(c('green', '✅ Build completed.'));
203
+ console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
204
+ }
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
+
215
+ const RECOMMENDED_TSCONFIG = {
216
+ compilerOptions: {
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',
223
+ strict: true,
224
+ esModuleInterop: true,
225
+ resolveJsonModule: true,
226
+ skipLibCheck: true,
227
+ sourceMap: true,
228
+ outDir: 'dist',
229
+ rootDir: 'src',
230
+ types: ['node'],
231
+ },
232
+ include: ['src/**/*'],
233
+ } as const;
234
+
235
+ function deepMerge<T extends Record<string, any>, U extends Record<string, any>>(base: T, patch: U): T & U {
236
+ const out: Record<string, any> = {...base};
237
+ for (const [k, v] of Object.entries(patch)) {
238
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
239
+ out[k] = deepMerge(base[k] ?? {}, v as Record<string, any>);
240
+ } else {
241
+ out[k] = v;
242
+ }
243
+ }
244
+ return out as T & U;
245
+ }
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
+
288
+ async function runInit(): Promise<void> {
289
+ const cwd = process.cwd();
290
+ const tsconfigPath = path.join(cwd, 'tsconfig.json');
291
+ const existing = await readJSON<Record<string, any>>(tsconfigPath);
292
+
293
+ if (!existing) {
294
+ console.log(c('yellow', 'tsconfig.json not found — creating one.'));
295
+ await writeJSON(tsconfigPath, RECOMMENDED_TSCONFIG);
296
+ console.log(c('green', '✅ Created tsconfig.json with required decorator settings.'));
297
+ return;
298
+ }
299
+
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).'));
306
+ }
307
+
308
+ function cmpSemver(a: string, b: string): number {
309
+ const pa = a.split('.').map((n) => parseInt(n, 10) || 0);
310
+ const pb = b.split('.').map((n) => parseInt(n, 10) || 0);
311
+ for (let i = 0; i < 3; i++) {
312
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
313
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
314
+ }
315
+ return 0;
316
+ }
317
+
318
+ async function runDoctor(): Promise<void> {
319
+ const MIN_NODE = '22.0.0';
320
+ const MIN_NPM = '10.0.0';
321
+ const cwd = process.cwd();
322
+
323
+ let ok = true;
324
+
325
+ // Node
326
+ const nodeVer = process.versions.node;
327
+ if (cmpSemver(nodeVer, MIN_NODE) >= 0) {
328
+ console.log(`✅ Node ${nodeVer} (min ${MIN_NODE})`);
329
+ } else {
330
+ ok = false;
331
+ console.log(`❌ Node ${nodeVer} — please upgrade to >= ${MIN_NODE}`);
332
+ }
333
+
334
+ // npm
335
+ let npmVer = 'unknown';
336
+ try {
337
+ npmVer = await new Promise<string>((resolve, reject) => {
338
+ const child = spawn('npm', ['-v'], {shell: true});
339
+ let out = '';
340
+ child.stdout?.on('data', (d) => (out += String(d)));
341
+ child.on('close', () => resolve(out.trim()));
342
+ child.on('error', reject);
343
+ });
344
+ if (cmpSemver(npmVer, MIN_NPM) >= 0) {
345
+ console.log(`✅ npm ${npmVer} (min ${MIN_NPM})`);
346
+ } else {
347
+ ok = false;
348
+ console.log(`❌ npm ${npmVer} — please upgrade to >= ${MIN_NPM}`);
349
+ }
350
+ } catch {
351
+ ok = false;
352
+ console.log('❌ npm not found in PATH');
353
+ }
354
+
355
+ // tsconfig.json presence + required fields
356
+ const tsconfigPath = path.join(cwd, 'tsconfig.json');
357
+ if (await fileExists(tsconfigPath)) {
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
+ }
368
+ } else {
369
+ ok = false;
370
+ console.log(`❌ tsconfig.json not found — run ${c('cyan', 'frontmcp init')}`);
371
+ }
372
+
373
+ // Entry check (nice to have)
374
+ try {
375
+ const entry = await resolveEntry(cwd);
376
+ console.log(`✅ entry detected: ${path.relative(cwd, entry)}`);
377
+ } catch (e: any) {
378
+ const firstLine = (e?.message as string | undefined)?.split('\n')?.[0] ?? 'entry not found';
379
+ console.log(`❌ entry not detected — ${firstLine}`);
380
+ }
381
+
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
+ }
387
+ }
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
+
554
+ /* --------------------------------- Main ----------------------------------- */
555
+
556
+ async function main(): Promise<void> {
557
+ const argv = process.argv.slice(2);
558
+ const parsed = parseArgs(argv);
559
+ const cmd = parsed._[0] as Command | undefined;
560
+
561
+ if (parsed.help || !cmd) {
562
+ showHelp();
563
+ process.exit(0);
564
+ }
565
+
566
+ try {
567
+ switch (cmd) {
568
+ case 'dev':
569
+ await runDev(parsed);
570
+ break;
571
+ case 'build':
572
+ parsed.outDir = parsed.outDir || 'dist';
573
+ await runBuild(parsed);
574
+ break;
575
+ case 'init':
576
+ await runInit();
577
+ break;
578
+ case 'doctor':
579
+ await runDoctor();
580
+ break;
581
+ case 'inspector':
582
+ await runInspector();
583
+ break;
584
+ case 'create':
585
+ await runCreate();
586
+ break;
587
+ case 'help':
588
+ showHelp();
589
+ break;
590
+ default:
591
+ console.error(c('red', `Unknown command: ${cmd}`));
592
+ showHelp();
593
+ process.exitCode = 1;
594
+ }
595
+ } catch (err: any) {
596
+ console.error('\n' + c('red', err instanceof Error ? err.stack || err.message : String(err)));
597
+ process.exit(1);
598
+ }
599
+ }
600
+
601
+ main();
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2016",
4
+ "module": "commonjs",
5
+ "esModuleInterop": true,
6
+ "forceConsistentCasingInFileNames": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist"
10
+ },
11
+ "include": [
12
+ "src"
13
+ ]
14
+ }
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="AgentMigrationStateService">
4
- <option name="migrationStatus" value="COMPLETED" />
5
- </component>
6
- </project>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="AskMigrationStateService">
4
- <option name="migrationStatus" value="COMPLETED" />
5
- </component>
6
- </project>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="Ask2AgentMigrationStateService">
4
- <option name="migrationStatus" value="COMPLETED" />
5
- </component>
6
- </project>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="EditMigrationStateService">
4
- <option name="migrationStatus" value="COMPLETED" />
5
- </component>
6
- </project>
package/.idea/mcp.iml DELETED
@@ -1,12 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module type="WEB_MODULE" version="4">
3
- <component name="NewModuleRootManager">
4
- <content url="file://$MODULE_DIR$">
5
- <excludeFolder url="file://$MODULE_DIR$/.tmp" />
6
- <excludeFolder url="file://$MODULE_DIR$/temp" />
7
- <excludeFolder url="file://$MODULE_DIR$/tmp" />
8
- </content>
9
- <orderEntry type="inheritedJdk" />
10
- <orderEntry type="sourceFolder" forTests="false" />
11
- </component>
12
- </module>
package/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/mcp.iml" filepath="$PROJECT_DIR$/.idea/mcp.iml" />
6
- </modules>
7
- </component>
8
- </project>
package/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
- </component>
6
- </project>
File without changes