@vinkius-core/mcp-fusion 3.1.4 → 3.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/cli/args.d.ts +26 -0
  2. package/dist/cli/args.d.ts.map +1 -0
  3. package/dist/cli/args.js +100 -0
  4. package/dist/cli/args.js.map +1 -0
  5. package/dist/cli/commands/create.d.ts +11 -0
  6. package/dist/cli/commands/create.d.ts.map +1 -0
  7. package/dist/cli/commands/create.js +121 -0
  8. package/dist/cli/commands/create.js.map +1 -0
  9. package/dist/cli/commands/deploy.d.ts +3 -0
  10. package/dist/cli/commands/deploy.d.ts.map +1 -0
  11. package/dist/cli/commands/deploy.js +234 -0
  12. package/dist/cli/commands/deploy.js.map +1 -0
  13. package/dist/cli/commands/dev.d.ts +9 -0
  14. package/dist/cli/commands/dev.d.ts.map +1 -0
  15. package/dist/cli/commands/dev.js +54 -0
  16. package/dist/cli/commands/dev.js.map +1 -0
  17. package/dist/cli/commands/lock.d.ts +5 -0
  18. package/dist/cli/commands/lock.d.ts.map +1 -0
  19. package/dist/cli/commands/lock.js +94 -0
  20. package/dist/cli/commands/lock.js.map +1 -0
  21. package/dist/cli/commands/remote.d.ts +3 -0
  22. package/dist/cli/commands/remote.d.ts.map +1 -0
  23. package/dist/cli/commands/remote.js +37 -0
  24. package/dist/cli/commands/remote.js.map +1 -0
  25. package/dist/cli/constants.d.ts +19 -0
  26. package/dist/cli/constants.d.ts.map +1 -0
  27. package/dist/cli/constants.js +86 -0
  28. package/dist/cli/constants.js.map +1 -0
  29. package/dist/cli/fusion.d.ts +13 -131
  30. package/dist/cli/fusion.d.ts.map +1 -1
  31. package/dist/cli/fusion.js +27 -642
  32. package/dist/cli/fusion.js.map +1 -1
  33. package/dist/cli/progress.d.ts +34 -0
  34. package/dist/cli/progress.d.ts.map +1 -0
  35. package/dist/cli/progress.js +102 -0
  36. package/dist/cli/progress.js.map +1 -0
  37. package/dist/cli/rc.d.ts +11 -0
  38. package/dist/cli/rc.d.ts.map +1 -0
  39. package/dist/cli/rc.js +66 -0
  40. package/dist/cli/rc.js.map +1 -0
  41. package/dist/cli/registry.d.ts +25 -0
  42. package/dist/cli/registry.d.ts.map +1 -0
  43. package/dist/cli/registry.js +86 -0
  44. package/dist/cli/registry.js.map +1 -0
  45. package/dist/cli/types.d.ts +7 -0
  46. package/dist/cli/types.d.ts.map +1 -1
  47. package/dist/cli/utils.d.ts +26 -0
  48. package/dist/cli/utils.d.ts.map +1 -0
  49. package/dist/cli/utils.js +63 -0
  50. package/dist/cli/utils.js.map +1 -0
  51. package/dist/core/createGroup.d.ts.map +1 -1
  52. package/dist/core/createGroup.js +17 -5
  53. package/dist/core/createGroup.js.map +1 -1
  54. package/dist/edge-stub.d.ts +73 -0
  55. package/dist/edge-stub.d.ts.map +1 -0
  56. package/dist/edge-stub.js +81 -0
  57. package/dist/edge-stub.js.map +1 -0
  58. package/dist/fsm/StateMachineGate.d.ts +20 -0
  59. package/dist/fsm/StateMachineGate.d.ts.map +1 -1
  60. package/dist/fsm/StateMachineGate.js +45 -1
  61. package/dist/fsm/StateMachineGate.js.map +1 -1
  62. package/dist/server/DevServer.d.ts.map +1 -1
  63. package/dist/server/DevServer.js +8 -2
  64. package/dist/server/DevServer.js.map +1 -1
  65. package/dist/server/ServerAttachment.d.ts.map +1 -1
  66. package/dist/server/ServerAttachment.js +27 -17
  67. package/dist/server/ServerAttachment.js.map +1 -1
  68. package/dist/server/startServer.d.ts +3 -0
  69. package/dist/server/startServer.d.ts.map +1 -1
  70. package/dist/server/startServer.js +49 -5
  71. package/dist/server/startServer.js.map +1 -1
  72. package/package.json +6 -2
