ebay-mcp 1.8.3 → 1.8.8

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 (53) hide show
  1. package/README.md +1 -2
  2. package/build/api/client-trading.js +12 -3
  3. package/build/api/communication/feedback.js +6 -31
  4. package/build/api/communication/message.js +21 -34
  5. package/build/api/communication/negotiation.js +17 -37
  6. package/build/api/communication/notification.js +14 -42
  7. package/build/api/communication/shared.js +71 -0
  8. package/build/api/other/vero.js +2 -7
  9. package/build/api/shared/query-params.js +35 -0
  10. package/build/api/trading/trading.js +20 -7
  11. package/build/auth/oauth.js +9 -8
  12. package/build/auth/scope-utils.js +7 -1
  13. package/build/auth/token-verifier.js +9 -4
  14. package/build/config/environment.js +1 -1
  15. package/build/index.js +1 -1
  16. package/build/scripts/auto-setup.js +43 -20
  17. package/build/scripts/diagnostics.js +5 -31
  18. package/build/scripts/interactive-setup.js +16 -27
  19. package/build/scripts/setup-shared.js +28 -0
  20. package/build/scripts/setup.js +36 -117
  21. package/build/server-http.js +4 -2
  22. package/build/utils/account-management/account.js +1 -7
  23. package/build/utils/communication/notification.js +1 -7
  24. package/build/utils/env-parser.js +30 -0
  25. package/build/utils/other/edelivery.js +1 -7
  26. package/build/utils/schema-helpers.js +13 -0
  27. package/build/utils/scope-helper.js +2 -2
  28. package/build/utils/setup-validator.js +1 -31
  29. package/build/utils/setup-wizard.js +172 -0
  30. package/build/utils/type-guards.js +6 -0
  31. package/build/utils/version.js +13 -1
  32. package/package.json +15 -3
  33. package/build/types/application-settings/developerAnalyticsV1BetaOas3.js +0 -5
  34. package/build/types/application-settings/developerClientRegistrationV1Oas3.js +0 -5
  35. package/build/types/application-settings/developerKeyManagementV1Oas3.js +0 -5
  36. package/build/types/sell-apps/account-management/sellAccountV1Oas3.js +0 -5
  37. package/build/types/sell-apps/analytics-and-report/sellAnalyticsV1Oas3.js +0 -5
  38. package/build/types/sell-apps/communication/commerceFeedbackV1BetaOas3.js +0 -5
  39. package/build/types/sell-apps/communication/commerceMessageV1Oas3.js +0 -5
  40. package/build/types/sell-apps/communication/commerceNotificationV1Oas3.js +0 -5
  41. package/build/types/sell-apps/communication/sellNegotiationV1Oas3.js +0 -5
  42. package/build/types/sell-apps/listing-management/sellInventoryV1Oas3.js +0 -5
  43. package/build/types/sell-apps/listing-metadata/sellMetadataV1Oas3.js +0 -5
  44. package/build/types/sell-apps/markeitng-and-promotions/sellMarketingV1Oas3.js +0 -5
  45. package/build/types/sell-apps/markeitng-and-promotions/sellRecommendationV1Oas3.js +0 -5
  46. package/build/types/sell-apps/order-management/sellFulfillmentV1Oas3.js +0 -5
  47. package/build/types/sell-apps/other-apis/commerceIdentityV1Oas3.js +0 -5
  48. package/build/types/sell-apps/other-apis/commerceTranslationV1BetaOas3.js +0 -5
  49. package/build/types/sell-apps/other-apis/commerceVeroV1Oas3.js +0 -5
  50. package/build/types/sell-apps/other-apis/sellComplianceV1Oas3.js +0 -5
  51. package/build/types/sell-apps/other-apis/sellEdeliveryInternationalShippingOas3.js +0 -5
  52. package/build/types/sell-apps/other-apis/sellMarketingV1Oas3.js +0 -5
  53. package/build/types/sell-apps/other-apis/sellRecommendationV1Oas3.js +0 -5
