mage-remote-run 0.13.0 → 0.15.0

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.
@@ -11,24 +11,24 @@ const pkg = require('../package.json');
11
11
  const program = new Command();
12
12
 
13
13
  program
14
- .name('mage-remote-run')
15
- .description('The remote swiss army knife for Magento Open Source, Mage-OS, Adobe Commerce')
16
- .version(pkg.version)
17
- .configureHelp({
18
- visibleCommands: (cmd) => {
19
- const commands = cmd.commands.filter(c => !c._hidden);
20
- return commands.sort((a, b) => {
21
- if (a.name() === 'connection') return -1;
22
- if (b.name() === 'connection') return 1;
23
- return a.name().localeCompare(b.name());
24
- });
25
- },
26
- subcommandTerm: (cmd) => chalk.cyan(cmd.name()),
27
- subcommandDescription: (cmd) => chalk.gray(cmd.description()),
28
- optionTerm: (option) => chalk.yellow(option.flags),
29
- optionDescription: (option) => chalk.gray(option.description)
30
- })
31
- .addHelpText('before', chalk.hex('#FFA500')(`
14
+ .name('mage-remote-run')
15
+ .description('The remote swiss army knife for Magento Open Source, Mage-OS, Adobe Commerce')
16
+ .version(pkg.version)
17
+ .configureHelp({
18
+ visibleCommands: (cmd) => {
19
+ const commands = cmd.commands.filter(c => !c._hidden);
20
+ return commands.sort((a, b) => {
21
+ if (a.name() === 'connection') return -1;
22
+ if (b.name() === 'connection') return 1;
23
+ return a.name().localeCompare(b.name());
24
+ });
25
+ },
26
+ subcommandTerm: (cmd) => chalk.cyan(cmd.name()),
27
+ subcommandDescription: (cmd) => chalk.gray(cmd.description()),
28
+ optionTerm: (option) => chalk.yellow(option.flags),
29
+ optionDescription: (option) => chalk.gray(option.description)
30
+ })
31
+ .addHelpText('before', chalk.hex('#FFA500')(`
32
32
  _ __ ___ __ _ __ _ ___ _ __ ___ _ __ ___ ___ | |_ ___ _ __ _ _ _ __
33
33
  | '_ \` _ \\ / _\` |/ _\` |/ _ \\____| '__/ _ \\ '_ \` _ \\ / _ \\| __/ _ \\____| '__| | | | '_ \\
34
34
  | | | | | | (_| | (_| | __/____| | | __/ | | | | | (_) | || __/____| | | |_| | | | |
@@ -38,27 +38,49 @@ program
38
38
 
39
39
 
40
40
 
41
- import { registerCommands } from '../lib/command-registry.js';
41
+ import {
42
+ registerConnectionCommands,
43
+ registerCoreCommands,
44
+ registerCloudCommands
45
+ } from '../lib/command-registry.js';
42
46
  import { getActiveProfile } from '../lib/config.js';
47
+ import { startMcpServer } from '../lib/mcp.js';
48
+
49
+ registerConnectionCommands(program);
50
+
51
+ program.command('mcp')
52
+ .description('Run as MCP server')
53
+ .option('--transport <type>', 'Transport type (stdio, http)', 'stdio')
54
+ .option('--host <host>', 'HTTP Host', '127.0.0.1')
55
+ .option('--port <port>', 'HTTP Port', '18098')
56
+ .action(async (options) => {
57
+ await startMcpServer(options);
58
+ });
43
59
 
44
60
  const profile = await getActiveProfile();
45
61
 
46
- registerCommands(program, profile);
62
+ if (profile) {
63
+ registerCoreCommands(program);
64
+
65
+ if (profile.type === 'ac-cloud-paas' || profile.type === 'ac-saas') {
66
+ registerCloudCommands(program);
67
+ }
68
+ }
47
69
 
48
70
  program.hook('preAction', async (thisCommand, actionCommand) => {
49
- // Check if we have an active profile and if format is not json/xml
50
- // Note: 'options' are available on the command that has them defined.
51
- // actionCommand is the command actually being executed.
52
- if (profile) {
53
- const config = await loadConfig();
54
- if (config.showActiveConnectionHeader !== false) {
55
- const opts = actionCommand.opts();
56
- if (opts.format !== 'json' && opts.format !== 'xml') {
57
- console.log(chalk.cyan(`Active Connection: ${chalk.bold(profile.name)} (${profile.type})`));
58
- console.log(chalk.gray('━'.repeat(60)) + '\n');
59
- }
60
- }
71
+ // Check if we have an active profile and if format is not json/xml
72
+ // Note: 'options' are available on the command that has them defined.
73
+ // actionCommand is the command actually being executed.
74
+ if (profile) {
75
+ const config = await loadConfig();
76
+ if (config.showActiveConnectionHeader !== false) {
77
+ const opts = actionCommand.opts();
78
+ if (opts.format !== 'json' && opts.format !== 'xml') {
79
+ console.log(chalk.cyan(`Active Connection: ${chalk.bold(profile.name)} (${profile.type})`));
80
+ console.log(chalk.gray('━'.repeat(60)) + '\n');
81
+ }
61
82
  }
83
+ }
62
84
  });
63
85
 
64
86
  import { expandCommandAbbreviations } from '../lib/command-helper.js';
@@ -72,28 +94,28 @@ const config = await loadConfig();
72
94
  const hasProfiles = Object.keys(config.profiles || {}).length > 0;
73
95
 
74
96
  if (!hasProfiles && args.length === 0) {
75
- console.log(chalk.bold.blue('Welcome to mage-remote-run! 🚀'));
76
- console.log(chalk.gray('The remote swiss army knife for Magento Open Source, Mage-OS, Adobe Commerce'));
77
- console.log(chalk.gray('It looks like you haven\'t configured any connections yet.'));
78
- console.log(chalk.gray('Let\'s set up your first connection now.\n'));
79
-
80
- // Trigger the interactive add command directly
81
- // We can simulate running the 'connection add' command
82
- // But since we are at top level, we might need to invoke it manually or parse specific args.
83
- // Easiest is to manually invoke program.parse with ['node', 'script', 'connection', 'add']
84
- // BUT program.parse executes asynchronously usually? commander is synchronous by default but actions are async.
85
- // Let's modify process.argv before parsing.
86
- args = ['connection', 'add'];
97
+ console.log(chalk.bold.blue('Welcome to mage-remote-run! 🚀'));
98
+ console.log(chalk.gray('The remote swiss army knife for Magento Open Source, Mage-OS, Adobe Commerce'));
99
+ console.log(chalk.gray('It looks like you haven\'t configured any connections yet.'));
100
+ console.log(chalk.gray('Let\'s set up your first connection now.\n'));
101
+
102
+ // Trigger the interactive add command directly
103
+ // We can simulate running the 'connection add' command
104
+ // But since we are at top level, we might need to invoke it manually or parse specific args.
105
+ // Easiest is to manually invoke program.parse with ['node', 'script', 'connection', 'add']
106
+ // BUT program.parse executes asynchronously usually? commander is synchronous by default but actions are async.
107
+ // Let's modify process.argv before parsing.
108
+ args = ['connection', 'add'];
87
109
  }
88
110
 
89
111
  try {
90
- args = expandCommandAbbreviations(program, args);
112
+ args = expandCommandAbbreviations(program, args);
91
113
  } catch (e) {
92
- if (e.isAmbiguous) {
93
- console.error(e.message);
94
- process.exit(1);
95
- }
96
- throw e;
114
+ if (e.isAmbiguous) {
115
+ console.error(e.message);
116
+ process.exit(1);
117
+ }
118
+ throw e;
97
119
  }
98
120
  process.argv = [...process.argv.slice(0, 2), ...args];
99
121
 
@@ -12,24 +12,57 @@ import { registerAdobeIoEventsCommands } from './commands/adobe-io-events.js';
12
12
  import { registerWebhooksCommands } from './commands/webhooks.js';
13
13
  import { registerConsoleCommand } from './commands/console.js';
14
14
 
15
- export function registerCommands(program, profile) {
16
- registerConnectionCommands(program);
17
- registerConsoleCommand(program);
18
-
19
- if (profile) {
20
- registerWebsitesCommands(program);
21
- registerStoresCommands(program);
22
- registerCustomersCommands(program);
23
- registerOrdersCommands(program);
24
- registerEavCommands(program);
25
- registerProductsCommands(program);
26
- registerTaxCommands(program);
27
- registerInventoryCommands(program);
28
-
29
- if (profile.type === 'ac-cloud-paas' || profile.type === 'ac-saas') {
30
- registerAdobeIoEventsCommands(program);
31
- registerCompanyCommands(program);
32
- registerWebhooksCommands(program);
33
- }
34
- }
15
+ export { registerConnectionCommands, registerConsoleCommand };
16
+
17
+ export function registerCoreCommands(program) {
18
+ registerWebsitesCommands(program);
19
+ registerStoresCommands(program);
20
+ registerCustomersCommands(program);
21
+ registerOrdersCommands(program);
22
+ registerEavCommands(program);
23
+ registerProductsCommands(program);
24
+ registerTaxCommands(program);
25
+ registerInventoryCommands(program);
26
+ registerConsoleCommand(program);
27
+ }
28
+
29
+ export function registerCloudCommands(program) {
30
+ registerAdobeIoEventsCommands(program);
31
+ registerCompanyCommands(program);
32
+ registerWebhooksCommands(program);
35
33
  }
34
+
35
+ export function registerAllCommands(program) {
36
+ registerConnectionCommands(program);
37
+ registerCoreCommands(program);
38
+ registerCloudCommands(program);
39
+ }
40
+
41
+ // Alias for backwards compatibility with console.js (and potential other usages)
42
+ export const registerCommands = (program, profile) => {
43
+ // Note: console.js passes profile, but registerAllCommands doesn't strictly use it
44
+ // (it registers everything, profile checks happen inside or via selective registration).
45
+ // The original behavior in console.js:
46
+ // const profile = await getActiveProfile();
47
+ // registerCommands(localProgram, profile);
48
+
49
+ // In bin/mage-remote-run.js old logic:
50
+ // registerConnectionCommands(program);
51
+ // if (profile) { registerWebsitesCommands... }
52
+
53
+ // We should replicate that 'selective' registration if we want to match exact behavior?
54
+ // BUT console.js wants to register ALL available commands for the profile.
55
+
56
+ registerConnectionCommands(program);
57
+
58
+ // Simple logic: If profile exists, register all.
59
+ // console.js usage implies profile is present if it's running (or it tries to get it).
60
+
61
+ if (profile) {
62
+ registerCoreCommands(program);
63
+ // Cloud check
64
+ if (profile.type === 'ac-cloud-paas' || profile.type === 'ac-saas') {
65
+ registerCloudCommands(program);
66
+ }
67
+ }
68
+ };
@@ -349,6 +349,10 @@ Examples:
349
349
  default: config.activeProfile
350
350
  });
351
351
 
352
+ if (process.env.DEBUG) {
353
+ console.log(chalk.gray(`DEBUG: Selected profile: ${selected}`));
354
+ }
355
+
352
356
  config.activeProfile = selected;
353
357
  await saveConfig(config);
354
358
  console.log(chalk.green(`Active profile set to "${selected}".`));
@@ -393,6 +397,8 @@ Examples:
393
397
  config.showActiveConnectionHeader = state === 'on';
394
398
  await saveConfig(config);
395
399
  console.log(chalk.green(`Active connection header is now ${state === 'on' ? 'enabled' : 'disabled'}.`));
396
- } catch (e) { handleError(e); }
400
+ } catch (e) {
401
+ handleError(e);
402
+ }
397
403
  });
