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.
Files changed (169) hide show
  1. package/README.de.md +3 -3
  2. package/README.es.md +3 -3
  3. package/README.fr.md +3 -3
  4. package/README.ja.md +3 -3
  5. package/README.ko.md +3 -3
  6. package/README.md +163 -41
  7. package/README.pt-BR.md +3 -3
  8. package/README.ru.md +3 -3
  9. package/README.zh-CN.md +3 -3
  10. package/build/api/account-management/account.js +633 -297
  11. package/build/api/analytics-and-report/analytics.js +111 -65
  12. package/build/api/client.js +199 -140
  13. package/build/api/clientRequestError.js +23 -0
  14. package/build/api/clientTrading.js +213 -0
  15. package/build/api/clientTradingError.js +4 -0
  16. package/build/api/communication/feedback.js +147 -59
  17. package/build/api/communication/message.js +134 -81
  18. package/build/api/communication/negotiation.js +65 -53
  19. package/build/api/communication/notification.js +458 -164
  20. package/build/api/developer/developer.js +99 -45
  21. package/build/api/index.js +23 -8
  22. package/build/api/listing-management/inventory.js +884 -338
  23. package/build/api/listing-metadata/metadata.js +416 -170
  24. package/build/api/listing-metadata/taxonomy.js +187 -42
  25. package/build/api/marketing-and-promotions/marketing.js +1392 -598
  26. package/build/api/marketing-and-promotions/recommendation.js +49 -24
  27. package/build/api/order-management/dispute.js +226 -45
  28. package/build/api/order-management/fulfillment.js +144 -71
  29. package/build/api/order-management/orderIssueHelpers.js +61 -0
  30. package/build/api/other/compliance.js +50 -38
  31. package/build/api/other/edelivery.js +603 -159
  32. package/build/api/other/finding.js +280 -0
  33. package/build/api/other/identity.js +30 -15
  34. package/build/api/other/translation.js +29 -14
  35. package/build/api/other/vero.js +113 -27
  36. package/build/api/rateLimitTracker.js +55 -0
  37. package/build/api/shared/request.js +189 -98
  38. package/build/api/trading/trading.js +154 -87
  39. package/build/auth/credentialSession.js +250 -0
  40. package/build/auth/oauth.js +298 -171
  41. package/build/auth/{oauth-metadata.js → oauthMetadata.js} +22 -6
  42. package/build/auth/oauthMiddleware.js +104 -0
  43. package/build/auth/{scope-utils.js → scopeUtils.js} +101 -28
  44. package/build/auth/tokenVerifier.js +243 -0
  45. package/build/auth/tokenVerifierError.js +21 -0
  46. package/build/config/environment.js +148 -113
  47. package/build/config/{tool-families.js → toolFamilies.js} +30 -14
  48. package/build/index.js +25 -19
  49. package/build/mcp/{http-transport.js → httpTransport.js} +102 -52
  50. package/build/mcp/readOnlyFilter.js +67 -0
  51. package/build/mcp/runtime.js +31 -13
  52. package/build/mcp/{tool-gating.js → toolGating.js} +77 -49
  53. package/build/mcp/{ui-bridge.js → uiBridge.js} +80 -23
  54. package/build/schemas/account-management/account.js +144 -6
  55. package/build/schemas/analytics/analytics.js +33 -19
  56. package/build/schemas/communication/messages.js +12 -42
  57. package/build/schemas/developer/developer.js +135 -0
  58. package/build/schemas/fulfillment/orders.js +183 -21
  59. package/build/schemas/index.js +27 -19
  60. package/build/schemas/inventory-management/inventory.js +17 -9
  61. package/build/schemas/marketing/marketing.js +480 -25
  62. package/build/schemas/metadata/metadata.js +11 -6
  63. package/build/schemas/other/finding.js +27 -0
  64. package/build/schemas/other/otherApis.js +424 -0
  65. package/build/schemas/taxonomy/taxonomy.js +11 -5
  66. package/build/scripts/{auto-setup.js → autoSetup.js} +87 -74
  67. package/build/scripts/{build-ui.js → buildUi.js} +6 -1
  68. package/build/scripts/{dev-sync.js → devSync.js} +169 -110
  69. package/build/scripts/diagnostics.js +52 -44
  70. package/build/scripts/{download-specs.js → downloadSpecs.js} +29 -21
  71. package/build/scripts/{interactive-setup.js → interactiveSetup.js} +84 -78
  72. package/build/scripts/setup.js +241 -178
  73. package/build/scripts/{setup-shared.js → setupShared.js} +29 -7
  74. package/build/scripts/skills.js +35 -27
  75. package/build/scripts/updateApiStatusDoc.js +71 -0
  76. package/build/serverHttp.js +90 -0
  77. package/build/skills/content.js +124 -118
  78. package/build/skills/index.js +22 -10
  79. package/build/skills/install.js +21 -10
  80. package/build/skills/markers.js +50 -15
  81. package/build/skills/{registry-snapshot.js → registrySnapshot.js} +8 -2
  82. package/build/skills/render.js +41 -14
  83. package/build/skills/targets.js +49 -17
  84. package/build/tools/categories/account.js +166 -264
  85. package/build/tools/categories/analytics.js +11 -22
  86. package/build/tools/categories/browse.js +17 -0
  87. package/build/tools/categories/communication.js +124 -399
  88. package/build/tools/categories/connector.js +23 -20
  89. package/build/tools/categories/developer.js +46 -159
  90. package/build/tools/categories/fulfillment.js +84 -215
  91. package/build/tools/categories/index.js +6 -14
  92. package/build/tools/categories/inventory.js +238 -293
  93. package/build/tools/categories/marketing.js +677 -39
  94. package/build/tools/categories/metadata.js +190 -117
  95. package/build/tools/categories/other.js +81 -208
  96. package/build/tools/categories/taxonomy.js +29 -20
  97. package/build/tools/categories/{token-management.js → tokenManagement.js} +187 -192
  98. package/build/tools/categories/trading.js +16 -47
  99. package/build/tools/contracts.js +34 -24
  100. package/build/tools/{define-tool.js → defineTool.js} +44 -29
  101. package/build/tools/registry.js +87 -29
  102. package/build/tools/schemas.js +3 -57
  103. package/build/tools/ui/archetypes.js +1 -0
  104. package/build/tools/ui/{map-helpers.js → mapHelpers.js} +69 -19
  105. package/build/tools/ui/maps.js +294 -80
  106. package/build/types/index.js +1 -1
  107. package/build/ui/card.html +14 -14
  108. package/build/ui/chart.html +14 -14
  109. package/build/ui/stat.html +87 -0
  110. package/build/ui/table.html +14 -14
  111. package/build/utils/apiStatusFeed.js +95 -0
  112. package/build/utils/cliOutput.js +15 -0
  113. package/build/utils/cliUi.js +108 -0
  114. package/build/utils/communication/feedback.js +57 -77
  115. package/build/utils/communication/message.js +69 -98
  116. package/build/utils/communication/negotiation.js +30 -57
  117. package/build/utils/communication/notification.js +112 -252
  118. package/build/utils/{date-converter.js → dateConverter.js} +62 -30
  119. package/build/utils/effectSchema.js +203 -0
  120. package/build/utils/effectSchemaAdapter.js +121 -0
  121. package/build/utils/effectSchemaChainMethods.js +146 -0
  122. package/build/utils/effectSchemaRefinements.js +104 -0
  123. package/build/utils/effectSchemaStringMethods.js +63 -0
  124. package/build/utils/effectSchemaTypes.js +47 -0
  125. package/build/utils/{env-parser.js → envParser.js} +10 -2
  126. package/build/utils/errors.js +7 -3
  127. package/build/utils/http.js +148 -89
  128. package/build/utils/{llm-client-detector.js → llmClientDetector.js} +295 -274
  129. package/build/utils/logger.js +127 -51
  130. package/build/utils/{oauth-helper.js → oauthHelper.js} +127 -90
  131. package/build/utils/other/compliance.js +18 -38
  132. package/build/utils/other/identity.js +2 -2
  133. package/build/utils/other/translation.js +4 -4
  134. package/build/utils/other/vero.js +36 -20
  135. package/build/utils/schemaHelpers.js +22 -0
  136. package/build/utils/scopeHelper.js +270 -0
  137. package/build/utils/securityChecker.js +316 -0
  138. package/build/utils/{setup-validator.js → setupValidator.js} +113 -94
  139. package/build/utils/{setup-wizard.js → setupWizard.js} +57 -46
  140. package/build/utils/{token-utils.js → tokenUtils.js} +2 -2
  141. package/build/utils/trading/trading.js +35 -0
  142. package/build/utils/typeGuards.js +12 -0
  143. package/build/utils/version.js +65 -29
  144. package/package.json +10 -8
  145. package/build/api/client-trading.js +0 -124
  146. package/build/api/communication/shared.js +0 -59
  147. package/build/api/shared/query-params.js +0 -16
  148. package/build/auth/credential-session.js +0 -166
  149. package/build/auth/oauth-middleware.js +0 -100
  150. package/build/auth/token-verifier.js +0 -186
  151. package/build/schemas/other/other-apis.js +0 -390
  152. package/build/scripts/update-api-status-doc.js +0 -44
  153. package/build/server-http.js +0 -85
  154. package/build/tools/definitions/marketing.js +0 -1381
  155. package/build/tools/tool-handlers/marketing.js +0 -312
  156. package/build/utils/account-management/account.js +0 -818
  157. package/build/utils/api-status-feed.js +0 -84
  158. package/build/utils/cli-ui.js +0 -47
  159. package/build/utils/order-management/dispute.js +0 -369
  160. package/build/utils/order-management/fulfillment.js +0 -205
  161. package/build/utils/other/edelivery.js +0 -235
  162. package/build/utils/schema-helpers.js +0 -13
  163. package/build/utils/scope-helper.js +0 -207
  164. package/build/utils/security-checker.js +0 -248
  165. package/build/utils/type-guards.js +0 -6
  166. /package/build/auth/{oauth-types.js → oauthTypes.js} +0 -0
  167. /package/build/{server-http.d.ts → serverHttp.d.ts} +0 -0
  168. /package/build/tools/ui/{view-models.js → viewModels.js} +0 -0
  169. /package/build/types/{ebay-enums.js → ebayEnums.js} +0 -0
