ebay-mcp 1.18.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +34 -16
  2. package/build/api/account-management/account.js +2 -2
  3. package/build/api/account-management/accountV2.js +0 -6
  4. package/build/api/account-management/finances.js +7 -5
  5. package/build/api/client/ebayApiClient.js +111 -10
  6. package/build/api/client/signEbayRequest.js +124 -0
  7. package/build/api/communication/feedback.js +4 -4
  8. package/build/api/developer/developer.js +43 -7
  9. package/build/api/ebaySellerApi.js +0 -4
  10. package/build/api/marketing-and-promotions/campaigns.js +0 -34
  11. package/build/api/order-management/dispute.js +29 -20
  12. package/build/api/order-management/fulfillment.js +2 -1
  13. package/build/api/other/edelivery.js +3 -3
  14. package/build/api/other/translation.js +1 -1
  15. package/build/config/environment.js +31 -3
  16. package/build/schemas/account-management/account.js +1 -1
  17. package/build/schemas/account-management/accountV2.js +3 -4
  18. package/build/schemas/communication/messages.js +2 -2
  19. package/build/schemas/marketing/campaigns.js +0 -21
  20. package/build/schemas/other/otherApis.js +2 -85
  21. package/build/scripts/devSync.js +75 -225
  22. package/build/scripts/specCoverage.js +78 -0
  23. package/build/scripts/specDownload.js +42 -0
  24. package/build/scripts/specRefs.js +2 -3
  25. package/build/scripts/specSources.js +4 -6
  26. package/build/scripts/specTypeNames.js +23 -0
  27. package/build/skills/content.js +2 -2
  28. package/build/tools/categories/account.js +3 -3
  29. package/build/tools/categories/communication.js +5 -5
  30. package/build/tools/categories/developer.js +1 -1
  31. package/build/tools/categories/finances.js +2 -2
  32. package/build/tools/categories/marketing.js +0 -16
  33. package/build/tools/categories/other.js +3 -17
  34. package/build/types/ebay.js +0 -1
  35. package/build/types/ebayEnums.js +0 -40
  36. package/build/ui/card.html +23 -23
  37. package/build/ui/chart.html +22 -22
  38. package/build/ui/stat.html +23 -23
  39. package/build/ui/table.html +21 -21
  40. package/build/utils/communication/feedback.js +3 -3
  41. package/build/utils/errors.js +1 -0
  42. package/package.json +5 -5
  43. package/build/api/other/compliance.js +0 -60
  44. package/build/utils/other/compliance.js +0 -56
@@ -3,13 +3,15 @@ import chalk from 'chalk';
3
3
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
4
4
  import { join, dirname, basename } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- import { execSync } from 'child_process';
6
+ import { execFileSync, execSync } from 'child_process';
7
7
  import { Effect, Either } from 'effect';
8
8
  import { getErrorMessage } from '../utils/errors.js';
9
- import { httpRequest } from '../utils/http.js';
10
9
  import process from 'node:process';
10
+ import { classifyOperation, collectImplementedNames, normalizeForMatching, } from './specCoverage.js';
11
+ import { refreshCachedSpec } from './specDownload.js';
11
12
  import { replaceDanglingSchemaRefs } from './specRefs.js';
12
- import { specSources } from './specSources.js';
13
+ import { specFileName, specSources } from './specSources.js';
14
+ import { generatedNamespaceNames, renameGeneratedNamespaces } from './specTypeNames.js';
13
15
  const __filename = fileURLToPath(import.meta.url);
14
16
  const __dirname = dirname(__filename);
15
17
  const PROJECT_ROOT = join(__dirname, '../..');
@@ -17,25 +19,6 @@ const PROJECT_ROOT = join(__dirname, '../..');
17
19
  const SPECS_DIR = join(PROJECT_ROOT, '.cache/ebay-specs');
18
20
  const TYPES_DIR = join(PROJECT_ROOT, 'src/types');
19
21
  const TOOLS_DIRS = [join(PROJECT_ROOT, 'src/tools/categories')];