@@ -2,646 +2,27 @@
2
2
  /**
3
3
  * MCP Fusion CLI — `fusion`
4
4
  *
5
- * Commands:
6
- *
7
- * fusion create <name> [--transport stdio|sse] [--vector blank|database|workflow|openapi] [--testing] [--yes|-y]
8
- * Scaffold a new MCP Fusion server project.
9
- *
10
- * fusion dev --server <entrypoint> [--dir <watchDir>]
11
- * Start HMR dev server with auto-reload and tool list notifications.
12
- *
13
- * fusion lock [--server <entrypoint>] [--name <serverName>]
14
- * Generate or update `mcp-fusion.lock`.
15
- *
16
- * fusion lock --check [--server <entrypoint>]
17
- * Verify the lockfile matches the current server.
18
- * Exits 0 if up-to-date, 1 if stale (CI gate).
5
+ * Slim entry point: parses args, dispatches to command modules.
6
+ * All logic lives in focused modules under `./commands/`.
19
7
  *
20
8
  * @module
21
9
  */
22
- import { resolve } from 'node:path';
23
- import { pathToFileURL } from 'node:url';
24
- import { existsSync } from 'node:fs';
25
- import { createInterface } from 'node:readline';
26
- import { execSync } from 'node:child_process';
27
- import { compileContracts } from '../introspection/ToolContract.js';
28
- import { generateLockfile, writeLockfile, readLockfile, checkLockfile, serializeLockfile, LOCKFILE_NAME, } from '../introspection/CapabilityLockfile.js';
29
- import { scaffold } from './scaffold.js';
30
- import { createDevServer } from '../server/DevServer.js';
31
- // ============================================================================
32
- // ANSI Styling (zero dependencies)
33
- // ============================================================================
34
- /** @internal exported for testing */
35
- export const ansi = {
36
- cyan: (s) => `\x1b[36m${s}\x1b[0m`,
37
- green: (s) => `\x1b[32m${s}\x1b[0m`,
38
- dim: (s) => `\x1b[2m${s}\x1b[0m`,
39
- bold: (s) => `\x1b[1m${s}\x1b[0m`,
40
- red: (s) => `\x1b[31m${s}\x1b[0m`,
41
- reset: '\x1b[0m',
42
- };
43
- /** Icon map for each step status */
44
- const STATUS_ICONS = {
45
- pending: '○',
46
- running: '◐',
47
- done: '●',
48
- failed: '✗',
49
- };
50
- /**
51
- * Create the default pretty-print progress reporter.
52
- * Output goes to stderr so it doesn't pollute piped stdout.
53
- * @internal exported for testing
54
- */
55
- export function createDefaultReporter() {
56
- return (step) => {
57
- const icon = STATUS_ICONS[step.status];
58
- const timing = step.durationMs !== undefined ? ` (${step.durationMs}ms)` : '';
59
- const detail = step.detail ? ` — ${step.detail}` : '';
60
- process.stderr.write(` ${icon} ${step.label}${detail}${timing}\n`);
61
- };
62
- }
63
- /**
64
- * A progress tracker that drives step-by-step progress reporting.
65
- * @internal exported for testing
66
- */
67
- export class ProgressTracker {
68
- reporter;
69
- startTimes = new Map();
70
- constructor(reporter) {
71
- this.reporter = reporter ?? createDefaultReporter();
72
- }
73
- /** Mark a step as running */
74
- start(id, label) {
75
- this.startTimes.set(id, Date.now());
76
- this.reporter({ id, label, status: 'running' });
77
- }
78
- /** Mark a step as completed */
79
- done(id, label, detail) {
80
- const durationMs = this.elapsed(id);
81
- this.reporter({
82
- id, label, status: 'done',
83
- ...(detail !== undefined ? { detail } : {}),
84
- ...(durationMs !== undefined ? { durationMs } : {}),
85
- });
86
- }
87
- /** Mark a step as failed */
88
- fail(id, label, detail) {
89
- const durationMs = this.elapsed(id);
90
- this.reporter({
91
- id, label, status: 'failed',
92
- ...(detail !== undefined ? { detail } : {}),
93
- ...(durationMs !== undefined ? { durationMs } : {}),
94
- });
95
- }
96
- elapsed(id) {
97
- const start = this.startTimes.get(id);
98
- if (start === undefined)
99
- return undefined;
100
- this.startTimes.delete(id);
101
- return Date.now() - start;
102
- }
103
- }
104
- // ============================================================================
105
- // Constants
106
- // ============================================================================
107
- /** @internal exported for testing */
108
- export const MCP_FUSION_VERSION = '1.1.0';
109
- /** @internal exported for testing */
110
- export const HELP = `
111
- fusion — MCP Fusion CLI
112
-
113
- USAGE
114
- fusion create <name> Scaffold a new MCP Fusion server
115
- fusion dev --server <entry> Start HMR dev server with auto-reload
116
- fusion lock Generate or update ${LOCKFILE_NAME}
117
- fusion lock --check Verify lockfile is up to date (CI gate)
118
- fusion inspect Launch the real-time TUI dashboard
119
- fusion insp --demo Launch TUI with built-in simulator
120
-
121
- CREATE OPTIONS
122
- --transport <stdio|sse> Transport layer (default: stdio)
123
- --vector <type> Ingestion vector: vanilla, prisma, n8n, openapi, oauth
124
- --testing Include test suite (default: true)
125
- --no-testing Skip test suite
126
- --yes, -y Skip prompts, use defaults
127
-
128
- DEV OPTIONS
129
- --server, -s <path> Path to server entrypoint (default: auto-detect)
130
- --dir, -d <path> Directory to watch for changes (default: auto-detect from server)
131
-
132
- INSPECTOR OPTIONS
133
- --demo, -d Launch with built-in simulator (no server needed)
134
- --out, -o <mode> Output: tui (default), stderr (headless ECS/K8s)
135
- --pid, -p <pid> Connect to a specific server PID
136
- --path <path> Custom IPC socket/pipe path
137
-
138
- LOCK OPTIONS
139
- --server, -s <path> Path to server entrypoint
140
- --name, -n <name> Server name for lockfile header
141
- --cwd <dir> Project root directory
142
-
143
- GLOBAL
144
- --help, -h Show this help message
145
-
146
- EXAMPLES
147
- fusion create my-server
148
- fusion create my-server -y
149
- fusion create my-server --vector prisma --transport sse
150
- fusion dev --server ./src/server.ts
151
- fusion dev --server ./src/server.ts --dir ./src/tools
152
- fusion lock --server ./src/server.ts
153
- fusion inspect --demo
154
- fusion insp --pid 12345
155
- `.trim();
156
- /** @internal exported for testing */
157
- export function parseArgs(argv) {
158
- const args = argv.slice(2);
159
- const result = {
160
- command: '',
161
- check: false,
162
- server: undefined,
163
- name: undefined,
164
- cwd: process.cwd(),
165
- help: false,
166
- projectName: undefined,
167
- transport: undefined,
168
- vector: undefined,
169
- testing: undefined,
170
- yes: false,
171
- dir: undefined,
172
- };
173
- let seenCommand = false;
174
- let seenProjectName = false;
175
- for (let i = 0; i < args.length; i++) {
176
- const arg = args[i];
177
- switch (arg) {
178
- case 'lock':
179
- case 'create':
180
- case 'dev':
181
- case 'inspect':
182
- case 'insp':
183
- case 'debug':
184
- case 'dbg':
185
- result.command = arg;
186
- seenCommand = true;
187
- break;
188
- case '--check':
189
- result.check = true;
190
- break;
191
- case '-s':
192
- case '--server':
193
- result.server = args[++i];
194
- break;
195
- case '-n':
196
- case '--name':
197
- result.name = args[++i];
198
- break;
199
- case '--cwd':
200
- result.cwd = args[++i] ?? process.cwd();
201
- break;
202
- case '-h':
203
- case '--help':
204
- result.help = true;
205
- break;
206
- case '--transport':
207
- result.transport = args[++i];
208
- break;
209
- case '--vector':
210
- result.vector = args[++i];
211
- break;
212
- case '--testing':
213
- result.testing = true;
214
- break;
215
- case '--no-testing':
216
- result.testing = false;
217
- break;
218
- case '-d':
219
- case '--dir':
220
- result.dir = args[++i];
221
- break;
222
- case '-y':
223
- case '--yes':
224
- result.yes = true;
225
- break;
226
- default:
227
- if (!seenCommand) {
228
- result.command = arg;
229
- seenCommand = true;
230
- }
231
- else if (result.command === 'create' && !seenProjectName && !arg.startsWith('-')) {
232
- result.projectName = arg;
233
- seenProjectName = true;
234
- }
235
- break;
236
- }
237
- }
238
- return result;
239
- }
240
- /**
241
- * Attempt to load and resolve a tool registry from a server entrypoint.
242
- *
243
- * Supports common export patterns:
244
- * - `export const registry = new ToolRegistry()`
245
- * - `export default { registry }`
246
- * - `export const fusion = initFusion()`
247
- *
248
- * @internal
249
- */
250
- /** @internal exported for testing */
251
- export async function resolveRegistry(serverPath) {
252
- const absolutePath = resolve(serverPath);
253
- const fileUrl = pathToFileURL(absolutePath).href;
254
- // Register tsx loader so dynamic import() can handle .ts files
255
- // and resolve .js extension imports to .ts (ESM convention).
256
- // Uses tsx/esm/api which is compatible with Node 22+ (--import style).
257
- // Resolve tsx from the USER's project (not from the CLI's dist location)
258
- // via createRequire anchored to the server file's directory.
259
- if (absolutePath.endsWith('.ts')) {
260
- try {
261
- const { createRequire } = await import('node:module');
262
- const userRequire = createRequire(absolutePath);
263
- const tsxApiPath = userRequire.resolve('tsx/esm/api');
264
- const { register } = await import(pathToFileURL(tsxApiPath).href);
265
- register();
266
- }
267
- catch {
268
- // tsx not available — fall through, import() will fail with
269
- // a clear "Cannot find module" error if .ts resolution is needed
270
- }
271
- }
272
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
273
- const mod = await import(fileUrl);
274
- /** Extract prompt registry from a module-like object */
275
- function extractPrompts(obj) {
276
- // Look for promptRegistry, prompts, or promptsRegistry
277
- for (const key of ['promptRegistry', 'prompts', 'promptsRegistry']) {
278
- const candidate = obj[key];
279
- if (candidate && typeof candidate === 'object' && candidate !== null) {
280
- return candidate;
281
- }
282
- }
283
- return undefined;
284
- }
285
- // Strategy 1: Named `registry` export (ToolRegistry pattern)
286
- if (mod.registry && typeof mod.registry.getBuilders === 'function') {
287
- const pr = extractPrompts(mod);
288
- return {
289
- registry: mod.registry,
290
- name: mod.serverName ?? 'mcp-fusion-server',
291
- ...(pr ? { promptRegistry: pr } : {}),
292
- };
293
- }
294
- // Strategy 2: Named `fusion` export (initFusion pattern)
295
- if (mod.fusion && mod.fusion.registry && typeof mod.fusion.registry.getBuilders === 'function') {
296
- const pr = extractPrompts(mod.fusion);
297
- return {
298
- registry: mod.fusion.registry,
299
- name: mod.fusion.name ?? 'mcp-fusion-server',
300
- ...(pr ? { promptRegistry: pr } : {}),
301
- };
302
- }
303
- // Strategy 3: Default export with registry
304
- if (mod.default) {
305
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
306
- const def = mod.default;
307
- if (def.registry && typeof def.registry.getBuilders === 'function') {
308
- const pr = extractPrompts(def);
309
- return {
310
- registry: def.registry,
311
- name: def.serverName ?? 'mcp-fusion-server',
312
- ...(pr ? { promptRegistry: pr } : {}),
313
- };
314
- }
315
- if (typeof def.getBuilders === 'function') {
316
- return { registry: def, name: 'mcp-fusion-server' };
317
- }
318
- }
319
- throw new Error(`Could not resolve a ToolRegistry from "${serverPath}".\n` +
320
- `Expected one of:\n` +
321
- ` export const registry = new ToolRegistry() // named 'registry' with getBuilders()\n` +
322
- ` export const fusion = initFusion() // named 'fusion' with .registry\n` +
323
- ` export default { registry } // default export with .registry`);
324
- }
325
- // ============================================================================
326
- // Commands
327
- // ============================================================================
328
- /** @internal exported for testing */
329
- export async function commandLock(args, reporter) {
330
- const progress = new ProgressTracker(reporter);
331
- if (!args.server) {
332
- const detected = inferServerEntry(args.cwd);
333
- if (!detected) {
334
- console.error('Error: Could not auto-detect server entrypoint.\n');
335
- console.error('Usage: fusion lock --server ./src/server.ts');
336
- process.exit(1);
337
- }
338
- args.server = detected;
339
- }
340
- const mode = args.check ? 'Verifying' : 'Generating';
341
- process.stderr.write(`\n fusion lock — ${mode} ${LOCKFILE_NAME}\n\n`);
342
- // Step 1: Resolve registry
343
- progress.start('resolve', 'Resolving server entrypoint');
344
- const { registry, name, promptRegistry } = await resolveRegistry(args.server);
345
- const serverName = args.name ?? name;
346
- progress.done('resolve', 'Resolving server entrypoint', serverName);
347
- // Step 2: Compile tool contracts
348
- progress.start('compile', 'Compiling tool contracts');
349
- const builders = [...registry.getBuilders()];
350
- const contracts = await compileContracts(builders);
351
- const toolCount = Object.keys(contracts).length;
352
- progress.done('compile', 'Compiling tool contracts', `${toolCount} tool${toolCount !== 1 ? 's' : ''}`);
353
- // Step 3: Discover prompts
354
- progress.start('prompts', 'Discovering prompts');
355
- const promptBuilders = [];
356
- if (promptRegistry && typeof promptRegistry.getBuilders === 'function') {
357
- promptBuilders.push(...promptRegistry.getBuilders());
358
- }
359
- const options = promptBuilders.length > 0 ? { prompts: promptBuilders } : undefined;
360
- const promptCount = promptBuilders.length;
361
- progress.done('prompts', 'Discovering prompts', `${promptCount} prompt${promptCount !== 1 ? 's' : ''}`);
362
- if (args.check) {
363
- // ── Check Mode ──
364
- progress.start('read', 'Reading existing lockfile');
365
- const existing = await readLockfile(args.cwd);
366
- if (!existing) {
367
- progress.fail('read', 'Reading existing lockfile', 'not found');
368
- console.error(`\n✗ No ${LOCKFILE_NAME} found. Run \`fusion lock\` to generate.`);
369
- process.exit(1);
370
- }
371
- progress.done('read', 'Reading existing lockfile');
372
- progress.start('verify', 'Verifying integrity');
373
- const result = await checkLockfile(existing, contracts, options);
374
- if (result.ok) {
375
- progress.done('verify', 'Verifying integrity', 'up to date');
376
- console.log(`\n✓ ${LOCKFILE_NAME} is up to date.`);
377
- process.exit(0);
378
- }
379
- else {
380
- progress.fail('verify', 'Verifying integrity', 'stale');
381
- console.error(`\n✗ ${result.message}`);
382
- if (result.added.length > 0)
383
- console.error(` + Tools added: ${result.added.join(', ')}`);
384
- if (result.removed.length > 0)
385
- console.error(` - Tools removed: ${result.removed.join(', ')}`);
386
- if (result.changed.length > 0)
387
- console.error(` ~ Tools changed: ${result.changed.join(', ')}`);
388
- if (result.addedPrompts.length > 0)
389
- console.error(` + Prompts added: ${result.addedPrompts.join(', ')}`);
390
- if (result.removedPrompts.length > 0)
391
- console.error(` - Prompts removed: ${result.removedPrompts.join(', ')}`);
392
- if (result.changedPrompts.length > 0)
393
- console.error(` ~ Prompts changed: ${result.changedPrompts.join(', ')}`);
394
- process.exit(1);
395
- }
396
- }
397
- else {
398
- // ── Generate Mode ──
399
- progress.start('generate', 'Computing behavioral digests');
400
- const lockfile = await generateLockfile(serverName, contracts, MCP_FUSION_VERSION, options);
401
- progress.done('generate', 'Computing behavioral digests');
402
- progress.start('write', `Writing ${LOCKFILE_NAME}`);
403
- await writeLockfile(lockfile, args.cwd);
404
- progress.done('write', `Writing ${LOCKFILE_NAME}`);
405
- const tc = Object.keys(lockfile.capabilities.tools).length;
406
- const pc = Object.keys(lockfile.capabilities.prompts ?? {}).length;
407
- const parts = [`${tc} tool${tc !== 1 ? 's' : ''}`];
408
- if (pc > 0)
409
- parts.push(`${pc} prompt${pc !== 1 ? 's' : ''}`);
410
- console.log(`\n✓ ${LOCKFILE_NAME} generated (${parts.join(', ')}).`);
411
- console.log(` Integrity: ${lockfile.integrityDigest}`);
412
- }
413
- }
414
- // ============================================================================
415
- // Dev Command — HMR Development Server
416
- // ============================================================================
417
- /** @internal exported for testing */
418
- export async function commandDev(args, reporter) {
419
- const progress = new ProgressTracker(reporter);
420
- if (!args.server) {
421
- const detected = inferServerEntry(args.cwd);
422
- if (!detected) {
423
- console.error('Error: Could not auto-detect server entrypoint.\n');
424
- console.error('Usage: fusion dev --server ./src/server.ts');
425
- process.exit(1);
426
- }
427
- args.server = detected;
428
- }
429
- // Narrowed: args.server is guaranteed to be a string from here
430
- const serverEntry = args.server;
431
- process.stderr.write(`\n ${ansi.bold('⚡ fusion dev')} ${ansi.dim('— HMR Development Server')}\n\n`);
432
- // Step 1: Resolve registry from server entrypoint
433
- progress.start('resolve', 'Resolving server entrypoint');
434
- const { registry, name } = await resolveRegistry(serverEntry);
435
- progress.done('resolve', 'Resolving server entrypoint', name);
436
- // Step 2: Determine watch directory
437
- const watchDir = args.dir ?? inferWatchDir(serverEntry);
438
- progress.start('watch', `Watching ${watchDir}`);
439
- progress.done('watch', `Watching ${watchDir}`);
440
- // Step 3: Create and start dev server
441
- const devServer = createDevServer({
442
- dir: watchDir,
443
- setup: async (reg) => {
444
- // Clear existing registrations if supported
445
- if ('clear' in reg && typeof reg.clear === 'function') {
446
- reg.clear();
447
- }
448
- // Re-resolve the registry (re-imports with cache-busting)
449
- try {
450
- const resolved = await resolveRegistry(serverEntry);
451
- // Copy builders from re-resolved registry into the dev server's registry
452
- for (const builder of resolved.registry.getBuilders()) {
453
- reg.register(builder);
454
- }
455
- }
456
- catch (err) {
457
- const message = err instanceof Error ? err.message : String(err);
458
- throw new Error(`Failed to reload: ${message}`);
459
- }
460
- },
461
- });
462
- // Handle SIGINT for clean shutdown
463
- process.on('SIGINT', () => {
464
- process.stderr.write(`\n ${ansi.dim('Shutting down...')}\n\n`);
465
- devServer.stop();
466
- process.exit(0);
467
- });
468
- await devServer.start();
469
- }
470
- /**
471
- * Auto-detect the server entrypoint by probing common file paths.
472
- *
473
- * Checks in order: `src/server.ts`, `src/index.ts`, `server.ts`, `index.ts`,
474
- * and their `.js` counterparts.
475
- *
476
- * @param cwd - Current working directory
477
- * @returns Detected file path, or undefined if none found
478
- * @internal
479
- */
480
- function inferServerEntry(cwd) {
481
- const candidates = [
482
- 'src/server.ts', 'src/index.ts',
483
- 'src/server.js', 'src/index.js',
484
- 'server.ts', 'index.ts',
485
- 'server.js', 'index.js',
486
- ];
487
- for (const candidate of candidates) {
488
- const fullPath = resolve(cwd, candidate);
489
- if (existsSync(fullPath))
490
- return fullPath;
491
- }
492
- return undefined;
493
- }
494
- /**
495
- * Infer the watch directory from the server entrypoint path.
496
- *
497
- * Heuristic: if the server is in `src/server.ts`, watch `src/`.
498
- * Falls back to the directory containing the entrypoint.
499
- *
500
- * @internal
501
- */
502
- function inferWatchDir(serverPath) {
503
- const dir = resolve(serverPath, '..');
504
- const dirName = dir.split(/[\\/]/).pop() ?? '';
505
- // If the server is directly in `src/`, watch `src/`
506
- if (dirName === 'src')
507
- return dir;
508
- // If the server is deeper (e.g. `src/server/index.ts`), walk up to `src/`
509
- const parentDir = resolve(dir, '..');
510
- const parentName = parentDir.split(/[\\/]/).pop() ?? '';
511
- if (parentName === 'src')
512
- return parentDir;
513
- // Fallback: watch the directory containing the entrypoint
514
- return dir;
515
- }
516
- // ============================================================================
517
- // Create Command — Interactive Wizard + Fast-Path
518
- // ============================================================================
519
- const VALID_TRANSPORTS = ['stdio', 'sse'];
520
- const VALID_VECTORS = ['vanilla', 'prisma', 'n8n', 'openapi', 'oauth'];
521
- /**
522
- * Ask a question via readline with styled ANSI output.
523
- * @internal exported for testing
524
- */
525
- export function ask(rl, prompt, fallback) {
526
- return new Promise((resolve) => {
527
- rl.question(` ${ansi.cyan('◇')} ${prompt} ${ansi.dim(`(${fallback})`)} `, (answer) => {
528
- resolve(answer.trim() || fallback);
529
- });
530
- });
531
- }
532
- /**
533
- * Collect project config — either from flags or interactive prompts.
534
- * @internal exported for testing
535
- */
536
- export async function collectConfig(args) {
537
- // ── Fast-path: --yes skips all prompts ────────────────
538
- if (args.yes) {
539
- const name = args.projectName ?? 'my-mcp-server';
540
- if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(name) && !/^[a-z0-9]$/.test(name)) {
541
- process.stderr.write(` ${ansi.red('✗')} Invalid name: must start with a letter/number, end with a letter/number, and contain only lowercase letters, numbers, and hyphens.\n`);
542
- return null;
543
- }
544
- const transport = validateTransport(args.transport);
545
- const vector = validateVector(args.vector);
546
- return {
547
- name,
548
- transport,
549
- vector,
550
- testing: args.testing ?? true,
551
- };
552
- }
553
- // ── Interactive wizard ────────────────────────────────
554
- const rl = createInterface({ input: process.stdin, output: process.stdout });
555
- try {
556
- process.stderr.write(`\n ${ansi.bold('⚡ MCP Fusion')} ${ansi.dim('— Create a new MCP server')}\n\n`);
557
- const name = args.projectName ?? await ask(rl, 'Project name?', 'my-mcp-server');
558
- if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(name) && !/^[a-z0-9]$/.test(name)) {
559
- process.stderr.write(` ${ansi.red('✗')} Invalid name: must start with a letter/number, end with a letter/number, and contain only lowercase letters, numbers, and hyphens.\n`);
560
- return null;
561
- }
562
- const transportRaw = args.transport ?? await ask(rl, 'Transport? [stdio, sse]', 'stdio');
563
- const transport = validateTransport(transportRaw);
564
- const vectorRaw = args.vector ?? await ask(rl, 'Vector? [vanilla, prisma, n8n, openapi, oauth]', 'vanilla');
565
- const vector = validateVector(vectorRaw);
566
- const testingRaw = args.testing ?? (await ask(rl, 'Include testing?', 'yes')).toLowerCase();
567
- const testing = typeof testingRaw === 'boolean' ? testingRaw : testingRaw !== 'no';
568
- process.stderr.write('\n');
569
- return { name, transport, vector, testing };
570
- }
571
- finally {
572
- rl.close();
573
- }
574
- }
575
- /** @internal Validate and warn on invalid transport */
576
- function validateTransport(raw) {
577
- if (!raw)
578
- return 'stdio';
579
- if (VALID_TRANSPORTS.includes(raw))
580
- return raw;
581
- process.stderr.write(` ${ansi.red('⚠')} Unknown transport "${raw}" — using ${ansi.bold('stdio')}. Valid: ${VALID_TRANSPORTS.join(', ')}\n`);
582
- return 'stdio';
583
- }
584
- /** @internal Validate and warn on invalid vector */
585
- function validateVector(raw) {
586
- if (!raw)
587
- return 'vanilla';
588
- if (VALID_VECTORS.includes(raw))
589
- return raw;
590
- process.stderr.write(` ${ansi.red('⚠')} Unknown vector "${raw}" — using ${ansi.bold('vanilla')}. Valid: ${VALID_VECTORS.join(', ')}\n`);
591
- return 'vanilla';
592
- }
593
- /** @internal exported for testing */
594
- export async function commandCreate(args, reporter) {
595
- const progress = new ProgressTracker(reporter);
596
- // ── Collect config ───────────────────────────────────
597
- const config = await collectConfig(args);
598
- if (!config) {
599
- process.exit(1);
600
- }
601
- const targetDir = resolve(args.cwd, config.name);
602
- // ── Guard: directory exists ──────────────────────────
603
- if (existsSync(targetDir)) {
604
- process.stderr.write(` ${ansi.red('✗')} Directory "${config.name}" already exists.\n`);
605
- process.exit(1);
606
- }
607
- // ── Scaffold ─────────────────────────────────────────
608
- progress.start('scaffold', 'Scaffolding project');
609
- const files = scaffold(targetDir, config);
610
- progress.done('scaffold', 'Scaffolding project', `${files.length} files`);
611
- // ── Install dependencies ─────────────────────────────
612
- progress.start('install', 'Installing dependencies');
613
- try {
614
- execSync('npm install', {
615
- cwd: targetDir,
616
- stdio: 'ignore',
617
- timeout: 120_000,
618
- });
619
- progress.done('install', 'Installing dependencies');
620
- }
621
- catch {
622
- progress.fail('install', 'Installing dependencies', 'run npm install manually');
623
- }
624
- // ── Done ─────────────────────────────────────────────
625
- const steps = [`cd ${config.name}`];
626
- if (config.transport === 'sse') {
627
- steps.push('fusion dev', '# then connect Cursor or Claude to http://localhost:3001/sse');
628
- }
629
- else {
630
- steps.push('fusion dev');
631
- }
632
- if (config.testing)
633
- steps.push('npm test');
634
- process.stderr.write(`\n ${ansi.green('✓')} ${ansi.bold(config.name)} is ready!\n\n`);
635
- process.stderr.write(` ${ansi.dim('Next steps:')}\n`);
636
- for (const step of steps) {
637
- process.stderr.write(` ${ansi.cyan('$')} ${step}\n`);
638
- }
639
- process.stderr.write(`\n ${ansi.dim('Cursor:')} .cursor/mcp.json is pre-configured — open in Cursor and go.\n`);
640
- process.stderr.write(` ${ansi.dim('Docs:')} ${ansi.cyan('https://mcp-fusion.vinkius.com/')}\n\n`);
641
- }
642
- // ============================================================================
643
- // Entry Point
644
- // ============================================================================
10
+ import { parseArgs } from './args.js';
11
+ import { HELP } from './constants.js';
12
+ import { commandLock } from './commands/lock.js';
13
+ import { commandDev } from './commands/dev.js';
14
+ import { commandCreate } from './commands/create.js';
15
+ import { commandRemote } from './commands/remote.js';
16
+ import { commandDeploy } from './commands/deploy.js';
17
+ // ─── Re-exports (backward compat — tests import from fusion.js) ──
18
+ export { parseArgs } from './args.js';
19
+ export { MCP_FUSION_VERSION, HELP, ansi } from './constants.js';
20
+ export { ProgressTracker, createDefaultReporter } from './progress.js';
21
+ export { resolveRegistry } from './registry.js';
22
+ export { collectConfig } from './commands/create.js';
23
+ export { commandLock, commandDev, commandCreate };
24
+ export { ask } from './utils.js';
25
+ // ─── Main ────────────────────────────────────────────────────────
645
26
  async function main() {
646
27
  const args = parseArgs(process.argv);
647
28
  if (args.help || !args.command) {
@@ -657,16 +38,20 @@ async function main() {
657
38
  break;
658
39
  case 'lock':
659
40
  await commandLock(args);
660
- // Force exit: imported server modules may keep the event loop
661
- // alive (e.g. transport listeners, telemetry bus, IPC sockets).
662
41
  process.exit(0);
663
- break; // unreachable, but keeps lint happy
42
+ break;
43
+ case 'deploy':
44
+ await commandDeploy(args);
45
+ process.exit(0);
46
+ break;
47
+ case 'remote':
48
+ await commandRemote(args);
49
+ break;
664
50
  case 'inspect':
665
51
  case 'insp':
666
52
  case 'debug':
667
53
  case 'dbg': {
668
- // Inspector subcommand: forward remaining args to inspector package
669
- const inspectArgv = process.argv.slice(3); // strip 'node fusion inspect'
54
+ const inspectArgv = process.argv.slice(3);
670
55
  try {
671
56
  const { runInspector } = await import('@vinkius-core/mcp-fusion-inspector');
672
57
  await runInspector(inspectArgv);