@anton.andrusenko/shopify-mcp-admin 2.1.1 → 2.2.1

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.
@@ -0,0 +1,697 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/setup-wizard.ts
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { dirname, join } from "path";
7
+ import { confirm, input, password, select } from "@inquirer/prompts";
8
+ var colors = {
9
+ reset: "\x1B[0m",
10
+ bold: "\x1B[1m",
11
+ dim: "\x1B[2m",
12
+ // Shopify-inspired colors
13
+ green: "\x1B[38;5;82m",
14
+ // Shopify green
15
+ darkGreen: "\x1B[38;5;34m",
16
+ cyan: "\x1B[38;5;87m",
17
+ yellow: "\x1B[38;5;226m",
18
+ red: "\x1B[38;5;196m",
19
+ magenta: "\x1B[38;5;207m",
20
+ blue: "\x1B[38;5;75m",
21
+ white: "\x1B[38;5;255m",
22
+ gray: "\x1B[38;5;245m"
23
+ };
24
+ var c = {
25
+ green: (s) => `${colors.green}${s}${colors.reset}`,
26
+ darkGreen: (s) => `${colors.darkGreen}${s}${colors.reset}`,
27
+ cyan: (s) => `${colors.cyan}${s}${colors.reset}`,
28
+ yellow: (s) => `${colors.yellow}${s}${colors.reset}`,
29
+ red: (s) => `${colors.red}${s}${colors.reset}`,
30
+ magenta: (s) => `${colors.magenta}${s}${colors.reset}`,
31
+ blue: (s) => `${colors.blue}${s}${colors.reset}`,
32
+ white: (s) => `${colors.white}${s}${colors.reset}`,
33
+ gray: (s) => `${colors.gray}${s}${colors.reset}`,
34
+ bold: (s) => `${colors.bold}${s}${colors.reset}`,
35
+ dim: (s) => `${colors.dim}${s}${colors.reset}`
36
+ };
37
+ function gradientLine(line) {
38
+ const gradientColors = [
39
+ "\x1B[38;5;82m",
40
+ // green
41
+ "\x1B[38;5;83m",
42
+ "\x1B[38;5;84m",
43
+ "\x1B[38;5;85m",
44
+ "\x1B[38;5;86m",
45
+ "\x1B[38;5;87m"
46
+ // cyan
47
+ ];
48
+ let result = "";
49
+ const chars = [...line];
50
+ for (let i = 0; i < chars.length; i++) {
51
+ const colorIndex = Math.floor(i / chars.length * gradientColors.length);
52
+ result += `${gradientColors[colorIndex]}${chars[i]}`;
53
+ }
54
+ return result + colors.reset;
55
+ }
56
+ var BANNER = `
57
+ ${c.green("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E")}
58
+ ${c.green("\u2502")} ${c.green("\u2502")}
59
+ ${c.green("\u2502")} ${gradientLine("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557")} ${c.green("\u2502")}
60
+ ${c.green("\u2502")} ${gradientLine("\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D")} ${c.green("\u2502")}
61
+ ${c.green("\u2502")} ${gradientLine("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2588\u2554\u255D ")} ${c.green("\u2502")}
62
+ ${c.green("\u2502")} ${gradientLine("\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u255A\u2588\u2588\u2554\u255D ")} ${c.green("\u2502")}
63
+ ${c.green("\u2502")} ${gradientLine("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 ")} ${c.green("\u2502")}
64
+ ${c.green("\u2502")} ${gradientLine("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D ")} ${c.green("\u2502")}
65
+ ${c.green("\u2502")} ${c.green("\u2502")}
66
+ ${c.green("\u2502")} ${c.bold("MCP ADMIN SERVER")} ${c.green("\u2502")}
67
+ ${c.green("\u2502")} ${c.green("\u2502")}
68
+ ${c.green("\u2502")} ${c.cyan("\u{1F6CD}\uFE0F Connect AI Agents to Your Shopify Store")} ${c.green("\u2502")}
69
+ ${c.green("\u2502")} ${c.green("\u2502")}
70
+ ${c.green("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F")}
71
+ `;
72
+ function validateStoreUrl(value) {
73
+ const trimmed = value.trim().toLowerCase();
74
+ const cleaned = trimmed.replace(/^https?:\/\//, "");
75
+ if (!cleaned) {
76
+ return "Store URL is required";
77
+ }
78
+ if (!cleaned.endsWith(".myshopify.com")) {
79
+ return "Must be a valid myshopify.com domain (e.g., your-store.myshopify.com)";
80
+ }
81
+ return true;
82
+ }
83
+ function validateAccessToken(value) {
84
+ const trimmed = value.trim();
85
+ if (!trimmed) {
86
+ return "Access token is required";
87
+ }
88
+ if (!trimmed.startsWith("shpat_")) {
89
+ return 'Access token should start with "shpat_"';
90
+ }
91
+ if (trimmed.length < 20) {
92
+ return "Access token seems too short";
93
+ }
94
+ return true;
95
+ }
96
+ function cleanStoreUrl(url) {
97
+ return url.trim().toLowerCase().replace(/^https?:\/\//, "");
98
+ }
99
+ var Spinner = class {
100
+ frames = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
101
+ frameIndex = 0;
102
+ intervalId = null;
103
+ message;
104
+ constructor(message) {
105
+ this.message = message;
106
+ }
107
+ start() {
108
+ process.stdout.write("\x1B[?25l");
109
+ this.intervalId = setInterval(() => {
110
+ const frame = this.frames[this.frameIndex];
111
+ process.stdout.write(`\r${c.cyan(frame)} ${this.message}`);
112
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
113
+ }, 100);
114
+ }
115
+ succeed(message) {
116
+ this.stop();
117
+ console.log(`\r${c.green("\u2714")} ${message}`);
118
+ }
119
+ fail(message) {
120
+ this.stop();
121
+ console.log(`\r${c.red("\u2716")} ${message}`);
122
+ }
123
+ stop() {
124
+ if (this.intervalId) {
125
+ clearInterval(this.intervalId);
126
+ this.intervalId = null;
127
+ }
128
+ process.stdout.write("\x1B[?25h");
129
+ process.stdout.write("\r\x1B[K");
130
+ }
131
+ };
132
+ async function testConnection(config) {
133
+ const spinner = new Spinner(`Verifying connection to ${config.storeUrl}...`);
134
+ spinner.start();
135
+ try {
136
+ const query = `{
137
+ shop {
138
+ name
139
+ plan { displayName }
140
+ email
141
+ }
142
+ }`;
143
+ let accessToken = config.accessToken;
144
+ if (config.authMethod === "oauth" && config.clientId && config.clientSecret) {
145
+ const tokenResponse = await fetch(`https://${config.storeUrl}/admin/oauth/access_token`, {
146
+ method: "POST",
147
+ headers: { "Content-Type": "application/json" },
148
+ body: JSON.stringify({
149
+ grant_type: "client_credentials",
150
+ client_id: config.clientId,
151
+ client_secret: config.clientSecret
152
+ })
153
+ });
154
+ if (!tokenResponse.ok) {
155
+ spinner.fail("Failed to authenticate with OAuth credentials");
156
+ return null;
157
+ }
158
+ const tokenData = await tokenResponse.json();
159
+ accessToken = tokenData.access_token;
160
+ }
161
+ const response = await fetch(`https://${config.storeUrl}/admin/api/2025-01/graphql.json`, {
162
+ method: "POST",
163
+ headers: {
164
+ "Content-Type": "application/json",
165
+ "X-Shopify-Access-Token": accessToken || ""
166
+ },
167
+ body: JSON.stringify({ query })
168
+ });
169
+ if (!response.ok) {
170
+ if (response.status === 401) {
171
+ spinner.fail("Authentication failed - check your access token");
172
+ } else if (response.status === 403) {
173
+ spinner.fail("Access denied - check API scopes");
174
+ } else {
175
+ spinner.fail(`Connection failed (HTTP ${response.status})`);
176
+ }
177
+ return null;
178
+ }
179
+ const data = await response.json();
180
+ if (data.errors) {
181
+ spinner.fail(`API error: ${data.errors[0]?.message || "Unknown error"}`);
182
+ return null;
183
+ }
184
+ if (!data.data) {
185
+ spinner.fail("Invalid response from Shopify API");
186
+ return null;
187
+ }
188
+ const storeInfo = {
189
+ name: data.data.shop.name,
190
+ plan: data.data.shop.plan.displayName,
191
+ email: data.data.shop.email
192
+ };
193
+ spinner.succeed(`Connected! Store: "${storeInfo.name}" (${storeInfo.plan})`);
194
+ return storeInfo;
195
+ } catch (error) {
196
+ spinner.fail(`Connection error: ${error instanceof Error ? error.message : "Unknown error"}`);
197
+ return null;
198
+ }
199
+ }
200
+ function buildEnvVars(config) {
201
+ const env = {
202
+ SHOPIFY_STORE_URL: config.storeUrl
203
+ };
204
+ if (config.authMethod === "token" && config.accessToken) {
205
+ env.SHOPIFY_ACCESS_TOKEN = config.accessToken;
206
+ } else {
207
+ env.SHOPIFY_CLIENT_ID = config.clientId || "";
208
+ env.SHOPIFY_CLIENT_SECRET = config.clientSecret || "";
209
+ }
210
+ if (config.lazyLoading) {
211
+ env.SHOPIFY_MCP_LAZY_LOADING = "true";
212
+ } else if (config.role) {
213
+ env.SHOPIFY_MCP_ROLE = config.role;
214
+ }
215
+ if (config.transport === "http") {
216
+ env.TRANSPORT = "http";
217
+ env.PORT = String(config.port || 3e3);
218
+ }
219
+ return env;
220
+ }
221
+ function generateJsonConfig(config) {
222
+ const env = buildEnvVars(config);
223
+ const mcpConfig = {
224
+ mcpServers: {
225
+ shopify: {
226
+ command: "npx",
227
+ args: ["-y", "@anton.andrusenko/shopify-mcp-admin"],
228
+ env
229
+ }
230
+ }
231
+ };
232
+ return JSON.stringify(mcpConfig, null, 2);
233
+ }
234
+ function generateLibreChatConfig(config) {
235
+ const env = buildEnvVars(config);
236
+ const envLines = Object.entries(env).map(([key, value]) => ` ${key}: "${value}"`).join("\n");
237
+ return `# LibreChat Configuration for Shopify MCP Admin
238
+ # Generated by shopify-mcp-admin setup wizard
239
+ # Docs: https://github.com/AntonAndrusenko/shopify-mcp-admin
240
+
241
+ version: 1.2.1
242
+ cache: true
243
+
244
+ interface:
245
+ customWelcome: 'Welcome to LibreChat with Shopify MCP! \u{1F6CD}\uFE0F'
246
+
247
+ mcpServers:
248
+ shopify:
249
+ type: ${config.transport === "http" ? "sse" : "stdio"}
250
+ ${config.transport === "http" ? `url: http://localhost:${config.port || 3e3}/sse` : `command: npx
251
+ args:
252
+ - -y
253
+ - "@anton.andrusenko/shopify-mcp-admin"`}
254
+ env:
255
+ ${envLines}
256
+ LOG_LEVEL: "info"
257
+ serverInstructions: true
258
+ timeout: 30000
259
+ initTimeout: 15000
260
+ `;
261
+ }
262
+ function generateEnvFile(config) {
263
+ const env = buildEnvVars(config);
264
+ let content = `# Shopify MCP Admin Configuration
265
+ # Generated by shopify-mcp-admin setup wizard
266
+ # Docs: https://github.com/AntonAndrusenko/shopify-mcp-admin
267
+
268
+ `;
269
+ for (const [key, value] of Object.entries(env)) {
270
+ content += `${key}=${value}
271
+ `;
272
+ }
273
+ return content;
274
+ }
275
+ function generateShellExport(config) {
276
+ const env = buildEnvVars(config);
277
+ let content = `# Shopify MCP Admin - Shell Environment
278
+ # Add to your ~/.bashrc, ~/.zshrc, or run: source <filename>
279
+
280
+ `;
281
+ for (const [key, value] of Object.entries(env)) {
282
+ content += `export ${key}="${value}"
283
+ `;
284
+ }
285
+ content += `
286
+ # Run the server with:
287
+ # npx @anton.andrusenko/shopify-mcp-admin
288
+ `;
289
+ return content;
290
+ }
291
+ function getClaudeDesktopConfigPath() {
292
+ const platform = process.platform;
293
+ if (platform === "darwin") {
294
+ return join(
295
+ homedir(),
296
+ "Library",
297
+ "Application Support",
298
+ "Claude",
299
+ "claude_desktop_config.json"
300
+ );
301
+ }
302
+ if (platform === "win32") {
303
+ return join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json");
304
+ }
305
+ return join(homedir(), ".config", "claude", "claude_desktop_config.json");
306
+ }
307
+ function getCursorConfigPath() {
308
+ return join(process.cwd(), ".cursor", "mcp.json");
309
+ }
310
+ function getWindsurfConfigPath() {
311
+ const platform = process.platform;
312
+ if (platform === "darwin") {
313
+ return join(homedir(), ".codeium", "windsurf", "mcp_config.json");
314
+ }
315
+ if (platform === "win32") {
316
+ return join(process.env.APPDATA || "", "Codeium", "windsurf", "mcp_config.json");
317
+ }
318
+ return join(homedir(), ".codeium", "windsurf", "mcp_config.json");
319
+ }
320
+ function getVSCodeConfigPath() {
321
+ return join(process.cwd(), ".vscode", "mcp.json");
322
+ }
323
+ function ensureDirectoryExists(filePath) {
324
+ const dir = dirname(filePath);
325
+ if (!existsSync(dir)) {
326
+ mkdirSync(dir, { recursive: true });
327
+ }
328
+ }
329
+ function mergeJsonConfig(existingPath, newConfig) {
330
+ if (!existsSync(existingPath)) {
331
+ return newConfig;
332
+ }
333
+ try {
334
+ const existing = JSON.parse(readFileSync(existingPath, "utf-8"));
335
+ const toMerge = JSON.parse(newConfig);
336
+ existing.mcpServers = {
337
+ ...existing.mcpServers,
338
+ ...toMerge.mcpServers
339
+ };
340
+ return JSON.stringify(existing, null, 2);
341
+ } catch {
342
+ return newConfig;
343
+ }
344
+ }
345
+ function printSuccessBox(client, configPath) {
346
+ const lines = ["", ` ${c.green("\u2728")} ${c.bold("You're all set!")}`, ""];
347
+ lines.push(` ${c.cyan("Next steps:")}`);
348
+ switch (client) {
349
+ case "claude-desktop":
350
+ lines.push(" 1. Restart Claude Desktop");
351
+ lines.push(` 2. Look for "shopify" in the MCP servers list`);
352
+ lines.push(` 3. Try: "List all products in my store"`);
353
+ break;
354
+ case "cursor":
355
+ lines.push(" 1. Restart Cursor");
356
+ lines.push(" 2. The Shopify MCP server will be available");
357
+ lines.push(` 3. Try: "List all products in my Shopify store"`);
358
+ break;
359
+ case "windsurf":
360
+ lines.push(" 1. Restart Windsurf");
361
+ lines.push(` 2. Look for "shopify" in the Cascade MCP list`);
362
+ lines.push(` 3. Try: "List all products in my store"`);
363
+ break;
364
+ case "vscode-copilot":
365
+ lines.push(" 1. Restart VS Code");
366
+ lines.push(` 2. Use Copilot Chat with "shopify" MCP`);
367
+ lines.push(" 3. Try: @shopify list products");
368
+ break;
369
+ case "librechat":
370
+ lines.push(" 1. Copy librechat.yaml to your LibreChat directory");
371
+ lines.push(" 2. Restart LibreChat: docker compose restart");
372
+ lines.push(` 3. Select "shopify" in the MCP servers dropdown`);
373
+ break;
374
+ case "openai-http":
375
+ lines.push(" 1. Start the server: npx @anton.andrusenko/shopify-mcp-admin");
376
+ lines.push(" 2. Server will listen on the configured port");
377
+ lines.push(" 3. Connect your OpenAI integration via HTTP");
378
+ break;
379
+ case "other":
380
+ lines.push(` 1. Source the env file: source ${configPath}`);
381
+ lines.push(" 2. Or copy variables to your shell profile");
382
+ lines.push(" 3. Run: npx @anton.andrusenko/shopify-mcp-admin");
383
+ break;
384
+ }
385
+ lines.push("");
386
+ lines.push(
387
+ ` ${c.gray("\u{1F4DA} Docs:")} ${c.cyan("https://github.com/AntonAndrusenko/shopify-mcp-admin")}`
388
+ );
389
+ lines.push("");
390
+ const maxLength = 65;
391
+ const boxTop = c.green(`\u256D${"\u2500".repeat(maxLength)}\u256E`);
392
+ const boxBottom = c.green(`\u2570${"\u2500".repeat(maxLength)}\u256F`);
393
+ console.log("");
394
+ console.log(boxTop);
395
+ const ansiRegex = /\x1b\[[0-9;]*m/g;
396
+ for (const line of lines) {
397
+ const visibleLength = line.replace(ansiRegex, "").length;
398
+ const padding = maxLength - visibleLength;
399
+ console.log(`${c.green("\u2502")}${line}${" ".repeat(Math.max(0, padding))}${c.green("\u2502")}`);
400
+ }
401
+ console.log(boxBottom);
402
+ console.log("");
403
+ }
404
+ async function runSetupWizard() {
405
+ console.clear();
406
+ console.log(BANNER);
407
+ const config = {
408
+ storeUrl: "",
409
+ authMethod: "token",
410
+ transport: "stdio",
411
+ lazyLoading: false,
412
+ client: "claude-desktop"
413
+ };
414
+ try {
415
+ console.log("");
416
+ const storeUrlInput = await input({
417
+ message: "What is your Shopify store URL?",
418
+ default: "your-store.myshopify.com",
419
+ validate: validateStoreUrl,
420
+ transformer: (value) => cleanStoreUrl(value)
421
+ });
422
+ config.storeUrl = cleanStoreUrl(storeUrlInput);
423
+ console.log("");
424
+ config.authMethod = await select({
425
+ message: "How would you like to authenticate?",
426
+ choices: [
427
+ {
428
+ name: "Access Token (Legacy Custom App)",
429
+ value: "token",
430
+ description: "I have a shpat_xxx token from a Custom App"
431
+ },
432
+ {
433
+ name: "OAuth 2.0 (Dev Dashboard)",
434
+ value: "oauth",
435
+ description: "I have a Client ID and Client Secret"
436
+ }
437
+ ]
438
+ });
439
+ console.log("");
440
+ if (config.authMethod === "token") {
441
+ config.accessToken = await password({
442
+ message: "Paste your access token (shpat_xxx)",
443
+ mask: "\u2022",
444
+ validate: validateAccessToken
445
+ });
446
+ } else {
447
+ config.clientId = await input({
448
+ message: "Enter your Client ID",
449
+ validate: (v) => v.trim().length > 0 || "Client ID is required"
450
+ });
451
+ config.clientSecret = await password({
452
+ message: "Enter your Client Secret",
453
+ mask: "\u2022",
454
+ validate: (v) => v.trim().length > 0 || "Client Secret is required"
455
+ });
456
+ }
457
+ console.log("");
458
+ const storeInfo = await testConnection(config);
459
+ if (!storeInfo) {
460
+ const retry = await confirm({
461
+ message: "Connection failed. Would you like to try different credentials?",
462
+ default: true
463
+ });
464
+ if (retry) {
465
+ return runSetupWizard();
466
+ }
467
+ console.log(c.yellow("\nSetup cancelled. Please check your credentials and try again."));
468
+ process.exit(1);
469
+ }
470
+ console.log("");
471
+ config.client = await select({
472
+ message: "Which AI client will you use?",
473
+ choices: [
474
+ {
475
+ name: "Claude Desktop",
476
+ value: "claude-desktop",
477
+ description: "Anthropic's official Claude app"
478
+ },
479
+ {
480
+ name: "Cursor",
481
+ value: "cursor",
482
+ description: "AI-powered code editor"
483
+ },
484
+ {
485
+ name: "Windsurf",
486
+ value: "windsurf",
487
+ description: "Codeium AI IDE with Cascade"
488
+ },
489
+ {
490
+ name: "VS Code (Copilot)",
491
+ value: "vscode-copilot",
492
+ description: "VS Code with GitHub Copilot MCP"
493
+ },
494
+ {
495
+ name: "LibreChat",
496
+ value: "librechat",
497
+ description: "Open-source chat UI (Docker)"
498
+ },
499
+ {
500
+ name: "OpenAI / HTTP Integration",
501
+ value: "openai-http",
502
+ description: "HTTP server for OpenAI function calling"
503
+ },
504
+ {
505
+ name: "Other / Manual Setup",
506
+ value: "other",
507
+ description: "Generate shell exports for manual configuration"
508
+ }
509
+ ]
510
+ });
511
+ console.log("");
512
+ const loadingStrategy = await select({
513
+ message: "How would you like to load tools?",
514
+ choices: [
515
+ {
516
+ name: `Load all tools at startup ${c.dim("(recommended)")}`,
517
+ value: "all",
518
+ description: "All 79 tools immediately available - best for most users"
519
+ },
520
+ {
521
+ name: `Load by role preset ${c.dim("(optimized)")}`,
522
+ value: "role",
523
+ description: "Load tools based on your workflow - reduces AI token usage"
524
+ },
525
+ {
526
+ name: `Lazy loading ${c.dim("(on-demand)")}`,
527
+ value: "lazy",
528
+ description: '15 core tools at start, load more with "load-module" command'
529
+ }
530
+ ]
531
+ });
532
+ if (loadingStrategy === "lazy") {
533
+ config.lazyLoading = true;
534
+ console.log(
535
+ c.gray(' \u2139\uFE0F Lazy loading: Use "list-modules" and "load-module" tools to add more tools')
536
+ );
537
+ } else if (loadingStrategy === "role") {
538
+ console.log("");
539
+ const roleChoice = await select({
540
+ message: "Select your role preset:",
541
+ choices: [
542
+ {
543
+ name: `Product Manager ${c.dim("(41 tools)")}`,
544
+ value: "product-manager",
545
+ description: "Products, inventory, collections, and metafields"
546
+ },
547
+ {
548
+ name: `Content Manager ${c.dim("(37 tools)")}`,
549
+ value: "content-manager",
550
+ description: "Pages, blogs, articles, and SEO content"
551
+ },
552
+ {
553
+ name: `International Manager ${c.dim("(46 tools)")}`,
554
+ value: "international-manager",
555
+ description: "Markets, locales, and translations"
556
+ },
557
+ {
558
+ name: `SEO Specialist ${c.dim("(38 tools)")}`,
559
+ value: "seo-specialist",
560
+ description: "SEO optimization, redirects, and URL management"
561
+ },
562
+ {
563
+ name: `Inventory Manager ${c.dim("(15 tools)")}`,
564
+ value: "inventory-manager",
565
+ description: "Core product and inventory tools only"
566
+ }
567
+ ]
568
+ });
569
+ config.role = roleChoice;
570
+ }
571
+ if (config.client === "librechat" || config.client === "openai-http" || config.client === "other") {
572
+ console.log("");
573
+ config.transport = await select({
574
+ message: "Which transport mode?",
575
+ default: config.client === "openai-http" ? "http" : "stdio",
576
+ choices: [
577
+ {
578
+ name: `STDIO ${c.dim("(recommended for most clients)")}`,
579
+ value: "stdio",
580
+ description: "Standard input/output communication"
581
+ },
582
+ {
583
+ name: `HTTP ${c.dim("(for web integrations)")}`,
584
+ value: "http",
585
+ description: "HTTP server with SSE support"
586
+ }
587
+ ]
588
+ });
589
+ if (config.transport === "http") {
590
+ const portInput = await input({
591
+ message: "HTTP server port",
592
+ default: "3000",
593
+ validate: (v) => {
594
+ const n = Number.parseInt(v, 10);
595
+ return n > 0 && n < 65536 || "Must be a valid port number";
596
+ }
597
+ });
598
+ config.port = Number.parseInt(portInput, 10);
599
+ }
600
+ }
601
+ if (config.client === "openai-http") {
602
+ config.transport = "http";
603
+ config.port = config.port || 3e3;
604
+ }
605
+ console.log("");
606
+ let configContent;
607
+ let configPath;
608
+ let configDescription;
609
+ switch (config.client) {
610
+ case "claude-desktop": {
611
+ configPath = getClaudeDesktopConfigPath();
612
+ configContent = generateJsonConfig(config);
613
+ configContent = mergeJsonConfig(configPath, configContent);
614
+ configDescription = "Claude Desktop config";
615
+ break;
616
+ }
617
+ case "cursor": {
618
+ configPath = getCursorConfigPath();
619
+ configContent = generateJsonConfig(config);
620
+ configContent = mergeJsonConfig(configPath, configContent);
621
+ configDescription = "Cursor MCP config";
622
+ break;
623
+ }
624
+ case "windsurf": {
625
+ configPath = getWindsurfConfigPath();
626
+ configContent = generateJsonConfig(config);
627
+ configContent = mergeJsonConfig(configPath, configContent);
628
+ configDescription = "Windsurf MCP config";
629
+ break;
630
+ }
631
+ case "vscode-copilot": {
632
+ configPath = getVSCodeConfigPath();
633
+ configContent = generateJsonConfig(config);
634
+ configContent = mergeJsonConfig(configPath, configContent);
635
+ configDescription = "VS Code MCP config";
636
+ break;
637
+ }
638
+ case "librechat": {
639
+ configPath = join(process.cwd(), "librechat.yaml");
640
+ configContent = generateLibreChatConfig(config);
641
+ configDescription = "LibreChat config";
642
+ break;
643
+ }
644
+ case "openai-http": {
645
+ configPath = join(process.cwd(), ".env.shopify-mcp");
646
+ configContent = generateEnvFile(config);
647
+ configDescription = "Environment file (HTTP mode)";
648
+ break;
649
+ }
650
+ default: {
651
+ configPath = join(process.cwd(), ".shopify-mcp.env");
652
+ configContent = generateShellExport(config);
653
+ configDescription = "Shell environment exports";
654
+ break;
655
+ }
656
+ }
657
+ console.log(c.cyan(`
658
+ \u{1F4C4} ${configDescription} preview:
659
+ `));
660
+ console.log(c.gray("\u2500".repeat(60)));
661
+ console.log(c.dim(configContent));
662
+ console.log(c.gray("\u2500".repeat(60)));
663
+ const shouldSave = await confirm({
664
+ message: `Save to ${configPath}?`,
665
+ default: true
666
+ });
667
+ if (shouldSave) {
668
+ ensureDirectoryExists(configPath);
669
+ writeFileSync(configPath, configContent, "utf-8");
670
+ console.log(`
671
+ ${c.green("\u2714")} Configuration saved to: ${c.cyan(configPath)}`);
672
+ } else {
673
+ console.log(`
674
+ ${c.yellow("\u2139")} Configuration not saved. You can copy the preview above.`);
675
+ }
676
+ printSuccessBox(config.client, configPath);
677
+ } catch (error) {
678
+ if (error instanceof Error && error.name === "ExitPromptError") {
679
+ console.log(c.yellow("\n\nSetup cancelled."));
680
+ process.exit(0);
681
+ }
682
+ throw error;
683
+ }
684
+ }
685
+ function isSetupCommand(args) {
686
+ return args.includes("init") || args.includes("setup") || args.includes("--setup");
687
+ }
688
+ if (process.argv[1]?.includes("setup-wizard")) {
689
+ runSetupWizard().catch((error) => {
690
+ console.error(c.red("Setup failed:"), error);
691
+ process.exit(1);
692
+ });
693
+ }
694
+ export {
695
+ isSetupCommand,
696
+ runSetupWizard
697
+ };