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