mage-remote-run 0.28.0 → 0.29.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.
@@ -57,6 +57,7 @@ program.command('mcp [args...]')
57
57
  .option('--transport <type>', 'Transport type (stdio, http)', 'stdio')
58
58
  .option('--host <host>', 'HTTP Host', '127.0.0.1')
59
59
  .option('--port <port>', 'HTTP Port', '18098')
60
+ .option('--token <token>', 'Authentication token')
60
61
  .allowExcessArguments(true)
61
62
  .allowUnknownOption(true)
62
63
  .action(async (args, options) => {
@@ -7,6 +7,10 @@ import { fileURLToPath } from 'url';
7
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
8
  const SPEC_DIR = path.resolve(__dirname, '../../api-specs/2.4.8');
9
9
 
10
+ // Simple in-memory cache for parsed specs
11
+ const specCache = {};
12
+ const shouldUseCache = process.env.NODE_ENV !== 'test';
13
+
10
14
  export function loadSpec(type) {
11
15
  let specFile;
12
16
  if (type === 'ac-saas' || type === 'saas') {
@@ -16,10 +20,18 @@ export function loadSpec(type) {
16
20
  specFile = 'swagger-paas.json';
17
21
  }
18
22
 
23
+ if (shouldUseCache && specCache[specFile]) {
24
+ return specCache[specFile];
25
+ }
26
+
19
27
  const filePath = path.join(SPEC_DIR, specFile);
20
28
  if (!fs.existsSync(filePath)) {
21
29
  throw new Error(`OpenAPI spec not found at: ${filePath}`);
22
30
  }
23
31
 
24
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
32
+ const spec = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
33
+ if (shouldUseCache) {
34
+ specCache[specFile] = spec;
35
+ }
36
+ return spec;
25
37
  }
@@ -1,5 +1,6 @@
1
1
  import { createClient } from '../api/factory.js';
2
2
  import { printTable, handleError } from '../utils.js';
3
+ import { COMPANY_CREATE_QUESTIONS, getAddressQuestions, getCompanyUpdateQuestions } from '../prompts/company.js';
3
4
  import chalk from 'chalk';
4
5
 
5
6
  export function registerCompanyCommands(program) {
@@ -148,28 +149,11 @@ Examples:
148
149
  const { default: inquirer } = await import('inquirer');
149
150
 
150
151
  // Prompt for basic info
151
- const answers = await inquirer.prompt([
152
- { name: 'company_name', message: 'Company Name:' },
153
- { name: 'company_email', message: 'Company Email:' },
154
- { name: 'legal_name', message: 'Legal Name (optional):' },
155
- { name: 'vat_tax_id', message: 'VAT/Tax ID (optional):' },
156
- { name: 'reseller_id', message: 'Reseller ID (optional):' },
157
- { name: 'customer_group_id', message: 'Customer Group ID:', default: '1' },
158
- { name: 'sales_representative_id', message: 'Sales Representative ID (Admin User ID):', default: '1' },
159
- { name: 'email', message: 'Super Admin Email:' },
160
- { name: 'firstname', message: 'Super Admin First Name:' },
161
- { name: 'lastname', message: 'Super Admin Last Name:' }
162
- ]);
152
+ const answers = await inquirer.prompt(COMPANY_CREATE_QUESTIONS);
163
153
 
164
154
  // Address is required usually
165
- const address = await inquirer.prompt([
166
- { name: 'street', message: 'Street:' },
167
- { name: 'city', message: 'City:' },
168
- { name: 'country_id', message: 'Country ID:', default: 'US' },
169
- { name: 'region', message: 'Region/State:' },
170
- { name: 'postcode', message: 'Postcode:' },
171
- { name: 'telephone', message: 'Telephone:' }
172
- ]);
155
+ const defaultCountry = process.env.MAGE_REMOTE_RUN_DEFAULT_COUNTRY || 'US';
156
+ const address = await inquirer.prompt(getAddressQuestions(defaultCountry));
173
157
 
174
158
  const payload = {
175
159
  company: {
@@ -223,12 +207,7 @@ Examples:
223
207
  throw new Error(`Company ${companyId} not found.`);
224
208
  }
225
209
 
226
- const answers = await inquirer.prompt([
227
- { name: 'company_name', message: 'Company Name:', default: current.company_name },
228
- { name: 'company_email', message: 'Company Email:', default: current.company_email },
229
- { name: 'sales_representative_id', message: 'Sales Rep ID:', default: String(current.sales_representative_id) },
230
- { name: 'customer_group_id', message: 'Customer Group ID:', default: String(current.customer_group_id) }
231
- ]);
210
+ const answers = await inquirer.prompt(getCompanyUpdateQuestions(current));
232
211
 
233
212
  // Merge
234
213
  const payload = {
@@ -6,6 +6,7 @@ import { input, confirm, select } from '@inquirer/prompts';
6
6
  import inquirer from 'inquirer';
7
7
  import chalk from 'chalk';
8
8
  import { getMissingB2BModules } from '../b2b.js';
9
+ import { getLogoForType } from '../logos.js';
9
10
 
10
11
  // Helper to test connection (non-interactive or one-shot)
11
12
  async function testConnection(name, settings) {
@@ -222,84 +223,7 @@ async function printConnectionStatus(config, options = {}) {
222
223
  return;
223
224
  }
224
225
 
225
- // ASCII Logos
226
- const logos = {
227
- adobe: chalk.red(`
228
- @@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@
229
- @@@@@@@@@@@@@@ @@@@@@@@@@@@@@
230
- @@@@@@@@@@@@@ @@@@@@@@@@@@@
231
- @@@@@@@@@@@@@ @@@@@@@@@@@@
232
- @@@@@@@@@@@@ @@@@@@@@@@@@
233
- @@@@@@@@@@@ @@@@@@@@@@@
234
- @@@@@@@@@@ @@@@@@@@@@
235
- @@@@@@@@@ @@@ @@@@@@@@@
236
- @@@@@@@@@ @@@ @@@@@@@@@
237
- @@@@@@@@ @@@@@ @@@@@@@@
238
- @@@@@@@ @@@@@@@ @@@@@@@
239
- @@@@@@ @@@@@@@@@ @@@@@@
240
- @@@@@@ @@@@@@@@@@@ @@@@@@
241
- @@@@@ @@@@@@@@@@@@ @@@@@
242
- @@@@ @@@@@@@@@@@@@ @@@@
243
- @@@ @@@@@@@ @@@
244
- @@ @@@@@@@ @@
245
- @@ @@@@@@ @@
246
-
247
-
248
- `),
249
- magento: chalk.hex('#FFA500')(`
250
- @@
251
- @@@@@@@@
252
- @@@@@@@@@@@@@@
253
- @@@@@@@@@@@@@@@@@@@@
254
- @@@@@@@@@@@@@@@@@@@@@@@@@@
255
- @@@@@@@@@@@@@@ @@@@@@@@@@@@@@
256
- @@@@@@@@@@@@@@ @@@@@@@@@@@@@@
257
- @@@@@@@@@@@@@ @@@@@@@@@@@@@
258
- @@@@@@@@@@ @@@@@@@@@@
259
- @@@@@@@ @@@@ @@@@ @@@@@@@
260
- @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
261
- @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
262
- @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
263
- @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
264
- @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
265
- @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
266
- @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
267
- @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
268
- @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
269
- @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
270
- @@@@@ @@@@@@@ @@@@@@@ @@@@@
271
- @@@ @@@@@@@ @@@@@@@ @@@
272
- @@@@@@@@@@@@@@@@@@
273
- @@@@@@@@@@@@@@@@@@
274
- @@@@@@@@@@@@@@
275
- @@@@@@@@
276
- @@ `),
277
- mageos: chalk.hex('#FFA500')(`
278
- ====== ======
279
- ============ ============
280
- ====================================
281
- ==========================================
282
- ================================================
283
- ================================-===============--
284
- =============----============----============-----
285
- =========--------=========-------=========--------
286
- ========---------========--------========---------
287
- ========---------========--------========---------
288
- ========---------========--------========---------
289
- =====------ ======------ =====------
290
- ==--- ===--- ==---
291
- `)
292
- };
293
-
294
- let logo = '';
295
- if (profile.type && profile.type.startsWith('ac-')) {
296
- logo = logos.adobe;
297
- } else if (profile.type === 'mage-os') {
298
- logo = logos.mageos;
299
- } else if (profile.type === 'magento-os') {
300
- logo = logos.magento;
301
- }
302
-
226
+ const logo = getLogoForType(profile.type);
303
227
  if (logo) {
304
228
  console.log(logo);
305
229
  }
@@ -542,10 +466,12 @@ Examples:
542
466
  .action(async (options) => {
543
467
  try {
544
468
  const config = await loadConfig();
545
- let updated = false;
546
- for (const [name, profile] of Object.entries(config.profiles || {})) {
547
- updated = (await ensureProfileCapabilities(name, profile, config)) || updated;
548
- }
469
+ const results = await Promise.all(
470
+ Object.entries(config.profiles || {}).map(([name, profile]) =>
471
+ ensureProfileCapabilities(name, profile, config)
472
+ )
473
+ );
474
+ const updated = results.some(result => result);
549
475
  if (updated) {
550
476
  await saveConfig(config);
551
477
  }
@@ -765,8 +691,7 @@ Examples:
765
691
  }
766
692
  console.log(chalk.blue(`Testing ${profiles.length} connections...\n`));
767
693
 
768
- const results = [];
769
- for (const name of profiles) {
694
+ const promises = profiles.map(async (name) => {
770
695
  try {
771
696
  const profileConfig = config.profiles[name];
772
697
  const client = await createClient(profileConfig);
@@ -775,11 +700,13 @@ Examples:
775
700
  await client.get('V1/store/storeViews');
776
701
  const duration = Date.now() - start;
777
702
 
778
- results.push([name, chalk.green('SUCCESS'), `${duration}ms`]);
703
+ return [name, chalk.green('SUCCESS'), `${duration}ms`];
779
704
  } catch (e) {
780
- results.push([name, chalk.red('FAILED'), e.message]);
705
+ return [name, chalk.red('FAILED'), e.message];
781
706
  }
782
- }
707
+ });
708
+
709
+ const results = await Promise.all(promises);
783
710
 
784
711
  console.log(chalk.bold('Connection Test Results:'));
785
712
  printTable(['Profile', 'Status', 'Details'], results);
package/lib/config.js CHANGED
@@ -59,7 +59,8 @@ export async function loadConfig() {
59
59
  export async function saveConfig(config) {
60
60
  try {
61
61
  await mkdirp(CONFIG_DIR);
62
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
62
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
63
+ fs.chmodSync(CONFIG_FILE, 0o600);
63
64
  if (process.env.DEBUG) {
64
65
  console.log(`Configuration saved to ${CONFIG_FILE}`);
65
66
  }
@@ -108,7 +109,8 @@ export async function loadTokenCache() {
108
109
  export async function saveTokenCache(cache) {
109
110
  try {
110
111
  await mkdirp(CACHE_DIR);
111
- fs.writeFileSync(TOKEN_CACHE_FILE, JSON.stringify(cache, null, 2));
112
+ fs.writeFileSync(TOKEN_CACHE_FILE, JSON.stringify(cache, null, 2), { mode: 0o600 });
113
+ fs.chmodSync(TOKEN_CACHE_FILE, 0o600);
112
114
  } catch (e) {
113
115
  console.error("Error saving token cache:", e.message);
114
116
  throw e;
package/lib/logos.js ADDED
@@ -0,0 +1,80 @@
1
+ import chalk from 'chalk';
2
+
3
+ // ASCII Logos
4
+ const logos = {
5
+ adobe: chalk.red(`
6
+ @@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@
7
+ @@@@@@@@@@@@@@ @@@@@@@@@@@@@@
8
+ @@@@@@@@@@@@@ @@@@@@@@@@@@@
9
+ @@@@@@@@@@@@@ @@@@@@@@@@@@
10
+ @@@@@@@@@@@@ @@@@@@@@@@@@
11
+ @@@@@@@@@@@ @@@@@@@@@@@
12
+ @@@@@@@@@@ @@@@@@@@@@
13
+ @@@@@@@@@ @@@ @@@@@@@@@
14
+ @@@@@@@@@ @@@ @@@@@@@@@
15
+ @@@@@@@@ @@@@@ @@@@@@@@
16
+ @@@@@@@ @@@@@@@ @@@@@@@
17
+ @@@@@@ @@@@@@@@@ @@@@@@
18
+ @@@@@@ @@@@@@@@@@@ @@@@@@
19
+ @@@@@ @@@@@@@@@@@@ @@@@@
20
+ @@@@ @@@@@@@@@@@@@ @@@@
21
+ @@@ @@@@@@@ @@@
22
+ @@ @@@@@@@ @@
23
+ @@ @@@@@@ @@
24
+
25
+
26
+ `),
27
+ magento: chalk.hex('#FFA500')(`
28
+ @@
29
+ @@@@@@@@
30
+ @@@@@@@@@@@@@@
31
+ @@@@@@@@@@@@@@@@@@@@
32
+ @@@@@@@@@@@@@@@@@@@@@@@@@@
33
+ @@@@@@@@@@@@@@ @@@@@@@@@@@@@@
34
+ @@@@@@@@@@@@@@ @@@@@@@@@@@@@@
35
+ @@@@@@@@@@@@@ @@@@@@@@@@@@@
36
+ @@@@@@@@@@ @@@@@@@@@@
37
+ @@@@@@@ @@@@ @@@@ @@@@@@@
38
+ @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
39
+ @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
40
+ @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
41
+ @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
42
+ @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
43
+ @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
44
+ @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
45
+ @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
46
+ @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@
47
+ @@@@@ @@@@@@@ @@@@@@@ @@@@@
48
+ @@@ @@@@@@@ @@@@@@@ @@@
49
+ @@@@@@@@@@@@@@@@@@
50
+ @@@@@@@@@@@@@@@@@@
51
+ @@@@@@@@@@@@@@
52
+ @@@@@@@@
53
+ @@ `),
54
+ mageos: chalk.hex('#FFA500')(`
55
+ ====== ======
56
+ ============ ============
57
+ ====================================
58
+ ==========================================
59
+ ================================================
60
+ ================================-===============--
61
+ =============----============----============-----
62
+ =========--------=========-------=========--------
63
+ ========---------========--------========---------
64
+ ========---------========--------========---------
65
+ ========---------========--------========---------
66
+ =====------ ======------ =====------
67
+ ==--- ===--- ==---
68
+ `)
69
+ };
70
+
71
+ export function getLogoForType(type) {
72
+ if (type && type.startsWith('ac-')) {
73
+ return logos.adobe;
74
+ } else if (type === 'mage-os') {
75
+ return logos.mageos;
76
+ } else if (type === 'magento-os') {
77
+ return logos.magento;
78
+ }
79
+ return '';
80
+ }
package/lib/mcp.js CHANGED
@@ -3,6 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
4
  import { z } from "zod";
5
5
  import http from "http";
6
+ import crypto from "crypto";
6
7
  import chalk from "chalk";
7
8
  import { Command } from "commander";
8
9
  import { readFileSync } from "fs";
@@ -46,10 +47,51 @@ export async function startMcpServer(options) {
46
47
  const host = options.host || '127.0.0.1';
47
48
  const port = options.port || 18098;
48
49
 
50
+ // Authentication logic
51
+ let token = options.token || process.env.MAGE_REMOTE_RUN_MCP_TOKEN;
52
+
53
+ if (!token) {
54
+ token = crypto.randomBytes(16).toString('hex');
55
+ console.error(chalk.yellow(`--------------------------------------------------------------------------------`));
56
+ console.error(chalk.yellow(`MCP Server Authentication Token: `) + chalk.green.bold(token));
57
+ console.error(chalk.yellow(`Use this token to connect to the MCP server via ?token=${token}`));
58
+ console.error(chalk.yellow(`--------------------------------------------------------------------------------`));
59
+ }
60
+
49
61
  const transport = new StreamableHTTPServerTransport();
50
62
 
51
63
  const httpServer = http.createServer(async (req, res) => {
52
- if (req.url === '/sse' || (req.url === '/messages' && req.method === 'POST')) {
64
+ const requestUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
65
+ const pathname = requestUrl.pathname;
66
+
67
+ // Check authentication
68
+ const queryToken = requestUrl.searchParams.get('token');
69
+ const authHeader = req.headers.authorization;
70
+
71
+ let authenticated = false;
72
+
73
+ try {
74
+ if (queryToken && queryToken.length === token.length && crypto.timingSafeEqual(Buffer.from(queryToken), Buffer.from(token))) {
75
+ authenticated = true;
76
+ } else if (authHeader && authHeader.startsWith('Bearer ')) {
77
+ const headerToken = authHeader.substring(7);
78
+ if (headerToken.length === token.length && crypto.timingSafeEqual(Buffer.from(headerToken), Buffer.from(token))) {
79
+ authenticated = true;
80
+ }
81
+ }
82
+ } catch (e) {
83
+ // Ignore crypto errors (e.g. encoding issues)
84
+ }
85
+
86
+ if (!authenticated) {
87
+ res.writeHead(401, { 'Content-Type': 'application/json' });
88
+ res.end(JSON.stringify({ error: 'Unauthorized', message: 'Invalid or missing authentication token' }));
89
+ return;
90
+ }
91
+
92
+ if (pathname === '/sse' || (pathname === '/messages' && req.method === 'POST')) {
93
+ // Ensure query parameters don't interfere with transport handling if it expects exact path
94
+ req.url = pathname;
53
95
  await transport.handleRequest(req, res);
54
96
  } else {
55
97
  res.writeHead(404);
@@ -0,0 +1,33 @@
1
+
2
+ export const COMPANY_CREATE_QUESTIONS = [
3
+ { name: 'company_name', message: 'Company Name:' },
4
+ { name: 'company_email', message: 'Company Email:' },
5
+ { name: 'legal_name', message: 'Legal Name (optional):' },
6
+ { name: 'vat_tax_id', message: 'VAT/Tax ID (optional):' },
7
+ { name: 'reseller_id', message: 'Reseller ID (optional):' },
8
+ { name: 'customer_group_id', message: 'Customer Group ID:', default: '1' },
9
+ { name: 'sales_representative_id', message: 'Sales Representative ID (Admin User ID):', default: '1' },
10
+ { name: 'email', message: 'Super Admin Email:' },
11
+ { name: 'firstname', message: 'Super Admin First Name:' },
12
+ { name: 'lastname', message: 'Super Admin Last Name:' }
13
+ ];
14
+
15
+ export function getAddressQuestions(defaultCountry = 'US') {
16
+ return [
17
+ { name: 'street', message: 'Street:' },
18
+ { name: 'city', message: 'City:' },
19
+ { name: 'country_id', message: 'Country ID:', default: defaultCountry },
20
+ { name: 'region', message: 'Region/State:' },
21
+ { name: 'postcode', message: 'Postcode:' },
22
+ { name: 'telephone', message: 'Telephone:' }
23
+ ];
24
+ }
25
+
26
+ export function getCompanyUpdateQuestions(current) {
27
+ return [
28
+ { name: 'company_name', message: 'Company Name:', default: current.company_name },
29
+ { name: 'company_email', message: 'Company Email:', default: current.company_email },
30
+ { name: 'sales_representative_id', message: 'Sales Rep ID:', default: String(current.sales_representative_id) },
31
+ { name: 'customer_group_id', message: 'Customer Group ID:', default: String(current.customer_group_id) }
32
+ ];
33
+ }
package/lib/utils.js CHANGED
@@ -4,12 +4,16 @@ import fs from 'fs';
4
4
  import os from 'os';
5
5
  import { loadConfig } from './config.js';
6
6
 
7
+ const TABLE_OPTIONS = {
8
+ style: { head: [] }
9
+ };
10
+
7
11
  export function printTable(headers, data) {
8
12
  const table = new Table({
13
+ ...TABLE_OPTIONS,
9
14
  head: headers.map(h => chalk.cyan(h)),
10
- style: { head: [] }
11
15
  });
12
- data.forEach(row => table.push(row));
16
+ table.push(...data);
13
17
  console.log(table.toString());
14
18
  }
15
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mage-remote-run",
3
- "version": "0.28.0",
3
+ "version": "0.29.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": {