frontmcp 0.0.1 → 0.1.2
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 +392 -0
- package/dist/index.js +2 -0
- package/package.json +20 -4
- package/src/cli.ts +359 -0
- package/tsconfig.json +14 -0
- package/.idea/copilot.data.migration.agent.xml +0 -6
- package/.idea/copilot.data.migration.ask.xml +0 -6
- package/.idea/copilot.data.migration.ask2agent.xml +0 -6
- package/.idea/copilot.data.migration.edit.xml +0 -6
- package/.idea/mcp.iml +0 -12
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- /package/{index.js → src/index.ts} +0 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
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 presence
|
|
78
|
+
help Show this help message
|
|
79
|
+
|
|
80
|
+
${c('bold', 'Options')}
|
|
81
|
+
-o, --out-dir <dir> Output directory (default: ./dist)
|
|
82
|
+
-e, --entry <path> Manually specify entry file path
|
|
83
|
+
|
|
84
|
+
${c('bold', 'Examples')}
|
|
85
|
+
frontmcp dev
|
|
86
|
+
frontmcp build --out-dir build
|
|
87
|
+
frontmcp init
|
|
88
|
+
frontmcp doctor
|
|
89
|
+
`);
|
|
90
|
+
}
|
|
91
|
+
function parseArgs(argv) {
|
|
92
|
+
const out = { _: [] };
|
|
93
|
+
for (let i = 0; i < argv.length; i++) {
|
|
94
|
+
const a = argv[i];
|
|
95
|
+
if (a === '--out-dir' || a === '-o')
|
|
96
|
+
out.outDir = argv[++i];
|
|
97
|
+
else if (a === '--entry' || a === '-e')
|
|
98
|
+
out.entry = argv[++i];
|
|
99
|
+
else if (a === '--help' || a === '-h')
|
|
100
|
+
out.help = true;
|
|
101
|
+
else
|
|
102
|
+
out._.push(a);
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
function fileExists(p) {
|
|
107
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
108
|
+
try {
|
|
109
|
+
yield fs_1.promises.access(p, fs.constants.F_OK);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
catch (_a) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function readJSON(jsonPath) {
|
|
118
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
119
|
+
try {
|
|
120
|
+
const buf = yield fs_1.promises.readFile(jsonPath, 'utf8');
|
|
121
|
+
return JSON.parse(buf);
|
|
122
|
+
}
|
|
123
|
+
catch (_a) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function tryCandidates(base) {
|
|
129
|
+
const exts = ['', '.ts', '.tsx', '.js', '.mjs', '.cjs'];
|
|
130
|
+
return exts.map((ext) => base + ext);
|
|
131
|
+
}
|
|
132
|
+
function resolveEntry(cwd, explicit) {
|
|
133
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
134
|
+
if (explicit) {
|
|
135
|
+
const full = path.resolve(cwd, explicit);
|
|
136
|
+
if (yield fileExists(full))
|
|
137
|
+
return full;
|
|
138
|
+
throw new Error(`Entry override not found: ${explicit}`);
|
|
139
|
+
}
|
|
140
|
+
// 1) package.json main
|
|
141
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
142
|
+
if (yield fileExists(pkgPath)) {
|
|
143
|
+
const pkg = yield readJSON(pkgPath);
|
|
144
|
+
if (pkg && typeof pkg.main === 'string' && pkg.main.trim()) {
|
|
145
|
+
const mainCandidates = tryCandidates(path.resolve(cwd, pkg.main));
|
|
146
|
+
for (const p of mainCandidates) {
|
|
147
|
+
if (yield fileExists(p))
|
|
148
|
+
return p;
|
|
149
|
+
}
|
|
150
|
+
// If "main" is a directory-like path, try index.* within it
|
|
151
|
+
const asDir = path.resolve(cwd, pkg.main);
|
|
152
|
+
const idxCandidates = tryCandidates(path.join(asDir, 'index'));
|
|
153
|
+
for (const p of idxCandidates) {
|
|
154
|
+
if (yield fileExists(p))
|
|
155
|
+
return p;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// 2) src/main.ts
|
|
160
|
+
const fallback = path.join(cwd, 'src', 'main.ts');
|
|
161
|
+
if (yield fileExists(fallback))
|
|
162
|
+
return fallback;
|
|
163
|
+
// 3) Not found
|
|
164
|
+
const msg = [
|
|
165
|
+
c('red', 'No entry file found.'),
|
|
166
|
+
'',
|
|
167
|
+
'I looked for:',
|
|
168
|
+
` • ${pkgPath} with a valid "main" field`,
|
|
169
|
+
` • ${path.relative(cwd, fallback)}`,
|
|
170
|
+
'',
|
|
171
|
+
'Please create an entry file (e.g. src/main.ts) or set "main" in package.json,',
|
|
172
|
+
'or run with an explicit path:',
|
|
173
|
+
` frontmcp dev --entry src/main.ts`,
|
|
174
|
+
].join('\n');
|
|
175
|
+
throw new Error(msg);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function runCmd(cmd, args, opts = {}) {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const child = (0, child_process_1.spawn)(cmd, args, Object.assign({ stdio: 'inherit', shell: true }, opts));
|
|
181
|
+
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${code}`))));
|
|
182
|
+
child.on('error', reject);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/* --------------------------------- Actions -------------------------------- */
|
|
186
|
+
function runDev(opts) {
|
|
187
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
188
|
+
const cwd = process.cwd();
|
|
189
|
+
const entry = yield resolveEntry(cwd, opts.entry);
|
|
190
|
+
console.log(`${c('cyan', '[dev]')} using entry: ${path.relative(cwd, entry)}`);
|
|
191
|
+
console.log(`${c('gray', 'hint:')} press Ctrl+C to stop`);
|
|
192
|
+
yield runCmd('npx', ['-y', 'tsx', '--watch', entry]);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
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
|
+
function isTsLike(p) {
|
|
201
|
+
return /\.tsx?$/i.test(p);
|
|
202
|
+
}
|
|
203
|
+
function runBuild(opts) {
|
|
204
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
205
|
+
const cwd = process.cwd();
|
|
206
|
+
const entry = yield resolveEntry(cwd, opts.entry);
|
|
207
|
+
const outDir = path.resolve(cwd, opts.outDir || 'dist');
|
|
208
|
+
yield ensureDir(outDir);
|
|
209
|
+
console.log(`${c('cyan', '[build]')} entry: ${path.relative(cwd, entry)}`);
|
|
210
|
+
console.log(`${c('cyan', '[build]')} outDir: ${path.relative(cwd, outDir)}`);
|
|
211
|
+
const args = [];
|
|
212
|
+
args.push('--outDir', outDir);
|
|
213
|
+
args.push('--skipLibCheck');
|
|
214
|
+
args.push('--rootDir', path.dirname(entry));
|
|
215
|
+
if (!isTsLike(entry)) {
|
|
216
|
+
args.push('--allowJs');
|
|
217
|
+
console.log(c('yellow', '[build] Entry is not TypeScript; enabling --allowJs'));
|
|
218
|
+
}
|
|
219
|
+
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
220
|
+
if (yield fileExists(tsconfigPath)) {
|
|
221
|
+
console.log(c('gray', `[build] tsconfig.json detected (project options will be respected where applicable)`));
|
|
222
|
+
}
|
|
223
|
+
// Compile the single entry file
|
|
224
|
+
yield runCmd('npx', ['-y', 'tsc', entry, ...args]);
|
|
225
|
+
console.log(c('green', '✅ Build completed.'));
|
|
226
|
+
console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const RECOMMENDED_TSCONFIG = {
|
|
230
|
+
compilerOptions: {
|
|
231
|
+
target: 'ES2020',
|
|
232
|
+
module: 'CommonJS',
|
|
233
|
+
moduleResolution: 'Node',
|
|
234
|
+
strict: true,
|
|
235
|
+
esModuleInterop: true,
|
|
236
|
+
resolveJsonModule: true,
|
|
237
|
+
skipLibCheck: true,
|
|
238
|
+
sourceMap: true,
|
|
239
|
+
outDir: 'dist',
|
|
240
|
+
rootDir: 'src',
|
|
241
|
+
types: ['node'],
|
|
242
|
+
},
|
|
243
|
+
include: ['src/**/*'],
|
|
244
|
+
};
|
|
245
|
+
function deepMerge(base, patch) {
|
|
246
|
+
var _a;
|
|
247
|
+
const out = Object.assign({}, base);
|
|
248
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
249
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
250
|
+
out[k] = deepMerge((_a = base[k]) !== null && _a !== void 0 ? _a : {}, v);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
out[k] = v;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
function runInit() {
|
|
259
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
260
|
+
const cwd = process.cwd();
|
|
261
|
+
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
262
|
+
const existing = yield readJSON(tsconfigPath);
|
|
263
|
+
if (!existing) {
|
|
264
|
+
console.log(c('yellow', 'tsconfig.json not found — creating one.'));
|
|
265
|
+
yield fs_1.promises.writeFile(tsconfigPath, JSON.stringify(RECOMMENDED_TSCONFIG, null, 2) + '\n', 'utf8');
|
|
266
|
+
console.log(c('green', '✅ Created tsconfig.json'));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// We want to ADD missing recommended fields but NOT override user's existing choices.
|
|
270
|
+
// So recommended (base) merged with existing (patch) -> existing wins.
|
|
271
|
+
const fixed = deepMerge(RECOMMENDED_TSCONFIG, existing);
|
|
272
|
+
yield fs_1.promises.writeFile(tsconfigPath, JSON.stringify(fixed, null, 2) + '\n', 'utf8');
|
|
273
|
+
console.log(c('green', '✅ tsconfig.json verified/updated'));
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function cmpSemver(a, b) {
|
|
277
|
+
const pa = a.split('.').map((n) => parseInt(n, 10) || 0);
|
|
278
|
+
const pb = b.split('.').map((n) => parseInt(n, 10) || 0);
|
|
279
|
+
for (let i = 0; i < 3; i++) {
|
|
280
|
+
if ((pa[i] || 0) > (pb[i] || 0))
|
|
281
|
+
return 1;
|
|
282
|
+
if ((pa[i] || 0) < (pb[i] || 0))
|
|
283
|
+
return -1;
|
|
284
|
+
}
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
function runDoctor() {
|
|
288
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
289
|
+
var _a, _b, _c;
|
|
290
|
+
const MIN_NODE = '18.0.0';
|
|
291
|
+
const MIN_NPM = '8.0.0';
|
|
292
|
+
const cwd = process.cwd();
|
|
293
|
+
let ok = true;
|
|
294
|
+
// Node
|
|
295
|
+
const nodeVer = process.versions.node;
|
|
296
|
+
if (cmpSemver(nodeVer, MIN_NODE) >= 0) {
|
|
297
|
+
console.log(`✅ Node ${nodeVer} (min ${MIN_NODE})`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
ok = false;
|
|
301
|
+
console.log(`❌ Node ${nodeVer} — please upgrade to >= ${MIN_NODE}`);
|
|
302
|
+
}
|
|
303
|
+
// npm
|
|
304
|
+
let npmVer = 'unknown';
|
|
305
|
+
try {
|
|
306
|
+
npmVer = yield new Promise((resolve, reject) => {
|
|
307
|
+
var _a;
|
|
308
|
+
const child = (0, child_process_1.spawn)('npm', ['-v'], { shell: true });
|
|
309
|
+
let out = '';
|
|
310
|
+
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (d) => (out += String(d)));
|
|
311
|
+
child.on('close', () => resolve(out.trim()));
|
|
312
|
+
child.on('error', reject);
|
|
313
|
+
});
|
|
314
|
+
if (cmpSemver(npmVer, MIN_NPM) >= 0) {
|
|
315
|
+
console.log(`✅ npm ${npmVer} (min ${MIN_NPM})`);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
ok = false;
|
|
319
|
+
console.log(`❌ npm ${npmVer} — please upgrade to >= ${MIN_NPM}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (_d) {
|
|
323
|
+
ok = false;
|
|
324
|
+
console.log('❌ npm not found in PATH');
|
|
325
|
+
}
|
|
326
|
+
// tsconfig.json presence
|
|
327
|
+
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
328
|
+
if (yield fileExists(tsconfigPath)) {
|
|
329
|
+
console.log(`✅ tsconfig.json found`);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
ok = false;
|
|
333
|
+
console.log(`❌ tsconfig.json not found — run ${c('cyan', 'frontmcp init')}`);
|
|
334
|
+
}
|
|
335
|
+
// Entry check (nice to have)
|
|
336
|
+
try {
|
|
337
|
+
const entry = yield resolveEntry(cwd);
|
|
338
|
+
console.log(`✅ entry detected: ${path.relative(cwd, entry)}`);
|
|
339
|
+
}
|
|
340
|
+
catch (e) {
|
|
341
|
+
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
|
+
console.log(`❌ entry not detected — ${firstLine}`);
|
|
343
|
+
}
|
|
344
|
+
if (ok) {
|
|
345
|
+
console.log(c('green', '\nAll checks passed. You are ready to go!'));
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/* --------------------------------- Main ----------------------------------- */
|
|
353
|
+
function main() {
|
|
354
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
355
|
+
const argv = process.argv.slice(2);
|
|
356
|
+
const parsed = parseArgs(argv);
|
|
357
|
+
const cmd = parsed._[0];
|
|
358
|
+
if (parsed.help || !cmd) {
|
|
359
|
+
showHelp();
|
|
360
|
+
process.exit(0);
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
switch (cmd) {
|
|
364
|
+
case 'dev':
|
|
365
|
+
yield runDev(parsed);
|
|
366
|
+
break;
|
|
367
|
+
case 'build':
|
|
368
|
+
parsed.outDir = parsed.outDir || 'dist';
|
|
369
|
+
yield runBuild(parsed);
|
|
370
|
+
break;
|
|
371
|
+
case 'init':
|
|
372
|
+
yield runInit();
|
|
373
|
+
break;
|
|
374
|
+
case 'doctor':
|
|
375
|
+
yield runDoctor();
|
|
376
|
+
break;
|
|
377
|
+
case 'help':
|
|
378
|
+
showHelp();
|
|
379
|
+
break;
|
|
380
|
+
default:
|
|
381
|
+
console.error(c('red', `Unknown command: ${cmd}`));
|
|
382
|
+
showHelp();
|
|
383
|
+
process.exitCode = 1;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
console.error('\n' + c('red', err instanceof Error ? err.stack || err.message : String(err)));
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
main();
|
package/dist/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frontmcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "",
|
|
5
|
-
"main": "index.js",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "FrontMCP command line interface",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"frontmcp": "dist/cli.js"
|
|
8
|
+
},
|
|
6
9
|
"scripts": {
|
|
7
|
-
"
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/cli.ts",
|
|
12
|
+
"prepare": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@frontmcp/sdk": "0.1.2",
|
|
16
|
+
"@frontmcp/core": "0.1.2",
|
|
17
|
+
"@frontmcp/plugins": "0.1.2",
|
|
18
|
+
"@frontmcp/adapters": "0.1.2",
|
|
19
|
+
"tsx": "^4.20.6",
|
|
20
|
+
"typescript": "^5.5.3"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "20.19.9"
|
|
8
24
|
}
|
|
9
25
|
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
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' | '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 presence
|
|
49
|
+
help Show this help message
|
|
50
|
+
|
|
51
|
+
${c('bold', 'Options')}
|
|
52
|
+
-o, --out-dir <dir> Output directory (default: ./dist)
|
|
53
|
+
-e, --entry <path> Manually specify entry file path
|
|
54
|
+
|
|
55
|
+
${c('bold', 'Examples')}
|
|
56
|
+
frontmcp dev
|
|
57
|
+
frontmcp build --out-dir build
|
|
58
|
+
frontmcp init
|
|
59
|
+
frontmcp doctor
|
|
60
|
+
`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
64
|
+
const out: ParsedArgs = {_: []};
|
|
65
|
+
for (let i = 0; i < argv.length; i++) {
|
|
66
|
+
const a = argv[i];
|
|
67
|
+
if (a === '--out-dir' || a === '-o') out.outDir = argv[++i];
|
|
68
|
+
else if (a === '--entry' || a === '-e') out.entry = argv[++i];
|
|
69
|
+
else if (a === '--help' || a === '-h') out.help = true;
|
|
70
|
+
else out._.push(a);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function fileExists(p: string): Promise<boolean> {
|
|
76
|
+
try {
|
|
77
|
+
await fsp.access(p, fs.constants.F_OK);
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function readJSON<T = any>(jsonPath: string): Promise<T | null> {
|
|
85
|
+
try {
|
|
86
|
+
const buf = await fsp.readFile(jsonPath, 'utf8');
|
|
87
|
+
return JSON.parse(buf) as T;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function tryCandidates(base: string): string[] {
|
|
94
|
+
const exts = ['', '.ts', '.tsx', '.js', '.mjs', '.cjs'];
|
|
95
|
+
return exts.map((ext) => base + ext);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function resolveEntry(cwd: string, explicit?: string): Promise<string> {
|
|
99
|
+
if (explicit) {
|
|
100
|
+
const full = path.resolve(cwd, explicit);
|
|
101
|
+
if (await fileExists(full)) return full;
|
|
102
|
+
throw new Error(`Entry override not found: ${explicit}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 1) package.json main
|
|
106
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
107
|
+
if (await fileExists(pkgPath)) {
|
|
108
|
+
const pkg = await readJSON<any>(pkgPath);
|
|
109
|
+
if (pkg && typeof pkg.main === 'string' && pkg.main.trim()) {
|
|
110
|
+
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
|
|
115
|
+
const asDir = path.resolve(cwd, pkg.main);
|
|
116
|
+
const idxCandidates = tryCandidates(path.join(asDir, 'index'));
|
|
117
|
+
for (const p of idxCandidates) {
|
|
118
|
+
if (await fileExists(p)) return p;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 2) src/main.ts
|
|
124
|
+
const fallback = path.join(cwd, 'src', 'main.ts');
|
|
125
|
+
if (await fileExists(fallback)) return fallback;
|
|
126
|
+
|
|
127
|
+
// 3) Not found
|
|
128
|
+
const msg = [
|
|
129
|
+
c('red', 'No entry file found.'),
|
|
130
|
+
'',
|
|
131
|
+
'I looked for:',
|
|
132
|
+
` • ${pkgPath} with a valid "main" field`,
|
|
133
|
+
` • ${path.relative(cwd, fallback)}`,
|
|
134
|
+
'',
|
|
135
|
+
'Please create an entry file (e.g. src/main.ts) or set "main" in package.json,',
|
|
136
|
+
'or run with an explicit path:',
|
|
137
|
+
` frontmcp dev --entry src/main.ts`,
|
|
138
|
+
].join('\n');
|
|
139
|
+
throw new Error(msg);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function runCmd(cmd: string, args: string[], opts: { cwd?: string } = {}): Promise<void> {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const child = spawn(cmd, args, {stdio: 'inherit', shell: true, ...opts});
|
|
145
|
+
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${code}`))));
|
|
146
|
+
child.on('error', reject);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* --------------------------------- Actions -------------------------------- */
|
|
151
|
+
|
|
152
|
+
async function runDev(opts: ParsedArgs): Promise<void> {
|
|
153
|
+
const cwd = process.cwd();
|
|
154
|
+
const entry = await resolveEntry(cwd, opts.entry);
|
|
155
|
+
console.log(`${c('cyan', '[dev]')} using entry: ${path.relative(cwd, entry)}`);
|
|
156
|
+
console.log(`${c('gray', 'hint:')} press Ctrl+C to stop`);
|
|
157
|
+
await runCmd('npx', ['-y', 'tsx', '--watch', entry]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function ensureDir(p: string): Promise<void> {
|
|
161
|
+
await fsp.mkdir(p, {recursive: true});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isTsLike(p: string): boolean {
|
|
165
|
+
return /\.tsx?$/i.test(p);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function runBuild(opts: ParsedArgs): Promise<void> {
|
|
169
|
+
const cwd = process.cwd();
|
|
170
|
+
const entry = await resolveEntry(cwd, opts.entry);
|
|
171
|
+
const outDir = path.resolve(cwd, opts.outDir || 'dist');
|
|
172
|
+
await ensureDir(outDir);
|
|
173
|
+
|
|
174
|
+
console.log(`${c('cyan', '[build]')} entry: ${path.relative(cwd, entry)}`);
|
|
175
|
+
console.log(`${c('cyan', '[build]')} outDir: ${path.relative(cwd, outDir)}`);
|
|
176
|
+
|
|
177
|
+
const args: string[] = [];
|
|
178
|
+
args.push('--outDir', outDir);
|
|
179
|
+
args.push('--skipLibCheck');
|
|
180
|
+
args.push('--rootDir', path.dirname(entry));
|
|
181
|
+
|
|
182
|
+
if (!isTsLike(entry)) {
|
|
183
|
+
args.push('--allowJs');
|
|
184
|
+
console.log(c('yellow', '[build] Entry is not TypeScript; enabling --allowJs'));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
188
|
+
if (await fileExists(tsconfigPath)) {
|
|
189
|
+
console.log(c('gray', `[build] tsconfig.json detected (project options will be respected where applicable)`));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Compile the single entry file
|
|
193
|
+
await runCmd('npx', ['-y', 'tsc', entry, ...args]);
|
|
194
|
+
console.log(c('green', '✅ Build completed.'));
|
|
195
|
+
console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const RECOMMENDED_TSCONFIG = {
|
|
199
|
+
compilerOptions: {
|
|
200
|
+
target: 'ES2020',
|
|
201
|
+
module: 'CommonJS',
|
|
202
|
+
moduleResolution: 'Node',
|
|
203
|
+
strict: true,
|
|
204
|
+
esModuleInterop: true,
|
|
205
|
+
resolveJsonModule: true,
|
|
206
|
+
skipLibCheck: true,
|
|
207
|
+
sourceMap: true,
|
|
208
|
+
outDir: 'dist',
|
|
209
|
+
rootDir: 'src',
|
|
210
|
+
types: ['node'],
|
|
211
|
+
},
|
|
212
|
+
include: ['src/**/*'],
|
|
213
|
+
} as const;
|
|
214
|
+
|
|
215
|
+
function deepMerge<T extends Record<string, any>, U extends Record<string, any>>(base: T, patch: U): T & U {
|
|
216
|
+
const out: Record<string, any> = {...base};
|
|
217
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
218
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
219
|
+
out[k] = deepMerge(base[k] ?? {}, v as Record<string, any>);
|
|
220
|
+
} else {
|
|
221
|
+
out[k] = v;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return out as T & U;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function runInit(): Promise<void> {
|
|
228
|
+
const cwd = process.cwd();
|
|
229
|
+
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
230
|
+
const existing = await readJSON<Record<string, any>>(tsconfigPath);
|
|
231
|
+
|
|
232
|
+
if (!existing) {
|
|
233
|
+
console.log(c('yellow', 'tsconfig.json not found — creating one.'));
|
|
234
|
+
await fsp.writeFile(tsconfigPath, JSON.stringify(RECOMMENDED_TSCONFIG, null, 2) + '\n', 'utf8');
|
|
235
|
+
console.log(c('green', '✅ Created tsconfig.json'));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// We want to ADD missing recommended fields but NOT override user's existing choices.
|
|
240
|
+
// So recommended (base) merged with existing (patch) -> existing wins.
|
|
241
|
+
const fixed = deepMerge(RECOMMENDED_TSCONFIG as any, existing);
|
|
242
|
+
await fsp.writeFile(tsconfigPath, JSON.stringify(fixed, null, 2) + '\n', 'utf8');
|
|
243
|
+
console.log(c('green', '✅ tsconfig.json verified/updated'));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function cmpSemver(a: string, b: string): number {
|
|
247
|
+
const pa = a.split('.').map((n) => parseInt(n, 10) || 0);
|
|
248
|
+
const pb = b.split('.').map((n) => parseInt(n, 10) || 0);
|
|
249
|
+
for (let i = 0; i < 3; i++) {
|
|
250
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
251
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
252
|
+
}
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function runDoctor(): Promise<void> {
|
|
257
|
+
const MIN_NODE = '18.0.0';
|
|
258
|
+
const MIN_NPM = '8.0.0';
|
|
259
|
+
const cwd = process.cwd();
|
|
260
|
+
|
|
261
|
+
let ok = true;
|
|
262
|
+
|
|
263
|
+
// Node
|
|
264
|
+
const nodeVer = process.versions.node;
|
|
265
|
+
if (cmpSemver(nodeVer, MIN_NODE) >= 0) {
|
|
266
|
+
console.log(`✅ Node ${nodeVer} (min ${MIN_NODE})`);
|
|
267
|
+
} else {
|
|
268
|
+
ok = false;
|
|
269
|
+
console.log(`❌ Node ${nodeVer} — please upgrade to >= ${MIN_NODE}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// npm
|
|
273
|
+
let npmVer = 'unknown';
|
|
274
|
+
try {
|
|
275
|
+
npmVer = await new Promise<string>((resolve, reject) => {
|
|
276
|
+
const child = spawn('npm', ['-v'], {shell: true});
|
|
277
|
+
let out = '';
|
|
278
|
+
child.stdout?.on('data', (d) => (out += String(d)));
|
|
279
|
+
child.on('close', () => resolve(out.trim()));
|
|
280
|
+
child.on('error', reject);
|
|
281
|
+
});
|
|
282
|
+
if (cmpSemver(npmVer, MIN_NPM) >= 0) {
|
|
283
|
+
console.log(`✅ npm ${npmVer} (min ${MIN_NPM})`);
|
|
284
|
+
} else {
|
|
285
|
+
ok = false;
|
|
286
|
+
console.log(`❌ npm ${npmVer} — please upgrade to >= ${MIN_NPM}`);
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
ok = false;
|
|
290
|
+
console.log('❌ npm not found in PATH');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// tsconfig.json presence
|
|
294
|
+
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
295
|
+
if (await fileExists(tsconfigPath)) {
|
|
296
|
+
console.log(`✅ tsconfig.json found`);
|
|
297
|
+
} else {
|
|
298
|
+
ok = false;
|
|
299
|
+
console.log(`❌ tsconfig.json not found — run ${c('cyan', 'frontmcp init')}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Entry check (nice to have)
|
|
303
|
+
try {
|
|
304
|
+
const entry = await resolveEntry(cwd);
|
|
305
|
+
console.log(`✅ entry detected: ${path.relative(cwd, entry)}`);
|
|
306
|
+
} catch (e: any) {
|
|
307
|
+
const firstLine = (e?.message as string | undefined)?.split('\n')?.[0] ?? 'entry not found';
|
|
308
|
+
console.log(`❌ entry not detected — ${firstLine}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (ok) {
|
|
312
|
+
console.log(c('green', '\nAll checks passed. You are ready to go!'));
|
|
313
|
+
} else {
|
|
314
|
+
console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* --------------------------------- Main ----------------------------------- */
|
|
319
|
+
|
|
320
|
+
async function main(): Promise<void> {
|
|
321
|
+
const argv = process.argv.slice(2);
|
|
322
|
+
const parsed = parseArgs(argv);
|
|
323
|
+
const cmd = parsed._[0] as Command | undefined;
|
|
324
|
+
|
|
325
|
+
if (parsed.help || !cmd) {
|
|
326
|
+
showHelp();
|
|
327
|
+
process.exit(0);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
switch (cmd) {
|
|
332
|
+
case 'dev':
|
|
333
|
+
await runDev(parsed);
|
|
334
|
+
break;
|
|
335
|
+
case 'build':
|
|
336
|
+
parsed.outDir = parsed.outDir || 'dist';
|
|
337
|
+
await runBuild(parsed);
|
|
338
|
+
break;
|
|
339
|
+
case 'init':
|
|
340
|
+
await runInit();
|
|
341
|
+
break;
|
|
342
|
+
case 'doctor':
|
|
343
|
+
await runDoctor();
|
|
344
|
+
break;
|
|
345
|
+
case 'help':
|
|
346
|
+
showHelp();
|
|
347
|
+
break;
|
|
348
|
+
default:
|
|
349
|
+
console.error(c('red', `Unknown command: ${cmd}`));
|
|
350
|
+
showHelp();
|
|
351
|
+
process.exitCode = 1;
|
|
352
|
+
}
|
|
353
|
+
} catch (err: any) {
|
|
354
|
+
console.error('\n' + c('red', err instanceof Error ? err.stack || err.message : String(err)));
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
main();
|
package/tsconfig.json
ADDED
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
package/.idea/vcs.xml
DELETED
|
File without changes
|