398
404
  }
@@ -25,48 +25,55 @@ export function registerConsoleCommand(program) {
25
25
  console.log(chalk.gray('Type "list" to see available commands.'));
26
26
  console.log(chalk.gray('Type .exit to quit.\n'));
27
27
 
28
- // Create a fresh program instance for the REPL
29
- const localProgram = new Command();
30
-
31
- // Configure custom output for REPL to avoid duplicate error printing when we catch it
32
- localProgram.configureOutput({
33
- writeOut: (str) => process.stdout.write(str),
34
- writeErr: (str) => process.stderr.write(str),
35
- });
36
-
37
- // Apply exitOverride recursively to ensure all subcommands (and sub-subcommands)
38
- // handle help/errors without exiting the process.
39
- const applyExitOverride = (cmd) => {
40
- cmd.exitOverride((err) => {
41
- throw err;
28
+ // State for the REPL
29
+ let localProgram;
30
+ let currentProfile;
31
+
32
+ // Function to load/reload commands based on current profile
33
+ const loadLocalCommands = async () => {
34
+ // Create a fresh program instance for the REPL
35
+ localProgram = new Command();
36
+
37
+ // Configure custom output for REPL to avoid duplicate error printing
38
+ localProgram.configureOutput({
39
+ writeOut: (str) => process.stdout.write(str),
40
+ writeErr: (str) => process.stderr.write(str),
42
41
  });
43
- // Check for subcommands
44
- if (cmd.commands) {
45
- cmd.commands.forEach(applyExitOverride);
46
- }
47
- };
48
42
 
49
- // Register all commands on the local instance
50
- const profile = await getActiveProfile();
51
- registerCommands(localProgram, profile);
43
+ // Apply exitOverride recursively
44
+ const applyExitOverride = (cmd) => {
45
+ cmd.exitOverride((err) => {
46
+ throw err;
47
+ });
48
+ if (cmd.commands) {
49
+ cmd.commands.forEach(applyExitOverride);
50
+ }
51
+ };
52
52
 
53
- // Apply overrides AFTER registration to catch all commands
54
- applyExitOverride(localProgram);
53
+ // Get current profile and register commands
54
+ const profile = await getActiveProfile();
55
+ registerCommands(localProgram, profile);
56
+ applyExitOverride(localProgram);
55
57
 
56
- const r = repl.start({
57
- prompt: chalk.green('mage> '),
58
- // We do NOT provide 'eval' here so we get the default one which supports top-level await
59
- });
58
+ // Update current profile state (store simple unique string for comparison)
59
+ currentProfile = profile ? `${profile.name}:${profile.type}` : 'null';
60
+
61
+ return { localProgram, profile };
62
+ };
60
63
 
61
- const defaultEval = r.eval;
64
+ // Initial load
65
+ await loadLocalCommands();
62
66
 
63
- // Capture default completer
64
- const defaultCompleter = r.completer;
67
+ // Use 'stream' module to create dummy REPL for capturing defaults
68
+ const { PassThrough } = await import('stream');
69
+ const dummy = repl.start({ input: new PassThrough(), output: new PassThrough(), terminal: false });
70
+ const defaultCompleter = dummy.completer;
71
+ const defaultEval = dummy.eval;
72
+ dummy.close();
65
73
 
66
- // Custom completer
74
+ // Custom completer definition
67
75
  const myCompleter = (line, callback) => {
68
76
  const parts = line.split(/\s+/);
69
- // Remove empty start if line starts with space (not typical for completion line but safe)
70
77
  if (parts[0] === '') parts.shift();
71
78
 
72
79
  let current = '';
@@ -80,19 +87,16 @@ export function registerConsoleCommand(program) {
80
87
  contextParts = parts;
81
88
  }
82
89
 
83
- // Helper to get candidates from a command object
84
90
  const getCandidates = (cmdObj) => {
85
91
  return cmdObj.commands.map(c => c.name());
86
92
  };
87
93
 
88
94
  let hits = [];
89
95
 
90
- // Top-level completion (start of line)
91
96
  if (contextParts.length === 0) {
92
97
  const candidates = getCandidates(localProgram);
93
98
  hits = candidates.filter(c => c.startsWith(current));
94
99
  } else {
95
- // Subcommand completion
96
100
  let cmd = localProgram;
97
101
  let validContext = true;
98
102
 
@@ -112,20 +116,18 @@ export function registerConsoleCommand(program) {
112
116
  }
113
117
  }
114
118
 
115
- // If we have distinct command hits, return them
116
119
  if (hits.length > 0) {
117
120
  return callback(null, [hits, current]);
118
121
  }
119
122
 
120
- // Fallback to default JS completion
121
123
  return defaultCompleter(line, callback);
122
124
  };
123
125
 
124
- // Override completer
125
- r.completer = myCompleter;
126
+ // Custom evaluator definition
127
+ const myEval = async function (cmd, context, filename, callback) {
128
+ // 'this' is the REPLServer instance
129
+ const rInstance = this;
126
130
 
127
- // Custom evaluator to support both CLI commands and JS
128
- const myEval = async (cmd, context, filename, callback) => {
129
131
  cmd = cmd.trim();
130
132
 
131
133
  if (!cmd) {
@@ -135,7 +137,6 @@ export function registerConsoleCommand(program) {
135
137
 
136
138
  if (cmd === 'list') {
137
139
  console.log(chalk.bold('\nAvailable Commands:'));
138
- // Use localProgram commands
139
140
  localProgram.commands.filter(c => !c._hidden).sort((a, b) => a.name().localeCompare(b.name())).forEach(c => {
140
141
  console.log(` ${chalk.cyan(c.name().padEnd(25))} ${c.description()}`);
141
142
  });
@@ -151,8 +152,6 @@ export function registerConsoleCommand(program) {
151
152
  }
152
153
 
153
154
  try {
154
- // Parse arguments nicely
155
- // This regex handles quoted arguments
156
155
  const args = (cmd.match(/[^\s"']+|"([^"]*)"|'([^']*)'/g) || []).map(arg => {
157
156
  if (arg.startsWith('"') && arg.endsWith('"')) return arg.slice(1, -1);
158
157
  if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
@@ -171,32 +170,10 @@ export function registerConsoleCommand(program) {
171
170
  }
172
171
  } catch (e) {
173
172
  if (e.isAmbiguous) {
174
- // If ambiguous, print error and return (don't execute JS, as user likelihood meant a command)
175
- // But wait, what if "c" is a var?
176
- // If I type "c", it is ambiguous.
177
- // If I fallback to JS, "c" might work.
178
- // If I print error, "c" access is blocked.
179
-
180
- // Compromise: If it LOOKS like a command usage (e.g. multiple args), treat as command error?
181
- // Or just always fallback to JS if ambiguous?
182
- // If fallback to JS, user sees "c is not defined".
183
- // User asks: "Why didn't it run command?". Answer: "Ambiguous".
184
- // Explicit ambiguity error is arguably better than generic ReferenceError.
185
-
186
- // However, "c = 1" or "const c = 1"
187
- // "const" -> ambiguous? "connection", "console"... no "const" is not in list of commands.
188
- // "const" might NOT match any command prefix if lucky.
189
- // But if it does...
190
-
191
- // Let's stick to safe approach: fallback to JS if ambiguous, unless we are SURE it's not JS?
192
- // Or: Just print the ambiguous error if the first arg matched *something*.
193
-
194
- // Let's try to assume command first. If ambiguous, show error.
195
173
  console.error(chalk.red(e.message));
196
174
  callback(null);
197
175
  return;
198
176
  }
199
- // Other errors?
200
177
  throw e;
201
178
  }
202
179
 
@@ -205,32 +182,84 @@ export function registerConsoleCommand(program) {
205
182
 
206
183
  if (knownCommands.includes(firstWord)) {
207
184
  // Valid command found
185
+ let keypressListeners = [];
208
186
  try {
209
- r.pause();
210
- await localProgram.parseAsync(['node', 'mage-remote-run', ...expandedArgs]);
187
+ rInstance.pause();
188
+
189
+ // Capture and remove keypress listeners to prevent REPL from intercepting input
190
+ // intended for interactive commands (like inquirer)
191
+ keypressListeners = process.stdin.listeners('keypress');
192
+ process.stdin.removeAllListeners('keypress');
211
193
 
212
- // Restore REPL state
213
194
  if (process.stdin.isTTY) {
214
- process.stdin.setRawMode(true);
195
+ process.stdin.setRawMode(false);
215
196
  }
216
- process.stdin.resume();
217
- r.resume();
218
- r.displayPrompt();
197
+
198
+ await localProgram.parseAsync(['node', 'mage-remote-run', ...expandedArgs]);
199
+
200
+ // Check for profile change after command execution
201
+ const newProfileObj = await getActiveProfile();
202
+ const newProfileKey = newProfileObj ? `${newProfileObj.name}:${newProfileObj.type}` : 'null';
203
+
204
+ if (process.env.DEBUG) {
205
+ console.log(chalk.gray(`DEBUG: Check Profile Switch. Current: ${currentProfile}, New: ${newProfileKey}`));
206
+ }
207
+
208
+ if (newProfileKey !== currentProfile) {
209
+ await loadLocalCommands();
210
+ // Update context variables
211
+ rInstance.context.config = await loadConfig();
212
+
213
+ if (process.env.DEBUG) {
214
+ console.log(chalk.green(`\nConnection switched to ${newProfileObj ? newProfileObj.name : 'none'}. Commands reloaded.`));
215
+ }
216
+ }
217
+
218
+ // Restore REPL state
219
+ setTimeout(() => {
220
+ // Restore keypress listeners
221
+ keypressListeners.forEach(fn => process.stdin.on('keypress', fn));
222
+
223
+ if (process.stdin.isTTY) {
224
+ process.stdin.setRawMode(true);
225
+ }
226
+ process.stdin.resume();
227
+
228
+ // Flush stdin to remove any buffered leftovers
229
+ let chunk;
230
+ while ((chunk = process.stdin.read()) !== null) { }
231
+
232
+ rInstance.resume();
233
+ rInstance.displayPrompt(true);
234
+ }, 100);
235
+
219
236
  } catch (e) {
237
+ // Restore listeners in error case too!
238
+ keypressListeners.forEach(fn => process.stdin.on('keypress', fn));
239
+
220
240
  if (process.stdin.isTTY) {
221
241
  process.stdin.setRawMode(true);
222
242
  }
223
243
  process.stdin.resume();
224
- r.resume();
244
+ rInstance.resume();
245
+
246
+ // Flush stdin
247
+ let chunk;
248
+ while ((chunk = process.stdin.read()) !== null) { }
249
+
225
250
  if (e.code === 'commander.helpDisplayed') {
226
251
  // Help was displayed, clean exit for us
252
+ setImmediate(() => rInstance.displayPrompt());
227
253
  } else if (e.code === 'commander.unknownOption' || e.code === 'commander.unknownCommand') {
228
254
  console.error(chalk.red(e.message));
255
+ setImmediate(() => rInstance.displayPrompt());
229
256
  } else {
230
257
  if (e.code) {
231
258
  // Likely a commander exit error we rethrew
259
+ setImmediate(() => rInstance.displayPrompt());
232
260
  } else {
233
261
  console.error(chalk.red('Command execution error:'), e);
262
+ setImmediate(() => rInstance.displayPrompt());
234
263
  }
235
264
  }
236
265
  }
@@ -239,27 +268,29 @@ export function registerConsoleCommand(program) {
239
268
  }
240
269
  }
241
270
  } catch (e) {
242
- // unexpected error during CLI detection
271
+ // unexpected error
243
272
  }
244
273
 
245
274
  // Fallback to default eval which supports top-level await
246
- defaultEval.call(r, cmd, context, filename, callback);
275
+ return defaultEval.call(rInstance, cmd, context, filename, callback);
247
276
  };
248
277
 
249
- // Override the eval function
250
- r.eval = myEval;
278
+ const r = repl.start({
279
+ prompt: chalk.green('mage> '),
280
+ eval: myEval,
281
+ completer: myCompleter
282
+ });
251
283
 
252
- // Initialize context
253
284
  r.context.client = createClient;
254
285
  r.context.config = await loadConfig();
255
286
  r.context.chalk = chalk;
256
- r.context.program = program; // Expose global program just in case? Or local?
257
- r.context.localProgram = localProgram; // Expose local debugging
258
287
 
259
288
  // Helper to reload config if changed
260
289
  r.context.reload = async () => {
261
290
  r.context.config = await loadConfig();
262
- console.log(chalk.gray('Config reloaded.'));
291
+ // We should also reload commands here in case a manual config edit changed the type
292
+ await loadLocalCommands();
293
+ console.log(chalk.gray('Config and commands reloaded.'));
263
294
  };
264
295
 
265
296
  r.on('exit', () => {
package/lib/mcp.js ADDED
@@ -0,0 +1,271 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import { z } from "zod";
5
+ import http from "http";
6
+ import chalk from "chalk";
7
+ import { Command } from "commander";
8
+ import { readFileSync } from "fs";
9
+
10
+ // Import command registry
11
+ import { registerAllCommands } from './command-registry.js';
12
+
13
+ // Helper to strip ANSI codes for cleaner output
14
+ function stripAnsi(str) {
15
+ return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
16
+ }
17
+
18
+ /**
19
+ * Starts the MCP server.
20
+ * @param {Object} options Configuration options
21
+ * @param {string} options.transport 'stdio' or 'http'
22
+ * @param {string} [options.host] Host for HTTP server
23
+ * @param {number} [options.port] Port for HTTP server
24
+ */
25
+ export async function startMcpServer(options) {
26
+ const packageJson = JSON.parse(
27
+ readFileSync(new URL("../package.json", import.meta.url))
28
+ );
29
+
30
+ // 1. Setup a dynamic program to discovery commands
31
+ const program = setupProgram();
32
+
33
+ const server = new McpServer({
34
+ name: "mage-remote-run",
35
+ version: packageJson.version
36
+ });
37
+
38
+ const toolsCount = registerTools(server, program);
39
+
40
+ if (options.transport === 'http') {
41
+ const host = options.host || '127.0.0.1';
42
+ const port = options.port || 18098;
43
+
44
+ const transport = new StreamableHTTPServerTransport();
45
+
46
+ const httpServer = http.createServer(async (req, res) => {
47
+ if (req.url === '/sse' || (req.url === '/messages' && req.method === 'POST')) {
48
+ await transport.handleRequest(req, res);
49
+ } else {
50
+ res.writeHead(404);
51
+ res.end();
52
+ }
53
+ });
54
+
55
+ httpServer.listen(port, host, () => {
56
+ console.error(`MCP Server running on http://${host}:${port}`);
57
+ console.error(`Protocol: HTTP (SSE)`);
58
+ console.error(`Registered Tools: ${toolsCount}`);
59
+ });
60
+
61
+ await server.connect(transport);
62
+
63
+ } else {
64
+ // STDIO
65
+ console.error(`Protocol: stdio`);
66
+ console.error(`Registered Tools: ${toolsCount}`);
67
+ const transport = new StdioServerTransport();
68
+ await server.connect(transport);
69
+ }
70
+ }
71
+
72
+ function registerTools(server, program) {
73
+ let count = 0;
74
+
75
+ function processCommand(cmd, parentName = '') {
76
+ const cmdName = parentName ? `${parentName}_${cmd.name()}` : cmd.name();
77
+
78
+ // If it has subcommands, process them
79
+ if (cmd.commands && cmd.commands.length > 0) {
80
+ cmd.commands.forEach(subCmd => processCommand(subCmd, cmdName));
81
+ return;
82
+ }
83
+
84
+ // It's a leaf command, register as tool
85
+ // Tool name: Replace spaces/colons with underscores.
86
+ // Example: website list -> website_list
87
+ const toolName = cmdName.replace(/[^a-zA-Z0-9_]/g, '_');
88
+
89
+ const schema = {};
90
+
91
+ // Arguments
92
+ // Commander args: cmd._args
93
+ // Options: cmd.options
94
+
95
+ const zodShape = {};
96
+
97
+ cmd._args.forEach(arg => {
98
+ // arg.name(), arg.required
99
+ if (arg.required) {
100
+ zodShape[arg.name()] = z.string().describe(arg.description || '');
101
+ } else {
102
+ zodShape[arg.name()] = z.string().optional().describe(arg.description || '');
103
+ }
104
+ });
105
+
106
+ cmd.options.forEach(opt => {
107
+ const name = opt.name(); // e.g. "format" for --format
108
+ // Check flags to guess type.
109
+ // -f, --format <type> -> string
110
+ // -v, --verbose -> boolean
111
+
112
+ if (opt.flags.includes('<')) {
113
+ // Takes an argument, assume string
114
+ zodShape[name] = z.string().optional().describe(opt.description);
115
+ } else {
116
+ // Boolean flag
117
+ zodShape[name] = z.boolean().optional().describe(opt.description);
118
+ }
119
+ });
120
+
121
+ const description = cmd.description() || `Execute mage-remote-run ${cmdName.replace(/_/g, ' ')}`;
122
+
123
+ server.tool(
124
+ toolName,
125
+ description,
126
+ zodShape,
127
+ async (args) => {
128
+ return await executeCommand(cmd, args, parentName);
129
+ }
130
+ );
131
+ count++;
132
+ }
133
+
134
+
135
+ program.commands.forEach(cmd => processCommand(cmd));
136
+ return count;
137
+ }
138
+
139
+ // Re-register all commands on a fresh program instance
140
+ // We export this logic so we can reuse it
141
+ function setupProgram() {
142
+ const program = new Command();
143
+
144
+ // Silence output for the main program instance to avoid double printing during parsing
145
+ program.configureOutput({
146
+ writeOut: (str) => { },
147
+ writeErr: (str) => { }
148
+ });
149
+
150
+ registerAllCommands(program);
151
+
152
+ return program;
153
+ }
154
+
155
+ async function executeCommand(cmdDefinition, args, parentName) {
156
+ // cmdDefinition is the original command object from discovery, used for context if needed,
157
+ // but here we mainly need the path to find it in the new program.
158
+ // Actually, we can just reconstruct the path from parentName + cmd.name()
159
+
160
+ // Intercept Console
161
+ let output = '';
162
+ const originalLog = console.log;
163
+ const originalError = console.error;
164
+
165
+ // Simple custom logger
166
+ const logInterceptor = (...msgArgs) => {
167
+ const line = msgArgs.map(String).join(' ');
168
+ output += stripAnsi(line) + '\n';
169
+ };
170
+
171
+ console.log = logInterceptor;
172
+ console.error = logInterceptor;
173
+
174
+ try {
175
+ const program = setupProgram();
176
+
177
+ // Construct argv
178
+ // We need to build [node, script, command, subcommand, ..., args, options]
179
+ const argv = ['node', 'mage-remote-run'];
180
+
181
+ // Reconstruct command path
182
+ if (parentName) {
183
+ // parentName might be "website" or "website_domain" (if nested deeper? current logic supports 1 level nesting)
184
+ // Current processCommand logic: `cmdName = parentName ? ${parentName}_${cmd.name()} : cmd.name()`
185
+ // But parentName passed to executeCommand is the prefix.
186
+ // Wait, registerTools calls: `executeCommand(cmd, args, parentName)`
187
+ // If parentName is "website", and cmd is "list", we need "website list"
188
+
189
+ // NOTE: parentName in processCommand is built recursively with underscores?
190
+ // In processCommand(subCmd, cmdName), cmdName is "parent_sub".
191
+ // So if we have website -> list.
192
+ // processCommand(website) -> processCommand(list, "website")
193
+ // parentName in executeCommand is "website".
194
+ // cmd.name() is "list".
195
+
196
+ // However, parentName might contain underscores if deeper nesting?
197
+ // "store_config_list" -> parent "store_config", cmd "list".
198
+ // Commander commands are usually space separated in argv.
199
+
200
+ // We need to parse parentName back to argv tokens?
201
+ // Or better: store the "command path" as an array in the tool context.
202
+
203
+ // Let's rely on standard splitting by underscore, assuming command names don't have underscores.
204
+ // Or we can assume parentName matches the command structure.
205
+
206
+ // Safest: splitting parentName by UNDERSCORE might be risky if command names have underscores.
207
+ // But standard commands here: website, store, etc. don't.
208
+
209
+ // Actually, we can just push parentName, then cmd.name()
210
+ // But parentName comes from `cmdName` variable passed as `parentName` to recursive call.
211
+ // `processCommand(subCmd, cmdName)`
212
+ // `cmdName` = `parentName_cmd.name()`.
213
+ // So for `website list`:
214
+ // `processCommand(website, '')` -> `cmdName="website"`.
215
+ // -> `processCommand(list, "website")`.
216
+ // -> register tool "website_list". `parentName` passed to execute is "website".
217
+
218
+ // So `parentName` is the accumulated prefix with underscores.
219
+
220
+ const parts = parentName.split('_');
221
+ argv.push(...parts);
222
+ }
223
+
224
+ argv.push(cmdDefinition.name());
225
+
226
+ // Add arguments and options from the tool args
227
+
228
+ // 1. Positional arguments
229
+ cmdDefinition._args.forEach(arg => {
230
+ if (args[arg.name()]) {
231
+ argv.push(args[arg.name()]);
232
+ }
233
+ });
234
+
235
+ // 2. Options
236
+ cmdDefinition.options.forEach(opt => {
237
+ const name = opt.name(); // e.g. "format"
238
+ if (args[name] !== undefined) {
239
+ const val = args[name];
240
+ if (opt.flags.includes('<')) {
241
+ // String option
242
+ argv.push(`--${name}`);
243
+ argv.push(val);
244
+ } else {
245
+ // Boolean flag
246
+ if (val === true) {
247
+ argv.push(`--${name}`);
248
+ }
249
+ }
250
+ }
251
+ });
252
+
253
+ // Execute
254
+ await program.parseAsync(argv);
255
+
256
+ return {
257
+ content: [{ type: "text", text: output }]
258
+ };
259
+
260
+ } catch (e) {
261
+ // Commander throws nicely formatted errors usually, but we suppressed output.
262
+ // If it throws, it might be a cleaner exit error.
263
+ return {
264
+ content: [{ type: "text", text: output + `\nError: ${e.message}` }],
265
+ isError: true
266
+ };
267
+ } finally {
268
+ console.log = originalLog;
269
+ console.error = originalError;
270
+ }
271
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mage-remote-run",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "The remote swiss army knife for Magento Open Source, Mage-OS, Adobe Commerce",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -31,6 +31,7 @@
31
31
  ],
32
32
  "dependencies": {
33
33
  "@inquirer/prompts": "^8.1.0",
34
+ "@modelcontextprotocol/sdk": "^1.25.1",
34
35
  "axios": "^1.13.2",
35
36
  "chalk": "^5.6.2",
36
37
  "cli-table3": "^0.6.5",
@@ -40,7 +41,8 @@
40
41
  "inquirer": "^13.1.0",
41
42
  "mkdirp": "^3.0.1",
42
43
  "oauth-1.0a": "^2.2.6",
43
- "openapi-client-axios": "^7.8.0"
44
+ "openapi-client-axios": "^7.8.0",
45
+ "zod": "^4.2.1"
44
46
  },
45
47
  "devDependencies": {
46
48
  "jest": "^30.2.0"