20
- /** Repo-specific OpenAPI namespace aliases preserved after type regeneration. */
21
- const GENERATED_TYPE_ALIASES = {
22
- developerAnalyticsV1BetaOas3: {
23
- components: 'DeveloperAnalyticsComponents',
24
- operations: 'DeveloperAnalyticsOperations',
25
- },
26
- developerClientRegistrationV1Oas3: {
27
- components: 'DeveloperClientRegistrationComponents',
28
- operations: 'DeveloperClientRegistrationOperations',
29
- },
30
- developerKeyManagementV1Oas3: {
31
- components: 'DeveloperKeyManagementComponents',
32
- operations: 'DeveloperKeyManagementOperations',
33
- },
34
- sellComplianceV1Oas3: {
35
- components: 'SellComplianceComponents',
36
- operations: 'SellComplianceOperations',
37
- },
38
- };
39
22
  const ui = {
40
23
  success: chalk.green,
41
24
  warning: chalk.yellow,
@@ -44,51 +27,14 @@ const ui = {
44
27
  dim: chalk.dim,
45
28
  bold: chalk.bold,
46
29
  };
47
- /**
48
- * Checks whether an openapi-typescript output exposes a generated namespace.
49
- *
50
- * @param content - Generated TypeScript declaration content to inspect.
51
- * @param exportName - Namespace export that must exist before adding an alias.
52
- * @returns True when the generated output exposes the requested namespace.
53
- *
54
- * @example
55
- * ```ts
56
- * hasGeneratedExport(content, 'components');
57
- * ```
58
- */
59
- const hasGeneratedExport = (content, exportName) => content.includes(`export interface ${exportName}`) ||
60
- content.includes(`export type ${exportName}`);
61
- /**
62
- * Appends repo-specific generated type aliases after openapi-typescript rewrites a file.
63
- *
64
- * @param outputPath - Generated TypeScript declaration file path.
65
- * @param camelCaseName - OpenAPI spec basename used as the generated type file name.
66
- * @returns Nothing; the generated file is updated in place when aliases are needed.
67
- *
68
- * @example
69
- * ```ts
70
- * preserveGeneratedTypeAliases(outputPath, 'sellComplianceV1Oas3');
71
- * ```
72
- */
73
- const preserveGeneratedTypeAliases = (outputPath, camelCaseName) => {
74
- const aliases = GENERATED_TYPE_ALIASES[camelCaseName];
75
- if (!aliases) {
76
- return;
77
- }
78
- const content = readFileSync(outputPath, 'utf-8');
79
- const aliasLines = [];
80
- if (hasGeneratedExport(content, 'components') &&
81
- !content.includes(`export type ${aliases.components}`)) {
82
- aliasLines.push('/** Repo-specific alias for generated OpenAPI components. */', `export type ${aliases.components} = components;`);
83
- }
84
- if (hasGeneratedExport(content, 'operations') &&
85
- !content.includes(`export type ${aliases.operations}`)) {
86
- aliasLines.push('/** Repo-specific alias for generated OpenAPI operations. */', `export type ${aliases.operations} = operations;`);
87
- }
88
- if (aliasLines.length === 0) {
30
+ const applyNamespaceNames = (outputPath, camelCaseName) => {
31
+ const names = generatedNamespaceNames[camelCaseName];
32
+ if (!names) {
89
33
  return;
90
34
  }
91
- writeFileSync(outputPath, `${content.trimEnd()}\n\n${aliasLines.join('\n')}\n`);
35
+ const generatedTypes = readFileSync(outputPath, 'utf-8');
36
+ const renamedTypes = renameGeneratedNamespaces(generatedTypes, names);
37
+ writeFileSync(outputPath, renamedTypes);
92
38
  };
93
39
  function showSpinner(message) {
94
40
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
@@ -107,41 +53,33 @@ async function downloadSpecs() {
107
53
  console.log(ui.bold('\n📥 Downloading OpenAPI Specifications\n'));
108
54
  console.log(ui.dim(` Found ${specSources.length} spec URLs\n`));
109
55
  let downloaded = 0;
110
- let failed = 0;
111
- for (const { api, folder, url } of specSources) {
112
- const fileName = basename(url);
113
- const folderPath = join(SPECS_DIR, folder);
114
- const filePath = join(folderPath, fileName);
56
+ const unavailable = [];
57
+ for (const source of specSources) {
58
+ const { api, folder, url } = source;
59
+ const fileName = specFileName(source);
60
+ const specPath = join(SPECS_DIR, folder, fileName);
115
61
  const stopSpinner = showSpinner(`Downloading ${api} (${fileName})...`);
116
- const downloadedSpec = await Effect.runPromise(Effect.either(Effect.tryPromise({
117
- try: async () => {
118
- mkdirSync(folderPath, { recursive: true });
119
- const specResponse = await httpRequest({
120
- url,
121
- responseType: 'arraybuffer',
122
- timeoutMs: 20_000,
123
- });
124
- writeFileSync(filePath, specResponse.data);
125
- },
126
- catch: (error) => error,
127
- }).pipe(Effect.ensuring(Effect.sync(stopSpinner)))));
128
- if (Either.isLeft(downloadedSpec)) {
129
- console.log(` ${ui.error('✗')} ${api} (${fileName}): ${getErrorMessage(downloadedSpec.left)}`);
130
- failed++;
131
- }
132
- else {
62
+ const refresh = refreshCachedSpec(url, specPath).pipe(Effect.either);
63
+ const refreshOutcome = await Effect.runPromise(refresh);
64
+ stopSpinner();
65
+ if (Either.isRight(refreshOutcome)) {
133
66
  console.log(` ${ui.success('✓')} ${api} (${fileName})`);
134
67
  downloaded++;
68
+ continue;
135
69
  }
70
+ const reason = refreshOutcome.left.message;
71
+ const cacheNote = existsSync(specPath) ? 'kept the cached copy' : 'no cached copy';
72
+ console.log(` ${ui.error('✗')} ${api} (${fileName}): unavailable, ${reason}; ${cacheNote}`);
73
+ unavailable.push({ api, fileName, url, reason });
136
74
  }
137
- if (failed > 0 && downloaded === 0) {
138
- console.log(ui.error(`\n ✗ All ${failed} spec downloads failed — eBay is likely blocking automated requests (HTTP 403).`));
75
+ if (unavailable.length > 0 && downloaded === 0) {
76
+ console.log(ui.error(`\n ✗ All ${unavailable.length} spec downloads failed — eBay is likely blocking automated requests (HTTP 403).`));
139
77
  }
140
- return downloaded;
78
+ return { downloaded, unavailable };
141
79
  }
142
80
  function generateTypes() {
143
81
  console.log(ui.bold('\n🔧 Generating TypeScript Types\n'));
144
- let generated = 0;
82
+ const generatedPaths = [];
145
83
  let skipped = 0;
146
84
  function processDirectory(dir) {
147
85
  if (!existsSync(dir))
@@ -199,9 +137,9 @@ function generateTypes() {
199
137
  catch: (error) => error,
200
138
  })));
201
139
  if (Either.isRight(generatedFile)) {
202
- preserveGeneratedTypeAliases(outputPath, camelCaseName);
140
+ applyNamespaceNames(outputPath, camelCaseName);
203
141
  console.log(` ${ui.success('✓')} ${camelCaseName}.ts`);
204
- generated++;
142
+ generatedPaths.push(outputPath);
205
143
  }
206
144
  else {
207
145
  console.log(` ${ui.error('✗')} ${camelCaseName}.ts (generation failed): ${getErrorMessage(generatedFile.left)}`);
@@ -210,8 +148,15 @@ function generateTypes() {
210
148
  }
211
149
  }
212
150
  processDirectory(SPECS_DIR);
213
- console.log(ui.dim(`\n Generated: ${generated}, Skipped: ${skipped}`));
214
- return generated;
151
+ // Format with the repo's Biome settings so regenerating unchanged specs leaves no diff.
152
+ if (generatedPaths.length > 0) {
153
+ execFileSync('npx', ['biome', 'format', '--write', ...generatedPaths], {
154
+ stdio: 'pipe',
155
+ cwd: PROJECT_ROOT,
156
+ });
157
+ }
158
+ console.log(ui.dim(`\n Generated: ${generatedPaths.length}, Skipped: ${skipped}`));
159
+ return generatedPaths.length;
215
160
  }
216
161
  function extractEndpointsFromSpecs() {
217
162
  const endpoints = [];
@@ -277,69 +222,6 @@ function getImplementedTools() {
277
222
  }
278
223
  return tools;
279
224
  }
280
- function camelToSnake(str) {
281
- return str
282
- .replace(/([A-Z])/g, '_$1')
283
- .toLowerCase()
284
- .replace(/^_/, '');
285
- }
286
- function normalizeForMatching(name) {
287
- return name.toLowerCase().replace(/[^a-z0-9]/g, '');
288
- }
289
- /**
290
- * Known mappings from OpenAPI operationId to our tool names
291
- * This handles cases where naming conventions differ significantly
292
- */
293
- const KNOWN_OPERATION_MAPPINGS = {
294
- // Feedback API
295
- getitemsawaitingfeedback: ['ebay_get_awaiting_feedback'],
296
- leavefeedback: ['ebay_leave_feedback_for_buyer'],
297
- // Notification API — tools use the ebay_*_notification_* naming convention
298
- getconfig: ['ebay_get_notification_config'],
299
- updateconfig: ['ebay_update_notification_config'],
300
- getdestinations: ['ebay_get_notification_destinations'],
301
- createdestination: ['ebay_create_notification_destination'],
302
- getdestination: ['ebay_get_notification_destination'],
303
- updatedestination: ['ebay_update_notification_destination'],
304
- deletedestination: ['ebay_delete_notification_destination'],
305
- getsubscriptions: ['ebay_get_notification_subscriptions'],
306
- createsubscription: ['ebay_create_notification_subscription'],
307
- getsubscription: ['ebay_get_notification_subscription'],
308
- updatesubscription: ['ebay_update_notification_subscription'],
309
- deletesubscription: ['ebay_delete_notification_subscription'],
310
- disablesubscription: ['ebay_disable_notification_subscription'],
311
- enablesubscription: ['ebay_enable_notification_subscription'],
312
- testsubscription: ['ebay_test_notification_subscription'],
313
- createsubscriptionfilter: ['ebay_create_notification_subscription_filter'],
314
- getsubscriptionfilter: ['ebay_get_notification_subscription_filter'],
315
- deletesubscriptionfilter: ['ebay_delete_notification_subscription_filter'],
316
- gettopic: ['ebay_get_notification_topic'],
317
- gettopics: ['ebay_get_notification_topics'],
318
- getpublickey: ['ebay_get_notification_public_key'],
319
- // Negotiation API
320
- findeligibleitems: ['ebay_find_eligible_items'],
321
- // Marketing API
322
- createadbylistingid: ['ebay_create_ad_by_listing_id'],
323
- updatebid: ['ebay_update_bid'],
324
- bulkcreatekeyword: ['ebay_bulk_create_keyword'],
325
- bulkupdatekeyword: ['ebay_bulk_update_keyword'],
326
- bulkcreatenegativekeyword: ['ebay_bulk_create_negative_keyword'],
327
- bulkupdatenegativekeyword: ['ebay_bulk_update_negative_keyword'],
328
- getnegativekeywords: ['ebay_get_negative_keywords'],
329
- createnegativekeyword: ['ebay_create_negative_keyword'],
330
- getnegativekeyword: ['ebay_get_negative_keyword'],
331
- updatenegativekeyword: ['ebay_update_negative_keyword'],
332
- getreportmetadata: ['ebay_get_report_metadata'],
333
- getreportmetadataforreporttype: ['ebay_get_report_metadata_for_report_type'],
334
- getpromotionreports: ['ebay_get_promotion_reports'],
335
- getaudiences: ['ebay_get_audiences'],
336
- // Dispute/Fulfillment API
337
- fetchevidencecontent: ['ebay_fetch_evidence_content'],
338
- getactivities: ['ebay_get_payment_dispute_activities', 'ebay_get_activities'],
339
- uploadevidencefile: ['ebay_upload_evidence_file'],
340
- addevidence: ['ebay_add_evidence'],
341
- updateevidence: ['ebay_update_evidence'],
342
- };
343
225
  /**
344
226
  * Get all implemented API methods from source files
345
227
  */
@@ -365,11 +247,11 @@ function getImplementedApiMethods() {
365
247
  // Many area classes omit the `public` keyword (notification, trading, feedback, …).
366
248
  const methodMatches = source.right.matchAll(/(?:async\s+(\w+)\s*\(|(?:public\s+)?(\w+)\s*=\s*(?:async\s*)?\()/g);
367
249
  for (const match of methodMatches) {
368
- const name = match[1] ?? match[2];
250
+ const name = match[1] || match[2];
369
251
  // Skip common non-method bindings that the broader regex can catch.
370
252
  if (!name || name === 'const' || name === 'let' || name === 'var')
371
253
  continue;
372
- methods.add(normalizeForMatching(name));
254
+ methods.add(name);
373
255
  }
374
256
  }
375
257
  }
@@ -378,63 +260,12 @@ function getImplementedApiMethods() {
378
260
  processDirectory(apiDir);
379
261
  return methods;
380
262
  }
381
- /**
382
- * Generate all possible name variations for an operationId
383
- */
384
- function generateNameVariations(opId) {
385
- const variations = [];
386
- const normalized = normalizeForMatching(opId);
387
- // Direct variations
388
- variations.push(normalized);
389
- variations.push(`ebay${normalized}`);
390
- variations.push(normalizeForMatching(`ebay_${camelToSnake(opId)}`));
391
- variations.push(normalizeForMatching(camelToSnake(opId)));
392
- // Remove common suffixes/prefixes that might differ
393
- const withoutItems = normalized.replace(/items?$/, '');
394
- if (withoutItems !== normalized) {
395
- variations.push(withoutItems);
396
- variations.push(`ebay${withoutItems}`);
397
- }
398
- // Handle "ByX" patterns -> remove them
399
- const withoutBy = normalized.replace(/by\w+$/, '');
400
- if (withoutBy !== normalized) {
401
- variations.push(withoutBy);
402
- variations.push(`ebay${withoutBy}`);
403
- }
404
- // Handle "ForX" patterns -> remove them
405
- const withoutFor = normalized.replace(/for\w+$/, '');
406
- if (withoutFor !== normalized) {
407
- variations.push(withoutFor);
408
- variations.push(`ebay${withoutFor}`);
409
- }
410
- // Handle plural/singular
411
- if (normalized.endsWith('s') && !normalized.endsWith('ss')) {
412
- const singular = normalized.slice(0, -1);
413
- variations.push(singular);
414
- variations.push(`ebay${singular}`);
415
- }
416
- else {
417
- const plural = normalized + 's';
418
- variations.push(plural);
419
- variations.push(`ebay${plural}`);
420
- }
421
- // Check known mappings
422
- if (KNOWN_OPERATION_MAPPINGS[normalized]) {
423
- for (const mapped of KNOWN_OPERATION_MAPPINGS[normalized]) {
424
- variations.push(normalizeForMatching(mapped));
425
- }
426
- }
427
- return [...new Set(variations)];
428
- }
429
263
  function analyzeEndpoints() {
430
264
  console.log(ui.bold('\n📊 Analyzing API Coverage\n'));
431
265
  const specEndpoints = extractEndpointsFromSpecs();
432
266
  const implementedTools = getImplementedTools();
433
267
  const implementedApiMethods = getImplementedApiMethods();
434
- // Normalize all tool names
435
- const normalizedTools = new Set(Array.from(implementedTools).map((toolName) => normalizeForMatching(toolName)));
436
- // Combine tools and API methods for matching
437
- const allImplemented = new Set([...normalizedTools, ...implementedApiMethods]);
268
+ const implemented = collectImplementedNames([...implementedApiMethods], [...implementedTools]);
438
269
  // Track unique endpoints (dedupe by operationId)
439
270
  const seenOperationIds = new Set();
440
271
  const uniqueEndpoints = [];
@@ -445,31 +276,42 @@ function analyzeEndpoints() {
445
276
  uniqueEndpoints.push(endpoint);
446
277
  }
447
278
  }
279
+ const fuzzy = [];
448
280
  const missing = [];
449
- let matchedCount = 0;
281
+ let exact = 0;
450
282
  for (const endpoint of uniqueEndpoints) {
451
- const opId = endpoint.operationId;
452
- const variations = generateNameVariations(opId);
453
- const isImplemented = variations.some((name) => allImplemented.has(name));
454
- if (isImplemented) {
455
- matchedCount++;
283
+ const coverage = classifyOperation(endpoint.operationId, implemented);
284
+ if (coverage === 'exact') {
285
+ exact++;
286
+ }
287
+ else if (coverage === 'fuzzy') {
288
+ fuzzy.push(endpoint);
456
289
  }
457
290
  else {
458
291
  missing.push(endpoint);
459
292
  }
460
293
  }
294
+ const matchedCount = exact + fuzzy.length;
461
295
  const coveragePercent = uniqueEndpoints.length > 0 ? ((matchedCount / uniqueEndpoints.length) * 100).toFixed(1) : '0';
462
296
  console.log(` ${ui.info('Total unique endpoints in specs:')} ${uniqueEndpoints.length}`);
463
297
  console.log(` ${ui.info('(Raw count with duplicates:')} ${specEndpoints.length}${ui.info(')')}`);
464
298
  console.log(` ${ui.success('Tools implemented:')} ${implementedTools.size}`);
465
299
  console.log(` ${ui.success('API methods found:')} ${implementedApiMethods.size}`);
466
300
  console.log(` ${ui.success('Endpoints covered:')} ${matchedCount} (${coveragePercent}%)`);
301
+ console.log(` ${ui.success('Exact operationId matches:')} ${exact}`);
302
+ console.log(` ${ui.warning('Fuzzy-only matches:')} ${fuzzy.length}`);
467
303
  console.log(` ${ui.warning('Potentially missing:')} ${missing.length}`);
468
- return {
469
- total: uniqueEndpoints.length,
470
- implemented: matchedCount,
471
- missing,
472
- };
304
+ return { total: uniqueEndpoints.length, exact, fuzzy, missing };
305
+ }
306
+ function showFuzzyMatches(fuzzy) {
307
+ if (fuzzy.length === 0) {
308
+ return;
309
+ }
310
+ console.log(ui.bold('\nFuzzy-only Matches\n'));
311
+ console.log(ui.dim(' No API method carries these operationIds exactly; a looser name matched:\n'));
312
+ for (const ep of fuzzy) {
313
+ console.log(` ${ui.dim(ep.method.padEnd(6))} ${ep.path} ${ui.dim(`(${ep.operationId})`)}`);
314
+ }
473
315
  }
474
316
  function showMissingEndpoints(missing) {
475
317
  if (missing.length === 0) {
@@ -539,13 +381,18 @@ ${ui.bold('What this does:')}
539
381
  }
540
382
  const report = {
541
383
  specsDownloaded: 0,
384
+ unavailableSpecs: [],
542
385
  typesGenerated: 0,
543
386
  endpointsInSpecs: 0,
544
387
  toolsImplemented: 0,
388
+ exactMatches: 0,
389
+ fuzzyMatchedEndpoints: [],
545
390
  missingEndpoints: [],
546
391
  };
547
392
  if (!(reportOnly || skipDownload)) {
548
- report.specsDownloaded = await downloadSpecs();
393
+ const downloads = await downloadSpecs();
394
+ report.specsDownloaded = downloads.downloaded;
395
+ report.unavailableSpecs = downloads.unavailable;
549
396
  }
550
397
  // If the download step ran but fetched nothing (for example, blocked
551
398
  // requests), the coverage numbers below describe the cached specs on disk —
@@ -561,8 +408,11 @@ ${ui.bold('What this does:')}
561
408
  }
562
409
  const analysis = analyzeEndpoints();
563
410
  report.endpointsInSpecs = analysis.total;
564
- report.toolsImplemented = analysis.implemented;
411
+ report.toolsImplemented = analysis.exact + analysis.fuzzy.length;
412
+ report.exactMatches = analysis.exact;
413
+ report.fuzzyMatchedEndpoints = analysis.fuzzy;
565
414
  report.missingEndpoints = analysis.missing;
415
+ showFuzzyMatches(analysis.fuzzy);
566
416
  showMissingEndpoints(analysis.missing);
567
417
  generateReport(report);
568
418
  if (usingStaleSpecs) {
@@ -0,0 +1,78 @@
1
+ const KNOWN_OPERATION_MAPPINGS = {
2
+ // Notification API — tools use the ebay_*_notification_* naming convention
3
+ getconfig: ['ebay_get_notification_config'],
4
+ updateconfig: ['ebay_update_notification_config'],
5
+ getdestinations: ['ebay_get_notification_destinations'],
6
+ createdestination: ['ebay_create_notification_destination'],
7
+ getdestination: ['ebay_get_notification_destination'],
8
+ updatedestination: ['ebay_update_notification_destination'],
9
+ deletedestination: ['ebay_delete_notification_destination'],
10
+ getsubscriptions: ['ebay_get_notification_subscriptions'],
11
+ createsubscription: ['ebay_create_notification_subscription'],
12
+ getsubscription: ['ebay_get_notification_subscription'],
13
+ updatesubscription: ['ebay_update_notification_subscription'],
14
+ deletesubscription: ['ebay_delete_notification_subscription'],
15
+ disablesubscription: ['ebay_disable_notification_subscription'],
16
+ enablesubscription: ['ebay_enable_notification_subscription'],
17
+ testsubscription: ['ebay_test_notification_subscription'],
18
+ createsubscriptionfilter: ['ebay_create_notification_subscription_filter'],
19
+ getsubscriptionfilter: ['ebay_get_notification_subscription_filter'],
20
+ deletesubscriptionfilter: ['ebay_delete_notification_subscription_filter'],
21
+ gettopic: ['ebay_get_notification_topic'],
22
+ gettopics: ['ebay_get_notification_topics'],
23
+ getpublickey: ['ebay_get_notification_public_key'],
24
+ // Negotiation API
25
+ findeligibleitems: ['ebay_find_eligible_items'],
26
+ // Marketing API
27
+ createadbylistingid: ['ebay_create_ad_by_listing_id'],
28
+ updatebid: ['ebay_update_bid'],
29
+ bulkcreatekeyword: ['ebay_bulk_create_keyword'],
30
+ bulkupdatekeyword: ['ebay_bulk_update_keyword'],
31
+ bulkcreatenegativekeyword: ['ebay_bulk_create_negative_keyword'],
32
+ bulkupdatenegativekeyword: ['ebay_bulk_update_negative_keyword'],
33
+ getnegativekeywords: ['ebay_get_negative_keywords'],
34
+ createnegativekeyword: ['ebay_create_negative_keyword'],
35
+ getnegativekeyword: ['ebay_get_negative_keyword'],
36
+ updatenegativekeyword: ['ebay_update_negative_keyword'],
37
+ getreportmetadata: ['ebay_get_report_metadata'],
38
+ getreportmetadataforreporttype: ['ebay_get_report_metadata_for_report_type'],
39
+ getpromotionreports: ['ebay_get_promotion_reports'],
40
+ getaudiences: ['ebay_get_audiences'],
41
+ // Dispute/Fulfillment API
42
+ fetchevidencecontent: ['ebay_fetch_evidence_content'],
43
+ getactivities: ['ebay_get_payment_dispute_activities', 'ebay_get_activities'],
44
+ uploadevidencefile: ['ebay_upload_evidence_file'],
45
+ addevidence: ['ebay_add_evidence'],
46
+ updateevidence: ['ebay_update_evidence'],
47
+ };
48
+ const ITEMS_SUFFIX = /items?$/;
49
+ const BY_SUFFIX = /by\w+$/;
50
+ const FOR_SUFFIX = /for\w+$/;
51
+ const singularOrPlural = (name) => name.endsWith('s') && !name.endsWith('ss') ? name.slice(0, -1) : `${name}s`;
52
+ const fuzzyNameVariations = (operationId) => {
53
+ const normalized = normalizeForMatching(operationId);
54
+ const stems = [
55
+ normalized,
56
+ normalized.replace(ITEMS_SUFFIX, ''),
57
+ normalized.replace(BY_SUFFIX, ''),
58
+ normalized.replace(FOR_SUFFIX, ''),
59
+ singularOrPlural(normalized),
60
+ ];
61
+ const prefixedStems = stems.map((stem) => `ebay${stem}`);
62
+ const mappedTools = KNOWN_OPERATION_MAPPINGS[normalized] || [];
63
+ const mappedNames = mappedTools.map(normalizeForMatching);
64
+ return new Set([...stems, ...prefixedStems, ...mappedNames]);
65
+ };
66
+ export const normalizeForMatching = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, '');
67
+ export const collectImplementedNames = (apiMethods, toolNames) => {
68
+ const normalizedNames = [...apiMethods, ...toolNames].map(normalizeForMatching);
69
+ return { apiMethods: new Set(apiMethods), normalizedNames: new Set(normalizedNames) };
70
+ };
71
+ export const classifyOperation = (operationId, implemented) => {
72
+ if (implemented.apiMethods.has(operationId)) {
73
+ return 'exact';
74
+ }
75
+ const variations = fuzzyNameVariations(operationId);
76
+ const hasFuzzyMatch = [...variations].some((name) => implemented.normalizedNames.has(name));
77
+ return hasFuzzyMatch ? 'fuzzy' : 'missing';
78
+ };
@@ -0,0 +1,42 @@
1
+ import { mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { Data, Effect } from 'effect';
4
+ import { getErrorMessage } from '../utils/errors.js';
5
+ import { httpRequestEffect } from '../utils/http.js';
6
+ const isVersionText = (version) => typeof version === 'string' && version.trim() !== '';
7
+ const hasSpecVersion = (spec) => spec !== null &&
8
+ typeof spec === 'object' &&
9
+ (('openapi' in spec && isVersionText(spec.openapi)) ||
10
+ ('swagger' in spec && isVersionText(spec.swagger)));
11
+ export class UnusableSpecError extends Data.TaggedError('UnusableSpecError') {
12
+ }
13
+ export class SpecCacheWriteError extends Data.TaggedError('SpecCacheWriteError') {
14
+ }
15
+ export const checkSpecDownload = (specBytes) => {
16
+ const specText = specBytes.toString('utf-8');
17
+ if (specText.trim() === '') {
18
+ return Effect.fail(new UnusableSpecError({ message: 'empty response' }));
19
+ }
20
+ return Effect.try({
21
+ try: () => JSON.parse(specText),
22
+ catch: () => new UnusableSpecError({ message: 'response is not JSON' }),
23
+ }).pipe(Effect.filterOrFail(hasSpecVersion, () => new UnusableSpecError({ message: 'JSON has no openapi or swagger version' })), Effect.as(specBytes));
24
+ };
25
+ export const refreshCachedSpec = (url, specPath) => httpRequestEffect({ url, responseType: 'arraybuffer', timeoutMs: 20_000 }).pipe(Effect.flatMap((specResponse) => checkSpecDownload(specResponse.data)), Effect.flatMap((specBytes) => {
26
+ // Write beside the cache, then rename, so a failed write never truncates the cached spec.
27
+ const downloadPath = `${specPath}.download`;
28
+ return Effect.try({
29
+ try: () => {
30
+ mkdirSync(dirname(specPath), { recursive: true });
31
+ writeFileSync(downloadPath, specBytes);
32
+ renameSync(downloadPath, specPath);
33
+ },
34
+ catch: (error) => {
35
+ rmSync(downloadPath, { force: true });
36
+ return new SpecCacheWriteError({
37
+ specPath,
38
+ message: getErrorMessage(error, 'write failed'),
39
+ });
40
+ },
41
+ });
42
+ }));
@@ -14,9 +14,8 @@ const definedSchemaNames = (spec) => {
14
14
  /**
15
15
  * Replaces references to component schemas the spec never defines with an open schema.
16
16
  *
17
- * eBay occasionally publishes a spec with a dangling `$ref` (Account v2 references
18
- * `SetUserPreferencesRequest` without defining it), and openapi-typescript aborts the
19
- * whole file on the first unresolved reference. Rewriting only the dangling references
17
+ * eBay occasionally publishes a spec with a dangling `$ref`, and openapi-typescript
18
+ * aborts the whole file on the first unresolved reference. Rewriting only the dangling references
20
19
  * keeps every defined type exact, while the undefined payload generates as `unknown`
21
20
  * with a description naming the missing schema.
22
21
  *
@@ -4,6 +4,8 @@
4
4
  * Each spec lands in `.cache/ebay-specs/<folder>/`, and its generated types land in
5
5
  * `src/types/<folder>/`, so `folder` must match the existing generated-type layout.
6
6
  */
7
+ import { basename } from 'node:path';
8
+ export const specFileName = ({ url, fileName }) => fileName || basename(url);
7
9
  /** Every spec `pnpm sync` downloads, grouped the same way as `src/types/`. */
8
10
  export const specSources = [
9
11
  {
@@ -64,7 +66,8 @@ export const specSources = [
64
66
  {
65
67
  api: 'Account API (v2)',
66
68
  folder: 'sell-apps/account-management',
67
- url: 'https://developer.ebay.com/api-docs/master/sell/account/v2/openapi/3/sell_account_v2_oas3.json',
69
+ url: 'https://developer.ebay.com/develop/api/spec/account_api_v2.json',
70
+ fileName: 'sell_account_v2_oas3.json',
68
71
  },
69
72
  {
70
73
  api: 'Finances API',
@@ -121,11 +124,6 @@ export const specSources = [
121
124
  folder: 'sell-apps/other-apis',
122
125
  url: 'https://developer.ebay.com/api-docs/master/commerce/translation/openapi/3/commerce_translation_v1_beta_oas3.json',
123
126
  },
124
- {
125
- api: 'Compliance API',
126
- folder: 'sell-apps/other-apis',
127
- url: 'https://developer.ebay.com/api-docs/master/sell/compliance/openapi/3/sell_compliance_v1_oas3.json',
128
- },
129
127
  {
130
128
  api: 'Identity API',
131
129
  folder: 'sell-apps/other-apis',
@@ -0,0 +1,23 @@
1
+ const COMPONENTS_DECLARATION = /\binterface components\b/g;
2
+ const COMPONENTS_REFERENCE = /\bcomponents\[/g;
3
+ const OPERATIONS_DECLARATION = /\binterface operations\b/g;
4
+ const OPERATIONS_REFERENCE = /\boperations\[/g;
5
+ export const generatedNamespaceNames = {
6
+ developerAnalyticsV1BetaOas3: {
7
+ components: 'DeveloperAnalyticsComponents',
8
+ operations: 'DeveloperAnalyticsOperations',
9
+ },
10
+ developerClientRegistrationV1Oas3: {
11
+ components: 'DeveloperClientRegistrationComponents',
12
+ operations: 'DeveloperClientRegistrationOperations',
13
+ },
14
+ developerKeyManagementV1Oas3: {
15
+ components: 'DeveloperKeyManagementComponents',
16
+ operations: 'DeveloperKeyManagementOperations',
17
+ },
18
+ };
19
+ export const renameGeneratedNamespaces = (generatedTypes, names) => generatedTypes
20
+ .replace(COMPONENTS_DECLARATION, `interface ${names.components}`)
21
+ .replace(COMPONENTS_REFERENCE, `${names.components}[`)
22
+ .replace(OPERATIONS_DECLARATION, `interface ${names.operations}`)
23
+ .replace(OPERATIONS_REFERENCE, `${names.operations}[`);
@@ -24,7 +24,7 @@ export const buildUsingDoc = (snapshot) => ({
24
24
  slug: 'ebay-mcp-using',
25
25
  title: 'Using the eBay MCP tools',
26
26
  description: `Drive eBay's Sell APIs through the ebay-mcp server: ${snapshot.toolCount} tools for listings, orders, marketing, and analytics. Use when creating or revising listings, fulfilling orders, issuing refunds, running Promoted Listings campaigns, or debugging eBay auth/rate-limit errors.`,
27
- intro: `The \`ebay-mcp\` server exposes **${snapshot.toolCount} tools across 100% of eBay's Sell APIs**, running locally over MCP. Every tool is named \`ebay_<verb>_<noun>\` (plus two ChatGPT-connector tools, \`search\`/\`fetch\`). You already see each tool's input schema via \`tools/list\` — this skill covers what discovery can't: which tools to chain, what eBay requires first, and how to read its errors.`,
27
+ intro: `The \`ebay-mcp\` server exposes **${snapshot.toolCount} tools covering 100% of the eBay Sell, Commerce and Developer APIs it tracks**, running locally over MCP. Every tool is named \`ebay_<verb>_<noun>\` (plus two ChatGPT-connector tools, \`search\`/\`fetch\`). You already see each tool's input schema via \`tools/list\` — this skill covers what discovery can't: which tools to chain, what eBay requires first, and how to read its errors.`,
28
28
  sections: [
29
29
  {
30
30
  heading: 'Authentication & environment',
@@ -102,7 +102,7 @@ export const buildContributingDoc = (snapshot) => ({
102
102
  slug: 'ebay-mcp-contributing',
103
103
  title: 'Contributing to ebay-mcp',
104
104
  description: `Work on the ebay-mcp server itself: ${snapshot.toolCount} tools across eBay's Sell APIs (TypeScript/ESM, Zod tool-input schemas, OpenAPI-generated types). Use when adding or changing eBay tools/endpoints, wiring the registry, or running the project's checks.`,
105
- intro: `A local MCP server exposing **${snapshot.toolCount} tools across 100% of eBay's Sell APIs** — TypeScript/Node.js (ESM), \`@modelcontextprotocol/sdk\`, Zod tool-input schemas, Effect for typed async errors, OpenAPI-generated types. Entry points: \`src/index.ts\` (STDIO) and \`src/serverHttp.ts\` (HTTP).`,
105
+ intro: `A local MCP server exposing **${snapshot.toolCount} tools covering 100% of the eBay Sell, Commerce and Developer APIs it tracks** — TypeScript/Node.js (ESM), \`@modelcontextprotocol/sdk\`, Zod tool-input schemas, Effect for typed async errors, OpenAPI-generated types. Entry points: \`src/index.ts\` (STDIO) and \`src/serverHttp.ts\` (HTTP).`,
106
106
  sections: [
107
107
  {
108
108
  heading: 'Validation (run before a PR)',