ebay-mcp 1.7.8 → 1.8.2

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.
@@ -6,31 +6,14 @@ import axios from 'axios';
6
6
  import chalk from 'chalk';
7
7
  import { checkForUpdates } from '../utils/version.js';
8
8
  import { config } from 'dotenv';
9
- import { exec } from 'child_process';
10
9
  import { fileURLToPath } from 'url';
11
10
  import { getOAuthAuthorizationUrl } from '../config/environment.js';
12
- import prompts from 'prompts';
11
+ import { defineWizard, runWizard, ClackRenderer } from 'grimoire-wizard';
13
12
  config({ quiet: true });
14
13
  checkForUpdates();
15
14
  const __filename = fileURLToPath(import.meta.url);
16
15
  const __dirname = dirname(__filename);
17
16
  const PROJECT_ROOT = join(__dirname, '../..');
18
- const TOTAL_STEPS = 6;
19
- const ebay = {
20
- red: chalk.hex('#E53238'),
21
- blue: chalk.hex('#0064D2'),
22
- yellow: chalk.hex('#F5AF02'),
23
- green: chalk.hex('#86B817'),
24
- };
25
- const ui = {
26
- dim: chalk.dim,
27
- bold: chalk.bold,
28
- success: chalk.green,
29
- warning: chalk.yellow,
30
- error: chalk.red,
31
- info: chalk.cyan,
32
- hint: chalk.gray,
33
- };
34
17
  const MARKETPLACE_OPTIONS = [
35
18
  { value: 'EBAY_US', label: 'EBAY_US — United States' },
36
19
  { value: 'EBAY_GB', label: 'EBAY_GB — United Kingdom' },
@@ -51,6 +34,12 @@ const CONTENT_LANGUAGE_OPTIONS = [
51
34
  { value: 'fr-CA', label: 'fr-CA — French (Canada)' },
52
35
  { value: 'nl-BE', label: 'nl-BE — Dutch (Belgium)' },
53
36
  ];
37
+ const ebay = {
38
+ red: chalk.hex('#E53238'),
39
+ blue: chalk.hex('#0064D2'),
40
+ yellow: chalk.hex('#F5AF02'),
41
+ green: chalk.hex('#86B817'),
42
+ };
54
43
  const LOGO = `
55
44
  ${ebay.red('███████╗')}${ebay.blue('██████╗ ')}${ebay.yellow('█████╗ ')}${ebay.green('██╗ ██╗')}
56
45
  ${ebay.red('██╔════╝')}${ebay.blue('██╔══██╗')}${ebay.yellow('██╔══██╗')}${ebay.green('╚██╗ ██╔╝')}
@@ -59,120 +48,35 @@ const LOGO = `
59
48
  ${ebay.red('███████╗')}${ebay.blue('██████╔╝')}${ebay.yellow('██║ ██║')}${ebay.green(' ██║ ')}
60
49
  ${ebay.red('╚══════╝')}${ebay.blue('╚═════╝ ')}${ebay.yellow('╚═╝ ╚═╝')}${ebay.green(' ╚═╝ ')}
61
50
  `;
62
- /**
63
- * Clear the terminal screen.
64
- */
65
- function clearScreen() {
66
- console.clear();
67
- }
68
- /**
69
- * Render the eBay ASCII logo and heading.
70
- */
71
- function showLogo() {
72
- console.log(LOGO);
73
- console.log(ui.bold.white(' MCP Server Setup Wizard by Yosef Hayim Sabag\n'));
74
- }
75
- /**
76
- * Render a step progress bar with title.
77
- */
78
- function showProgress(step, title) {
79
- const filled = '●'.repeat(step);
80
- const empty = '○'.repeat(TOTAL_STEPS - step);
81
- const progress = `${ui.info(filled)}${ui.dim(empty)}`;
82
- console.log(ui.dim('─'.repeat(60)));
83
- console.log(` ${progress} ${ui.bold(`Step ${step}/${TOTAL_STEPS}`)}: ${title}`);
84
- console.log(ui.dim('─'.repeat(60)) + '\n');
85
- }
86
- /**
87
- * Render keyboard hints for the current step.
88
- */
89
- function showKeyboardHints(hints) {
90
- const hintText = hints.map((h) => ui.dim(h)).join(' │ ');
91
- console.log(`\n ${hintText}\n`);
92
- }
93
- /**
94
- * Render a tip callout.
95
- */
96
- function showTip(message) {
97
- console.log(` ${ebay.yellow('💡 Tip:')} ${ui.dim(message)}\n`);
98
- }
99
- /**
100
- * Render a success line.
101
- */
102
- function showSuccess(message) {
103
- console.log(` ${ui.success('✓')} ${message}`);
104
- }
105
- /**
106
- * Render an error line.
107
- */
108
- function showError(message) {
109
- console.log(` ${ui.error('✗')} ${message}`);
110
- }
111
- /**
112
- * Open a URL in the default browser (cross-platform).
113
- */
114
- function openBrowser(url) {
115
- return new Promise((resolve, reject) => {
116
- const os = platform();
117
- let command;
118
- switch (os) {
119
- case 'darwin':
120
- command = `open "${url}"`;
121
- break;
122
- case 'win32':
123
- command = `start "" "${url}"`;
124
- break;
125
- default:
126
- command = `xdg-open "${url}"`;
127
- }
128
- exec(command, (error) => {
129
- if (error) {
130
- reject(error);
131
- }
132
- else {
133
- resolve();
134
- }
135
- });
136
- });
137
- }
138
- /**
139
- * Render a warning line.
140
- */
141
- function showWarning(message) {
142
- console.log(` ${ui.warning('⚠')} ${message}`);
143
- }
144
- /**
145
- * Parse authorization code from callback URL or raw code
146
- * Handles both full URLs and just the code parameter
147
- */
51
+ const ui = {
52
+ dim: chalk.dim,
53
+ bold: chalk.bold,
54
+ success: chalk.green,
55
+ warning: chalk.yellow,
56
+ error: chalk.red,
57
+ info: chalk.cyan,
58
+ };
59
+ // ─── Business-logic helpers (preserved from original) ─────────────────────────
148
60
  function parseAuthorizationCode(input) {
149
61
  const trimmed = input.trim();
150
- // If it looks like a URL, parse the code parameter
151
62
  if (trimmed.includes('code=') || trimmed.includes('?') || trimmed.includes('&')) {
152
63
  try {
153
- // Handle both full URLs and query strings
154
64
  let searchParams;
155
65
  if (trimmed.startsWith('http')) {
156
- const url = new URL(trimmed);
157
- searchParams = url.searchParams;
66
+ searchParams = new URL(trimmed).searchParams;
158
67
  }
159
68
  else {
160
- // It might just be query params like "code=xxx&expires_in=299"
161
69
  searchParams = new URLSearchParams(trimmed.startsWith('?') ? trimmed.slice(1) : trimmed);
162
70
  }
163
71
  const code = searchParams.get('code');
164
- if (code) {
165
- // URL decode the code (it's often URL-encoded)
72
+ if (code)
166
73
  return decodeURIComponent(code);
167
- }
168
74
  }
169
75
  catch {
170
- // Fall through to try as raw code
76
+ // fall through
171
77
  }
172
78
  }
173
- // Check if it looks like a raw authorization code (starts with v^1.1#)
174
79
  if (trimmed.startsWith('v^1.1#') || trimmed.startsWith('v%5E1.1')) {
175
- // Decode if URL-encoded
176
80
  try {
177
81
  return decodeURIComponent(trimmed);
178
82
  }
@@ -182,25 +86,10 @@ function parseAuthorizationCode(input) {
182
86
  }
183
87
  return null;
184
88
  }
185
- /**
186
- * Exchange authorization code for tokens using eBay API
187
- * This mirrors the logic in auth/oauth.ts EbayOAuthClient.exchangeCodeForToken()
188
- */
189
89
  async function exchangeAuthorizationCode(code, clientId, clientSecret, redirectUri, environment) {
190
- const tokenUrl = environment === 'production'
191
- ? 'https://api.ebay.com/identity/v1/oauth2/token'
192
- : 'https://api.sandbox.ebay.com/identity/v1/oauth2/token';
90
+ const baseUrl = environment === 'production' ? 'https://api.ebay.com' : 'https://api.sandbox.ebay.com';
193
91
  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
194
- const response = await axios.post(tokenUrl, new URLSearchParams({
195
- grant_type: 'authorization_code',
196
- code,
197
- redirect_uri: redirectUri,
198
- }).toString(), {
199
- headers: {
200
- 'Content-Type': 'application/x-www-form-urlencoded',
201
- Authorization: `Basic ${credentials}`,
202
- },
203
- });
92
+ const response = await axios.post(`${baseUrl}/identity/v1/oauth2/token`, new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: redirectUri }).toString(), { headers: { Authorization: `Basic ${credentials}`, 'Content-Type': 'application/x-www-form-urlencoded' } });
204
93
  return {
205
94
  accessToken: response.data.access_token,
206
95
  refreshToken: response.data.refresh_token,
@@ -208,142 +97,37 @@ async function exchangeAuthorizationCode(code, clientId, clientSecret, redirectU
208
97
  refreshTokenExpiresIn: response.data.refresh_token_expires_in,
209
98
  };
210
99
  }
211
- /**
212
- * Get app access token using client credentials flow
213
- * This mirrors the logic in auth/oauth.ts EbayOAuthClient.getOrRefreshAppAccessToken()
214
- */
215
100
  async function getAppAccessToken(clientId, clientSecret, environment) {
216
- const tokenUrl = environment === 'production'
217
- ? 'https://api.ebay.com/identity/v1/oauth2/token'
218
- : 'https://api.sandbox.ebay.com/identity/v1/oauth2/token';
101
+ const baseUrl = environment === 'production' ? 'https://api.ebay.com' : 'https://api.sandbox.ebay.com';
219
102
  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
220
- const response = await axios.post(tokenUrl, new URLSearchParams({
103
+ const response = await axios.post(`${baseUrl}/identity/v1/oauth2/token`, new URLSearchParams({
221
104
  grant_type: 'client_credentials',
222
105
  scope: 'https://api.ebay.com/oauth/api_scope',
223
- }).toString(), {
224
- headers: {
225
- 'Content-Type': 'application/x-www-form-urlencoded',
226
- Authorization: `Basic ${credentials}`,
227
- },
228
- });
106
+ }).toString(), { headers: { Authorization: `Basic ${credentials}`, 'Content-Type': 'application/x-www-form-urlencoded' } });
229
107
  return response.data.access_token;
230
108
  }
231
- /**
232
- * Refresh access token using refresh token
233
- */
234
- async function refreshAccessToken(refreshToken, clientId, clientSecret, environment) {
235
- const tokenUrl = environment === 'production'
236
- ? 'https://api.ebay.com/identity/v1/oauth2/token'
237
- : 'https://api.sandbox.ebay.com/identity/v1/oauth2/token';
109
+ async function verifyRefreshToken(refreshToken, clientId, clientSecret, environment) {
110
+ const baseUrl = environment === 'production' ? 'https://api.ebay.com' : 'https://api.sandbox.ebay.com';
238
111
  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
239
- const response = await axios.post(tokenUrl, new URLSearchParams({
112
+ const tokenResponse = await axios.post(`${baseUrl}/identity/v1/oauth2/token`, new URLSearchParams({
240
113
  grant_type: 'refresh_token',
241
114
  refresh_token: refreshToken,
242
- }).toString(), {
243
- headers: {
244
- 'Content-Type': 'application/x-www-form-urlencoded',
245
- Authorization: `Basic ${credentials}`,
246
- },
115
+ scope: 'https://api.ebay.com/oauth/api_scope https://api.ebay.com/oauth/api_scope/sell.inventory',
116
+ }).toString(), { headers: { Authorization: `Basic ${credentials}`, 'Content-Type': 'application/x-www-form-urlencoded' } });
117
+ const accessToken = tokenResponse.data.access_token;
118
+ const identityBase = environment === 'production' ? 'https://apiz.ebay.com' : 'https://apiz.sandbox.ebay.com';
119
+ const userResponse = await axios.get(`${identityBase}/commerce/identity/v1/user/`, {
120
+ headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
247
121
  });
248
- return {
249
- accessToken: response.data.access_token,
250
- expiresIn: response.data.expires_in,
251
- };
122
+ return { accessToken, userInfo: userResponse.data };
252
123
  }
253
- /**
254
- * Verify refresh token by getting an access token and fetching user info
255
- */
256
- async function verifyRefreshToken(refreshToken, clientId, clientSecret, environment) {
257
- // First, refresh to get an access token
258
- const { accessToken } = await refreshAccessToken(refreshToken, clientId, clientSecret, environment);
259
- // Then fetch user info to verify everything works
260
- const userInfo = await fetchEbayUserInfo(accessToken, environment);
261
- return { accessToken, userInfo };
262
- }
263
- /**
264
- * Fetch eBay user info using the Identity API
265
- * Uses apiz.ebay.com subdomain as per eBay API requirements
266
- */
267
124
  async function fetchEbayUserInfo(accessToken, environment) {
268
- const identityBaseUrl = environment === 'production' ? 'https://apiz.ebay.com' : 'https://apiz.sandbox.ebay.com';
269
- const response = await axios.get(`${identityBaseUrl}/commerce/identity/v1/user/`, {
270
- headers: {
271
- Authorization: `Bearer ${accessToken}`,
272
- 'Content-Type': 'application/json',
273
- },
125
+ const identityBase = environment === 'production' ? 'https://apiz.ebay.com' : 'https://apiz.sandbox.ebay.com';
126
+ const response = await axios.get(`${identityBase}/commerce/identity/v1/user/`, {
127
+ headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
274
128
  });
275
129
  return response.data;
276
130
  }
277
- /**
278
- * Display eBay user info in a formatted box
279
- */
280
- function displayUserInfo(userInfo) {
281
- const accountName = userInfo.individualAccount?.firstName && userInfo.individualAccount?.lastName
282
- ? `${userInfo.individualAccount.firstName} ${userInfo.individualAccount.lastName}`
283
- : userInfo.businessAccount?.name || 'N/A';
284
- const email = userInfo.individualAccount?.email || userInfo.businessAccount?.email || 'N/A';
285
- const marketplaceMap = {
286
- EBAY_US: 'eBay United States',
287
- EBAY_GB: 'eBay United Kingdom',
288
- EBAY_DE: 'eBay Germany',
289
- EBAY_AU: 'eBay Australia',
290
- EBAY_CA: 'eBay Canada',
291
- EBAY_FR: 'eBay France',
292
- EBAY_IT: 'eBay Italy',
293
- EBAY_ES: 'eBay Spain',
294
- };
295
- const marketplace = marketplaceMap[userInfo.registrationMarketplaceId || ''] ||
296
- userInfo.registrationMarketplaceId ||
297
- 'N/A';
298
- showBox('eBay Account Verified', [
299
- `Username: ${userInfo.username}`,
300
- `Account Name: ${accountName}`,
301
- `Email: ${email}`,
302
- `Account Type: ${userInfo.accountType || 'N/A'}`,
303
- `Marketplace: ${marketplace}`,
304
- `User ID: ${userInfo.userId?.slice(0, 30)}...`,
305
- ]);
306
- }
307
- /**
308
- * Render an informational line.
309
- */
310
- function showInfo(message) {
311
- console.log(` ${ui.info('ℹ')} ${message}`);
312
- }
313
- /**
314
- * Render a spinner and return a stop callback.
315
- */
316
- function showSpinner(message) {
317
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
318
- let i = 0;
319
- process.stdout.write(` ${ui.info(frames[0])} ${message}`);
320
- const interval = setInterval(() => {
321
- i = (i + 1) % frames.length;
322
- process.stdout.write(`\r ${ui.info(frames[i])} ${message}`);
323
- }, 80);
324
- return () => {
325
- clearInterval(interval);
326
- process.stdout.write('\r' + ' '.repeat(message.length + 10) + '\r');
327
- };
328
- }
329
- /**
330
- * Render a bordered info box.
331
- */
332
- function showBox(title, content) {
333
- const width = 60;
334
- const line = '─'.repeat(width - 2);
335
- console.log(`\n ${ui.dim('┌' + line + '┐')}`);
336
- console.log(` ${ui.dim('│')} ${ui.bold(title.padEnd(width - 3))}${ui.dim('│')}`);
337
- console.log(` ${ui.dim('├' + line + '┤')}`);
338
- for (const item of content) {
339
- const displayItem = item.length > width - 4 ? item.slice(0, width - 7) + '...' : item;
340
- console.log(` ${ui.dim('│')} ${displayItem.padEnd(width - 3)}${ui.dim('│')}`);
341
- }
342
- console.log(` ${ui.dim('└' + line + '┘')}\n`);
343
- }
344
- /**
345
- * Compute MCP client config paths by OS.
346
- */
347
131
  function getConfigPaths() {
348
132
  const home = homedir();
349
133
  const os = platform();
@@ -378,40 +162,23 @@ function getConfigPaths() {
378
162
  path: join(home, '.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'),
379
163
  };
380
164
  }
381
- paths.continue = {
382
- display: 'Continue.dev',
383
- path: join(home, '.continue/config.json'),
384
- };
165
+ paths.continue = { display: 'Continue.dev', path: join(home, '.continue/config.json') };
385
166
  return paths;
386
167
  }
387
- /**
388
- * Detect installed MCP-compatible clients.
389
- */
390
168
  function detectLLMClients() {
391
- const paths = getConfigPaths();
392
- const clients = [];
393
- for (const [name, info] of Object.entries(paths)) {
394
- const configExists = existsSync(info.path);
395
- const parentExists = existsSync(dirname(info.path));
396
- clients.push({
397
- name,
398
- displayName: info.display,
399
- configPath: info.path,
400
- detected: parentExists,
401
- configExists,
402
- });
403
- }
404
- return clients;
169
+ return Object.entries(getConfigPaths()).map(([name, info]) => ({
170
+ name,
171
+ displayName: info.display,
172
+ configPath: info.path,
173
+ detected: existsSync(dirname(info.path)),
174
+ configExists: existsSync(info.path),
175
+ }));
405
176
  }
406
- /**
407
- * Write MCP server config for a detected client.
408
- */
409
177
  function configureLLMClient(client, projectRoot) {
410
178
  try {
411
179
  const configDir = dirname(client.configPath);
412
- if (!existsSync(configDir)) {
180
+ if (!existsSync(configDir))
413
181
  mkdirSync(configDir, { recursive: true });
414
- }
415
182
  let existingConfig = {};
416
183
  if (existsSync(client.configPath)) {
417
184
  try {
@@ -421,24 +188,18 @@ function configureLLMClient(client, projectRoot) {
421
188
  existingConfig = {};
422
189
  }
423
190
  }
424
- const serverConfig = {
425
- command: 'node',
426
- args: [join(projectRoot, 'build/index.js')],
427
- };
191
+ const serverConfig = { command: 'node', args: [join(projectRoot, 'build/index.js')] };
428
192
  if (client.name === 'continue') {
429
193
  if (!existingConfig.experimental)
430
194
  existingConfig.experimental = {};
431
- if (!existingConfig.experimental.modelContextProtocolServers) {
195
+ if (!existingConfig.experimental.modelContextProtocolServers)
432
196
  existingConfig.experimental.modelContextProtocolServers = [];
433
- }
434
197
  const servers = existingConfig.experimental.modelContextProtocolServers;
435
- const existingIndex = servers.findIndex((s) => s?.args?.[0]?.includes('ebay-mcp'));
436
- if (existingIndex >= 0) {
437
- servers[existingIndex] = serverConfig;
438
- }
439
- else {
198
+ const idx = servers.findIndex((s) => s?.args?.[0]?.includes('ebay-mcp'));
199
+ if (idx >= 0)
200
+ servers[idx] = serverConfig;
201
+ else
440
202
  servers.push(serverConfig);
441
- }
442
203
  }
443
204
  else {
444
205
  if (!existingConfig.mcpServers)
@@ -452,151 +213,93 @@ function configureLLMClient(client, projectRoot) {
452
213
  return false;
453
214
  }
454
215
  }
455
- /**
456
- * Get Claude Desktop config path for the current platform
457
- */
458
216
  function getClaudeDesktopConfigPath() {
459
217
  const home = homedir();
460
218
  const os = platform();
461
- if (os === 'darwin') {
219
+ if (os === 'darwin')
462
220
  return join(home, 'Library/Application Support/Claude/claude_desktop_config.json');
463
- }
464
- else if (os === 'win32') {
221
+ if (os === 'win32')
465
222
  return join(home, 'AppData/Roaming/Claude/claude_desktop_config.json');
466
- }
467
- else {
468
- return join(home, '.config/Claude/claude_desktop_config.json');
469
- }
223
+ return join(home, '.config/Claude/claude_desktop_config.json');
470
224
  }
471
- /**
472
- * Check if Claude Desktop is installed
473
- */
474
225
  function isClaudeDesktopInstalled() {
475
- const configPath = getClaudeDesktopConfigPath();
476
- const configDir = dirname(configPath);
477
- return existsSync(configDir);
226
+ return existsSync(dirname(getClaudeDesktopConfigPath()));
478
227
  }
479
- /**
480
- * Update Claude Desktop config with eBay MCP server credentials
481
- * This ensures Claude Desktop has access to the verified tokens
482
- * IMPORTANT: This preserves all existing mcpServers and other config
483
- */
484
228
  function updateClaudeDesktopConfig(envConfig, environment) {
485
229
  const configPath = getClaudeDesktopConfigPath();
486
- const configDir = dirname(configPath);
487
- // Check if Claude Desktop is installed
488
- if (!existsSync(configDir)) {
230
+ if (!existsSync(dirname(configPath)))
489
231
  return { success: false, configPath, error: 'Claude Desktop not installed' };
490
- }
491
232
  try {
492
- // Read existing config - preserve everything
493
- let existingConfig = {};
233
+ let existing = {};
494
234
  if (existsSync(configPath)) {
495
235
  try {
496
- const fileContent = readFileSync(configPath, 'utf-8');
497
- existingConfig = JSON.parse(fileContent);
236
+ existing = JSON.parse(readFileSync(configPath, 'utf-8'));
498
237
  }
499
- catch (parseError) {
500
- // If JSON is invalid, start fresh but warn user
238
+ catch (e) {
501
239
  return {
502
240
  success: false,
503
241
  configPath,
504
- error: `Invalid JSON in config file: ${parseError instanceof Error ? parseError.message : 'Parse error'}`,
242
+ error: `Invalid JSON in config: ${e instanceof Error ? e.message : 'parse error'}`,
505
243
  details: 'Please fix the JSON syntax in your Claude config file',
506
244
  };
507
245
  }
508
246
  }
509
- // Ensure mcpServers exists as an object
510
- if (!existingConfig.mcpServers || typeof existingConfig.mcpServers !== 'object') {
511
- existingConfig.mcpServers = {};
512
- }
513
- const mcpServers = existingConfig.mcpServers;
514
- // Build env object with all credentials
515
- const envVars = {
516
- EBAY_ENVIRONMENT: environment,
517
- };
518
- if (envConfig.EBAY_CLIENT_ID) {
247
+ if (!existing.mcpServers || typeof existing.mcpServers !== 'object')
248
+ existing.mcpServers = {};
249
+ const mcpServers = existing.mcpServers;
250
+ const envVars = { EBAY_ENVIRONMENT: environment };
251
+ if (envConfig.EBAY_CLIENT_ID)
519
252
  envVars.EBAY_CLIENT_ID = envConfig.EBAY_CLIENT_ID;
520
- }
521
- if (envConfig.EBAY_CLIENT_SECRET) {
253
+ if (envConfig.EBAY_CLIENT_SECRET)
522
254
  envVars.EBAY_CLIENT_SECRET = envConfig.EBAY_CLIENT_SECRET;
523
- }
524
- if (envConfig.EBAY_REDIRECT_URI) {
255
+ if (envConfig.EBAY_REDIRECT_URI)
525
256
  envVars.EBAY_REDIRECT_URI = envConfig.EBAY_REDIRECT_URI;
526
- }
527
- if (envConfig.EBAY_MARKETPLACE_ID) {
257
+ if (envConfig.EBAY_MARKETPLACE_ID)
528
258
  envVars.EBAY_MARKETPLACE_ID = envConfig.EBAY_MARKETPLACE_ID;
529
- }
530
- if (envConfig.EBAY_CONTENT_LANGUAGE) {
259
+ if (envConfig.EBAY_CONTENT_LANGUAGE)
531
260
  envVars.EBAY_CONTENT_LANGUAGE = envConfig.EBAY_CONTENT_LANGUAGE;
532
- }
533
- if (envConfig.EBAY_USER_REFRESH_TOKEN) {
261
+ if (envConfig.EBAY_USER_REFRESH_TOKEN)
534
262
  envVars.EBAY_USER_REFRESH_TOKEN = envConfig.EBAY_USER_REFRESH_TOKEN;
535
- }
536
- // Only include access tokens if they exist and are not empty
537
- if (envConfig.EBAY_USER_ACCESS_TOKEN && envConfig.EBAY_USER_ACCESS_TOKEN.startsWith('v^')) {
263
+ if (envConfig.EBAY_USER_ACCESS_TOKEN?.startsWith('v^'))
538
264
  envVars.EBAY_USER_ACCESS_TOKEN = envConfig.EBAY_USER_ACCESS_TOKEN;
539
- }
540
- if (envConfig.EBAY_APP_ACCESS_TOKEN && envConfig.EBAY_APP_ACCESS_TOKEN.startsWith('v^')) {
265
+ if (envConfig.EBAY_APP_ACCESS_TOKEN?.startsWith('v^'))
541
266
  envVars.EBAY_APP_ACCESS_TOKEN = envConfig.EBAY_APP_ACCESS_TOKEN;
542
- }
543
- // Add or update only the 'ebay' server - preserve all other servers
544
- // Use npx with --yes flag and suppress npm/node output to keep stdout clean for MCP
545
267
  mcpServers['ebay'] = {
546
268
  command: 'npx',
547
269
  args: ['--yes', '--quiet', 'ebay-mcp'],
548
- env: {
549
- ...envVars,
550
- NODE_NO_WARNINGS: '1',
551
- NPM_CONFIG_UPDATE_NOTIFIER: 'false',
552
- },
270
+ env: { ...envVars, NODE_NO_WARNINGS: '1', NPM_CONFIG_UPDATE_NOTIFIER: 'false' },
553
271
  };
554
- // Write back the complete config with proper formatting
555
- writeFileSync(configPath, JSON.stringify(existingConfig, null, 2));
556
- // Count existing servers for confirmation message
557
- const serverCount = Object.keys(mcpServers).length;
272
+ writeFileSync(configPath, JSON.stringify(existing, null, 2));
558
273
  const otherServers = Object.keys(mcpServers).filter((k) => k !== 'ebay');
559
274
  return {
560
275
  success: true,
561
276
  configPath,
562
277
  details: otherServers.length > 0
563
278
  ? `Preserved ${otherServers.length} existing server(s): ${otherServers.join(', ')}`
564
- : `Added ebay server (${serverCount} total)`,
279
+ : `Added ebay server (${Object.keys(mcpServers).length} total)`,
565
280
  };
566
281
  }
567
282
  catch (error) {
568
- return {
569
- success: false,
570
- configPath,
571
- error: error instanceof Error ? error.message : 'Unknown error',
572
- };
283
+ return { success: false, configPath, error: error instanceof Error ? error.message : 'Unknown' };
573
284
  }
574
285
  }
575
- /**
576
- * Load existing environment variables from the .env file.
577
- */
578
286
  function loadExistingConfig() {
579
287
  const envPath = join(PROJECT_ROOT, '.env');
580
288
  const envConfig = {};
581
- if (existsSync(envPath)) {
582
- const content = readFileSync(envPath, 'utf-8');
583
- for (const line of content.split('\n')) {
584
- if (line.trim() && !line.startsWith('#')) {
585
- const [key, ...valueParts] = line.split('=');
586
- const value = valueParts.join('=').trim();
587
- if (key && value && !value.includes('_here')) {
588
- envConfig[key.trim()] = value;
589
- }
590
- }
289
+ if (!existsSync(envPath))
290
+ return envConfig;
291
+ for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
292
+ if (line.trim() && !line.startsWith('#')) {
293
+ const [key, ...valueParts] = line.split('=');
294
+ const value = valueParts.join('=').trim();
295
+ if (key && value && !value.includes('_here'))
296
+ envConfig[key.trim()] = value;
591
297
  }
592
298
  }
593
299
  return envConfig;
594
300
  }
595
- /**
596
- * Format a date for display in the .env header.
597
- */
598
301
  function formatDate(date) {
599
- const options = {
302
+ return date.toLocaleString('en-US', {
600
303
  weekday: 'long',
601
304
  year: 'numeric',
602
305
  month: 'long',
@@ -605,23 +308,18 @@ function formatDate(date) {
605
308
  minute: '2-digit',
606
309
  second: '2-digit',
607
310
  timeZoneName: 'short',
608
- };
609
- return date.toLocaleString('en-US', options);
311
+ });
610
312
  }
611
- /**
612
- * Persist setup configuration to the project .env file.
613
- */
614
313
  function saveConfig(envConfig, environment) {
615
314
  const envPath = join(PROJECT_ROOT, '.env');
616
- const now = new Date();
617
315
  const marketplaceLine = envConfig.EBAY_MARKETPLACE_ID
618
316
  ? `EBAY_MARKETPLACE_ID=${envConfig.EBAY_MARKETPLACE_ID}`
619
317
  : '# EBAY_MARKETPLACE_ID=EBAY_US';
620
318
  const contentLanguageLine = envConfig.EBAY_CONTENT_LANGUAGE
621
319
  ? `EBAY_CONTENT_LANGUAGE=${envConfig.EBAY_CONTENT_LANGUAGE}`
622
320
  : '# EBAY_CONTENT_LANGUAGE=en-US';
623
- const content = `# eBay MCP Server Configuration
624
- # Last Updated: ${formatDate(now)}
321
+ writeFileSync(envPath, `# eBay MCP Server Configuration
322
+ # Last Updated: ${formatDate(new Date())}
625
323
  # Environment: ${environment}
626
324
 
627
325
  EBAY_CLIENT_ID=${envConfig.EBAY_CLIENT_ID || ''}
@@ -634,12 +332,68 @@ ${contentLanguageLine}
634
332
  EBAY_USER_REFRESH_TOKEN=${envConfig.EBAY_USER_REFRESH_TOKEN || ''}
635
333
  EBAY_USER_ACCESS_TOKEN=${envConfig.EBAY_USER_ACCESS_TOKEN || ''}
636
334
  EBAY_APP_ACCESS_TOKEN=${envConfig.EBAY_APP_ACCESS_TOKEN || ''}
637
- `;
638
- writeFileSync(envPath, content, 'utf-8');
335
+ `, 'utf-8');
336
+ }
337
+ function showInfo(message) {
338
+ console.log(` ${ui.info('ℹ')} ${message}`);
339
+ }
340
+ function showWarning(message) {
341
+ console.log(` ${ui.warning('⚠')} ${message}`);
342
+ }
343
+ function showSuccess(message) {
344
+ console.log(` ${ui.success('✓')} ${message}`);
345
+ }
346
+ function showError(message) {
347
+ console.log(` ${ui.error('✗')} ${message}`);
348
+ }
349
+ function showBox(title, content) {
350
+ const width = 54;
351
+ const line = '─'.repeat(width);
352
+ console.log(` ${ui.dim('┌─')} ${ui.bold(title)} ${ui.dim('─'.repeat(width - title.length - 2))}┐`);
353
+ for (const item of content) {
354
+ const displayItem = item.length > width - 2 ? item.slice(0, width - 5) + '...' : item;
355
+ console.log(` ${ui.dim('│')} ${displayItem.padEnd(width)}${ui.dim('│')}`);
356
+ }
357
+ console.log(` ${ui.dim('└' + line + '┘')}\n`);
358
+ }
359
+ function showSpinner(message) {
360
+ console.log(` ⠋ ${message}...`);
361
+ return () => { };
639
362
  }
640
- async function stepWelcome(state) {
641
- clearScreen();
642
- showLogo();
363
+ function displayUserInfo(userInfo) {
364
+ const name = userInfo.individualAccount?.firstName && userInfo.individualAccount?.lastName
365
+ ? `${userInfo.individualAccount.firstName} ${userInfo.individualAccount.lastName}`
366
+ : userInfo.businessAccount?.name || 'N/A';
367
+ const email = userInfo.individualAccount?.email || userInfo.businessAccount?.email || 'N/A';
368
+ const marketplaceMap = {
369
+ EBAY_US: 'eBay United States', EBAY_GB: 'eBay United Kingdom',
370
+ EBAY_DE: 'eBay Germany', EBAY_AU: 'eBay Australia',
371
+ EBAY_CA: 'eBay Canada', EBAY_FR: 'eBay France',
372
+ EBAY_IT: 'eBay Italy', EBAY_ES: 'eBay Spain',
373
+ };
374
+ const marketplace = marketplaceMap[userInfo.registrationMarketplaceId || ''] ||
375
+ userInfo.registrationMarketplaceId ||
376
+ 'N/A';
377
+ console.log(` ${ui.dim('┌─ eBay Account Verified ─────────────────────────')}`);
378
+ console.log(` ${ui.dim('│')} Username: ${userInfo.username}`);
379
+ console.log(` ${ui.dim('│')} Name: ${name}`);
380
+ console.log(` ${ui.dim('│')} Email: ${email}`);
381
+ console.log(` ${ui.dim('│')} Type: ${userInfo.accountType || 'N/A'}`);
382
+ console.log(` ${ui.dim('│')} Marketplace: ${marketplace}`);
383
+ console.log(` ${ui.dim('└─────────────────────────────────────────────────')}`);
384
+ console.log('');
385
+ }
386
+ // ─── Grimoire wizard ──────────────────────────────────────────────────────────
387
+ export async function runSetup() {
388
+ const existingConfig = loadExistingConfig();
389
+ const detectedClients = detectLLMClients();
390
+ const availableClients = detectedClients.filter((c) => c.detected);
391
+ const tokens = {};
392
+ const finalStepId = availableClients.length > 0 ? 'mcp-clients' : 'no-mcp-clients';
393
+ const args = parseArgs();
394
+ console.log(LOGO);
395
+ console.log(ui.bold.white(' MCP Server Setup Wizard by Yosef Hayim Sabag'));
396
+ console.log(ui.dim(' Powered by ') + chalk.hex('#0064D2').bold('grimoire-wizard') + '\n');
643
397
  console.log(ui.dim(' Welcome to the eBay MCP Server setup wizard!\n'));
644
398
  console.log(' This wizard will help you:\n');
645
399
  console.log(` ${ui.success('1.')} Choose environment (sandbox/production)`);
@@ -648,759 +402,425 @@ async function stepWelcome(state) {
648
402
  console.log(` ${ui.success('4.')} Set up OAuth authentication`);
649
403
  console.log(` ${ui.success('5.')} Configure your MCP client (Claude, Cline, etc.)`);
650
404
  console.log(` ${ui.success('6.')} Validate your setup\n`);
651
- if (state.hasExistingConfig) {
405
+ if (Object.keys(existingConfig).length > 0) {
652
406
  showInfo('Existing configuration detected. You can update or keep current values.');
407
+ console.log('');
653
408
  }
654
- showKeyboardHints(['Enter: Continue', 'Ctrl+C: Exit']);
655
- const response = await prompts({
656
- type: 'confirm',
657
- name: 'continue',
658
- message: 'Ready to begin?',
659
- initial: true,
660
- });
661
- return response.continue !== false ? 'continue' : 'cancel';
662
- }
663
- /**
664
- * Select the eBay environment for this configuration.
665
- */
666
- async function stepEnvironment(state) {
667
- clearScreen();
668
- showLogo();
669
- showProgress(1, 'Select Environment');
670
- console.log(' Choose which eBay environment to configure:\n');
671
- showBox('Environment Options', [
672
- '🧪 Sandbox - For development & testing',
673
- ' • Free test transactions',
674
- ' No real money involved',
675
- ' Separate test accounts',
676
- '',
677
- '🚀 Production - For live trading',
678
- ' • Real transactions',
679
- ' • Actual eBay marketplace',
680
- ' • Requires approved app',
681
- ]);
682
- showTip('Start with Sandbox to test your integration safely.');
683
- const response = await prompts({
684
- type: 'select',
685
- name: 'environment',
686
- message: 'Select environment:',
687
- choices: [
688
- { title: '🧪 Sandbox (Recommended for testing)', value: 'sandbox' },
689
- { title: '🚀 Production (Live trading)', value: 'production' },
690
- { title: ui.dim('← Go back'), value: 'back' },
691
- ],
692
- initial: state.config.EBAY_ENVIRONMENT === 'production' ? 1 : 0,
693
- });
694
- if (!response.environment)
695
- return 'cancel';
696
- if (response.environment === 'back')
697
- return 'back';
698
- state.environment = response.environment;
699
- state.config.EBAY_ENVIRONMENT = response.environment;
700
- return 'continue';
701
- }
702
- /**
703
- * Configure optional marketplace and content-language defaults.
704
- */
705
- async function stepMarketplaceSettings(state) {
706
- clearScreen();
707
- showLogo();
708
- showProgress(2, 'Marketplace Settings');
709
- if (state.isQuickMode) {
710
- showInfo('Quick setup enabled. Skipping optional marketplace configuration.');
711
- await new Promise((r) => setTimeout(r, 600));
712
- return 'continue';
713
- }
714
- console.log(' Configure default marketplace and language for API requests.\n');
715
- showBox('Marketplace Settings', [
716
- 'These are optional defaults used for request headers.',
717
- 'Marketplace can be overridden in many tools; language is global.',
718
- ]);
719
- const marketplaceChoices = [
720
- { title: ui.dim('← Go back'), value: '__back__' },
721
- { title: 'Skip (leave unset)', value: '' },
722
- ...MARKETPLACE_OPTIONS.map((option) => ({
723
- title: option.label,
724
- value: option.value,
725
- })),
726
- { title: 'Other (enter manually)', value: '__custom__' },
727
- ];
728
- const currentMarketplace = state.config.EBAY_MARKETPLACE_ID || '';
729
- const marketplaceDefault = currentMarketplace || 'EBAY_US';
730
- const marketplaceMatchIndex = marketplaceChoices.findIndex((choice) => choice.value === marketplaceDefault);
731
- const marketplaceInitial = marketplaceMatchIndex >= 0
732
- ? marketplaceMatchIndex
733
- : marketplaceChoices.findIndex((choice) => choice.value === '__custom__');
734
- const marketplaceResponse = await prompts({
735
- type: 'select',
736
- name: 'marketplaceId',
737
- message: 'Select your default eBay marketplace:',
738
- choices: marketplaceChoices,
739
- initial: marketplaceInitial >= 0 ? marketplaceInitial : 0,
740
- });
741
- if (marketplaceResponse.marketplaceId === undefined) {
742
- return 'cancel';
743
- }
744
- if (marketplaceResponse.marketplaceId === '__back__') {
745
- return 'back';
746
- }
747
- let marketplaceId = marketplaceResponse.marketplaceId;
748
- if (marketplaceId === '__custom__') {
749
- const customMarketplace = await prompts({
750
- type: 'text',
751
- name: 'customMarketplaceId',
752
- message: 'Enter marketplace ID (e.g., EBAY_US, EBAY_DE):',
753
- initial: currentMarketplace || 'EBAY_US',
754
- validate: (value) => value.trim().length === 0 ? 'Marketplace ID cannot be empty' : true,
755
- });
756
- if (customMarketplace.customMarketplaceId === undefined) {
757
- return 'cancel';
758
- }
759
- marketplaceId = customMarketplace.customMarketplaceId.trim();
760
- }
761
- if (marketplaceId) {
762
- state.config.EBAY_MARKETPLACE_ID = marketplaceId;
763
- }
764
- else {
765
- delete state.config.EBAY_MARKETPLACE_ID;
766
- }
767
- const languageChoices = [
768
- { title: ui.dim(' Go back'), value: '__back__' },
769
- { title: 'Skip (leave unset)', value: '' },
770
- ...CONTENT_LANGUAGE_OPTIONS.map((option) => ({
771
- title: option.label,
772
- value: option.value,
773
- })),
774
- { title: 'Other (enter manually)', value: '__custom__' },
775
- ];
776
- const currentLanguage = state.config.EBAY_CONTENT_LANGUAGE || '';
777
- const languageDefault = currentLanguage || 'en-US';
778
- const languageMatchIndex = languageChoices.findIndex((choice) => choice.value === languageDefault);
779
- const languageInitial = languageMatchIndex >= 0
780
- ? languageMatchIndex
781
- : languageChoices.findIndex((choice) => choice.value === '__custom__');
782
- const languageResponse = await prompts({
783
- type: 'select',
784
- name: 'contentLanguage',
785
- message: 'Select your preferred Content-Language:',
786
- choices: languageChoices,
787
- initial: languageInitial >= 0 ? languageInitial : 0,
788
- });
789
- if (languageResponse.contentLanguage === undefined) {
790
- return 'cancel';
791
- }
792
- if (languageResponse.contentLanguage === '__back__') {
793
- return 'back';
794
- }
795
- let contentLanguage = languageResponse.contentLanguage;
796
- if (contentLanguage === '__custom__') {
797
- const customLanguage = await prompts({
798
- type: 'text',
799
- name: 'customContentLanguage',
800
- message: 'Enter Content-Language (e.g., en-US, de-DE):',
801
- initial: currentLanguage || 'en-US',
802
- validate: (value) => value.trim().length === 0 ? 'Content-Language cannot be empty' : true,
803
- });
804
- if (customLanguage.customContentLanguage === undefined) {
805
- return 'cancel';
806
- }
807
- contentLanguage = customLanguage.customContentLanguage.trim();
808
- }
809
- if (contentLanguage) {
810
- state.config.EBAY_CONTENT_LANGUAGE = contentLanguage;
811
- }
812
- else {
813
- delete state.config.EBAY_CONTENT_LANGUAGE;
814
- }
815
- return 'continue';
816
- }
817
- /**
818
- * Collect eBay app credentials from the user.
819
- */
820
- async function stepCredentials(state) {
821
- clearScreen();
822
- showLogo();
823
- showProgress(3, 'eBay Credentials');
824
- console.log(' Enter your eBay Developer credentials:\n');
825
- showTip('Get credentials at: https://developer.ebay.com/my/keys');
826
- showKeyboardHints(['Tab: Next field', 'Enter: Submit', 'Ctrl+C: Cancel']);
827
- // First ask if they want to go back
828
- const navChoice = await prompts({
829
- type: 'select',
830
- name: 'action',
831
- message: 'What would you like to do?',
832
- choices: [
833
- { title: '📝 Enter/update credentials', value: 'enter' },
834
- { title: ui.dim('← Go back'), value: 'back' },
409
+ const wizardConfig = defineWizard({
410
+ meta: {
411
+ name: 'eBay MCP',
412
+ description: 'Server Setup Wizard — powered by grimoire',
413
+ },
414
+ theme: {
415
+ tokens: {
416
+ primary: '#0064D2',
417
+ success: '#86B817',
418
+ warning: '#F5AF02',
419
+ error: '#E53238',
420
+ },
421
+ },
422
+ steps: [
423
+ {
424
+ id: 'environment',
425
+ type: 'select',
426
+ message: 'Select eBay environment:',
427
+ description: '🧪 Sandbox for testing • 🚀 Production for live trading',
428
+ options: [
429
+ { value: 'sandbox', label: '🧪 Sandbox — development & testing (recommended)' },
430
+ { value: 'production', label: '🚀 Production — live trading' },
431
+ ],
432
+ default: existingConfig.EBAY_ENVIRONMENT || 'sandbox',
433
+ },
434
+ {
435
+ id: 'marketplace',
436
+ type: 'select',
437
+ message: 'Default eBay marketplace:',
438
+ description: 'Used as a default header for API requests — optional',
439
+ options: [
440
+ { value: '', label: 'Skip — leave unset' },
441
+ ...MARKETPLACE_OPTIONS.map((o) => ({ value: o.value, label: o.label })),
442
+ { value: '__custom__', label: 'Other — enter manually' },
443
+ ],
444
+ default: existingConfig.EBAY_MARKETPLACE_ID || 'EBAY_US',
445
+ },
446
+ {
447
+ id: 'marketplace-custom',
448
+ type: 'text',
449
+ message: 'Enter marketplace ID:',
450
+ description: 'e.g. EBAY_US, EBAY_DE',
451
+ when: { field: 'marketplace', equals: '__custom__' },
452
+ default: existingConfig.EBAY_MARKETPLACE_ID || 'EBAY_US',
453
+ validate: [{ rule: 'required' }],
454
+ },
455
+ {
456
+ id: 'content-language',
457
+ type: 'select',
458
+ message: 'Default Content-Language:',
459
+ description: 'Used as a default header for API requests — optional',
460
+ options: [
461
+ { value: '', label: 'Skip — leave unset' },
462
+ ...CONTENT_LANGUAGE_OPTIONS.map((o) => ({ value: o.value, label: o.label })),
463
+ { value: '__custom__', label: 'Other — enter manually' },
464
+ ],
465
+ default: existingConfig.EBAY_CONTENT_LANGUAGE || 'en-US',
466
+ },
467
+ {
468
+ id: 'content-language-custom',
469
+ type: 'text',
470
+ message: 'Enter Content-Language:',
471
+ description: 'e.g. en-US, de-DE',
472
+ when: { field: 'content-language', equals: '__custom__' },
473
+ default: existingConfig.EBAY_CONTENT_LANGUAGE || 'en-US',
474
+ validate: [{ rule: 'required' }],
475
+ },
476
+ {
477
+ id: 'client-id',
478
+ type: 'text',
479
+ message: 'Client ID (App ID):',
480
+ description: 'From https://developer.ebay.com/my/keys',
481
+ default: existingConfig.EBAY_CLIENT_ID || '',
482
+ validate: [{ rule: 'required' }],
483
+ },
484
+ {
485
+ id: 'client-secret',
486
+ type: 'password',
487
+ message: 'Client Secret (Cert ID):',
488
+ description: existingConfig.EBAY_CLIENT_SECRET
489
+ ? 'Press Enter to keep existing secret'
490
+ : 'From https://developer.ebay.com/my/keys',
491
+ required: existingConfig.EBAY_CLIENT_SECRET ? false : true,
492
+ },
493
+ {
494
+ id: 'redirect-uri',
495
+ type: 'text',
496
+ message: 'Redirect URI (RuName):',
497
+ description: 'The RuName configured in your eBay developer account',
498
+ default: existingConfig.EBAY_REDIRECT_URI || '',
499
+ validate: [{ rule: 'required' }],
500
+ },
501
+ {
502
+ id: 'oauth-method',
503
+ type: 'select',
504
+ message: 'Set up OAuth for higher API rate limits:',
505
+ description: 'App credentials: 1,000 req/day • User OAuth: 10,000–50,000 req/day',
506
+ options: [
507
+ { value: 'existing', label: '📝 I have a refresh token' },
508
+ { value: 'manual', label: '🔗 Generate OAuth URL (opens browser)' },
509
+ { value: 'code', label: '🔑 Paste authorization code (already have code)' },
510
+ { value: 'skip', label: '⏭️ Skip for now (1,000 req/day limit)' },
511
+ ],
512
+ },
513
+ {
514
+ id: 'oauth-token',
515
+ type: 'text',
516
+ message: 'Paste your refresh token:',
517
+ description: 'Should start with v^1.1#',
518
+ default: '',
519
+ },
520
+ {
521
+ id: 'oauth-code',
522
+ type: 'text',
523
+ message: 'Paste the callback URL or authorization code:',
524
+ description: 'Copy the full redirect URL or just the code= parameter',
525
+ default: '',
526
+ },
527
+ ...(availableClients.length > 0
528
+ ? [
529
+ {
530
+ id: 'mcp-clients',
531
+ type: 'multiselect',
532
+ message: 'Configure MCP clients:',
533
+ description: 'Select AI assistants to connect to the eBay MCP server',
534
+ required: false,
535
+ options: availableClients.map((c) => ({
536
+ value: c.name,
537
+ label: `${c.displayName}${c.configExists ? ' (update)' : ' (new)'}`,
538
+ })),
539
+ },
540
+ ]
541
+ : [
542
+ {
543
+ id: 'no-mcp-clients',
544
+ type: 'note',
545
+ message: 'No MCP clients detected',
546
+ description: 'Install Claude Desktop, Cline (VSCode), or Continue.dev, then run setup again.',
547
+ },
548
+ ]),
835
549
  ],
836
- initial: 0,
837
550
  });
838
- if (!navChoice.action)
839
- return 'cancel';
840
- if (navChoice.action === 'back')
841
- return 'back';
842
- const responses = await prompts([
843
- {
844
- type: 'text',
845
- name: 'clientId',
846
- message: 'Client ID (App ID):',
847
- initial: state.config.EBAY_CLIENT_ID || '',
848
- validate: (v) => v.trim().length > 0 || 'Required',
849
- },
850
- {
851
- type: 'password',
852
- name: 'clientSecret',
853
- message: 'Client Secret (Cert ID):',
854
- initial: state.config.EBAY_CLIENT_SECRET || '',
855
- validate: (v) => v.trim().length > 0 || 'Required',
551
+ const answers = await runWizard(wizardConfig, {
552
+ renderer: new ClackRenderer(),
553
+ quiet: true,
554
+ optionsProvider: async (stepId) => {
555
+ if (stepId === 'oauth-method') {
556
+ const hasToken = existingConfig.EBAY_USER_REFRESH_TOKEN?.startsWith('v^1.1#');
557
+ if (hasToken) {
558
+ return [
559
+ { value: 'keep', label: '✓ Keep and verify existing token' },
560
+ { value: 'existing', label: '📝 Replace with a different refresh token' },
561
+ { value: 'manual', label: '🔗 Generate OAuth URL (opens browser)' },
562
+ { value: 'code', label: '🔑 Paste authorization code' },
563
+ { value: 'skip', label: '⏭️ Skip for now' },
564
+ ];
565
+ }
566
+ }
567
+ return undefined;
856
568
  },
857
- {
858
- type: 'text',
859
- name: 'redirectUri',
860
- message: 'Redirect URI (RuName):',
861
- initial: state.config.EBAY_REDIRECT_URI || '',
862
- validate: (v) => v.trim().length > 0 || 'Required',
569
+ asyncValidate: async (stepId, value) => {
570
+ if (stepId === 'oauth-token') {
571
+ const clean = String(value).trim().replace(/^["']|["']$/g, '');
572
+ if (!clean)
573
+ return 'Token is required';
574
+ if (!clean.startsWith('v^1.1#'))
575
+ return 'Token should start with v^1.1#';
576
+ }
577
+ if (stepId === 'oauth-code') {
578
+ const code = parseAuthorizationCode(String(value));
579
+ if (!code)
580
+ return 'Could not find authorization code. Paste the full redirect URL or the code parameter.';
581
+ }
582
+ return null;
863
583
  },
864
- ]);
865
- if (!responses.clientId)
866
- return 'cancel';
867
- state.config.EBAY_CLIENT_ID = responses.clientId;
868
- state.config.EBAY_CLIENT_SECRET = responses.clientSecret;
869
- state.config.EBAY_REDIRECT_URI = responses.redirectUri;
870
- return 'continue';
871
- }
872
- /**
873
- * Acquire and validate OAuth tokens for the configured credentials.
874
- */
875
- async function stepOAuth(state) {
876
- clearScreen();
877
- showLogo();
878
- showProgress(4, 'OAuth Setup');
879
- console.log(' Configure user authentication for higher API rate limits:\n');
880
- showBox('Rate Limits by Auth Type', [
881
- 'App Credentials Only: 1,000 req/day',
882
- 'User Token (OAuth): 10,000-50,000 req/day',
883
- '',
884
- 'User tokens require completing an OAuth flow.',
885
- ]);
886
- const hasToken = state.config.EBAY_USER_REFRESH_TOKEN?.startsWith('v^1.1#');
887
- if (hasToken) {
888
- showSuccess('Existing refresh token detected.');
889
- const keepToken = await prompts({
890
- type: 'select',
891
- name: 'action',
892
- message: 'What would you like to do?',
893
- choices: [
894
- { title: '✓ Keep and verify existing token', value: 'keep' },
895
- { title: '🔄 Set up new OAuth token', value: 'new' },
896
- { title: ui.dim('← Go back'), value: 'back' },
897
- ],
898
- initial: 0,
899
- });
900
- if (!keepToken.action)
901
- return 'cancel';
902
- if (keepToken.action === 'back')
903
- return 'back';
904
- if (keepToken.action === 'keep') {
905
- // Verify the existing token works by fetching user info
906
- console.log('\n ' + ui.info('Verifying existing refresh token...'));
907
- try {
908
- const { accessToken, userInfo } = await verifyRefreshToken(state.config.EBAY_USER_REFRESH_TOKEN, state.config.EBAY_CLIENT_ID, state.config.EBAY_CLIENT_SECRET, state.environment);
909
- showSuccess('Refresh token verified successfully!');
910
- state.config.EBAY_USER_ACCESS_TOKEN = accessToken;
911
- displayUserInfo(userInfo);
912
- // Update Claude Desktop config if installed
913
- if (isClaudeDesktopInstalled()) {
914
- console.log(' ' + ui.info('Updating Claude Desktop configuration...'));
915
- const claudeResult = updateClaudeDesktopConfig(state.config, state.environment);
916
- if (claudeResult.success) {
917
- showSuccess('Claude Desktop config updated!');
918
- if (claudeResult.details) {
919
- showInfo(claudeResult.details);
584
+ onAfterStep: async (stepId, value, context) => {
585
+ const a = context.answers;
586
+ const environment = a['environment'] || 'sandbox';
587
+ const clientId = a['client-id'] || existingConfig.EBAY_CLIENT_ID || '';
588
+ const clientSecret = a['client-secret'] || existingConfig.EBAY_CLIENT_SECRET || '';
589
+ const redirectUri = a['redirect-uri'] || existingConfig.EBAY_REDIRECT_URI || '';
590
+ if (stepId === 'environment' && args.quick) {
591
+ showInfo('Quick setup enabled — skipping optional marketplace configuration.');
592
+ context.setNextStep('client-id');
593
+ }
594
+ // ── oauth-method: dispatch to correct sub-flow ─────────────────────────
595
+ if (stepId === 'oauth-method') {
596
+ const method = value;
597
+ if (method === 'keep') {
598
+ const stopSpinner = showSpinner('Verifying existing refresh token...');
599
+ try {
600
+ const { accessToken, userInfo } = await verifyRefreshToken(existingConfig.EBAY_USER_REFRESH_TOKEN, clientId, clientSecret, environment);
601
+ stopSpinner();
602
+ showSuccess('Refresh token verified!');
603
+ tokens.refreshToken = existingConfig.EBAY_USER_REFRESH_TOKEN;
604
+ tokens.accessToken = accessToken;
605
+ displayUserInfo(userInfo);
606
+ try {
607
+ tokens.appAccessToken = await getAppAccessToken(clientId, clientSecret, environment);
608
+ showSuccess('App access token obtained.');
920
609
  }
921
- showInfo(`Config: ${claudeResult.configPath}`);
922
- }
923
- else {
924
- showError(`Could not update Claude Desktop: ${claudeResult.error}`);
925
- if (claudeResult.details) {
926
- showInfo(claudeResult.details);
610
+ catch {
611
+ showWarning('Could not obtain app access token (user tokens still work).');
612
+ }
613
+ if (isClaudeDesktopInstalled()) {
614
+ const r = updateClaudeDesktopConfig({ ...a, EBAY_USER_REFRESH_TOKEN: tokens.refreshToken ?? '' }, environment);
615
+ if (r.success) {
616
+ showSuccess('Claude Desktop config updated.');
617
+ if (r.details)
618
+ showInfo(r.details);
619
+ }
620
+ else
621
+ showWarning(`Could not update Claude Desktop: ${r.error}`);
927
622
  }
928
623
  }
624
+ catch (error) {
625
+ stopSpinner();
626
+ const msg = axios.isAxiosError(error)
627
+ ? error.response?.data?.error_description || error.response?.data?.errors?.[0]?.message || error.message
628
+ : error instanceof Error ? error.message : 'Unknown error';
629
+ showError(`Token verification failed: ${msg}`);
630
+ if (msg.toLowerCase().includes('access denied'))
631
+ showWarning('Token may be missing required OAuth scopes.');
632
+ else
633
+ showWarning('Existing token may be expired or invalid.');
634
+ showInfo('Continuing with existing token — re-run setup to refresh it.');
635
+ tokens.refreshToken = existingConfig.EBAY_USER_REFRESH_TOKEN;
636
+ }
637
+ context.setNextStep(finalStepId);
929
638
  }
930
- else {
931
- showInfo('Claude Desktop not detected. You can configure it manually later.');
639
+ else if (method === 'existing') {
640
+ context.setNextStep('oauth-token');
932
641
  }
933
- console.log('');
934
- showKeyboardHints(['Enter: Continue to next step']);
935
- await prompts({ type: 'text', name: 'continue', message: 'Press Enter to continue...' });
936
- }
937
- catch (error) {
938
- const errorMsg = axios.isAxiosError(error)
939
- ? error.response?.data?.error_description ||
940
- error.response?.data?.errors?.[0]?.message ||
941
- error.message
942
- : error instanceof Error
943
- ? error.message
944
- : 'Unknown error';
945
- showError(`Token verification failed: ${errorMsg}`);
946
- // Provide specific guidance based on error type
947
- if (errorMsg.toLowerCase().includes('access denied')) {
948
- showWarning('Your token may be missing required OAuth scopes.');
949
- showInfo('Generating a new token via OAuth URL will include all necessary scopes.');
642
+ else if (method === 'manual') {
643
+ const authUrl = getOAuthAuthorizationUrl(clientId, redirectUri, environment);
644
+ context.showNote('OAuth Authorization URL', authUrl);
645
+ await context.openBrowser(authUrl);
646
+ showInfo('1. Sign in to your eBay account in the browser');
647
+ showInfo('2. Grant permissions to your app');
648
+ showInfo('3. Copy the redirect URL or the code parameter, then paste it below');
649
+ console.log('');
650
+ context.setNextStep('oauth-code');
950
651
  }
951
- else {
952
- showWarning('Your existing refresh token may be expired or invalid.');
652
+ else if (method === 'code') {
653
+ context.setNextStep('oauth-code');
953
654
  }
954
- const continueAnyway = await prompts({
955
- type: 'confirm',
956
- name: 'continue',
957
- message: 'Would you like to set up a new OAuth token?',
958
- initial: true,
959
- });
960
- if (!continueAnyway.continue) {
961
- showInfo("Keeping existing token. You may need to re-authenticate if it doesn't work.");
962
- return 'continue';
655
+ else if (method === 'skip') {
656
+ showWarning("Skipping OAuth — you'll be limited to 1,000 requests/day.");
657
+ context.setNextStep(finalStepId);
963
658
  }
964
- // Clear invalid tokens so we fall through to OAuth setup options
965
- state.config.EBAY_USER_ACCESS_TOKEN = '';
966
- state.config.EBAY_USER_REFRESH_TOKEN = '';
967
- }
968
- if (state.config.EBAY_USER_ACCESS_TOKEN) {
969
- return 'continue';
970
659
  }
971
- }
972
- // User chose 'new' - fall through to OAuth setup options
973
- }
974
- // Check if we just cleared tokens due to failure (recommend OAuth URL in that case)
975
- const hadTokenFailure = hasToken && !state.config.EBAY_USER_REFRESH_TOKEN;
976
- const tokenChoice = await prompts({
977
- type: 'select',
978
- name: 'method',
979
- message: 'How would you like to set up OAuth?',
980
- choices: hadTokenFailure
981
- ? [
982
- // After token failure, prioritize OAuth URL to get fresh token with proper scopes
983
- { title: '🔗 Generate OAuth URL (recommended)', value: 'manual' },
984
- { title: '🔑 Paste authorization code (already have code)', value: 'code' },
985
- { title: '📝 I have a different refresh token', value: 'existing' },
986
- { title: '⏭️ Skip for now (1k req/day limit)', value: 'skip' },
987
- { title: ui.dim('← Go back'), value: 'back' },
988
- ]
989
- : [
990
- { title: '📝 I have a refresh token', value: 'existing' },
991
- { title: '🔗 Generate OAuth URL (opens browser)', value: 'manual' },
992
- { title: '🔑 Paste authorization code (already have code)', value: 'code' },
993
- { title: '⏭️ Skip for now (1k req/day limit)', value: 'skip' },
994
- { title: ui.dim('← Go back'), value: 'back' },
995
- ],
996
- initial: 0,
997
- });
998
- if (!tokenChoice.method)
999
- return 'cancel';
1000
- if (tokenChoice.method === 'back')
1001
- return 'back';
1002
- if (tokenChoice.method === 'existing') {
1003
- const tokenInput = await prompts({
1004
- type: 'text',
1005
- name: 'token',
1006
- message: 'Paste your refresh token:',
1007
- validate: (v) => {
1008
- const clean = v.trim().replace(/^["']|["']$/g, '');
1009
- if (!clean)
1010
- return 'Token is required';
1011
- if (!clean.startsWith('v^1.1#'))
1012
- return 'Token should start with v^1.1#';
1013
- return true;
1014
- },
1015
- });
1016
- if (tokenInput.token) {
1017
- const cleanToken = tokenInput.token.trim().replace(/^["']|["']$/g, '');
1018
- state.config.EBAY_USER_REFRESH_TOKEN = cleanToken;
1019
- // Verify the pasted token works
1020
- console.log('\n ' + ui.info('Verifying refresh token...'));
1021
- try {
1022
- const { accessToken, userInfo } = await verifyRefreshToken(cleanToken, state.config.EBAY_CLIENT_ID, state.config.EBAY_CLIENT_SECRET, state.environment);
1023
- showSuccess('Refresh token verified successfully!');
1024
- state.config.EBAY_USER_ACCESS_TOKEN = accessToken;
1025
- displayUserInfo(userInfo);
1026
- // Get app access token too
1027
- console.log(' ' + ui.info('Getting app access token...'));
660
+ // ── oauth-token: verify pasted refresh token ───────────────────────────
661
+ if (stepId === 'oauth-token') {
662
+ const rawToken = String(value).trim().replace(/^["']|["']$/g, '');
663
+ tokens.refreshToken = rawToken;
664
+ const stopSpinner = showSpinner('Verifying refresh token...');
1028
665
  try {
1029
- const appToken = await getAppAccessToken(state.config.EBAY_CLIENT_ID, state.config.EBAY_CLIENT_SECRET, state.environment);
1030
- state.config.EBAY_APP_ACCESS_TOKEN = appToken;
1031
- showSuccess('App access token obtained!');
1032
- }
1033
- catch {
1034
- showWarning('Could not get app access token (user tokens will still work).');
1035
- }
1036
- // Update Claude Desktop config if installed
1037
- if (isClaudeDesktopInstalled()) {
1038
- console.log(' ' + ui.info('Updating Claude Desktop configuration...'));
1039
- const claudeResult = updateClaudeDesktopConfig(state.config, state.environment);
1040
- if (claudeResult.success) {
1041
- showSuccess('Claude Desktop config updated with credentials!');
1042
- showInfo(`Config: ${claudeResult.configPath}`);
666
+ const { accessToken, userInfo } = await verifyRefreshToken(rawToken, clientId, clientSecret, environment);
667
+ stopSpinner();
668
+ showSuccess('Refresh token verified!');
669
+ tokens.accessToken = accessToken;
670
+ displayUserInfo(userInfo);
671
+ try {
672
+ tokens.appAccessToken = await getAppAccessToken(clientId, clientSecret, environment);
673
+ showSuccess('App access token obtained.');
1043
674
  }
1044
- else {
1045
- showWarning(`Could not update Claude Desktop: ${claudeResult.error}`);
675
+ catch {
676
+ showWarning('Could not obtain app access token (user tokens still work).');
677
+ }
678
+ if (isClaudeDesktopInstalled()) {
679
+ const r = updateClaudeDesktopConfig({ ...a, EBAY_USER_REFRESH_TOKEN: rawToken }, environment);
680
+ if (r.success) {
681
+ showSuccess('Claude Desktop config updated.');
682
+ if (r.details)
683
+ showInfo(r.details);
684
+ }
685
+ else
686
+ showWarning(`Could not update Claude Desktop: ${r.error}`);
1046
687
  }
1047
688
  }
1048
- console.log('\n ' + ui.success('✓') + ' OAuth setup complete!\n');
1049
- showKeyboardHints(['Enter: Continue to next step']);
1050
- await prompts({ type: 'text', name: 'continue', message: 'Press Enter to continue...' });
1051
- }
1052
- catch (error) {
1053
- const errorMsg = axios.isAxiosError(error)
1054
- ? error.response?.data?.error_description ||
1055
- error.response?.data?.errors?.[0]?.message ||
1056
- error.message
1057
- : error instanceof Error
1058
- ? error.message
1059
- : 'Unknown error';
1060
- showError(`Token verification failed: ${errorMsg}`);
1061
- showWarning('The refresh token may be expired or invalid.');
1062
- showInfo("Token saved anyway. You may need to generate a new token if it doesn't work.\n");
1063
- showKeyboardHints(['Enter: Continue to next step']);
1064
- await prompts({ type: 'text', name: 'continue', message: 'Press Enter to continue...' });
1065
- }
1066
- }
1067
- }
1068
- else if (tokenChoice.method === 'manual') {
1069
- const authUrl = getOAuthAuthorizationUrl(state.config.EBAY_CLIENT_ID, state.config.EBAY_REDIRECT_URI, state.environment);
1070
- console.log('\n ' + ui.bold('OAuth Authorization URL:'));
1071
- console.log(ui.dim(' ' + '─'.repeat(56)));
1072
- console.log(` ${ui.info(authUrl)}`);
1073
- console.log(ui.dim(' ' + '─'.repeat(56)));
1074
- // Automatically open the URL in the browser
1075
- console.log('\n ' + ui.info('Opening browser...'));
1076
- try {
1077
- await openBrowser(authUrl);
1078
- showSuccess('Browser opened successfully!');
1079
- }
1080
- catch {
1081
- showWarning('Could not open browser automatically. Please copy the URL above.');
1082
- }
1083
- console.log('\n ' + ui.bold('Steps:'));
1084
- console.log(' 1. Sign in to your eBay account in the browser');
1085
- console.log(' 2. Grant permissions to your app');
1086
- console.log(' 3. You will be redirected - copy the URL or code parameter');
1087
- console.log(' 4. Paste it below to complete the setup\n');
1088
- // Ask for the callback URL or code
1089
- const codeInput = await prompts({
1090
- type: 'text',
1091
- name: 'code',
1092
- message: 'Paste the callback URL or authorization code:',
1093
- validate: (v) => {
1094
- if (!v.trim())
1095
- return 'Please paste the URL or code from the callback';
1096
- const code = parseAuthorizationCode(v);
1097
- if (!code)
1098
- return 'Could not find authorization code. Paste the full URL or the code parameter.';
1099
- return true;
1100
- },
1101
- });
1102
- if (!codeInput.code) {
1103
- showWarning('OAuth setup cancelled.');
1104
- return 'continue';
1105
- }
1106
- const authCode = parseAuthorizationCode(codeInput.code);
1107
- if (!authCode) {
1108
- showError('Could not parse authorization code.');
1109
- return 'continue';
1110
- }
1111
- // Exchange code for tokens
1112
- console.log('\n ' + ui.info('Exchanging authorization code for tokens...'));
1113
- try {
1114
- const tokens = await exchangeAuthorizationCode(authCode, state.config.EBAY_CLIENT_ID, state.config.EBAY_CLIENT_SECRET, state.config.EBAY_REDIRECT_URI, state.environment);
1115
- showSuccess('Authorization code exchanged successfully!');
1116
- // Store all user tokens in state.config (will be saved to .env by saveConfig)
1117
- state.config.EBAY_USER_REFRESH_TOKEN = tokens.refreshToken;
1118
- state.config.EBAY_USER_ACCESS_TOKEN = tokens.accessToken;
1119
- // Verify setup by fetching user info (optional - requires identity scope)
1120
- console.log(' ' + ui.info('Verifying setup by fetching your eBay account info...'));
1121
- try {
1122
- const userInfo = await fetchEbayUserInfo(tokens.accessToken, state.environment);
1123
- showSuccess('Account verified successfully!');
1124
- displayUserInfo(userInfo);
1125
- }
1126
- catch (userError) {
1127
- const userErrorMsg = axios.isAxiosError(userError)
1128
- ? userError.response?.data?.errors?.[0]?.message || userError.message
1129
- : userError instanceof Error
1130
- ? userError.message
1131
- : 'Unknown error';
1132
- showWarning(`Could not fetch user info: ${userErrorMsg}`);
1133
- if (userErrorMsg.toLowerCase().includes('access denied')) {
1134
- showInfo('This is normal if your RuName does not include the commerce.identity.readonly scope.');
1135
- showInfo('Your OAuth tokens are valid and all other APIs will work correctly.');
1136
- }
1137
- else {
1138
- showInfo('OAuth tokens were saved successfully. You can still use the MCP server.');
1139
- }
1140
- }
1141
- // Also get app access token for client credentials flow
1142
- console.log(' ' + ui.info('Getting app access token...'));
1143
- try {
1144
- const appToken = await getAppAccessToken(state.config.EBAY_CLIENT_ID, state.config.EBAY_CLIENT_SECRET, state.environment);
1145
- state.config.EBAY_APP_ACCESS_TOKEN = appToken;
1146
- showSuccess('App access token obtained!');
1147
- }
1148
- catch {
1149
- showWarning('Could not get app access token (user tokens will still work).');
1150
- }
1151
- // Update Claude Desktop config if installed
1152
- if (isClaudeDesktopInstalled()) {
1153
- console.log(' ' + ui.info('Updating Claude Desktop configuration...'));
1154
- const claudeResult = updateClaudeDesktopConfig(state.config, state.environment);
1155
- if (claudeResult.success) {
1156
- showSuccess('Claude Desktop config updated with credentials!');
1157
- showInfo(`Config: ${claudeResult.configPath}`);
1158
- }
1159
- else {
1160
- showWarning(`Could not update Claude Desktop: ${claudeResult.error}`);
689
+ catch (error) {
690
+ stopSpinner();
691
+ const msg = axios.isAxiosError(error)
692
+ ? error.response?.data?.error_description || error.response?.data?.errors?.[0]?.message || error.message
693
+ : error instanceof Error ? error.message : 'Unknown error';
694
+ showError(`Token verification failed: ${msg}`);
695
+ showWarning("Token saved anyway — you may need to re-authenticate if it doesn't work.");
1161
696
  }
697
+ context.setNextStep(finalStepId);
1162
698
  }
1163
- console.log('\n ' + ui.success('✓') + ' OAuth setup complete!');
1164
- console.log(` ${ui.dim('Access token expires in:')} ${Math.floor(tokens.expiresIn / 60)} minutes`);
1165
- console.log(` ${ui.dim('Refresh token expires in:')} ${Math.floor(tokens.refreshTokenExpiresIn / 60 / 60 / 24)} days`);
1166
- console.log(` ${ui.dim('All tokens will be saved to .env')}\n`);
1167
- showKeyboardHints(['Enter: Continue to next step']);
1168
- await prompts({ type: 'text', name: 'continue', message: 'Press Enter to continue...' });
1169
- }
1170
- catch (error) {
1171
- const errorMsg = axios.isAxiosError(error)
1172
- ? error.response?.data?.error_description || error.message
1173
- : error instanceof Error
1174
- ? error.message
1175
- : 'Unknown error';
1176
- showError(`Failed to exchange code: ${errorMsg}`);
1177
- console.log('\n ' + ui.dim('Common issues:'));
1178
- console.log(' • Authorization code expired (codes are valid for ~5 minutes)');
1179
- console.log(' • Code was already used (each code can only be used once)');
1180
- console.log(' • Redirect URI mismatch (must match exactly what is configured in eBay)\n');
1181
- showKeyboardHints(['Enter: Continue']);
1182
- await prompts({ type: 'text', name: 'continue', message: 'Press Enter to continue...' });
1183
- }
1184
- }
1185
- else if (tokenChoice.method === 'code') {
1186
- // Direct code paste - user already has an authorization code
1187
- console.log('\n ' + ui.bold('Paste Authorization Code'));
1188
- console.log(ui.dim(' If you already completed OAuth in a browser and have the code from the callback URL.\n'));
1189
- const codeInput = await prompts({
1190
- type: 'text',
1191
- name: 'code',
1192
- message: 'Paste the callback URL or authorization code:',
1193
- validate: (v) => {
1194
- if (!v.trim())
1195
- return 'Please paste the URL or code from the callback';
1196
- const code = parseAuthorizationCode(v);
1197
- if (!code)
1198
- return 'Could not find authorization code. Paste the full URL or the code parameter.';
1199
- return true;
1200
- },
1201
- });
1202
- if (!codeInput.code) {
1203
- showWarning('OAuth setup cancelled.');
1204
- return 'continue';
1205
- }
1206
- const authCode = parseAuthorizationCode(codeInput.code);
1207
- if (!authCode) {
1208
- showError('Could not parse authorization code.');
1209
- return 'continue';
1210
- }
1211
- // Exchange code for tokens
1212
- console.log('\n ' + ui.info('Exchanging authorization code for tokens...'));
1213
- try {
1214
- const tokens = await exchangeAuthorizationCode(authCode, state.config.EBAY_CLIENT_ID, state.config.EBAY_CLIENT_SECRET, state.config.EBAY_REDIRECT_URI, state.environment);
1215
- showSuccess('Authorization code exchanged successfully!');
1216
- // Store all user tokens in state.config (will be saved to .env by saveConfig)
1217
- state.config.EBAY_USER_REFRESH_TOKEN = tokens.refreshToken;
1218
- state.config.EBAY_USER_ACCESS_TOKEN = tokens.accessToken;
1219
- // Verify setup by fetching user info (optional - requires identity scope)
1220
- console.log(' ' + ui.info('Verifying setup by fetching your eBay account info...'));
1221
- try {
1222
- const userInfo = await fetchEbayUserInfo(tokens.accessToken, state.environment);
1223
- showSuccess('Account verified successfully!');
1224
- displayUserInfo(userInfo);
1225
- }
1226
- catch (userError) {
1227
- const userErrorMsg = axios.isAxiosError(userError)
1228
- ? userError.response?.data?.errors?.[0]?.message || userError.message
1229
- : userError instanceof Error
1230
- ? userError.message
1231
- : 'Unknown error';
1232
- showWarning(`Could not fetch user info: ${userErrorMsg}`);
1233
- if (userErrorMsg.toLowerCase().includes('access denied')) {
1234
- showInfo('This is normal if your RuName does not include the commerce.identity.readonly scope.');
1235
- showInfo('Your OAuth tokens are valid and all other APIs will work correctly.');
699
+ // ── oauth-code: exchange authorization code for tokens ─────────────────
700
+ if (stepId === 'oauth-code') {
701
+ const authCode = parseAuthorizationCode(String(value));
702
+ if (!authCode)
703
+ return;
704
+ const stopSpinner = showSpinner('Exchanging authorization code for tokens...');
705
+ try {
706
+ const result = await exchangeAuthorizationCode(authCode, clientId, clientSecret, redirectUri, environment);
707
+ stopSpinner();
708
+ showSuccess('Authorization code exchanged successfully!');
709
+ tokens.refreshToken = result.refreshToken;
710
+ tokens.accessToken = result.accessToken;
711
+ try {
712
+ const userInfo = await fetchEbayUserInfo(result.accessToken, environment);
713
+ showSuccess('Account verified!');
714
+ displayUserInfo(userInfo);
715
+ }
716
+ catch (userError) {
717
+ const userMsg = axios.isAxiosError(userError)
718
+ ? userError.response?.data?.errors?.[0]?.message || userError.message
719
+ : userError instanceof Error ? userError.message : 'Unknown';
720
+ showWarning(`Could not fetch account info: ${userMsg}`);
721
+ if (userMsg.toLowerCase().includes('access denied'))
722
+ showInfo('Normal if RuName lacks commerce.identity.readonly scope tokens are valid.');
723
+ }
724
+ try {
725
+ tokens.appAccessToken = await getAppAccessToken(clientId, clientSecret, environment);
726
+ showSuccess('App access token obtained.');
727
+ }
728
+ catch {
729
+ showWarning('Could not obtain app access token (user tokens still work).');
730
+ }
731
+ showInfo(`Access token expires in: ${Math.floor(result.expiresIn / 60)} minutes`);
732
+ showInfo(`Refresh token expires in: ${Math.floor(result.refreshTokenExpiresIn / 60 / 60 / 24)} days`);
733
+ if (isClaudeDesktopInstalled()) {
734
+ const r = updateClaudeDesktopConfig({
735
+ ...a,
736
+ EBAY_USER_REFRESH_TOKEN: tokens.refreshToken ?? '',
737
+ EBAY_USER_ACCESS_TOKEN: tokens.accessToken ?? '',
738
+ }, environment);
739
+ if (r.success) {
740
+ showSuccess('Claude Desktop config updated.');
741
+ if (r.details)
742
+ showInfo(r.details);
743
+ }
744
+ else
745
+ showWarning(`Could not update Claude Desktop: ${r.error}`);
746
+ }
1236
747
  }
1237
- else {
1238
- showInfo('OAuth tokens were saved successfully. You can still use the MCP server.');
748
+ catch (error) {
749
+ stopSpinner();
750
+ const msg = axios.isAxiosError(error)
751
+ ? error.response?.data?.error_description || error.message
752
+ : error instanceof Error ? error.message : 'Unknown error';
753
+ showError(`Failed to exchange code: ${msg}`);
754
+ console.log(' Common issues:');
755
+ console.log(' • Authorization code expired (codes are valid for ~5 minutes)');
756
+ console.log(' • Code was already used (each code can only be used once)');
757
+ console.log(' • Redirect URI mismatch\n');
1239
758
  }
759
+ context.setNextStep(finalStepId);
1240
760
  }
1241
- // Also get app access token for client credentials flow
1242
- console.log(' ' + ui.info('Getting app access token...'));
1243
- try {
1244
- const appToken = await getAppAccessToken(state.config.EBAY_CLIENT_ID, state.config.EBAY_CLIENT_SECRET, state.environment);
1245
- state.config.EBAY_APP_ACCESS_TOKEN = appToken;
1246
- showSuccess('App access token obtained!');
1247
- }
1248
- catch {
1249
- showWarning('Could not get app access token (user tokens will still work).');
1250
- }
1251
- // Update Claude Desktop config if installed
1252
- if (isClaudeDesktopInstalled()) {
1253
- console.log(' ' + ui.info('Updating Claude Desktop configuration...'));
1254
- const claudeResult = updateClaudeDesktopConfig(state.config, state.environment);
1255
- if (claudeResult.success) {
1256
- showSuccess('Claude Desktop config updated with credentials!');
1257
- showInfo(`Config: ${claudeResult.configPath}`);
1258
- }
1259
- else {
1260
- showWarning(`Could not update Claude Desktop: ${claudeResult.error}`);
761
+ // ── mcp-clients: configure selected clients ────────────────────────────
762
+ if (stepId === 'mcp-clients') {
763
+ const selectedNames = value;
764
+ for (const name of selectedNames) {
765
+ const client = detectedClients.find((c) => c.name === name);
766
+ if (!client)
767
+ continue;
768
+ const stopSpinner = showSpinner(`Configuring ${client.displayName}...`);
769
+ await new Promise((r) => setTimeout(r, 400));
770
+ const success = configureLLMClient(client, PROJECT_ROOT);
771
+ stopSpinner();
772
+ if (success)
773
+ showSuccess(`Configured ${client.displayName}`);
774
+ else
775
+ showError(`Failed to configure ${client.displayName}`);
1261
776
  }
1262
777
  }
1263
- console.log('\n ' + ui.success('✓') + ' OAuth setup complete!');
1264
- console.log(` ${ui.dim('Access token expires in:')} ${Math.floor(tokens.expiresIn / 60)} minutes`);
1265
- console.log(` ${ui.dim('Refresh token expires in:')} ${Math.floor(tokens.refreshTokenExpiresIn / 60 / 60 / 24)} days`);
1266
- console.log(` ${ui.dim('All tokens will be saved to .env')}\n`);
1267
- showKeyboardHints(['Enter: Continue to next step']);
1268
- await prompts({ type: 'text', name: 'continue', message: 'Press Enter to continue...' });
1269
- }
1270
- catch (error) {
1271
- const errorMsg = axios.isAxiosError(error)
1272
- ? error.response?.data?.error_description || error.message
1273
- : error instanceof Error
1274
- ? error.message
1275
- : 'Unknown error';
1276
- showError(`Failed to exchange code: ${errorMsg}`);
1277
- console.log('\n ' + ui.dim('Common issues:'));
1278
- console.log(' • Authorization code expired (codes are valid for ~5 minutes)');
1279
- console.log(' • Code was already used (each code can only be used once)');
1280
- console.log(' • Redirect URI mismatch (must match exactly what is configured in eBay)\n');
1281
- showKeyboardHints(['Enter: Continue']);
1282
- await prompts({ type: 'text', name: 'continue', message: 'Press Enter to continue...' });
1283
- }
1284
- }
1285
- else if (tokenChoice.method === 'skip') {
1286
- showWarning("Skipping OAuth. You'll be limited to 1,000 requests/day.");
1287
- }
1288
- return 'continue';
1289
- }
1290
- /**
1291
- * Configure MCP clients with the generated environment variables.
1292
- */
1293
- async function stepMCPClients(state) {
1294
- clearScreen();
1295
- showLogo();
1296
- showProgress(5, 'MCP Client Setup');
1297
- console.log(' Configure your AI assistant to use the eBay MCP server:\n');
1298
- state.detectedClients = detectLLMClients();
1299
- const detected = state.detectedClients.filter((c) => c.detected);
1300
- if (detected.length === 0) {
1301
- showWarning('No supported MCP clients detected.\n');
1302
- console.log(' Supported clients:');
1303
- console.log(' • Claude Desktop (Anthropic)');
1304
- console.log(' • Cline (VSCode extension)');
1305
- console.log(' • Continue.dev (VSCode/JetBrains)\n');
1306
- showInfo('Install one of these clients and run setup again.');
1307
- const navChoice = await prompts({
1308
- type: 'select',
1309
- name: 'action',
1310
- message: 'What would you like to do?',
1311
- choices: [
1312
- { title: '→ Continue to finish setup', value: 'continue' },
1313
- { title: ui.dim('← Go back'), value: 'back' },
1314
- ],
1315
- initial: 0,
1316
- });
1317
- if (!navChoice.action)
1318
- return 'cancel';
1319
- if (navChoice.action === 'back')
1320
- return 'back';
1321
- return 'continue';
1322
- }
1323
- showBox('Detected MCP Clients', detected.map((c) => {
1324
- const status = c.configExists
1325
- ? `${c.displayName} [Already configured]`
1326
- : `${c.displayName} [Not configured]`;
1327
- return status;
1328
- }));
1329
- // First ask if they want to configure or go back
1330
- const navChoice = await prompts({
1331
- type: 'select',
1332
- name: 'action',
1333
- message: 'What would you like to do?',
1334
- choices: [
1335
- { title: '⚙️ Configure MCP clients', value: 'configure' },
1336
- { title: '⏭️ Skip client configuration', value: 'skip' },
1337
- { title: ui.dim('← Go back'), value: 'back' },
1338
- ],
1339
- initial: 0,
1340
- });
1341
- if (!navChoice.action)
1342
- return 'cancel';
1343
- if (navChoice.action === 'back')
1344
- return 'back';
1345
- if (navChoice.action === 'skip') {
1346
- showInfo('Skipping client configuration.');
1347
- return 'continue';
1348
- }
1349
- const clientChoice = await prompts({
1350
- type: 'multiselect',
1351
- name: 'clients',
1352
- message: 'Select clients to configure:',
1353
- choices: detected.map((c) => ({
1354
- title: c.displayName + (c.configExists ? chalk.yellow(' [Update]') : chalk.green(' [New]')),
1355
- value: c.name,
1356
- selected: !c.configExists,
1357
- })),
1358
- hint: 'Space: Toggle Enter: Confirm',
1359
- instructions: false,
778
+ },
779
+ onCancel: () => {
780
+ console.log(ui.warning('\n Setup cancelled.\n'));
781
+ process.exit(0);
782
+ },
1360
783
  });
1361
- if (!clientChoice.clients || clientChoice.clients.length === 0) {
1362
- showInfo('Skipping client configuration.');
1363
- return 'continue';
1364
- }
1365
- console.log('');
1366
- for (const clientName of clientChoice.clients) {
1367
- const client = detected.find((c) => c.name === clientName);
1368
- if (!client)
1369
- continue;
1370
- const stopSpinner = showSpinner(`Configuring ${client.displayName}...`);
1371
- await new Promise((r) => setTimeout(r, 500));
1372
- const success = configureLLMClient(client, PROJECT_ROOT);
1373
- stopSpinner();
1374
- if (success) {
1375
- showSuccess(`Configured ${client.displayName}`);
1376
- }
1377
- else {
1378
- showError(`Failed to configure ${client.displayName}`);
1379
- }
1380
- }
1381
- return 'continue';
1382
- }
1383
- /**
1384
- * Finalize setup and display summary information.
1385
- */
1386
- async function stepComplete(state) {
1387
- clearScreen();
1388
- showLogo();
1389
- showProgress(6, 'Setup Complete');
1390
- const stopSpinner = showSpinner('Saving configuration...');
784
+ // ── Persist final .env ─────────────────────────────────────────────────────
785
+ const marketplaceId = answers['marketplace'] === '__custom__'
786
+ ? answers['marketplace-custom']
787
+ : answers['marketplace'];
788
+ const contentLanguage = answers['content-language'] === '__custom__'
789
+ ? answers['content-language-custom']
790
+ : answers['content-language'];
791
+ const environment = answers['environment'];
792
+ const finalConfig = {
793
+ EBAY_CLIENT_ID: answers['client-id'],
794
+ EBAY_CLIENT_SECRET: answers['client-secret'] || existingConfig.EBAY_CLIENT_SECRET || '',
795
+ EBAY_REDIRECT_URI: answers['redirect-uri'],
796
+ EBAY_ENVIRONMENT: environment,
797
+ ...(marketplaceId ? { EBAY_MARKETPLACE_ID: marketplaceId } : {}),
798
+ ...(contentLanguage ? { EBAY_CONTENT_LANGUAGE: contentLanguage } : {}),
799
+ ...(tokens.refreshToken
800
+ ? { EBAY_USER_REFRESH_TOKEN: tokens.refreshToken }
801
+ : existingConfig.EBAY_USER_REFRESH_TOKEN
802
+ ? { EBAY_USER_REFRESH_TOKEN: existingConfig.EBAY_USER_REFRESH_TOKEN }
803
+ : {}),
804
+ ...(tokens.accessToken ? { EBAY_USER_ACCESS_TOKEN: tokens.accessToken } : {}),
805
+ ...(tokens.appAccessToken ? { EBAY_APP_ACCESS_TOKEN: tokens.appAccessToken } : {}),
806
+ };
807
+ const stopSave = showSpinner('Saving configuration...');
1391
808
  await new Promise((r) => setTimeout(r, 300));
1392
- saveConfig(state.config, state.environment);
1393
- stopSpinner();
809
+ saveConfig(finalConfig, environment);
810
+ stopSave();
1394
811
  showSuccess('Configuration saved to .env\n');
812
+ console.log(LOGO);
813
+ console.log(ui.bold.white(' MCP Server Setup Wizard by Yosef Hayim Sabag'));
814
+ console.log(ui.dim(' Powered by ') + chalk.hex('#0064D2').bold('grimoire-wizard') + '\n');
1395
815
  console.log(ui.bold.green('\n 🎉 Setup Complete!\n'));
1396
816
  showBox('Configuration Summary', [
1397
- `Environment: ${state.environment}`,
1398
- `Marketplace ID: ${state.config.EBAY_MARKETPLACE_ID || 'Not set'}`,
1399
- `Content-Lang: ${state.config.EBAY_CONTENT_LANGUAGE || 'Not set'}`,
1400
- `Client ID: ${state.config.EBAY_CLIENT_ID?.slice(0, 20)}...`,
1401
- `Redirect URI: ${state.config.EBAY_REDIRECT_URI?.slice(0, 30)}...`,
1402
- `OAuth Token: ${state.config.EBAY_USER_REFRESH_TOKEN ? '✓ Configured' : '✗ Not set'}`,
1403
- `Rate Limit: ${state.config.EBAY_USER_REFRESH_TOKEN ? '10k-50k/day' : '1k/day'}`,
817
+ `Environment: ${environment}`,
818
+ `Marketplace ID: ${finalConfig.EBAY_MARKETPLACE_ID || 'Not set'}`,
819
+ `Content-Lang: ${finalConfig.EBAY_CONTENT_LANGUAGE || 'Not set'}`,
820
+ `Client ID: ${(finalConfig.EBAY_CLIENT_ID || '').slice(0, 20)}...`,
821
+ `Redirect URI: ${(finalConfig.EBAY_REDIRECT_URI || '').slice(0, 30)}...`,
822
+ `OAuth Token: ${finalConfig.EBAY_USER_REFRESH_TOKEN ? '✓ Configured' : '✗ Not set'}`,
823
+ `Rate Limit: ${finalConfig.EBAY_USER_REFRESH_TOKEN ? '10k-50k/day' : '1k/day'}`,
1404
824
  ]);
1405
825
  console.log(ui.bold.cyan('\n 📋 Quick Reference\n'));
1406
826
  console.log(' ' + ui.dim('─'.repeat(56)));
@@ -1416,6 +836,7 @@ async function stepComplete(state) {
1416
836
  console.log(ui.dim(' Documentation: ') + ui.info('https://github.com/YosefHayim/ebay-mcp'));
1417
837
  console.log(ui.dim(' Get Help: ') + ui.info('https://github.com/YosefHayim/ebay-mcp/issues\n'));
1418
838
  }
839
+ // ─── Entry point ──────────────────────────────────────────────────────────────
1419
840
  function parseArgs() {
1420
841
  const args = process.argv.slice(2);
1421
842
  return {
@@ -1426,14 +847,14 @@ function parseArgs() {
1426
847
  }
1427
848
  function showHelp() {
1428
849
  console.log(`
1429
- ${chalk.bold('eBay MCP Server Setup')}
850
+ ${chalk.bold('eBay MCP Server Setup')} ${chalk.dim('powered by grimoire-wizard')}
1430
851
 
1431
852
  ${chalk.bold('Usage:')}
1432
853
  npm run setup [options]
1433
854
 
1434
855
  ${chalk.bold('Options:')}
1435
856
  --help, -h Show this help message
1436
- --quick, -q Quick setup (skip optional steps)
857
+ --quick, -q Quick setup (skip optional configuration)
1437
858
  --diagnose, -d Run diagnostics only
1438
859
 
1439
860
  ${chalk.bold('Examples:')}
@@ -1451,8 +872,7 @@ async function main() {
1451
872
  if (args.diagnose) {
1452
873
  const { runSecurityChecks, displaySecurityResults } = await import('../utils/security-checker.js');
1453
874
  const { validateSetup, displayRecommendations } = await import('../utils/setup-validator.js');
1454
- clearScreen();
1455
- showLogo();
875
+ console.clear();
1456
876
  console.log(ui.bold.cyan(' Running Diagnostics...\n'));
1457
877
  const securityResults = await runSecurityChecks(PROJECT_ROOT);
1458
878
  displaySecurityResults(securityResults);
@@ -1460,50 +880,16 @@ async function main() {
1460
880
  displayRecommendations(summary);
1461
881
  process.exit(0);
1462
882
  }
1463
- const existingConfig = loadExistingConfig();
1464
- const state = {
1465
- currentStep: 0,
1466
- config: existingConfig,
1467
- detectedClients: [],
1468
- environment: existingConfig.EBAY_ENVIRONMENT || 'sandbox',
1469
- hasExistingConfig: Object.keys(existingConfig).length > 0,
1470
- isQuickMode: args.quick,
1471
- };
1472
- const steps = [
1473
- stepWelcome,
1474
- stepEnvironment,
1475
- stepMarketplaceSettings,
1476
- stepCredentials,
1477
- stepOAuth,
1478
- stepMCPClients,
1479
- ];
1480
- let stepIndex = 0;
1481
- while (stepIndex < steps.length) {
1482
- const result = await steps[stepIndex](state);
1483
- if (result === 'cancel') {
1484
- console.log(ui.warning('\n Setup cancelled.\n'));
1485
- process.exit(0);
1486
- }
1487
- else if (result === 'back' && stepIndex > 0) {
1488
- stepIndex--;
1489
- }
1490
- else {
1491
- stepIndex++;
1492
- }
1493
- }
1494
- await stepComplete(state);
883
+ await runSetup();
1495
884
  }
1496
885
  process.on('SIGINT', () => {
1497
886
  console.log(ui.warning('\n\n Setup interrupted.\n'));
1498
887
  process.exit(0);
1499
888
  });
1500
- export async function runSetup() {
1501
- await main();
1502
- }
1503
889
  const entryPath = process.argv[1] ? resolve(process.argv[1]) : undefined;
1504
890
  const modulePath = resolve(fileURLToPath(import.meta.url));
1505
891
  if (entryPath && modulePath === entryPath) {
1506
- runSetup().catch((error) => {
892
+ main().catch((error) => {
1507
893
  console.error(ui.error('\n Setup failed:'), error);
1508
894
  process.exit(1);
1509
895
  });