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