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.
- package/README.md +34 -16
- package/build/api/account-management/account.js +2 -2
- package/build/api/account-management/accountV2.js +0 -6
- package/build/api/account-management/finances.js +7 -5
- package/build/api/client/ebayApiClient.js +111 -10
- package/build/api/client/signEbayRequest.js +124 -0
- package/build/api/communication/feedback.js +4 -4
- package/build/api/developer/developer.js +43 -7
- package/build/api/ebaySellerApi.js +0 -4
- package/build/api/marketing-and-promotions/campaigns.js +0 -34
- package/build/api/order-management/dispute.js +29 -20
- package/build/api/order-management/fulfillment.js +2 -1
- package/build/api/other/edelivery.js +3 -3
- package/build/api/other/translation.js +1 -1
- package/build/config/environment.js +31 -3
- package/build/schemas/account-management/account.js +1 -1
- package/build/schemas/account-management/accountV2.js +3 -4
- package/build/schemas/communication/messages.js +2 -2
- package/build/schemas/marketing/campaigns.js +0 -21
- package/build/schemas/other/otherApis.js +2 -85
- package/build/scripts/devSync.js +75 -225
- package/build/scripts/specCoverage.js +78 -0
- package/build/scripts/specDownload.js +42 -0
- package/build/scripts/specRefs.js +2 -3
- package/build/scripts/specSources.js +4 -6
- package/build/scripts/specTypeNames.js +23 -0
- package/build/skills/content.js +2 -2
- package/build/tools/categories/account.js +3 -3
- package/build/tools/categories/communication.js +5 -5
- package/build/tools/categories/developer.js +1 -1
- package/build/tools/categories/finances.js +2 -2
- package/build/tools/categories/marketing.js +0 -16
- package/build/tools/categories/other.js +3 -17
- package/build/types/ebay.js +0 -1
- package/build/types/ebayEnums.js +0 -40
- package/build/ui/card.html +23 -23
- package/build/ui/chart.html +22 -22
- package/build/ui/stat.html +23 -23
- package/build/ui/table.html +21 -21
- package/build/utils/communication/feedback.js +3 -3
- package/build/utils/errors.js +1 -0
- package/package.json +5 -5
- package/build/api/other/compliance.js +0 -60
- package/build/utils/other/compliance.js +0 -56
package/build/scripts/devSync.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
for (const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 (
|
|
138
|
-
console.log(ui.error(`\n ✗ All ${
|
|
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
|
-
|
|
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
|
-
|
|
140
|
+
applyNamespaceNames(outputPath, camelCaseName);
|
|
203
141
|
console.log(` ${ui.success('✓')} ${camelCaseName}.ts`);
|
|
204
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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]
|
|
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(
|
|
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
|
-
|
|
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
|
|
281
|
+
let exact = 0;
|
|
450
282
|
for (const endpoint of uniqueEndpoints) {
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
18
|
-
*
|
|
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
|
|
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}[`);
|
package/build/skills/content.js
CHANGED
|
@@ -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
|
|
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
|
|
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)',
|