package/README.md CHANGED
@@ -8,7 +8,6 @@
8
8
  [![API Coverage](https://img.shields.io/badge/API%20coverage-100%25-success)](src/tools/)
9
9
  [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
10
10
  [![Contributors Welcome](https://img.shields.io/badge/contributors-welcome-brightgreen.svg)](CONTRIBUTING.md)
11
- [![grimoire-wizard](https://img.shields.io/npm/v/grimoire-wizard?label=wizard%3A+grimoire&color=0064D2)](https://github.com/YosefHayim/grimoire)
12
11
 
13
12
  [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/yosefhayim-ebay-api-mcp-server-badge.png)](https://mseep.ai/app/yosefhayim-ebay-api-mcp-server)
14
13
  <a href="https://www.buymeacoffee.com/yosefhayim" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
@@ -158,7 +157,7 @@ The interactive setup wizard handles everything for you:
158
157
  npm run setup
159
158
  ```
160
159
 
161
- > **Powered by [grimoire-wizard](https://github.com/YosefHayim/grimoire)** a config-driven CLI wizard framework [![grimoire-wizard](https://img.shields.io/npm/v/grimoire-wizard?label=grimoire-wizard&color=0064D2)](https://www.npmjs.com/package/grimoire-wizard)
160
+ > Built with a standard Node CLI prompt stack for reliable interactive setup.
162
161
 
163
162
  The wizard will:
164
163
 
@@ -1,6 +1,7 @@
1
1
  import axios from 'axios';
2
2
  import { XMLBuilder, XMLParser } from 'fast-xml-parser';
3
3
  import { apiLogger } from '../utils/logger.js';
4
+ import { isRecord } from '../utils/type-guards.js';
4
5
  const COMPAT_LEVEL = '1451';
5
6
  const SITE_ID = '0';
6
7
  export class TradingApiClient {
@@ -42,7 +43,7 @@ export class TradingApiClient {
42
43
  suppressEmptyNode: true,
43
44
  });
44
45
  }
45
- getBaseUrl() {
46
+ getTradingBaseUrl() {
46
47
  return this.baseUrl;
47
48
  }
48
49
  async execute(callName, params) {
@@ -75,12 +76,20 @@ export class TradingApiClient {
75
76
  }
76
77
  let parsed;
77
78
  try {
78
- parsed = this.parser.parse(response.data);
79
+ const parsedValue = this.parser.parse(response.data);
80
+ if (!isRecord(parsedValue)) {
81
+ throw new Error('Trading API response must be an object');
82
+ }
83
+ parsed = parsedValue;
79
84
  }
80
85
  catch (e) {
81
86
  throw new Error(`Failed to parse Trading API ${callName} response: ${e instanceof Error ? e.message : String(e)}`);
82
87
  }
83
- const result = (parsed[responseTag] || parsed);
88
+ const resultValue = parsed[responseTag] || parsed;
89
+ if (!isRecord(resultValue)) {
90
+ throw new Error(`Trading API ${callName} response payload is not an object`);
91
+ }
92
+ const result = resultValue;
84
93
  // Log warnings without failing
85
94
  if (result.Ack === 'Warning') {
86
95
  apiLogger.warn(`Trading API ${callName} returned warnings`, {
@@ -1,3 +1,4 @@
1
+ import { getPaginatedWithContextError, getPathWithContextError } from './shared.js';
1
2
  /**
2
3
  * Feedback API - Manage buyer and seller feedback
3
4
  * Based on: docs/sell-apps/communication/commerce_feedback_v1_beta_oas3.json
@@ -11,34 +12,13 @@ export class FeedbackApi {
11
12
  /**
12
13
  * Get items awaiting feedback
13
14
  * Endpoint: GET /awaiting_feedback
15
+ * @param filter API filter expression.
16
+ * @param limit Maximum number of records to return.
17
+ * @param offset Zero-based pagination offset.
14
18
  * @throws Error if the request fails
15
19
  */
16
20
  async getAwaitingFeedback(filter, limit, offset) {
17
- const params = {};
18
- if (filter !== undefined) {
19
- if (typeof filter !== 'string') {
20
- throw new Error('filter must be a string when provided');
21
- }
22
- params.filter = filter;
23
- }
24
- if (limit !== undefined) {
25
- if (typeof limit !== 'number' || limit < 1) {
26
- throw new Error('limit must be a positive number when provided');
27
- }
28
- params.limit = limit;
29
- }
30
- if (offset !== undefined) {
31
- if (typeof offset !== 'number' || offset < 0) {
32
- throw new Error('offset must be a non-negative number when provided');
33
- }
34
- params.offset = offset;
35
- }
36
- try {
37
- return await this.client.get(`${this.basePath}/awaiting_feedback`, params);
38
- }
39
- catch (error) {
40
- throw new Error(`Failed to get awaiting feedback: ${error instanceof Error ? error.message : 'Unknown error'}`);
41
- }
21
+ return await getPaginatedWithContextError(this.client, `${this.basePath}/awaiting_feedback`, 'Failed to get awaiting feedback', filter, limit, offset);
42
22
  }
43
23
  /**
44
24
  * Get feedback for a transaction
@@ -64,12 +44,7 @@ export class FeedbackApi {
64
44
  * @throws Error if the request fails
65
45
  */
66
46
  async getFeedbackRatingSummary() {
67
- try {
68
- return await this.client.get(`${this.basePath}/feedback_rating_summary`);
69
- }
70
- catch (error) {
71
- throw new Error(`Failed to get feedback rating summary: ${error instanceof Error ? error.message : 'Unknown error'}`);
72
- }
47
+ return await getPathWithContextError(this.client, `${this.basePath}/feedback_rating_summary`, 'Failed to get feedback rating summary');
73
48
  }
74
49
  /**
75
50
  * Leave feedback for a buyer
@@ -1,3 +1,4 @@
1
+ import { assertRequiredString, buildPaginatedQueryParams, getPathWithContextError, getWithContextError, } from './shared.js';
1
2
  /**
2
3
  * Message API - Buyer-seller messaging
3
4
  * Based on: docs/sell-apps/communication/commerce_message_v1_oas3.json
@@ -27,50 +28,36 @@ export class MessageApi {
27
28
  /**
28
29
  * Get conversations
29
30
  * Endpoint: GET /conversation
31
+ * @param filter API filter expression.
32
+ * @param limit Maximum number of records to return.
33
+ * @param offset Zero-based pagination offset.
30
34
  * @throws Error if the request fails
31
35
  */
32
36
  async getConversations(filter, limit, offset) {
33
- const params = {};
34
- if (filter !== undefined) {
35
- if (typeof filter !== 'string') {
36
- throw new Error('filter must be a string when provided');
37
- }
38
- params.filter = filter;
39
- }
40
- if (limit !== undefined) {
41
- if (typeof limit !== 'number' || limit < 1) {
42
- throw new Error('limit must be a positive number when provided');
43
- }
44
- params.limit = limit;
45
- }
46
- if (offset !== undefined) {
47
- if (typeof offset !== 'number' || offset < 0) {
48
- throw new Error('offset must be a non-negative number when provided');
49
- }
50
- params.offset = offset;
51
- }
52
- try {
53
- return await this.client.get(`${this.basePath}/conversation`, params);
54
- }
55
- catch (error) {
56
- throw new Error(`Failed to get conversations: ${error instanceof Error ? error.message : 'Unknown error'}`);
57
- }
37
+ const conversationPath = `${this.basePath}/conversation`;
38
+ const queryParams = this.buildConversationQueryParams(filter, limit, offset);
39
+ return await getWithContextError(this.client, conversationPath, queryParams, 'Failed to get conversations');
40
+ }
41
+ /**
42
+ * Build query params used by conversation listing endpoints.
43
+ *
44
+ * @param filter API filter expression.
45
+ * @param limit Maximum number of records to return.
46
+ * @param offset Zero-based pagination offset.
47
+ * @returns Validated conversation query params.
48
+ */
49
+ buildConversationQueryParams(filter, limit, offset) {
50
+ return buildPaginatedQueryParams(filter, limit, offset);
58
51
  }
59
52
  /**
60
53
  * Get a specific conversation
61
54
  * Endpoint: GET /conversation/{conversation_id}
55
+ * @param conversationId Conversation identifier.
62
56
  * @throws Error if required parameters are missing or invalid
63
57
  */
64
58
  async getConversation(conversationId) {
65
- if (!conversationId || typeof conversationId !== 'string') {
66
- throw new Error('conversationId is required and must be a string');
67
- }
68
- try {
69
- return await this.client.get(`${this.basePath}/conversation/${conversationId}`);
70
- }
71
- catch (error) {
72
- throw new Error(`Failed to get conversation: ${error instanceof Error ? error.message : 'Unknown error'}`);
73
- }
59
+ assertRequiredString(conversationId, 'conversationId');
60
+ return await getPathWithContextError(this.client, `${this.basePath}/conversation/${conversationId}`, 'Failed to get conversation');
74
61
  }
75
62
  /**
76
63
  * Send a message
@@ -1,3 +1,5 @@
1
+ import { assertRequiredString, buildPaginatedQueryParams, getPathWithContextError, getWithContextError, } from './shared.js';
2
+ import { buildTruthyPaginatedParams } from '../shared/query-params.js';
1
3
  /**
2
4
  * Negotiation API - Buyer-seller negotiations and offers
3
5
  * Based on: docs/sell-apps/communication/sell_negotiation_v1_oas3.json
@@ -11,30 +13,17 @@ export class NegotiationApi {
11
13
  /**
12
14
  * Find eligible items for a seller-initiated offer
13
15
  * Endpoint: GET /find_eligible_items
16
+ * @param filter API filter expression.
17
+ * @param limit Maximum number of records to return.
18
+ * @param offset Zero-based pagination offset.
14
19
  * @throws Error if the request fails
15
20
  */
16
21
  async findEligibleItems(filter, limit, offset) {
17
- const params = {};
18
- if (filter !== undefined) {
19
- if (typeof filter !== 'string') {
20
- throw new Error('filter must be a string when provided');
21
- }
22
- params.filter = filter;
23
- }
24
- if (limit !== undefined) {
25
- if (typeof limit !== 'number' || limit < 1) {
26
- throw new Error('limit must be a positive number when provided');
27
- }
28
- params.limit = limit;
29
- }
30
- if (offset !== undefined) {
31
- if (typeof offset !== 'number' || offset < 0) {
32
- throw new Error('offset must be a non-negative number when provided');
33
- }
34
- params.offset = offset;
35
- }
22
+ const eligibleItemsPath = `${this.basePath}/find_eligible_items`;
23
+ const queryParams = buildPaginatedQueryParams(filter, limit, offset);
36
24
  try {
37
- return await this.client.get(`${this.basePath}/find_eligible_items`, params);
25
+ const response = await this.client.get(eligibleItemsPath, queryParams);
26
+ return response;
38
27
  }
39
28
  catch (error) {
40
29
  throw new Error(`Failed to find eligible items: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -58,17 +47,14 @@ export class NegotiationApi {
58
47
  }
59
48
  /**
60
49
  * Get offers to buyers (Best Offers)
50
+ * @param filter API filter expression.
51
+ * @param limit Maximum number of records to return.
52
+ * @param offset Zero-based pagination offset.
61
53
  * @deprecated This method does not match any endpoint in the OpenAPI spec
62
54
  */
63
55
  async getOffersToBuyers(filter, limit, offset) {
64
- const params = {};
65
- if (filter)
66
- params.filter = filter;
67
- if (limit)
68
- params.limit = limit;
69
- if (offset)
70
- params.offset = offset;
71
- return await this.client.get(`${this.basePath}/offer`, params);
56
+ const params = buildTruthyPaginatedParams(filter, limit, offset);
57
+ return await getWithContextError(this.client, `${this.basePath}/offer`, params, 'Failed to get offers to buyers');
72
58
  }
73
59
  /**
74
60
  * Get offers for listing (alias for getOffersToBuyers)
@@ -81,17 +67,11 @@ export class NegotiationApi {
81
67
  /**
82
68
  * Get a specific offer
83
69
  * Endpoint: GET /offer/{offerId}
70
+ * @param offerId Offer identifier.
84
71
  * @throws Error if required parameters are missing or invalid
85
72
  */
86
73
  async getOffer(offerId) {
87
- if (!offerId || typeof offerId !== 'string') {
88
- throw new Error('offerId is required and must be a string');
89
- }
90
- try {
91
- return await this.client.get(`${this.basePath}/offer/${offerId}`);
92
- }
93
- catch (error) {
94
- throw new Error(`Failed to get offer: ${error instanceof Error ? error.message : 'Unknown error'}`);
95
- }
74
+ assertRequiredString(offerId, 'offerId');
75
+ return await getPathWithContextError(this.client, `${this.basePath}/offer/${offerId}`, 'Failed to get offer');
96
76
  }
97
77
  }
@@ -1,3 +1,4 @@
1
+ import { assertRequiredString, getPathWithContextError } from './shared.js';
1
2
  /**
2
3
  * Notification API - Event notifications and subscriptions
3
4
  * Based on: docs/sell-apps/communication/commerce_notification_v1_oas3.json
@@ -10,30 +11,19 @@ export class NotificationApi {
10
11
  }
11
12
  /**
12
13
  * Get public key for validating notifications
14
+ * @param publicKeyId Public key identifier.
13
15
  * @throws Error if required parameters are missing or invalid
14
16
  */
15
17
  async getPublicKey(publicKeyId) {
16
- if (!publicKeyId || typeof publicKeyId !== 'string') {
17
- throw new Error('publicKeyId is required and must be a string');
18
- }
19
- try {
20
- return await this.client.get(`${this.basePath}/public_key/${publicKeyId}`);
21
- }
22
- catch (error) {
23
- throw new Error(`Failed to get public key: ${error instanceof Error ? error.message : 'Unknown error'}`);
24
- }
18
+ assertRequiredString(publicKeyId, 'publicKeyId');
19
+ return await getPathWithContextError(this.client, `${this.basePath}/public_key/${publicKeyId}`, 'Failed to get public key');
25
20
  }
26
21
  /**
27
22
  * Get notification config
28
23
  * @throws Error if the request fails
29
24
  */
30
25
  async getConfig() {
31
- try {
32
- return await this.client.get(`${this.basePath}/config`);
33
- }
34
- catch (error) {
35
- throw new Error(`Failed to get config: ${error instanceof Error ? error.message : 'Unknown error'}`);
36
- }
26
+ return await getPathWithContextError(this.client, `${this.basePath}/config`, 'Failed to get config');
37
27
  }
38
28
  /**
39
29
  * Update notification config
@@ -69,18 +59,12 @@ export class NotificationApi {
69
59
  }
70
60
  /**
71
61
  * Get destination
62
+ * @param destinationId Destination identifier.
72
63
  * @throws Error if required parameters are missing or invalid
73
64
  */
74
65
  async getDestination(destinationId) {
75
- if (!destinationId || typeof destinationId !== 'string') {
76
- throw new Error('destinationId is required and must be a string');
77
- }
78
- try {
79
- return await this.client.get(`${this.basePath}/destination/${destinationId}`);
80
- }
81
- catch (error) {
82
- throw new Error(`Failed to get destination: ${error instanceof Error ? error.message : 'Unknown error'}`);
83
- }
66
+ assertRequiredString(destinationId, 'destinationId');
67
+ return await getPathWithContextError(this.client, `${this.basePath}/destination/${destinationId}`, 'Failed to get destination');
84
68
  }
85
69
  /**
86
70
  * Create destination
@@ -175,18 +159,12 @@ export class NotificationApi {
175
159
  /**
176
160
  * Get a subscription
177
161
  * Endpoint: GET /subscription/{subscription_id}
162
+ * @param subscriptionId Subscription identifier.
178
163
  * @throws Error if required parameters are missing or invalid
179
164
  */
180
165
  async getSubscription(subscriptionId) {
181
- if (!subscriptionId || typeof subscriptionId !== 'string') {
182
- throw new Error('subscriptionId is required and must be a string');
183
- }
184
- try {
185
- return await this.client.get(`${this.basePath}/subscription/${subscriptionId}`);
186
- }
187
- catch (error) {
188
- throw new Error(`Failed to get subscription: ${error instanceof Error ? error.message : 'Unknown error'}`);
189
- }
166
+ assertRequiredString(subscriptionId, 'subscriptionId');
167
+ return await getPathWithContextError(this.client, `${this.basePath}/subscription/${subscriptionId}`, 'Failed to get subscription');
190
168
  }
191
169
  /**
192
170
  * Update a subscription
@@ -274,18 +252,12 @@ export class NotificationApi {
274
252
  /**
275
253
  * Get a topic
276
254
  * Endpoint: GET /topic/{topic_id}
255
+ * @param topicId Topic identifier.
277
256
  * @throws Error if required parameters are missing or invalid
278
257
  */
279
258
  async getTopic(topicId) {
280
- if (!topicId || typeof topicId !== 'string') {
281
- throw new Error('topicId is required and must be a string');
282
- }
283
- try {
284
- return await this.client.get(`${this.basePath}/topic/${topicId}`);
285
- }
286
- catch (error) {
287
- throw new Error(`Failed to get topic: ${error instanceof Error ? error.message : 'Unknown error'}`);
288
- }
259
+ assertRequiredString(topicId, 'topicId');
260
+ return await getPathWithContextError(this.client, `${this.basePath}/topic/${topicId}`, 'Failed to get topic');
289
261
  }
290
262
  /**
291
263
  * Get all topics
@@ -0,0 +1,71 @@
1
+ import { buildValidatedPaginatedParams } from '../shared/query-params.js';
2
+ /**
3
+ * Build validated pagination query params.
4
+ *
5
+ * @param filter API filter expression.
6
+ * @param limit Maximum number of records.
7
+ * @param offset Zero-based pagination offset.
8
+ * @returns Query parameter object for paginated GET calls.
9
+ */
10
+ export function buildPaginatedQueryParams(filter, limit, offset) {
11
+ return buildValidatedPaginatedParams(filter, limit, offset);
12
+ }
13
+ /**
14
+ * Validate that a required parameter is a non-empty string.
15
+ *
16
+ * @param value Value to validate.
17
+ * @param paramName Parameter name used in error text.
18
+ */
19
+ export function assertRequiredString(value, paramName) {
20
+ if (!value || typeof value !== 'string') {
21
+ throw new Error(`${paramName} is required and must be a string`);
22
+ }
23
+ }
24
+ /**
25
+ * Perform a GET request and wrap errors with endpoint-specific context.
26
+ *
27
+ * @param client Configured eBay API client.
28
+ * @param path Endpoint path to request.
29
+ * @param failureMessage Prefix text for thrown error messages.
30
+ * @returns API response payload.
31
+ */
32
+ export async function getPathWithContextError(client, path, failureMessage) {
33
+ try {
34
+ return await client.get(path);
35
+ }
36
+ catch (error) {
37
+ throw new Error(`${failureMessage}: ${error instanceof Error ? error.message : 'Unknown error'}`);
38
+ }
39
+ }
40
+ /**
41
+ * Perform a GET request with query params and wrap errors with endpoint-specific context.
42
+ *
43
+ * @param client Configured eBay API client.
44
+ * @param path Endpoint path to request.
45
+ * @param params Query parameters sent with the request.
46
+ * @param failureMessage Prefix text for thrown error messages.
47
+ * @returns API response payload.
48
+ */
49
+ export async function getWithContextError(client, path, params, failureMessage) {
50
+ try {
51
+ return await client.get(path, params);
52
+ }
53
+ catch (error) {
54
+ throw new Error(`${failureMessage}: ${error instanceof Error ? error.message : 'Unknown error'}`);
55
+ }
56
+ }
57
+ /**
58
+ * Perform a paginated GET request with validated filter/limit/offset params.
59
+ *
60
+ * @param client Configured eBay API client.
61
+ * @param path Endpoint path to request.
62
+ * @param failureMessage Prefix text for thrown error messages.
63
+ * @param filter API filter expression.
64
+ * @param limit Maximum number of records.
65
+ * @param offset Zero-based pagination offset.
66
+ * @returns API response payload.
67
+ */
68
+ export async function getPaginatedWithContextError(client, path, failureMessage, filter, limit, offset) {
69
+ const params = buildPaginatedQueryParams(filter, limit, offset);
70
+ return await getWithContextError(client, path, params, failureMessage);
71
+ }
@@ -1,3 +1,4 @@
1
+ import { buildTruthyPaginatedParams } from '../shared/query-params.js';
1
2
  /**
2
3
  * VERO API - Verified Rights Owner program
3
4
  * Based on: docs/sell-apps/other-apis/commerce_vero_v1_oas3.json
@@ -24,13 +25,7 @@ export class VeroApi {
24
25
  * Get VERO report items (listings reported for infringement)
25
26
  */
26
27
  async getVeroReportItems(filter, limit, offset) {
27
- const params = {};
28
- if (filter)
29
- params.filter = filter;
30
- if (limit)
31
- params.limit = limit;
32
- if (offset)
33
- params.offset = offset;
28
+ const params = buildTruthyPaginatedParams(filter, limit, offset);
34
29
  return await this.client.get(`${this.basePath}/vero_report_items`, params);
35
30
  }
36
31
  /**
@@ -0,0 +1,35 @@
1
+ export function buildValidatedPaginatedParams(filter, limit, offset) {
2
+ const params = {};
3
+ if (filter !== undefined) {
4
+ if (typeof filter !== 'string') {
5
+ throw new Error('filter must be a string when provided');
6
+ }
7
+ params.filter = filter;
8
+ }
9
+ if (limit !== undefined) {
10
+ if (typeof limit !== 'number' || limit < 1) {
11
+ throw new Error('limit must be a positive number when provided');
12
+ }
13
+ params.limit = limit;
14
+ }
15
+ if (offset !== undefined) {
16
+ if (typeof offset !== 'number' || offset < 0) {
17
+ throw new Error('offset must be a non-negative number when provided');
18
+ }
19
+ params.offset = offset;
20
+ }
21
+ return params;
22
+ }
23
+ export function buildTruthyPaginatedParams(filter, limit, offset) {
24
+ const params = {};
25
+ if (filter) {
26
+ params.filter = filter;
27
+ }
28
+ if (limit) {
29
+ params.limit = limit;
30
+ }
31
+ if (offset) {
32
+ params.offset = offset;
33
+ }
34
+ return params;
35
+ }
@@ -1,3 +1,13 @@
1
+ import { isRecord } from '../../utils/type-guards.js';
2
+ function asRecord(value) {
3
+ return isRecord(value) ? value : undefined;
4
+ }
5
+ function asRecordArray(value) {
6
+ if (!Array.isArray(value)) {
7
+ return [];
8
+ }
9
+ return value.filter((entry) => typeof entry === 'object' && entry !== null && !Array.isArray(entry));
10
+ }
1
11
  export class TradingApi {
2
12
  client;
3
13
  constructor(client) {
@@ -13,13 +23,16 @@ export class TradingApi {
13
23
  },
14
24
  },
15
25
  });
16
- const activeList = result.ActiveList;
17
- const itemArray = activeList?.ItemArray;
18
- const items = itemArray?.Item || [];
19
- const pagination = activeList?.PaginationResult;
26
+ const activeList = asRecord(result.ActiveList);
27
+ const itemArray = asRecord(activeList?.ItemArray);
28
+ const items = asRecordArray(itemArray?.Item);
29
+ const pagination = asRecord(activeList?.PaginationResult);
20
30
  const listings = items.map((item) => {
21
- const sellingStatus = item.SellingStatus;
22
- const currentPrice = sellingStatus?.CurrentPrice;
31
+ const sellingStatus = asRecord(item.SellingStatus);
32
+ const currentPriceRaw = sellingStatus?.CurrentPrice;
33
+ const currentPrice = typeof currentPriceRaw === 'number' || isRecord(currentPriceRaw)
34
+ ? currentPriceRaw
35
+ : undefined;
23
36
  const priceValue = typeof currentPrice === 'object' && currentPrice !== null
24
37
  ? Number(currentPrice['#text'] || 0)
25
38
  : Number(currentPrice || 0);
@@ -47,7 +60,7 @@ export class TradingApi {
47
60
  ItemID: itemId,
48
61
  DetailLevel: 'ReturnAll',
49
62
  });
50
- const items = result.Item;
63
+ const items = asRecordArray(result.Item);
51
64
  return items?.[0] || result;
52
65
  }
53
66
  async createListing(item) {
@@ -208,7 +208,7 @@ export class EbayOAuthClient {
208
208
  }
209
209
  /**
210
210
  * Exchange authorization code for user access token
211
- * Note: After receiving tokens, manually add EBAY_USER_REFRESH_TOKEN to .env file
211
+ * Persists received tokens to .env automatically
212
212
  */
213
213
  async exchangeCodeForToken(code) {
214
214
  if (!this.config.redirectUri) {
@@ -241,8 +241,11 @@ export class EbayOAuthClient {
241
241
  userRefreshTokenExpiry: now + tokenData.refresh_token_expires_in * 1000,
242
242
  scope: tokenData.scope,
243
243
  };
244
- // Tokens are automatically saved to .env file by updateEnvFile()
245
- // No console output needed here to avoid interfering with MCP JSON protocol
244
+ // Persist tokens to .env so they survive process restarts.
245
+ updateEnvFile({
246
+ EBAY_USER_ACCESS_TOKEN: tokenData.access_token,
247
+ EBAY_USER_REFRESH_TOKEN: tokenData.refresh_token,
248
+ });
246
249
  return tokenData;
247
250
  }
248
251
  catch (error) {
@@ -298,11 +301,9 @@ export class EbayOAuthClient {
298
301
  const envUpdates = {
299
302
  EBAY_USER_ACCESS_TOKEN: tokenData.access_token,
300
303
  };
301
- // If eBay provided a new refresh token, update it too
302
- if (tokenData.refresh_token &&
303
- tokenData.refresh_token !== process.env.EBAY_USER_REFRESH_TOKEN) {
304
- envUpdates.EBAY_USER_REFRESH_TOKEN = tokenData.refresh_token;
305
- // New refresh token updated silently
304
+ // Reconcile .env with the authoritative in-memory refresh token.
305
+ if (this.userTokens.userRefreshToken !== process.env.EBAY_USER_REFRESH_TOKEN) {
306
+ envUpdates.EBAY_USER_REFRESH_TOKEN = this.userTokens.userRefreshToken;
306
307
  }
307
308
  // Write updates to .env file
308
309
  updateEnvFile(envUpdates);
@@ -272,7 +272,7 @@ export function getReadonlyScope(writeScope) {
272
272
  /**
273
273
  * Get scope description from scope name
274
274
  */
275
- export function getScopeDescription(scope) {
275
+ export function getScopeTypeDescription(scope) {
276
276
  // Extract the last part of the scope
277
277
  const parts = scope.split('/');
278
278
  const scopeType = parts[parts.length - 1];
@@ -302,3 +302,9 @@ export function getScopeDescription(scope) {
302
302
  };
303
303
  return descriptions[scopeType] || `Access to ${scopeType}`;
304
304
  }
305
+ /**
306
+ * Backward-compatible alias for scope description lookup.
307
+ */
308
+ export function getScopeDescription(scope) {
309
+ return getScopeTypeDescription(scope);
310
+ }
@@ -3,7 +3,7 @@
3
3
  * and JWT validation
4
4
  */
5
5
  import axios from 'axios';
6
- import * as jose from 'jose';
6
+ import { createRemoteJWKSet, jwtVerify } from 'jose';
7
7
  export class TokenVerifier {
8
8
  config;
9
9
  metadata = null;
@@ -121,9 +121,9 @@ export class TokenVerifier {
121
121
  }
122
122
  try {
123
123
  // Get JWKS from authorization server
124
- const JWKS = jose.createRemoteJWKSet(new URL(this.metadata.jwks_uri));
124
+ const JWKS = createRemoteJWKSet(new URL(this.metadata.jwks_uri));
125
125
  // Verify JWT
126
- const { payload } = await jose.jwtVerify(token, JWKS, {
126
+ const { payload } = await jwtVerify(token, JWKS, {
127
127
  issuer: this.metadata.issuer,
128
128
  audience: this.config.expectedAudience,
129
129
  });
@@ -140,9 +140,14 @@ export class TokenVerifier {
140
140
  throw new Error(`Missing required scopes. Required: ${this.config.requiredScopes.join(', ')}, Got: ${scopes.join(', ')}`);
141
141
  }
142
142
  }
143
+ const clientId = typeof payload.client_id === 'string'
144
+ ? payload.client_id
145
+ : typeof payload.azp === 'string'
146
+ ? payload.azp
147
+ : 'unknown';
143
148
  return {
144
149
  token,
145
- clientId: payload.client_id || payload.azp || 'unknown',
150
+ clientId,
146
151
  scopes,
147
152
  expiresAt: payload.exp,
148
153
  audience: payload.aud,