@@ -1,83 +1,129 @@
1
- import { withApiError } from '../../api/shared/request.js';
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
- * Get traffic report for listings
14
- * @throws Error if required parameters are missing or invalid
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
- async getTrafficReport(dimension, filter, metric, sort) {
17
- // Input validation
18
- if (!dimension || typeof dimension !== 'string') {
19
- throw new Error('dimension is required and must be a string');
20
- }
21
- if (!filter || typeof filter !== 'string') {
22
- throw new Error('filter is required and must be a string');
23
- }
24
- if (!metric || typeof metric !== 'string') {
25
- throw new Error('metric is required and must be a string');
26
- }
27
- if (sort !== undefined && typeof sort !== 'string') {
28
- throw new Error('sort must be a string when provided');
29
- }
30
- const params = {
31
- dimension,
32
- filter,
33
- metric,
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
- * Find all seller standards profiles
41
- * Endpoint: GET /seller_standards_profile
42
- * @throws Error if the request fails
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
- async findSellerStandardsProfiles() {
45
- return await withApiError('Failed to find seller standards profiles', () => this.client.get(`${this.basePath}/seller_standards_profile`));
46
- }
63
+ findSellerStandardsProfiles = (input = {}) => {
64
+ void input;
65
+ return requestGetEffect(this.client, `${this.basePath}/seller_standards_profile`);
66
+ };
47
67
  /**
48
- * Get a specific seller standards profile
49
- * Endpoint: GET /seller_standards_profile/{program}/{cycle}
50
- * @throws Error if required parameters are missing or invalid
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
- async getSellerStandardsProfile(program, cycle) {
53
- // Input validation
54
- if (!program || typeof program !== 'string') {
55
- throw new Error('program is required and must be a string');
56
- }
57
- if (!cycle || typeof cycle !== 'string') {
58
- throw new Error('cycle is required and must be a string');
59
- }
60
- return await withApiError('Failed to get seller standards profile', () => this.client.get(`${this.basePath}/seller_standards_profile/${program}/${cycle}`));
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
- * Get customer service metrics
64
- * Endpoint: GET /customer_service_metric/{customer_service_metric_type}/{evaluation_type}
65
- * @throws Error if required parameters are missing or invalid
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
- async getCustomerServiceMetric(customerServiceMetricType, evaluationType, evaluationMarketplaceId) {
68
- // Input validation
69
- if (!customerServiceMetricType || typeof customerServiceMetricType !== 'string') {
70
- throw new Error('customerServiceMetricType is required and must be a string');
71
- }
72
- if (!evaluationType || typeof evaluationType !== 'string') {
73
- throw new Error('evaluationType is required and must be a string');
74
- }
75
- if (!evaluationMarketplaceId || typeof evaluationMarketplaceId !== 'string') {
76
- throw new Error('evaluationMarketplaceId is required and must be a string');
77
- }
78
- const params = {
79
- evaluation_marketplace_id: evaluationMarketplaceId,
80
- };
81
- return await withApiError('Failed to get customer service metric', () => this.client.get(`${this.basePath}/customer_service_metric/${customerServiceMetricType}/${evaluationType}`, params));
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
  }
@@ -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 { httpRequest, isHttpError } from '../utils/http.js';
4
- import { isRecord } from '../utils/type-guards.js';
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
- * Rate limit tracking
8
- */
9
- class RateLimitTracker {
10
- requestTimestamps = [];
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 = 30000;
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
- * Validate that access token is available before making API request
54
+ * Return the credential validation error that would block an authenticated request.
71
55
  */
72
- validateAccessToken() {
73
- if (!this.config.clientId || !this.config.clientSecret) {
74
- throw new Error('Missing required eBay credentials. Please set EBAY_CLIENT_ID and EBAY_CLIENT_SECRET in your .env file.');
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[0];
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
- * Core request path shared by every verb. Injects auth + default headers,
95
- * enforces the rate limit, logs, and applies eBay's retry/refresh semantics.
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
- async request(method, endpoint, options) {
103
- // In proxy auth mode the upstream proxy supplies credentials, so the server
104
- // neither requires nor validates its own. See EBAY_MCP_DISABLE_AUTH_HEADER.
105
- if (!this.config.disableAuthHeader) {
106
- this.validateAccessToken();
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
- let authRetried = false;
110
- let serverRetries = 0;
111
- // Retry loop: each pass either returns the body (success), `continue`s to
112
- // retry, or throws. Retries are bounded so this can never spin forever —
113
- // `authRetried` allows one token-refresh retry on 401, and `serverRetries`
114
- // allows up to three backoff retries on 5xx. Once both are spent, the next
115
- // failure throws and the loop exits.
116
- //
117
- // Example: a 401 then two 503s then success runs four passes —
118
- // pass 1: 401 → refresh token, continue (authRetried → true)
119
- // pass 2: 503 → backoff, continue (serverRetries → 1)
120
- // pass 3: 503 → backoff, continue (serverRetries → 2)
121
- // pass 4: 200 → return response.data
122
- while (true) {
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
- throw new Error(`Rate limit exceeded: ${stats.current}/${stats.max} requests in ${stats.windowMs}ms window. Please wait before making more requests.`);
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 = await this.authClient.getAccessToken();
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
- throw new Error('Access token is missing. Provide EBAY_USER_REFRESH_TOKEN or valid app credentials, then retry.');
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
- try {
143
- const response = await httpRequest({
144
- method,
145
- url,
146
- params: options.params,
147
- headers,
148
- body: options.data,
149
- timeoutMs: this.timeoutMs,
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
- catch (error) {
155
- if (!isHttpError(error)) {
156
- throw error;
157
- }
158
- if (error.status != null) {
159
- logErrorResponse(error.status, error.statusText, url, error.data);
160
- }
161
- else {
162
- apiLogger.error('No response received from server', { url });
163
- }
164
- // 401 — refresh the token once, then retry the request. Skipped in proxy
165
- // auth mode: the server holds no token to refresh, so a 401 (the proxy's
166
- // own auth failing) is surfaced directly rather than retried.
167
- if (error.status === 401 && !this.config.disableAuthHeader) {
168
- if (!authRetried) {
169
- authRetried = true;
170
- apiLogger.warn('Authentication error (401). Attempting to refresh user token...');
171
- try {
172
- await this.authClient.getAccessToken();
173
- }
174
- catch (refreshError) {
175
- const reason = refreshError instanceof Error ? refreshError.message : 'Unknown error';
176
- apiLogger.error('Failed to refresh token', { error: reason });
177
- const detail = this.ebayErrorDetail(error.data) ?? 'Invalid access token';
178
- throw new Error(`${detail}. Token refresh failed: ${reason}. ` +
179
- `Please use the ebay_set_user_tokens_with_expiry tool to provide valid tokens.`);
180
- }
181
- apiLogger.info('Token refreshed successfully. Retrying request...');
182
- continue;
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
- throw new Error(`${detail}. Automatic token refresh failed. ` +
186
- `Please use the ebay_set_user_tokens_with_expiry tool to provide valid tokens.`);
187
- }
188
- // 429 — surface a clear rate-limit message with the server's retry hint.
189
- if (error.status === 429) {
190
- const retryAfter = error.headers['retry-after'];
191
- const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : 60000;
192
- throw new Error(`eBay API rate limit exceeded. Retry after ${waitTime / 1000} seconds. ` +
193
- `Consider reducing request frequency or upgrading to user tokens for higher limits.`);
194
- }
195
- // 5xx — retry up to three times with exponential backoff.
196
- if (error.status != null && error.status >= 500 && serverRetries < 3) {
197
- const delay = Math.pow(2, serverRetries) * 1000;
198
- serverRetries++;
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
- await new Promise((resolve) => {
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
- async initialize() {
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
- async refreshUserToken() {
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