ebay-mcp-remote-edition 4.2.1 → 4.4.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/build/api/browse/browse.js +43 -0
- package/build/api/client-trading.js +13 -5
- package/build/api/index.js +7 -0
- package/build/api/listing-management/inventory.js +4 -6
- package/build/api/media/media.js +244 -0
- package/build/api/trading/trading.js +10 -1
- package/build/auth/oauth.js +9 -3
- package/build/config/environment.js +8 -9
- package/build/server-http.js +4 -4
- package/build/tools/definitions/browse.js +83 -0
- package/build/tools/definitions/index.js +3 -1
- package/build/tools/definitions/inventory.js +88 -5
- package/build/tools/definitions/taxonomy.js +17 -0
- package/build/tools/definitions/token-management.js +5 -1
- package/build/tools/definitions/trading.js +16 -0
- package/build/tools/index.js +437 -28
- package/build/tools/schemas.js +5 -3
- package/build/utils/communication/feedback.js +10 -6
- package/build/utils/communication/message.js +5 -3
- package/build/utils/image-processor.js +113 -0
- package/docs/auth/production_scopes.json +58 -54
- package/package.json +17 -17
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getBaseUrl } from '../../config/environment.js';
|
|
2
|
+
/**
|
|
3
|
+
* Browse API - Search and browse eBay product catalog
|
|
4
|
+
* Based on: eBay Buy Browse API
|
|
5
|
+
* Base endpoint: /buy/browse/v1
|
|
6
|
+
*/
|
|
7
|
+
export class BrowseApi {
|
|
8
|
+
client;
|
|
9
|
+
constructor(client) {
|
|
10
|
+
this.client = client;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Search the eBay product catalog by query.
|
|
14
|
+
* Supports category filtering, sorting, and price filtering.
|
|
15
|
+
*/
|
|
16
|
+
async searchProducts(query, options) {
|
|
17
|
+
const environment = this.client.getConfig().environment;
|
|
18
|
+
const browseUrl = new URL('/buy/browse/v1/item_summary/search', getBaseUrl(environment)).toString();
|
|
19
|
+
const params = { q: query };
|
|
20
|
+
if (options?.categoryId)
|
|
21
|
+
params.category_ids = options.categoryId;
|
|
22
|
+
if (options?.limit)
|
|
23
|
+
params.limit = options.limit;
|
|
24
|
+
if (options?.sort)
|
|
25
|
+
params.sort = options.sort;
|
|
26
|
+
if (options?.filter)
|
|
27
|
+
params.filter = options.filter;
|
|
28
|
+
if (options?.aspectFilter)
|
|
29
|
+
params.aspect_filter = options.aspectFilter;
|
|
30
|
+
return await this.client.getWithFullUrl(browseUrl, params);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get listing/product suggestions based on search query.
|
|
34
|
+
* Returns relevant item summaries with pricing and shipping info.
|
|
35
|
+
*/
|
|
36
|
+
async getSuggestions(query, options) {
|
|
37
|
+
return await this.searchProducts(query, {
|
|
38
|
+
marketplaceId: options?.marketplaceId,
|
|
39
|
+
limit: options?.limit ?? 20,
|
|
40
|
+
sort: 'bestMatch',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -49,7 +49,7 @@ export class TradingApiClient {
|
|
|
49
49
|
* Handles nested objects like PrimaryCategory, ShippingDetails, ReturnPolicy,
|
|
50
50
|
* PicturesDetails, and ItemSpecifics that require special XML structure.
|
|
51
51
|
*/
|
|
52
|
-
transformItemForXML(item) {
|
|
52
|
+
transformItemForXML(item, options = { ensureCountry: true }) {
|
|
53
53
|
const transformed = {};
|
|
54
54
|
for (const [key, value] of Object.entries(item)) {
|
|
55
55
|
if (value === undefined || value === null)
|
|
@@ -117,6 +117,13 @@ export class TradingApiClient {
|
|
|
117
117
|
transformed.Currency = 'USD';
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
+
// Ensure Country is present for full listing creation/publish transforms.
|
|
121
|
+
// ReviseFixedPriceItem may update only a subset of fields; injecting a default Country
|
|
122
|
+
// can accidentally attempt to change an active listing's country and eBay rejects it as
|
|
123
|
+
// "Input data is invalid." Skip this default for partial revise payloads.
|
|
124
|
+
if (options.ensureCountry !== false && !transformed.Country) {
|
|
125
|
+
transformed.Country = 'USA';
|
|
126
|
+
}
|
|
120
127
|
return transformed;
|
|
121
128
|
}
|
|
122
129
|
/**
|
|
@@ -124,13 +131,14 @@ export class TradingApiClient {
|
|
|
124
131
|
* eBay Trading API expects: <StartPrice currencyID="USD">34.99</StartPrice>
|
|
125
132
|
*/
|
|
126
133
|
transformPrice(price) {
|
|
127
|
-
if (typeof price === 'string') {
|
|
128
|
-
return price;
|
|
134
|
+
if (typeof price === 'string' || typeof price === 'number') {
|
|
135
|
+
return String(price);
|
|
129
136
|
}
|
|
137
|
+
const currencyID = price.currencyID ?? price.currency;
|
|
130
138
|
// Use fast-xml-parser attribute convention: @_<attrName> for attributes, #text for content
|
|
131
139
|
return {
|
|
132
140
|
'#text': String(price.value),
|
|
133
|
-
...(
|
|
141
|
+
...(currencyID && { '@_currencyID': currencyID }),
|
|
134
142
|
};
|
|
135
143
|
}
|
|
136
144
|
/**
|
|
@@ -264,7 +272,7 @@ export class TradingApiClient {
|
|
|
264
272
|
// Transform Item field for proper XML serialization
|
|
265
273
|
const transformedParams = { ...params };
|
|
266
274
|
if (transformedParams.Item && typeof transformedParams.Item === 'object') {
|
|
267
|
-
transformedParams.Item = this.transformItemForXML(transformedParams.Item);
|
|
275
|
+
transformedParams.Item = this.transformItemForXML(transformedParams.Item, { ensureCountry: callName !== 'ReviseFixedPriceItem' });
|
|
268
276
|
}
|
|
269
277
|
const xmlObj = {};
|
|
270
278
|
xmlObj[requestTag] = {
|
package/build/api/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { MessageApi } from '../api/communication/message.js';
|
|
|
6
6
|
import { NegotiationApi } from '../api/communication/negotiation.js';
|
|
7
7
|
import { NotificationApi } from '../api/communication/notification.js';
|
|
8
8
|
import { DeveloperApi } from '../api/developer/developer.js';
|
|
9
|
+
import { MediaApi } from '../api/media/media.js';
|
|
9
10
|
import { InventoryApi } from '../api/listing-management/inventory.js';
|
|
10
11
|
import { MetadataApi } from '../api/listing-metadata/metadata.js';
|
|
11
12
|
import { TaxonomyApi } from '../api/listing-metadata/taxonomy.js';
|
|
@@ -18,6 +19,7 @@ import { EDeliveryApi } from '../api/other/edelivery.js';
|
|
|
18
19
|
import { IdentityApi } from '../api/other/identity.js';
|
|
19
20
|
import { TranslationApi } from '../api/other/translation.js';
|
|
20
21
|
import { VeroApi } from '../api/other/vero.js';
|
|
22
|
+
import { BrowseApi } from '../api/browse/browse.js';
|
|
21
23
|
import { TradingApiClient } from '../api/client-trading.js';
|
|
22
24
|
import { TradingApi } from '../api/trading/trading.js';
|
|
23
25
|
export class EbaySellerApi {
|
|
@@ -41,7 +43,9 @@ export class EbaySellerApi {
|
|
|
41
43
|
translation;
|
|
42
44
|
edelivery;
|
|
43
45
|
developer;
|
|
46
|
+
media;
|
|
44
47
|
trading;
|
|
48
|
+
browse;
|
|
45
49
|
constructor(config, context) {
|
|
46
50
|
this.client = new EbayApiClient(config, context);
|
|
47
51
|
this.account = new AccountApi(this.client);
|
|
@@ -63,8 +67,10 @@ export class EbaySellerApi {
|
|
|
63
67
|
this.translation = new TranslationApi(this.client);
|
|
64
68
|
this.edelivery = new EDeliveryApi(this.client);
|
|
65
69
|
this.developer = new DeveloperApi(this.client);
|
|
70
|
+
this.media = new MediaApi(this.client);
|
|
66
71
|
const tradingClient = new TradingApiClient(this.client);
|
|
67
72
|
this.trading = new TradingApi(tradingClient);
|
|
73
|
+
this.browse = new BrowseApi(this.client);
|
|
68
74
|
}
|
|
69
75
|
async initialize() {
|
|
70
76
|
await this.client.initialize();
|
|
@@ -107,3 +113,4 @@ export * from '../api/other/vero.js';
|
|
|
107
113
|
export * from '../api/developer/developer.js';
|
|
108
114
|
export * from '../api/trading/trading.js';
|
|
109
115
|
export * from '../api/client-trading.js';
|
|
116
|
+
export * from '../api/browse/browse.js';
|
|
@@ -360,17 +360,15 @@ export class InventoryApi {
|
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
362
|
/**
|
|
363
|
-
* Get
|
|
363
|
+
* Get offers for a specific SKU (sku is required)
|
|
364
364
|
* @throws Error if parameters are invalid
|
|
365
365
|
*/
|
|
366
366
|
async getOffers(sku, marketplaceId, limit) {
|
|
367
367
|
const params = {};
|
|
368
|
-
if (sku !==
|
|
369
|
-
|
|
370
|
-
throw new Error('sku must be a string when provided');
|
|
371
|
-
}
|
|
372
|
-
params.sku = sku;
|
|
368
|
+
if (!sku || typeof sku !== 'string') {
|
|
369
|
+
throw new Error('sku is required for getOffers');
|
|
373
370
|
}
|
|
371
|
+
params.sku = sku;
|
|
374
372
|
if (marketplaceId !== undefined) {
|
|
375
373
|
if (typeof marketplaceId !== 'string') {
|
|
376
374
|
throw new Error('marketplaceId must be a string when provided');
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
/**
|
|
5
|
+
* Commerce Media API (v1_beta) - Upload and manage images via eBay Picture Services
|
|
6
|
+
* Based on: https://developer.ebay.com/api-docs/commerce/media/resources/image/from_url/methods
|
|
7
|
+
*/
|
|
8
|
+
export class MediaApi {
|
|
9
|
+
client;
|
|
10
|
+
basePath = '/commerce/media/v1_beta';
|
|
11
|
+
constructor(client) {
|
|
12
|
+
this.client = client;
|
|
13
|
+
}
|
|
14
|
+
async getAccessToken() {
|
|
15
|
+
return await this.client.getOAuthClient().getAccessToken();
|
|
16
|
+
}
|
|
17
|
+
getMediaBaseUrl() {
|
|
18
|
+
const env = this.client.getConfig().environment;
|
|
19
|
+
return env === 'production' ? 'https://apim.ebay.com' : 'https://apim.sandbox.ebay.com';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Upload an image from a public URL to eBay Picture Services.
|
|
23
|
+
*
|
|
24
|
+
* Flow:
|
|
25
|
+
* 1. Download image from URL
|
|
26
|
+
* 2. Validate dimensions (min 500px, max 4800px)
|
|
27
|
+
* 3. Resize if needed (maintain aspect ratio)
|
|
28
|
+
* 4. Optimize and convert to JPEG
|
|
29
|
+
* 5. Upload to eBay via multipart/form-data
|
|
30
|
+
* 6. Return hosted URL
|
|
31
|
+
*
|
|
32
|
+
* Supported formats: JPG, GIF, PNG, BMP, TIFF, AVIF, HEIC, WEBP
|
|
33
|
+
* Max file size: 10MB per image
|
|
34
|
+
*
|
|
35
|
+
* @param imageUrl - Public URL of the image to upload
|
|
36
|
+
* @param description - Optional description for the image
|
|
37
|
+
* @returns Object with image ID and eBay-hosted image URL
|
|
38
|
+
*/
|
|
39
|
+
async createImageFromUrl(imageUrl, description) {
|
|
40
|
+
if (!imageUrl || typeof imageUrl !== 'string') {
|
|
41
|
+
throw new Error('imageUrl is required and must be a string');
|
|
42
|
+
}
|
|
43
|
+
const token = await this.getAccessToken();
|
|
44
|
+
const baseUrl = this.getMediaBaseUrl();
|
|
45
|
+
try {
|
|
46
|
+
// Try direct createImageFromUrl endpoint first (eBay downloads server-side)
|
|
47
|
+
// This avoids the need to download+process+upload ourselves
|
|
48
|
+
const requestBody = { imageUrl };
|
|
49
|
+
if (description) {
|
|
50
|
+
requestBody.description = description;
|
|
51
|
+
}
|
|
52
|
+
const response = await axios.post(`${baseUrl}${this.basePath}/image/create_image_from_url`, requestBody, {
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: `Bearer ${token}`,
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
Accept: 'application/json',
|
|
57
|
+
},
|
|
58
|
+
timeout: 30000,
|
|
59
|
+
});
|
|
60
|
+
const data = response.data;
|
|
61
|
+
const imageId = data.id;
|
|
62
|
+
if (imageId) {
|
|
63
|
+
return await this.getImage(imageId);
|
|
64
|
+
}
|
|
65
|
+
// Fallback: if no image ID returned, try download+upload method
|
|
66
|
+
throw new Error('No image ID returned from createImageFromUrl endpoint');
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (axios.isAxiosError(error)) {
|
|
70
|
+
const status = error.response?.status;
|
|
71
|
+
const data = error.response?.data;
|
|
72
|
+
const message = typeof data === 'object' && data !== null && 'errors' in data
|
|
73
|
+
? data.errors?.[0]?.longMessage ||
|
|
74
|
+
data.errors?.[0]?.message ||
|
|
75
|
+
error.message
|
|
76
|
+
: error.message;
|
|
77
|
+
throw new Error(`Failed to upload image from URL (status ${status}): ${message}`, {
|
|
78
|
+
cause: error,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`Failed to upload image from URL: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Upload an image from a local file to eBay Picture Services.
|
|
86
|
+
*
|
|
87
|
+
* Endpoint: POST /commerce/media/v1/image/create_image_from_file
|
|
88
|
+
* Content-Type: multipart/form-data
|
|
89
|
+
*
|
|
90
|
+
* Supported formats: JPG, GIF, PNG, BMP, TIFF, AVIF, HEIC, WEBP
|
|
91
|
+
* Max file size: 10MB per image
|
|
92
|
+
*
|
|
93
|
+
* @param filePath - Local file path of the image to upload
|
|
94
|
+
* @param description - Optional description for the image
|
|
95
|
+
* @returns Object with image ID and eBay-hosted image URL
|
|
96
|
+
*/
|
|
97
|
+
async createImageFromFile(filePath, description) {
|
|
98
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
99
|
+
throw new Error('filePath is required and must be a string');
|
|
100
|
+
}
|
|
101
|
+
if (!fs.existsSync(filePath)) {
|
|
102
|
+
throw new Error(`File not found: ${filePath}`);
|
|
103
|
+
}
|
|
104
|
+
const token = await this.getAccessToken();
|
|
105
|
+
const baseUrl = this.getMediaBaseUrl();
|
|
106
|
+
try {
|
|
107
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
108
|
+
// Build multipart/form-data body manually
|
|
109
|
+
const boundary = `----FormBoundary${Date.now()}`;
|
|
110
|
+
const fileName = path.basename(filePath);
|
|
111
|
+
// Build the multipart body
|
|
112
|
+
let body = '';
|
|
113
|
+
// File part
|
|
114
|
+
body += `--${boundary}\r\n`;
|
|
115
|
+
body += `Content-Disposition: form-data; name="imageFile"; filename="${fileName}"\r\n`;
|
|
116
|
+
body += `Content-Type: application/octet-stream\r\n\r\n`;
|
|
117
|
+
// Description part (optional)
|
|
118
|
+
if (description) {
|
|
119
|
+
body += `--${boundary}\r\n`;
|
|
120
|
+
body += `Content-Disposition: form-data; name="description"\r\n\r\n`;
|
|
121
|
+
body += `${description}\r\n`;
|
|
122
|
+
}
|
|
123
|
+
body += `--${boundary}--\r\n`;
|
|
124
|
+
const multipartBody = Buffer.concat([Buffer.from(body, 'utf-8'), Buffer.from(fileBuffer)]);
|
|
125
|
+
const createResponse = await axios.post(`${baseUrl}${this.basePath}/image/create_image_from_file`, multipartBody, {
|
|
126
|
+
headers: {
|
|
127
|
+
Authorization: `Bearer ${token}`,
|
|
128
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
129
|
+
Prefer: 'return=representation',
|
|
130
|
+
},
|
|
131
|
+
timeout: 30000,
|
|
132
|
+
});
|
|
133
|
+
// Extract image ID from response body or Location header
|
|
134
|
+
const responseData = createResponse.data;
|
|
135
|
+
const imageId = typeof responseData.id === 'string'
|
|
136
|
+
? responseData.id
|
|
137
|
+
: createResponse.headers.location?.split('/').pop();
|
|
138
|
+
if (!imageId) {
|
|
139
|
+
throw new Error('No image ID returned from create endpoint');
|
|
140
|
+
}
|
|
141
|
+
// Fetch image details to get the eBay-hosted URL
|
|
142
|
+
return await this.getImage(imageId);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (axios.isAxiosError(error)) {
|
|
146
|
+
const status = error.response?.status;
|
|
147
|
+
const data = error.response?.data;
|
|
148
|
+
const message = typeof data === 'object' && data !== null && 'errors' in data
|
|
149
|
+
? data.errors?.[0]?.longMessage ||
|
|
150
|
+
data.errors?.[0]?.message ||
|
|
151
|
+
error.message
|
|
152
|
+
: error.message;
|
|
153
|
+
throw new Error(`Failed to upload image from file (status ${status}): ${message}`, {
|
|
154
|
+
cause: error,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
throw new Error(`Failed to upload image from file: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Upload a processed image buffer to eBay Picture Services.
|
|
162
|
+
*
|
|
163
|
+
* @param buffer - Processed image buffer
|
|
164
|
+
* @param metadata - Image metadata
|
|
165
|
+
* @param token - OAuth access token
|
|
166
|
+
* @param baseUrl - Media API base URL
|
|
167
|
+
* @param description - Optional description
|
|
168
|
+
* @returns Object with image ID and eBay-hosted image URL
|
|
169
|
+
*/
|
|
170
|
+
async uploadProcessedImage(buffer, metadata, token, baseUrl, description) {
|
|
171
|
+
// Build multipart/form-data body
|
|
172
|
+
const boundary = `----FormBoundary${Date.now()}`;
|
|
173
|
+
const fileName = `image_${Date.now()}.jpg`;
|
|
174
|
+
let body = `--${boundary}\r\n`;
|
|
175
|
+
body += `Content-Disposition: form-data; name="imageFile"; filename="${fileName}"\r\n`;
|
|
176
|
+
body += `Content-Type: image/jpeg\r\n\r\n`;
|
|
177
|
+
if (description) {
|
|
178
|
+
body += `--${boundary}\r\n`;
|
|
179
|
+
body += `Content-Disposition: form-data; name="description"\r\n\r\n`;
|
|
180
|
+
body += `${description}\r\n`;
|
|
181
|
+
}
|
|
182
|
+
body += `--${boundary}--\r\n`;
|
|
183
|
+
const multipartBody = Buffer.concat([Buffer.from(body, 'utf-8'), buffer]);
|
|
184
|
+
const createResponse = await axios.post(`${baseUrl}${this.basePath}/image/create_image_from_file`, multipartBody, {
|
|
185
|
+
headers: {
|
|
186
|
+
Authorization: `Bearer ${token}`,
|
|
187
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
188
|
+
Prefer: 'return=representation',
|
|
189
|
+
},
|
|
190
|
+
timeout: 30000,
|
|
191
|
+
});
|
|
192
|
+
const responseData = createResponse.data;
|
|
193
|
+
const imageId = typeof responseData.id === 'string'
|
|
194
|
+
? responseData.id
|
|
195
|
+
: createResponse.headers.location?.split('/').pop();
|
|
196
|
+
if (!imageId) {
|
|
197
|
+
throw new Error('No image ID returned from create endpoint');
|
|
198
|
+
}
|
|
199
|
+
return await this.getImage(imageId);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get image details including the eBay-hosted URL.
|
|
203
|
+
*
|
|
204
|
+
* @param imageId - The image ID returned from createImageFromUrl
|
|
205
|
+
* @returns Image details including hosted URL
|
|
206
|
+
*/
|
|
207
|
+
async getImage(imageId) {
|
|
208
|
+
if (!imageId || typeof imageId !== 'string') {
|
|
209
|
+
throw new Error('imageId is required and must be a string');
|
|
210
|
+
}
|
|
211
|
+
const token = await this.getAccessToken();
|
|
212
|
+
const baseUrl = this.getMediaBaseUrl();
|
|
213
|
+
try {
|
|
214
|
+
const response = await axios.get(`${baseUrl}${this.basePath}/image/${imageId}`, {
|
|
215
|
+
headers: {
|
|
216
|
+
Authorization: `Bearer ${token}`,
|
|
217
|
+
Accept: 'application/json',
|
|
218
|
+
},
|
|
219
|
+
timeout: 30000,
|
|
220
|
+
});
|
|
221
|
+
const data = response.data;
|
|
222
|
+
return {
|
|
223
|
+
id: data.id,
|
|
224
|
+
imageUrl: data.imageUrl,
|
|
225
|
+
description: typeof data.description === 'string' ? data.description : undefined,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (axios.isAxiosError(error)) {
|
|
230
|
+
const status = error.response?.status;
|
|
231
|
+
const data = error.response?.data;
|
|
232
|
+
const message = typeof data === 'object' && data !== null && 'errors' in data
|
|
233
|
+
? data.errors?.[0]?.longMessage ||
|
|
234
|
+
data.errors?.[0]?.message ||
|
|
235
|
+
error.message
|
|
236
|
+
: error.message;
|
|
237
|
+
throw new Error(`Failed to get image details (status ${status}): ${message}`, {
|
|
238
|
+
cause: error,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
throw new Error(`Failed to get image details: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -3,6 +3,14 @@ export class TradingApi {
|
|
|
3
3
|
constructor(client) {
|
|
4
4
|
this.client = client;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Transform an item for Trading API XML serialization.
|
|
8
|
+
* Ensures required fields like Currency and Country are always present.
|
|
9
|
+
* Used by publish_offer flow to pre-transform inventory items before publishing.
|
|
10
|
+
*/
|
|
11
|
+
transformItemForXML(item) {
|
|
12
|
+
return this.client.transformItemForXML(item);
|
|
13
|
+
}
|
|
6
14
|
async getActiveListings(page = 1, entriesPerPage = 50) {
|
|
7
15
|
const result = await this.client.execute('GetMyeBaySelling', {
|
|
8
16
|
ActiveList: {
|
|
@@ -51,7 +59,8 @@ export class TradingApi {
|
|
|
51
59
|
return items?.[0] || result;
|
|
52
60
|
}
|
|
53
61
|
async createListing(item) {
|
|
54
|
-
|
|
62
|
+
const transformed = this.client.transformItemForXML(item);
|
|
63
|
+
return await this.client.execute('AddFixedPriceItem', { Item: transformed });
|
|
55
64
|
}
|
|
56
65
|
async reviseListing(itemId, fields) {
|
|
57
66
|
if (!itemId)
|
package/build/auth/oauth.js
CHANGED
|
@@ -46,6 +46,7 @@ export class EbayOAuthClient {
|
|
|
46
46
|
clientId: this.config.clientId,
|
|
47
47
|
clientSecret: this.config.clientSecret,
|
|
48
48
|
redirectUri: this.config.redirectUri,
|
|
49
|
+
ruName: this.config.ruName,
|
|
49
50
|
userAccessToken: tokenData.access_token,
|
|
50
51
|
userRefreshToken: tokenData.refresh_token || envRefreshToken,
|
|
51
52
|
tokenType: tokenData.token_type,
|
|
@@ -99,6 +100,7 @@ export class EbayOAuthClient {
|
|
|
99
100
|
clientId: this.config.clientId,
|
|
100
101
|
clientSecret: this.config.clientSecret,
|
|
101
102
|
redirectUri: this.config.redirectUri,
|
|
103
|
+
ruName: this.config.ruName,
|
|
102
104
|
userAccessToken: accessToken,
|
|
103
105
|
userRefreshToken: refreshToken,
|
|
104
106
|
tokenType: 'Bearer',
|
|
@@ -128,8 +130,9 @@ export class EbayOAuthClient {
|
|
|
128
130
|
return this.appAccessToken;
|
|
129
131
|
}
|
|
130
132
|
async exchangeCodeForToken(code) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
const redirectUriForExchange = this.config.ruName || this.config.redirectUri;
|
|
134
|
+
if (!redirectUriForExchange) {
|
|
135
|
+
throw new Error('RuName (EBAY_RUNAME) or redirectUri is required for authorization code exchange');
|
|
133
136
|
}
|
|
134
137
|
const tokenUrl = this.getTokenEndpoint();
|
|
135
138
|
const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
|
|
@@ -137,7 +140,7 @@ export class EbayOAuthClient {
|
|
|
137
140
|
const response = await axios.post(tokenUrl, new URLSearchParams({
|
|
138
141
|
grant_type: 'authorization_code',
|
|
139
142
|
code,
|
|
140
|
-
redirect_uri:
|
|
143
|
+
redirect_uri: redirectUriForExchange,
|
|
141
144
|
}).toString(), {
|
|
142
145
|
headers: {
|
|
143
146
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -150,6 +153,7 @@ export class EbayOAuthClient {
|
|
|
150
153
|
clientId: this.config.clientId,
|
|
151
154
|
clientSecret: this.config.clientSecret,
|
|
152
155
|
redirectUri: this.config.redirectUri,
|
|
156
|
+
ruName: this.config.ruName,
|
|
153
157
|
userAccessToken: tokenData.access_token,
|
|
154
158
|
userRefreshToken: tokenData.refresh_token,
|
|
155
159
|
tokenType: tokenData.token_type,
|
|
@@ -195,6 +199,7 @@ export class EbayOAuthClient {
|
|
|
195
199
|
clientId: this.config.clientId,
|
|
196
200
|
clientSecret: this.config.clientSecret,
|
|
197
201
|
redirectUri: this.config.redirectUri,
|
|
202
|
+
ruName: this.config.ruName,
|
|
198
203
|
userAccessToken: tokenData.access_token,
|
|
199
204
|
userRefreshToken: tokenData.refresh_token || this.userTokens.userRefreshToken,
|
|
200
205
|
tokenType: tokenData.token_type,
|
|
@@ -215,6 +220,7 @@ export class EbayOAuthClient {
|
|
|
215
220
|
hasRefreshToken: !!this.userTokens?.userRefreshToken,
|
|
216
221
|
hasAccessToken: !!this.userTokens?.userAccessToken,
|
|
217
222
|
hasRedirectUri: !!this.config.redirectUri,
|
|
223
|
+
hasRuName: !!this.config.ruName,
|
|
218
224
|
...(this.userTokens?.userRefreshTokenExpiry
|
|
219
225
|
? { refreshTokenExpiry: this.userTokens.userRefreshTokenExpiry }
|
|
220
226
|
: {}),
|
|
@@ -268,19 +268,18 @@ export function getEbayConfig(environmentOverride) {
|
|
|
268
268
|
: process.env.EBAY_SANDBOX_CLIENT_SECRET || process.env.EBAY_CLIENT_SECRET || '';
|
|
269
269
|
// Preferred var is EBAY_RUNAME (clearer naming — it IS the RuName, not a URL).
|
|
270
270
|
// EBAY_REDIRECT_URI is kept for backward compatibility.
|
|
271
|
+
// RuName is for token exchange; redirectUri is for browser OAuth flow. Separate purposes.
|
|
272
|
+
const fallbackRuName = environment === 'production'
|
|
273
|
+
? process.env.EBAY_PRODUCTION_RUNAME || process.env.EBAY_RUNAME || ''
|
|
274
|
+
: process.env.EBAY_SANDBOX_RUNAME || process.env.EBAY_RUNAME || '';
|
|
271
275
|
const fallbackRedirectUri = environment === 'production'
|
|
272
|
-
? process.env.
|
|
273
|
-
|
|
274
|
-
process.env.EBAY_RUNAME ||
|
|
275
|
-
process.env.EBAY_REDIRECT_URI
|
|
276
|
-
: process.env.EBAY_SANDBOX_RUNAME ||
|
|
277
|
-
process.env.EBAY_SANDBOX_REDIRECT_URI ||
|
|
278
|
-
process.env.EBAY_RUNAME ||
|
|
279
|
-
process.env.EBAY_REDIRECT_URI;
|
|
276
|
+
? process.env.EBAY_PRODUCTION_REDIRECT_URI || process.env.EBAY_REDIRECT_URI || ''
|
|
277
|
+
: process.env.EBAY_SANDBOX_REDIRECT_URI || process.env.EBAY_REDIRECT_URI || '';
|
|
280
278
|
return {
|
|
281
279
|
clientId: secretConfig?.clientId || fallbackClientId,
|
|
282
280
|
clientSecret: secretConfig?.clientSecret || fallbackClientSecret,
|
|
283
|
-
redirectUri: secretConfig?.redirectUri || fallbackRedirectUri,
|
|
281
|
+
redirectUri: secretConfig?.redirectUri || fallbackRedirectUri || undefined,
|
|
282
|
+
ruName: secretConfig?.ruName || fallbackRuName || undefined,
|
|
284
283
|
marketplaceId: (process.env.EBAY_MARKETPLACE_ID ?? '').trim() || 'EBAY_US',
|
|
285
284
|
contentLanguage: (process.env.EBAY_CONTENT_LANGUAGE ?? '').trim() || 'en-US',
|
|
286
285
|
environment,
|
package/build/server-http.js
CHANGED
|
@@ -745,7 +745,7 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
745
745
|
serverLogger.info(`[${prefix || 'root'}/authorize] Credential check`, {
|
|
746
746
|
environment,
|
|
747
747
|
envSource,
|
|
748
|
-
ruName: ebayConfig.redirectUri,
|
|
748
|
+
ruName: ebayConfig.ruName || ebayConfig.redirectUri,
|
|
749
749
|
ruNameDetectedEnv: credCheck.detectedEnv,
|
|
750
750
|
credentialValid: credCheck.valid,
|
|
751
751
|
});
|
|
@@ -770,7 +770,7 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
770
770
|
serverLogger.info(`[${prefix || 'root'}/authorize] Redirecting to eBay OAuth`, {
|
|
771
771
|
state: stateRecord.state,
|
|
772
772
|
environment,
|
|
773
|
-
ruName: ebayConfig.redirectUri,
|
|
773
|
+
ruName: ebayConfig.ruName || ebayConfig.redirectUri,
|
|
774
774
|
expectedCallbackUrl: getExpectedOAuthCallbackUrl(serverUrl),
|
|
775
775
|
});
|
|
776
776
|
res.redirect(oauthUrl);
|
|
@@ -874,7 +874,7 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
874
874
|
serverLogger.info(`[${prefix || 'root'}/oauth/start] Redirecting to eBay OAuth`, {
|
|
875
875
|
state: stateRecord.state,
|
|
876
876
|
environment,
|
|
877
|
-
ruName: ebayConfig.redirectUri,
|
|
877
|
+
ruName: ebayConfig.ruName || ebayConfig.redirectUri,
|
|
878
878
|
expectedCallbackUrl: getExpectedOAuthCallbackUrl(serverUrl),
|
|
879
879
|
returnTo,
|
|
880
880
|
});
|
|
@@ -1132,7 +1132,7 @@ async function handleOAuthCallback(req, res, serverUrl) {
|
|
|
1132
1132
|
environment,
|
|
1133
1133
|
tokenBaseUrl: getBaseUrl(environment),
|
|
1134
1134
|
clientIdPrefix: ebayConfig.clientId ? `${ebayConfig.clientId.slice(0, 12)}...` : '(missing)',
|
|
1135
|
-
ruName: ebayConfig.redirectUri ?? '(missing)',
|
|
1135
|
+
ruName: (ebayConfig.ruName || ebayConfig.redirectUri) ?? '(missing)',
|
|
1136
1136
|
});
|
|
1137
1137
|
const userId = randomUUID();
|
|
1138
1138
|
const api = await createUserScopedApi(userId, environment);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const browseTools = [
|
|
3
|
+
{
|
|
4
|
+
name: 'ebay_get_suggestions',
|
|
5
|
+
description: 'Get listing/product suggestions based on search query. Returns relevant item summaries with pricing and shipping info from the eBay Browse API.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
query: z.string().describe('Search query for product suggestions'),
|
|
8
|
+
marketplaceId: z.string().default('EBAY_US').describe('Marketplace ID (default: EBAY_US)'),
|
|
9
|
+
limit: z.number().default(20).describe('Number of suggestions to return (default: 20)'),
|
|
10
|
+
},
|
|
11
|
+
outputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
success: { type: 'boolean' },
|
|
15
|
+
data: { type: 'object' },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
annotations: {
|
|
19
|
+
title: 'Get Suggestions',
|
|
20
|
+
readOnlyHint: true,
|
|
21
|
+
},
|
|
22
|
+
_meta: {
|
|
23
|
+
category: 'browse',
|
|
24
|
+
version: '1.0.0',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'ebay_search_products',
|
|
29
|
+
description: 'Search the eBay product catalog by query. Supports category filtering, sorting, and price filtering. Uses the eBay Browse API.',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
query: z.string().describe('Search query for products'),
|
|
32
|
+
marketplaceId: z.string().describe('Marketplace ID (e.g., EBAY_US)'),
|
|
33
|
+
categoryId: z.string().optional().describe('Filter by category ID'),
|
|
34
|
+
limit: z.number().default(20).describe('Results to return (1-200, default: 20)'),
|
|
35
|
+
sort: z
|
|
36
|
+
.enum(['bestMatch', 'newlyListed'])
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Sort order (default: bestMatch)'),
|
|
39
|
+
filter: z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('Filter expression (e.g., price:[300..800],priceCurrency:USD)'),
|
|
43
|
+
},
|
|
44
|
+
outputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
success: { type: 'boolean' },
|
|
48
|
+
data: { type: 'object' },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
annotations: {
|
|
52
|
+
title: 'Search Products',
|
|
53
|
+
readOnlyHint: true,
|
|
54
|
+
},
|
|
55
|
+
_meta: {
|
|
56
|
+
category: 'browse',
|
|
57
|
+
version: '1.0.0',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'ebay_get_item_specifics',
|
|
62
|
+
description: 'Get required and optional item specifics (aspects) for a given category. Use this to understand what fields are needed for listing in a category. This is an alias to ebay_get_item_aspects_for_category.',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
categoryTreeId: z.string().describe('Category tree ID'),
|
|
65
|
+
categoryId: z.string().describe('Category ID'),
|
|
66
|
+
},
|
|
67
|
+
outputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
success: { type: 'boolean' },
|
|
71
|
+
data: { type: 'object' },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
annotations: {
|
|
75
|
+
title: 'Get Item Specifics',
|
|
76
|
+
readOnlyHint: true,
|
|
77
|
+
},
|
|
78
|
+
_meta: {
|
|
79
|
+
category: 'browse',
|
|
80
|
+
version: '1.0.0',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
];
|
|
@@ -18,12 +18,13 @@ import { marketingTools } from './marketing.js';
|
|
|
18
18
|
import { analyticsTools } from './analytics.js';
|
|
19
19
|
import { metadataTools } from './metadata.js';
|
|
20
20
|
import { taxonomyTools } from './taxonomy.js';
|
|
21
|
+
import { browseTools } from './browse.js';
|
|
21
22
|
import { communicationTools } from './communication.js';
|
|
22
23
|
import { otherApiTools } from './other.js';
|
|
23
24
|
import { developerTools } from './developer.js';
|
|
24
25
|
import { tradingTools } from './trading.js';
|
|
25
26
|
// Export individual categories
|
|
26
|
-
export { tokenManagementTools, accountTools, inventoryTools, fulfillmentTools, marketingTools, analyticsTools, metadataTools, taxonomyTools, communicationTools, otherApiTools, developerTools, tradingTools, };
|
|
27
|
+
export { tokenManagementTools, accountTools, inventoryTools, fulfillmentTools, marketingTools, analyticsTools, metadataTools, taxonomyTools, browseTools, communicationTools, otherApiTools, developerTools, tradingTools, };
|
|
27
28
|
// Export all tools as a single array
|
|
28
29
|
export const allTools = [
|
|
29
30
|
...tokenManagementTools,
|
|
@@ -34,6 +35,7 @@ export const allTools = [
|
|
|
34
35
|
...analyticsTools,
|
|
35
36
|
...metadataTools,
|
|
36
37
|
...taxonomyTools,
|
|
38
|
+
...browseTools,
|
|
37
39
|
...communicationTools,
|
|
38
40
|
...otherApiTools,
|
|
39
41
|
...developerTools,
|