ebay-mcp 1.13.2 → 1.14.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.de.md +3 -3
- package/README.es.md +3 -3
- package/README.fr.md +3 -3
- package/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +163 -41
- package/README.pt-BR.md +3 -3
- package/README.ru.md +3 -3
- package/README.zh-CN.md +3 -3
- package/build/api/account-management/account.js +633 -297
- package/build/api/analytics-and-report/analytics.js +111 -65
- package/build/api/client.js +199 -140
- package/build/api/clientRequestError.js +23 -0
- package/build/api/clientTrading.js +213 -0
- package/build/api/clientTradingError.js +4 -0
- package/build/api/communication/feedback.js +147 -59
- package/build/api/communication/message.js +134 -81
- package/build/api/communication/negotiation.js +65 -53
- package/build/api/communication/notification.js +458 -164
- package/build/api/developer/developer.js +99 -45
- package/build/api/index.js +23 -8
- package/build/api/listing-management/inventory.js +884 -338
- package/build/api/listing-metadata/metadata.js +416 -170
- package/build/api/listing-metadata/taxonomy.js +187 -42
- package/build/api/marketing-and-promotions/marketing.js +1392 -598
- package/build/api/marketing-and-promotions/recommendation.js +49 -24
- package/build/api/order-management/dispute.js +226 -45
- package/build/api/order-management/fulfillment.js +144 -71
- package/build/api/order-management/orderIssueHelpers.js +61 -0
- package/build/api/other/compliance.js +50 -38
- package/build/api/other/edelivery.js +603 -159
- package/build/api/other/finding.js +280 -0
- package/build/api/other/identity.js +30 -15
- package/build/api/other/translation.js +29 -14
- package/build/api/other/vero.js +113 -27
- package/build/api/rateLimitTracker.js +55 -0
- package/build/api/shared/request.js +189 -98
- package/build/api/trading/trading.js +154 -87
- package/build/auth/credentialSession.js +250 -0
- package/build/auth/oauth.js +298 -171
- package/build/auth/{oauth-metadata.js → oauthMetadata.js} +22 -6
- package/build/auth/oauthMiddleware.js +104 -0
- package/build/auth/{scope-utils.js → scopeUtils.js} +101 -28
- package/build/auth/tokenVerifier.js +243 -0
- package/build/auth/tokenVerifierError.js +21 -0
- package/build/config/environment.js +148 -113
- package/build/config/{tool-families.js → toolFamilies.js} +30 -14
- package/build/index.js +25 -19
- package/build/mcp/{http-transport.js → httpTransport.js} +102 -52
- package/build/mcp/readOnlyFilter.js +67 -0
- package/build/mcp/runtime.js +31 -13
- package/build/mcp/{tool-gating.js → toolGating.js} +77 -49
- package/build/mcp/{ui-bridge.js → uiBridge.js} +80 -23
- package/build/schemas/account-management/account.js +144 -6
- package/build/schemas/analytics/analytics.js +33 -19
- package/build/schemas/communication/messages.js +12 -42
- package/build/schemas/developer/developer.js +135 -0
- package/build/schemas/fulfillment/orders.js +183 -21
- package/build/schemas/index.js +27 -19
- package/build/schemas/inventory-management/inventory.js +17 -9
- package/build/schemas/marketing/marketing.js +480 -25
- package/build/schemas/metadata/metadata.js +11 -6
- package/build/schemas/other/finding.js +27 -0
- package/build/schemas/other/otherApis.js +424 -0
- package/build/schemas/taxonomy/taxonomy.js +11 -5
- package/build/scripts/{auto-setup.js → autoSetup.js} +87 -74
- package/build/scripts/{build-ui.js → buildUi.js} +6 -1
- package/build/scripts/{dev-sync.js → devSync.js} +169 -110
- package/build/scripts/diagnostics.js +52 -44
- package/build/scripts/{download-specs.js → downloadSpecs.js} +29 -21
- package/build/scripts/{interactive-setup.js → interactiveSetup.js} +84 -78
- package/build/scripts/setup.js +241 -178
- package/build/scripts/{setup-shared.js → setupShared.js} +29 -7
- package/build/scripts/skills.js +35 -27
- package/build/scripts/updateApiStatusDoc.js +71 -0
- package/build/serverHttp.js +90 -0
- package/build/skills/content.js +124 -118
- package/build/skills/index.js +22 -10
- package/build/skills/install.js +21 -10
- package/build/skills/markers.js +50 -15
- package/build/skills/{registry-snapshot.js → registrySnapshot.js} +8 -2
- package/build/skills/render.js +41 -14
- package/build/skills/targets.js +49 -17
- package/build/tools/categories/account.js +166 -264
- package/build/tools/categories/analytics.js +11 -22
- package/build/tools/categories/browse.js +17 -0
- package/build/tools/categories/communication.js +124 -399
- package/build/tools/categories/connector.js +23 -20
- package/build/tools/categories/developer.js +46 -159
- package/build/tools/categories/fulfillment.js +84 -215
- package/build/tools/categories/index.js +6 -14
- package/build/tools/categories/inventory.js +238 -293
- package/build/tools/categories/marketing.js +677 -39
- package/build/tools/categories/metadata.js +190 -117
- package/build/tools/categories/other.js +81 -208
- package/build/tools/categories/taxonomy.js +29 -20
- package/build/tools/categories/{token-management.js → tokenManagement.js} +187 -192
- package/build/tools/categories/trading.js +16 -47
- package/build/tools/contracts.js +34 -24
- package/build/tools/{define-tool.js → defineTool.js} +44 -29
- package/build/tools/registry.js +87 -29
- package/build/tools/schemas.js +3 -57
- package/build/tools/ui/archetypes.js +1 -0
- package/build/tools/ui/{map-helpers.js → mapHelpers.js} +69 -19
- package/build/tools/ui/maps.js +294 -80
- package/build/types/index.js +1 -1
- package/build/ui/card.html +14 -14
- package/build/ui/chart.html +14 -14
- package/build/ui/stat.html +87 -0
- package/build/ui/table.html +14 -14
- package/build/utils/apiStatusFeed.js +95 -0
- package/build/utils/cliOutput.js +15 -0
- package/build/utils/cliUi.js +108 -0
- package/build/utils/communication/feedback.js +57 -77
- package/build/utils/communication/message.js +69 -98
- package/build/utils/communication/negotiation.js +30 -57
- package/build/utils/communication/notification.js +112 -252
- package/build/utils/{date-converter.js → dateConverter.js} +62 -30
- package/build/utils/effectSchema.js +203 -0
- package/build/utils/effectSchemaAdapter.js +121 -0
- package/build/utils/effectSchemaChainMethods.js +146 -0
- package/build/utils/effectSchemaRefinements.js +104 -0
- package/build/utils/effectSchemaStringMethods.js +63 -0
- package/build/utils/effectSchemaTypes.js +47 -0
- package/build/utils/{env-parser.js → envParser.js} +10 -2
- package/build/utils/errors.js +7 -3
- package/build/utils/http.js +148 -89
- package/build/utils/{llm-client-detector.js → llmClientDetector.js} +295 -274
- package/build/utils/logger.js +127 -51
- package/build/utils/{oauth-helper.js → oauthHelper.js} +127 -90
- package/build/utils/other/compliance.js +18 -38
- package/build/utils/other/identity.js +2 -2
- package/build/utils/other/translation.js +4 -4
- package/build/utils/other/vero.js +36 -20
- package/build/utils/schemaHelpers.js +22 -0
- package/build/utils/scopeHelper.js +270 -0
- package/build/utils/securityChecker.js +316 -0
- package/build/utils/{setup-validator.js → setupValidator.js} +113 -94
- package/build/utils/{setup-wizard.js → setupWizard.js} +57 -46
- package/build/utils/{token-utils.js → tokenUtils.js} +2 -2
- package/build/utils/trading/trading.js +35 -0
- package/build/utils/typeGuards.js +12 -0
- package/build/utils/version.js +65 -29
- package/package.json +10 -8
- package/build/api/client-trading.js +0 -124
- package/build/api/communication/shared.js +0 -59
- package/build/api/shared/query-params.js +0 -16
- package/build/auth/credential-session.js +0 -166
- package/build/auth/oauth-middleware.js +0 -100
- package/build/auth/token-verifier.js +0 -186
- package/build/schemas/other/other-apis.js +0 -390
- package/build/scripts/update-api-status-doc.js +0 -44
- package/build/server-http.js +0 -85
- package/build/tools/definitions/marketing.js +0 -1381
- package/build/tools/tool-handlers/marketing.js +0 -312
- package/build/utils/account-management/account.js +0 -818
- package/build/utils/api-status-feed.js +0 -84
- package/build/utils/cli-ui.js +0 -47
- package/build/utils/order-management/dispute.js +0 -369
- package/build/utils/order-management/fulfillment.js +0 -205
- package/build/utils/other/edelivery.js +0 -235
- package/build/utils/schema-helpers.js +0 -13
- package/build/utils/scope-helper.js +0 -207
- package/build/utils/security-checker.js +0 -248
- package/build/utils/type-guards.js +0 -6
- /package/build/auth/{oauth-types.js → oauthTypes.js} +0 -0
- /package/build/{server-http.d.ts → serverHttp.d.ts} +0 -0
- /package/build/tools/ui/{view-models.js → viewModels.js} +0 -0
- /package/build/types/{ebay-enums.js → ebayEnums.js} +0 -0
|
@@ -1,83 +1,129 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildEndpointParams, optionalStringEffect, requestGetEffect, requireObjectEffect, requireStringEffect, } from '../../api/shared/request.js';
|
|
2
|
+
import { Effect } from 'effect';
|
|
2
3
|
/**
|
|
3
4
|
* Analytics API - Sales and traffic analytics
|
|
4
5
|
* Based on: docs/sell-apps/analytics-and-report/sell_analytics_v1_oas3.json
|
|
5
6
|
*/
|
|
6
7
|
export class AnalyticsApi {
|
|
7
|
-
client;
|
|
8
8
|
basePath = '/sell/analytics/v1';
|
|
9
|
+
client;
|
|
9
10
|
constructor(client) {
|
|
10
11
|
this.client = client;
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* Retrieves listing traffic report metrics.
|
|
15
|
+
*
|
|
16
|
+
* @param input - Traffic report dimension, filter, metric, and optional sort expression.
|
|
17
|
+
* @returns An Effect that succeeds with eBay's generated traffic report response.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const report = await Effect.runPromise(
|
|
22
|
+
* analyticsApi.getTrafficReport({
|
|
23
|
+
* dimension: 'LISTING',
|
|
24
|
+
* filter: 'listing_ids:{123}',
|
|
25
|
+
* metric: 'IMPRESSION',
|
|
26
|
+
* }),
|
|
27
|
+
* );
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @see https://developer.ebay.com/api-docs/sell/analytics/resources/traffic_report/methods/getTrafficReport
|
|
15
31
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
35
|
-
if (sort)
|
|
36
|
-
params.sort = sort;
|
|
37
|
-
return await withApiError('Failed to get traffic report', () => this.client.get(`${this.basePath}/traffic_report`, params));
|
|
38
|
-
}
|
|
32
|
+
getTrafficReport = (input) => {
|
|
33
|
+
const { basePath, client } = this;
|
|
34
|
+
const path = `${basePath}/traffic_report`;
|
|
35
|
+
return Effect.gen(function* () {
|
|
36
|
+
const validatedInput = yield* requireObjectEffect(input, 'input');
|
|
37
|
+
const validatedDimension = yield* requireStringEffect(validatedInput.dimension, 'dimension');
|
|
38
|
+
const validatedFilter = yield* requireStringEffect(validatedInput.filter, 'filter');
|
|
39
|
+
const validatedMetric = yield* requireStringEffect(validatedInput.metric, 'metric');
|
|
40
|
+
const validatedSort = yield* optionalStringEffect(validatedInput.sort, 'sort');
|
|
41
|
+
const params = buildEndpointParams({
|
|
42
|
+
dimension: { wireName: 'dimension', value: validatedDimension },
|
|
43
|
+
filter: { wireName: 'filter', value: validatedFilter },
|
|
44
|
+
metric: { wireName: 'metric', value: validatedMetric },
|
|
45
|
+
sort: { wireName: 'sort', value: validatedSort },
|
|
46
|
+
});
|
|
47
|
+
return yield* requestGetEffect(client, path, params);
|
|
48
|
+
});
|
|
49
|
+
};
|
|
39
50
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* @
|
|
51
|
+
* Retrieves every seller standards profile available for the seller.
|
|
52
|
+
*
|
|
53
|
+
* @param input - Empty endpoint input object.
|
|
54
|
+
* @returns An Effect that succeeds with eBay's generated seller standards profile collection.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const profiles = await Effect.runPromise(analyticsApi.findSellerStandardsProfiles({}));
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @see https://developer.ebay.com/api-docs/sell/analytics/resources/seller_standards_profile/methods/findSellerStandardsProfiles
|
|
43
62
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
findSellerStandardsProfiles = (input = {}) => {
|
|
64
|
+
void input;
|
|
65
|
+
return requestGetEffect(this.client, `${this.basePath}/seller_standards_profile`);
|
|
66
|
+
};
|
|
47
67
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* @
|
|
68
|
+
* Retrieves one seller standards profile by program and cycle.
|
|
69
|
+
*
|
|
70
|
+
* @param input - Seller standards program identifier and cycle.
|
|
71
|
+
* @returns An Effect that succeeds with eBay's generated StandardsProfile response.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const profile = await Effect.runPromise(
|
|
76
|
+
* analyticsApi.getSellerStandardsProfile({
|
|
77
|
+
* program: 'CUSTOMER_SERVICE',
|
|
78
|
+
* cycle: 'CURRENT',
|
|
79
|
+
* }),
|
|
80
|
+
* );
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @see https://developer.ebay.com/api-docs/sell/analytics/resources/seller_standards_profile/methods/getSellerStandardsProfile
|
|
51
84
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
}
|
|
85
|
+
getSellerStandardsProfile = (input) => {
|
|
86
|
+
const { basePath, client } = this;
|
|
87
|
+
return Effect.gen(function* () {
|
|
88
|
+
const validatedInput = yield* requireObjectEffect(input, 'input');
|
|
89
|
+
const validatedProgram = yield* requireStringEffect(validatedInput.program, 'program');
|
|
90
|
+
const validatedCycle = yield* requireStringEffect(validatedInput.cycle, 'cycle');
|
|
91
|
+
return yield* requestGetEffect(client, `${basePath}/seller_standards_profile/${validatedProgram}/${validatedCycle}`);
|
|
92
|
+
});
|
|
93
|
+
};
|
|
62
94
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* @
|
|
95
|
+
* Retrieves customer service metric benchmarks for a marketplace.
|
|
96
|
+
*
|
|
97
|
+
* @param input - Metric type, evaluation type, and marketplace identifier.
|
|
98
|
+
* @returns An Effect that succeeds with eBay's generated customer service metric response.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* const metric = await Effect.runPromise(
|
|
103
|
+
* analyticsApi.getCustomerServiceMetric({
|
|
104
|
+
* customerServiceMetricType: 'ITEM_NOT_AS_DESCRIBED',
|
|
105
|
+
* evaluationType: 'CURRENT',
|
|
106
|
+
* evaluationMarketplaceId: 'EBAY_US',
|
|
107
|
+
* }),
|
|
108
|
+
* );
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* @see https://developer.ebay.com/api-docs/sell/analytics/resources/customer_service_metric/methods/getCustomerServiceMetric
|
|
66
112
|
*/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
113
|
+
getCustomerServiceMetric = (input) => {
|
|
114
|
+
const { basePath, client } = this;
|
|
115
|
+
return Effect.gen(function* () {
|
|
116
|
+
const validatedInput = yield* requireObjectEffect(input, 'input');
|
|
117
|
+
const validatedMetricType = yield* requireStringEffect(validatedInput.customerServiceMetricType, 'customerServiceMetricType');
|
|
118
|
+
const validatedEvaluationType = yield* requireStringEffect(validatedInput.evaluationType, 'evaluationType');
|
|
119
|
+
const validatedMarketplaceId = yield* requireStringEffect(validatedInput.evaluationMarketplaceId, 'evaluationMarketplaceId');
|
|
120
|
+
const params = buildEndpointParams({
|
|
121
|
+
evaluationMarketplaceId: {
|
|
122
|
+
wireName: 'evaluation_marketplace_id',
|
|
123
|
+
value: validatedMarketplaceId,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
return yield* requestGetEffect(client, `${basePath}/customer_service_metric/${validatedMetricType}/${validatedEvaluationType}`, params);
|
|
127
|
+
});
|
|
128
|
+
};
|
|
83
129
|
}
|
package/build/api/client.js
CHANGED
|
@@ -1,34 +1,16 @@
|
|
|
1
1
|
import { EbayOAuthClient } from '../auth/oauth.js';
|
|
2
|
+
import { clientRequestError } from '../api/clientRequestError.js';
|
|
3
|
+
import { RateLimitTracker } from '../api/rateLimitTracker.js';
|
|
2
4
|
import { getBaseUrl } from '../config/environment.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
+
import { getErrorMessage } from '../utils/errors.js';
|
|
6
|
+
import { httpRequestEffect, isHttpError } from '../utils/http.js';
|
|
7
|
+
import { isRecord } from '../utils/typeGuards.js';
|
|
5
8
|
import { apiLogger, logRequest, logResponse, logErrorResponse } from '../utils/logger.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
windowMs = 60000; // 1 minute window
|
|
12
|
-
maxRequests = 5000; // Conservative limit
|
|
13
|
-
canMakeRequest() {
|
|
14
|
-
const now = Date.now();
|
|
15
|
-
// Remove timestamps older than window
|
|
16
|
-
this.requestTimestamps = this.requestTimestamps.filter((timestamp) => now - timestamp < this.windowMs);
|
|
17
|
-
return this.requestTimestamps.length < this.maxRequests;
|
|
18
|
-
}
|
|
19
|
-
recordRequest() {
|
|
20
|
-
this.requestTimestamps.push(Date.now());
|
|
21
|
-
}
|
|
22
|
-
getStats() {
|
|
23
|
-
const now = Date.now();
|
|
24
|
-
this.requestTimestamps = this.requestTimestamps.filter((timestamp) => now - timestamp < this.windowMs);
|
|
25
|
-
return {
|
|
26
|
-
current: this.requestTimestamps.length,
|
|
27
|
-
max: this.maxRequests,
|
|
28
|
-
windowMs: this.windowMs,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
}
|
|
9
|
+
import { Effect } from 'effect';
|
|
10
|
+
/** Sleep for a retry backoff delay without exposing timers to callers. */
|
|
11
|
+
const sleep = (delayMs) => Effect.promise(() => new Promise((resolve) => {
|
|
12
|
+
setTimeout(resolve, delayMs);
|
|
13
|
+
}));
|
|
32
14
|
/**
|
|
33
15
|
* Base client for making eBay API requests.
|
|
34
16
|
*
|
|
@@ -43,7 +25,7 @@ export class EbayApiClient {
|
|
|
43
25
|
baseUrl;
|
|
44
26
|
rateLimitTracker;
|
|
45
27
|
config;
|
|
46
|
-
timeoutMs =
|
|
28
|
+
timeoutMs = 30_000;
|
|
47
29
|
/**
|
|
48
30
|
* Build default request headers based on configured marketplace and language.
|
|
49
31
|
*/
|
|
@@ -54,6 +36,8 @@ export class EbayApiClient {
|
|
|
54
36
|
};
|
|
55
37
|
if (this.config.contentLanguage) {
|
|
56
38
|
headers['Content-Language'] = this.config.contentLanguage;
|
|
39
|
+
// undici defaults Accept-Language to `*`, which eBay Inventory rejects (error 25709).
|
|
40
|
+
headers['Accept-Language'] = this.config.contentLanguage;
|
|
57
41
|
}
|
|
58
42
|
if (this.config.marketplaceId) {
|
|
59
43
|
headers['X-EBAY-C-MARKETPLACE-ID'] = this.config.marketplaceId;
|
|
@@ -67,11 +51,16 @@ export class EbayApiClient {
|
|
|
67
51
|
this.rateLimitTracker = new RateLimitTracker();
|
|
68
52
|
}
|
|
69
53
|
/**
|
|
70
|
-
*
|
|
54
|
+
* Return the credential validation error that would block an authenticated request.
|
|
71
55
|
*/
|
|
72
|
-
|
|
73
|
-
if (!this.config.clientId
|
|
74
|
-
|
|
56
|
+
accessTokenValidationError(method, url) {
|
|
57
|
+
if (!(this.config.clientId && this.config.clientSecret)) {
|
|
58
|
+
return clientRequestError({
|
|
59
|
+
kind: 'missingCredentials',
|
|
60
|
+
method,
|
|
61
|
+
url,
|
|
62
|
+
message: 'Missing required eBay credentials. Please set EBAY_CLIENT_ID and EBAY_CLIENT_SECRET in your .env file.',
|
|
63
|
+
});
|
|
75
64
|
}
|
|
76
65
|
}
|
|
77
66
|
/**
|
|
@@ -80,7 +69,7 @@ export class EbayApiClient {
|
|
|
80
69
|
*/
|
|
81
70
|
ebayErrorDetail(data) {
|
|
82
71
|
if (isRecord(data) && Array.isArray(data.errors)) {
|
|
83
|
-
const firstError = data.errors
|
|
72
|
+
const [firstError] = data.errors;
|
|
84
73
|
if (isRecord(firstError)) {
|
|
85
74
|
const detail = firstError.longMessage ?? firstError.message;
|
|
86
75
|
if (typeof detail === 'string') {
|
|
@@ -88,41 +77,58 @@ export class EbayApiClient {
|
|
|
88
77
|
}
|
|
89
78
|
}
|
|
90
79
|
}
|
|
91
|
-
return undefined;
|
|
92
80
|
}
|
|
93
81
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* Resolves with the decoded response body; throws an `Error` with a
|
|
97
|
-
* caller-friendly message on failure.
|
|
98
|
-
*
|
|
99
|
-
* @param absolute - When true, `endpoint` is treated as a full URL (used by
|
|
100
|
-
* APIs on a different host, e.g. Identity on `apiz.ebay.com`).
|
|
82
|
+
* Serialize the full error response body so structured fields (errorId, parameters)
|
|
83
|
+
* survive into the thrown message for diagnostics.
|
|
101
84
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
85
|
+
ebayErrorBody(data) {
|
|
86
|
+
if (data == null)
|
|
87
|
+
return '';
|
|
88
|
+
try {
|
|
89
|
+
return ` | response: ${JSON.stringify(data)}`;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return '';
|
|
107
93
|
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Core request boundary shared by every verb.
|
|
97
|
+
*/
|
|
98
|
+
async request(method, endpoint, options) {
|
|
99
|
+
return await Effect.runPromise(this.requestEffect(method, endpoint, options));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Build the request Effect that owns auth, logging, retry, and transport errors.
|
|
103
|
+
*/
|
|
104
|
+
requestEffect(method, endpoint, options) {
|
|
108
105
|
const url = options.absolute ? endpoint : `${this.baseUrl}${endpoint}`;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
106
|
+
return this.sendWithRetry(method, url, options, {
|
|
107
|
+
authRetried: false,
|
|
108
|
+
serverRetries: 0,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Execute one request attempt, recursively re-entering for bounded retries.
|
|
113
|
+
*/
|
|
114
|
+
sendWithRetry(method, url, options, state) {
|
|
115
|
+
return Effect.gen(this, function* () {
|
|
116
|
+
// In proxy auth mode the upstream proxy supplies credentials, so the server
|
|
117
|
+
// neither requires nor validates its own. See EBAY_MCP_DISABLE_AUTH_HEADER.
|
|
118
|
+
if (!this.config.disableAuthHeader) {
|
|
119
|
+
const validationError = this.accessTokenValidationError(method, url);
|
|
120
|
+
if (validationError) {
|
|
121
|
+
return yield* Effect.fail(validationError);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
123
124
|
if (!this.rateLimitTracker.canMakeRequest()) {
|
|
124
125
|
const stats = this.rateLimitTracker.getStats();
|
|
125
|
-
|
|
126
|
+
return yield* Effect.fail(clientRequestError({
|
|
127
|
+
kind: 'localRateLimit',
|
|
128
|
+
method,
|
|
129
|
+
url,
|
|
130
|
+
message: `Rate limit exceeded: ${stats.current}/${stats.max} requests in ${stats.windowMs}ms window. Please wait before making more requests.`,
|
|
131
|
+
}));
|
|
126
132
|
}
|
|
127
133
|
const headers = {
|
|
128
134
|
...this.getDefaultHeaders(),
|
|
@@ -131,86 +137,142 @@ export class EbayApiClient {
|
|
|
131
137
|
// Proxy auth mode: attach no Authorization header and acquire no token —
|
|
132
138
|
// the upstream proxy injects whatever credentials eBay requires.
|
|
133
139
|
if (!this.config.disableAuthHeader) {
|
|
134
|
-
const token =
|
|
140
|
+
const token = yield* this.authClient.getAccessToken().pipe(Effect.mapError((cause) => clientRequestError({
|
|
141
|
+
kind: 'tokenAcquisition',
|
|
142
|
+
method,
|
|
143
|
+
url,
|
|
144
|
+
message: `Failed to get access token: ${getErrorMessage(cause)}`,
|
|
145
|
+
cause,
|
|
146
|
+
})));
|
|
135
147
|
if (!token) {
|
|
136
|
-
|
|
148
|
+
return yield* Effect.fail(clientRequestError({
|
|
149
|
+
kind: 'missingAccessToken',
|
|
150
|
+
method,
|
|
151
|
+
url,
|
|
152
|
+
message: 'Access token is missing. Provide EBAY_USER_REFRESH_TOKEN or valid app credentials, then retry.',
|
|
153
|
+
}));
|
|
137
154
|
}
|
|
138
155
|
headers.Authorization = `Bearer ${token}`;
|
|
139
156
|
}
|
|
140
157
|
this.rateLimitTracker.recordRequest();
|
|
141
158
|
logRequest(method, url, options.params, options.data);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
return yield* httpRequestEffect({
|
|
160
|
+
method,
|
|
161
|
+
url,
|
|
162
|
+
params: options.params,
|
|
163
|
+
headers,
|
|
164
|
+
body: options.data,
|
|
165
|
+
timeoutMs: this.timeoutMs,
|
|
166
|
+
responseType: options.responseType,
|
|
167
|
+
}).pipe(Effect.map((response) => {
|
|
151
168
|
logResponse(response.status, response.statusText, response.data, response.headers['x-ebay-c-ratelimit-remaining'], response.headers['x-ebay-c-ratelimit-limit']);
|
|
152
169
|
return response.data;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
170
|
+
}), Effect.catchAll((error) => this.handleRequestFailure(error, { method, url, options, state })));
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Convert an HTTP failure into a bounded retry or final request error.
|
|
175
|
+
*/
|
|
176
|
+
handleRequestFailure(error, context) {
|
|
177
|
+
const { method, url, options, state } = context;
|
|
178
|
+
if (!isHttpError(error)) {
|
|
179
|
+
return Effect.fail(clientRequestError({
|
|
180
|
+
kind: 'transport',
|
|
181
|
+
method,
|
|
182
|
+
url,
|
|
183
|
+
message: getErrorMessage(error),
|
|
184
|
+
cause: error,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
if (error.status == null) {
|
|
188
|
+
apiLogger.error('No response received from server', { url });
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
logErrorResponse(error.status, error.statusText, url, error.data);
|
|
192
|
+
}
|
|
193
|
+
// 401 — refresh the token once, then retry the request. Skipped in proxy
|
|
194
|
+
// auth mode: the server holds no token to refresh, so a 401 (the proxy's
|
|
195
|
+
// own auth failing) is surfaced directly rather than retried.
|
|
196
|
+
if (error.status === 401 && !this.config.disableAuthHeader) {
|
|
197
|
+
if (!state.authRetried) {
|
|
198
|
+
apiLogger.warn('Authentication error (401). Attempting to refresh user token...');
|
|
199
|
+
return this.authClient.getAccessToken().pipe(Effect.catchAll((refreshError) => {
|
|
200
|
+
const reason = getErrorMessage(refreshError);
|
|
201
|
+
apiLogger.error('Failed to refresh token', { error: reason });
|
|
184
202
|
const detail = this.ebayErrorDetail(error.data) ?? 'Invalid access token';
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
apiLogger.warn(`Server error (${error.status}). Retrying...`, {
|
|
200
|
-
attempt: `${serverRetries}/3`,
|
|
201
|
-
delayMs: Math.min(delay, 5000),
|
|
203
|
+
return Effect.fail(clientRequestError({
|
|
204
|
+
kind: 'tokenRefresh',
|
|
205
|
+
method,
|
|
206
|
+
url,
|
|
207
|
+
status: error.status,
|
|
208
|
+
message: `${detail}. Token refresh failed: ${reason}. ` +
|
|
209
|
+
'Please use the ebay_set_user_tokens_with_expiry tool to provide valid tokens.',
|
|
210
|
+
cause: refreshError,
|
|
211
|
+
}));
|
|
212
|
+
}), Effect.flatMap(() => {
|
|
213
|
+
apiLogger.info('Token refreshed successfully. Retrying request...');
|
|
214
|
+
return this.sendWithRetry(method, url, options, {
|
|
215
|
+
...state,
|
|
216
|
+
authRetried: true,
|
|
202
217
|
});
|
|
203
|
-
|
|
204
|
-
setTimeout(resolve, Math.min(delay, 5000));
|
|
205
|
-
});
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
if (error.status != null) {
|
|
209
|
-
throw new Error(`eBay API Error: ${this.ebayErrorDetail(error.data) ?? error.message}`);
|
|
210
|
-
}
|
|
211
|
-
throw error;
|
|
218
|
+
}));
|
|
212
219
|
}
|
|
220
|
+
const detail = this.ebayErrorDetail(error.data) ?? 'Invalid access token';
|
|
221
|
+
return Effect.fail(clientRequestError({
|
|
222
|
+
kind: 'tokenRefresh',
|
|
223
|
+
method,
|
|
224
|
+
url,
|
|
225
|
+
status: error.status,
|
|
226
|
+
message: `${detail}. Automatic token refresh failed. ` +
|
|
227
|
+
'Please use the ebay_set_user_tokens_with_expiry tool to provide valid tokens.',
|
|
228
|
+
cause: error,
|
|
229
|
+
}));
|
|
213
230
|
}
|
|
231
|
+
// 429 — surface a clear rate-limit message with the server's retry hint.
|
|
232
|
+
if (error.status === 429) {
|
|
233
|
+
const retryAfter = error.headers['retry-after'];
|
|
234
|
+
const waitTime = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : 60_000;
|
|
235
|
+
return Effect.fail(clientRequestError({
|
|
236
|
+
kind: 'remoteRateLimit',
|
|
237
|
+
method,
|
|
238
|
+
url,
|
|
239
|
+
status: error.status,
|
|
240
|
+
message: `eBay API rate limit exceeded. Retry after ${waitTime / 1000} seconds. ` +
|
|
241
|
+
'Consider reducing request frequency or upgrading to user tokens for higher limits.',
|
|
242
|
+
cause: error,
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
// 5xx — retry up to three times with exponential backoff.
|
|
246
|
+
if (error.status != null && error.status >= 500 && state.serverRetries < 3) {
|
|
247
|
+
const delay = 2 ** state.serverRetries * 1000;
|
|
248
|
+
const delayMs = Math.min(delay, 5000);
|
|
249
|
+
const nextServerRetries = state.serverRetries + 1;
|
|
250
|
+
apiLogger.warn(`Server error (${error.status}). Retrying...`, {
|
|
251
|
+
attempt: `${nextServerRetries}/3`,
|
|
252
|
+
delayMs,
|
|
253
|
+
});
|
|
254
|
+
return sleep(delayMs).pipe(Effect.flatMap(() => this.sendWithRetry(method, url, options, {
|
|
255
|
+
...state,
|
|
256
|
+
serverRetries: nextServerRetries,
|
|
257
|
+
})));
|
|
258
|
+
}
|
|
259
|
+
if (error.status != null) {
|
|
260
|
+
return Effect.fail(clientRequestError({
|
|
261
|
+
kind: 'httpStatus',
|
|
262
|
+
method,
|
|
263
|
+
url,
|
|
264
|
+
status: error.status,
|
|
265
|
+
message: `eBay API Error: ${this.ebayErrorDetail(error.data) ?? error.message}${this.ebayErrorBody(error.data)}`,
|
|
266
|
+
cause: error,
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
return Effect.fail(clientRequestError({
|
|
270
|
+
kind: 'transport',
|
|
271
|
+
method,
|
|
272
|
+
url,
|
|
273
|
+
message: error.message,
|
|
274
|
+
cause: error,
|
|
275
|
+
}));
|
|
214
276
|
}
|
|
215
277
|
/**
|
|
216
278
|
* Make a GET request to eBay API
|
|
@@ -219,6 +281,7 @@ export class EbayApiClient {
|
|
|
219
281
|
return await this.request('GET', endpoint, {
|
|
220
282
|
params: { ...params, ...config?.params },
|
|
221
283
|
headers: config?.headers,
|
|
284
|
+
responseType: config?.responseType,
|
|
222
285
|
});
|
|
223
286
|
}
|
|
224
287
|
/**
|
|
@@ -229,6 +292,7 @@ export class EbayApiClient {
|
|
|
229
292
|
data,
|
|
230
293
|
params: config?.params,
|
|
231
294
|
headers: config?.headers,
|
|
295
|
+
responseType: config?.responseType,
|
|
232
296
|
});
|
|
233
297
|
}
|
|
234
298
|
/**
|
|
@@ -239,6 +303,7 @@ export class EbayApiClient {
|
|
|
239
303
|
data,
|
|
240
304
|
params: config?.params,
|
|
241
305
|
headers: config?.headers,
|
|
306
|
+
responseType: config?.responseType,
|
|
242
307
|
});
|
|
243
308
|
}
|
|
244
309
|
/**
|
|
@@ -248,14 +313,13 @@ export class EbayApiClient {
|
|
|
248
313
|
return await this.request('DELETE', endpoint, {
|
|
249
314
|
params: config?.params,
|
|
250
315
|
headers: config?.headers,
|
|
316
|
+
responseType: config?.responseType,
|
|
251
317
|
});
|
|
252
318
|
}
|
|
253
319
|
/**
|
|
254
320
|
* Initialize the client (load user tokens from storage)
|
|
255
321
|
*/
|
|
256
|
-
|
|
257
|
-
await this.authClient.initialize();
|
|
258
|
-
}
|
|
322
|
+
initialize = () => this.authClient.initialize();
|
|
259
323
|
/**
|
|
260
324
|
* Check if client is authenticated
|
|
261
325
|
*/
|
|
@@ -271,10 +335,7 @@ export class EbayApiClient {
|
|
|
271
335
|
/**
|
|
272
336
|
* Set user access and refresh tokens
|
|
273
337
|
*/
|
|
274
|
-
setUserTokens(accessToken, refreshToken, accessTokenExpiry, refreshTokenExpiry)
|
|
275
|
-
this.authClient.setUserTokens(accessToken, refreshToken, accessTokenExpiry, refreshTokenExpiry);
|
|
276
|
-
return Promise.resolve();
|
|
277
|
-
}
|
|
338
|
+
setUserTokens = (accessToken, refreshToken, accessTokenExpiry, refreshTokenExpiry) => this.authClient.setUserTokens(accessToken, refreshToken, accessTokenExpiry, refreshTokenExpiry);
|
|
278
339
|
/**
|
|
279
340
|
* Get token information for debugging
|
|
280
341
|
*/
|
|
@@ -304,9 +365,7 @@ export class EbayApiClient {
|
|
|
304
365
|
* This is useful when you encounter "Invalid access token" errors
|
|
305
366
|
* The token will be automatically saved to storage after refresh
|
|
306
367
|
*/
|
|
307
|
-
|
|
308
|
-
await this.authClient.refreshUserToken();
|
|
309
|
-
}
|
|
368
|
+
refreshUserToken = () => this.authClient.refreshUserToken();
|
|
310
369
|
/**
|
|
311
370
|
* Make a GET request with a full URL (for APIs that use different base URLs)
|
|
312
371
|
* Used by Identity API which uses the apiz subdomain
|