@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.
- package/dist/cli/args.d.ts +26 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +100 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/commands/create.d.ts +11 -0
- package/dist/cli/commands/create.d.ts.map +1 -0
- package/dist/cli/commands/create.js +121 -0
- package/dist/cli/commands/create.js.map +1 -0
- package/dist/cli/commands/deploy.d.ts +3 -0
- package/dist/cli/commands/deploy.d.ts.map +1 -0
- package/dist/cli/commands/deploy.js +234 -0
- package/dist/cli/commands/deploy.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +9 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/dev.js +54 -0
- package/dist/cli/commands/dev.js.map +1 -0
- package/dist/cli/commands/lock.d.ts +5 -0
- package/dist/cli/commands/lock.d.ts.map +1 -0
- package/dist/cli/commands/lock.js +94 -0
- package/dist/cli/commands/lock.js.map +1 -0
- package/dist/cli/commands/remote.d.ts +3 -0
- package/dist/cli/commands/remote.d.ts.map +1 -0
- package/dist/cli/commands/remote.js +37 -0
- package/dist/cli/commands/remote.js.map +1 -0
- package/dist/cli/constants.d.ts +19 -0
- package/dist/cli/constants.d.ts.map +1 -0
- package/dist/cli/constants.js +86 -0
- package/dist/cli/constants.js.map +1 -0
- package/dist/cli/fusion.d.ts +13 -131
- package/dist/cli/fusion.d.ts.map +1 -1
- package/dist/cli/fusion.js +27 -642
- package/dist/cli/fusion.js.map +1 -1
- package/dist/cli/progress.d.ts +34 -0
- package/dist/cli/progress.d.ts.map +1 -0
- package/dist/cli/progress.js +102 -0
- package/dist/cli/progress.js.map +1 -0
- package/dist/cli/rc.d.ts +11 -0
- package/dist/cli/rc.d.ts.map +1 -0
- package/dist/cli/rc.js +66 -0
- package/dist/cli/rc.js.map +1 -0
- package/dist/cli/registry.d.ts +25 -0
- package/dist/cli/registry.d.ts.map +1 -0
- package/dist/cli/registry.js +86 -0
- package/dist/cli/registry.js.map +1 -0
- package/dist/cli/types.d.ts +7 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/utils.d.ts +26 -0
- package/dist/cli/utils.d.ts.map +1 -0
- package/dist/cli/utils.js +63 -0
- package/dist/cli/utils.js.map +1 -0
- package/dist/core/createGroup.d.ts.map +1 -1
- package/dist/core/createGroup.js +17 -5
- package/dist/core/createGroup.js.map +1 -1
- package/dist/edge-stub.d.ts +73 -0
- package/dist/edge-stub.d.ts.map +1 -0
- package/dist/edge-stub.js +81 -0
- package/dist/edge-stub.js.map +1 -0
- package/dist/fsm/StateMachineGate.d.ts +20 -0
- package/dist/fsm/StateMachineGate.d.ts.map +1 -1
- package/dist/fsm/StateMachineGate.js +45 -1
- package/dist/fsm/StateMachineGate.js.map +1 -1
- package/dist/server/DevServer.d.ts.map +1 -1
- package/dist/server/DevServer.js +8 -2
- package/dist/server/DevServer.js.map +1 -1
- package/dist/server/ServerAttachment.d.ts.map +1 -1
- package/dist/server/ServerAttachment.js +27 -17
- package/dist/server/ServerAttachment.js.map +1 -1
- package/dist/server/startServer.d.ts +3 -0
- package/dist/server/startServer.d.ts.map +1 -1
- package/dist/server/startServer.js +49 -5
- package/dist/server/startServer.js.map +1 -1
- package/package.json +6 -2
package/dist/cli/fusion.js
CHANGED
|
@@ -2,646 +2,27 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* MCP Fusion CLI — `fusion`
|
|
4
4
|
*
|
|
5
|
-
*
|
|
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 {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
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;
|
|
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
|
-
|
|
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);
|