ebay-mcp-remote-edition 4.3.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 +4 -0
- package/build/api/listing-management/inventory.js +4 -6
- package/build/api/media/media.js +133 -13
- package/build/api/trading/trading.js +8 -0
- 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 +64 -2
- package/build/tools/definitions/taxonomy.js +17 -0
- package/build/tools/definitions/token-management.js +5 -1
- package/build/tools/definitions/trading.js +7 -2
- package/build/tools/index.js +347 -30
- package/build/tools/schemas.js +2 -2
- 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 +4 -4
|
@@ -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
|
@@ -19,6 +19,7 @@ import { EDeliveryApi } from '../api/other/edelivery.js';
|
|
|
19
19
|
import { IdentityApi } from '../api/other/identity.js';
|
|
20
20
|
import { TranslationApi } from '../api/other/translation.js';
|
|
21
21
|
import { VeroApi } from '../api/other/vero.js';
|
|
22
|
+
import { BrowseApi } from '../api/browse/browse.js';
|
|
22
23
|
import { TradingApiClient } from '../api/client-trading.js';
|
|
23
24
|
import { TradingApi } from '../api/trading/trading.js';
|
|
24
25
|
export class EbaySellerApi {
|
|
@@ -44,6 +45,7 @@ export class EbaySellerApi {
|
|
|
44
45
|
developer;
|
|
45
46
|
media;
|
|
46
47
|
trading;
|
|
48
|
+
browse;
|
|
47
49
|
constructor(config, context) {
|
|
48
50
|
this.client = new EbayApiClient(config, context);
|
|
49
51
|
this.account = new AccountApi(this.client);
|
|
@@ -68,6 +70,7 @@ export class EbaySellerApi {
|
|
|
68
70
|
this.media = new MediaApi(this.client);
|
|
69
71
|
const tradingClient = new TradingApiClient(this.client);
|
|
70
72
|
this.trading = new TradingApi(tradingClient);
|
|
73
|
+
this.browse = new BrowseApi(this.client);
|
|
71
74
|
}
|
|
72
75
|
async initialize() {
|
|
73
76
|
await this.client.initialize();
|
|
@@ -110,3 +113,4 @@ export * from '../api/other/vero.js';
|
|
|
110
113
|
export * from '../api/developer/developer.js';
|
|
111
114
|
export * from '../api/trading/trading.js';
|
|
112
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');
|
package/build/api/media/media.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { getBaseUrl } from '../../config/environment.js';
|
|
2
1
|
import axios from 'axios';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
3
4
|
/**
|
|
4
5
|
* Commerce Media API (v1_beta) - Upload and manage images via eBay Picture Services
|
|
5
6
|
* Based on: https://developer.ebay.com/api-docs/commerce/media/resources/image/from_url/methods
|
|
6
7
|
*/
|
|
7
8
|
export class MediaApi {
|
|
8
9
|
client;
|
|
9
|
-
basePath = '/commerce/media/
|
|
10
|
+
basePath = '/commerce/media/v1_beta';
|
|
10
11
|
constructor(client) {
|
|
11
12
|
this.client = client;
|
|
12
13
|
}
|
|
@@ -15,14 +16,18 @@ export class MediaApi {
|
|
|
15
16
|
}
|
|
16
17
|
getMediaBaseUrl() {
|
|
17
18
|
const env = this.client.getConfig().environment;
|
|
18
|
-
return
|
|
19
|
+
return env === 'production' ? 'https://apim.ebay.com' : 'https://apim.sandbox.ebay.com';
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
22
|
* Upload an image from a public URL to eBay Picture Services.
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
-
* 1.
|
|
25
|
-
* 2.
|
|
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
|
|
26
31
|
*
|
|
27
32
|
* Supported formats: JPG, GIF, PNG, BMP, TIFF, AVIF, HEIC, WEBP
|
|
28
33
|
* Max file size: 10MB per image
|
|
@@ -38,15 +43,89 @@ export class MediaApi {
|
|
|
38
43
|
const token = await this.getAccessToken();
|
|
39
44
|
const baseUrl = this.getMediaBaseUrl();
|
|
40
45
|
try {
|
|
41
|
-
//
|
|
42
|
-
|
|
46
|
+
// Try direct createImageFromUrl endpoint first (eBay downloads server-side)
|
|
47
|
+
// This avoids the need to download+process+upload ourselves
|
|
48
|
+
const requestBody = { imageUrl };
|
|
43
49
|
if (description) {
|
|
44
|
-
|
|
50
|
+
requestBody.description = description;
|
|
45
51
|
}
|
|
46
|
-
const
|
|
52
|
+
const response = await axios.post(`${baseUrl}${this.basePath}/image/create_image_from_url`, requestBody, {
|
|
47
53
|
headers: {
|
|
48
54
|
Authorization: `Bearer ${token}`,
|
|
49
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}`,
|
|
50
129
|
Prefer: 'return=representation',
|
|
51
130
|
},
|
|
52
131
|
timeout: 30000,
|
|
@@ -59,7 +138,7 @@ export class MediaApi {
|
|
|
59
138
|
if (!imageId) {
|
|
60
139
|
throw new Error('No image ID returned from create endpoint');
|
|
61
140
|
}
|
|
62
|
-
//
|
|
141
|
+
// Fetch image details to get the eBay-hosted URL
|
|
63
142
|
return await this.getImage(imageId);
|
|
64
143
|
}
|
|
65
144
|
catch (error) {
|
|
@@ -71,12 +150,53 @@ export class MediaApi {
|
|
|
71
150
|
data.errors?.[0]?.message ||
|
|
72
151
|
error.message
|
|
73
152
|
: error.message;
|
|
74
|
-
throw new Error(`Failed to upload image from
|
|
153
|
+
throw new Error(`Failed to upload image from file (status ${status}): ${message}`, {
|
|
75
154
|
cause: error,
|
|
76
155
|
});
|
|
77
156
|
}
|
|
78
|
-
throw new Error(`Failed to upload image from
|
|
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');
|
|
79
198
|
}
|
|
199
|
+
return await this.getImage(imageId);
|
|
80
200
|
}
|
|
81
201
|
/**
|
|
82
202
|
* Get image details including the eBay-hosted URL.
|
|
@@ -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: {
|
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,
|
|
@@ -71,11 +71,73 @@ export const inventoryTools = [
|
|
|
71
71
|
properties: {},
|
|
72
72
|
},
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
name: 'ebay_update_inventory_item',
|
|
76
|
+
description: 'Update an existing inventory item by SKU. Supports partial updates — only provide the fields you want to change. Fetches existing data, merges your updates, then saves back. Use this for price changes, quantity updates, title edits, etc.\n\nIf the SKU does not exist, returns an error. Create it first with ebay_create_inventory_item.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
sku: z.string().describe('The seller-defined SKU to update (must already exist)'),
|
|
79
|
+
availability: z
|
|
80
|
+
.object({
|
|
81
|
+
shipToLocationAvailability: z
|
|
82
|
+
.object({
|
|
83
|
+
quantity: z.number().describe('New quantity'),
|
|
84
|
+
})
|
|
85
|
+
.optional(),
|
|
86
|
+
})
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('Availability updates'),
|
|
89
|
+
product: z
|
|
90
|
+
.object({
|
|
91
|
+
title: z.string().optional().describe('New title (max 80 characters)'),
|
|
92
|
+
brand: z.string().optional().describe('Brand name'),
|
|
93
|
+
description: z.string().optional().describe('HTML description'),
|
|
94
|
+
imageUrls: z.array(z.string()).optional().describe('Array of image URLs'),
|
|
95
|
+
mpn: z.string().optional().describe('Manufacturer Part Number'),
|
|
96
|
+
aspects: z
|
|
97
|
+
.record(z.string(), z.array(z.string()))
|
|
98
|
+
.optional()
|
|
99
|
+
.describe('Item aspects/attributes (e.g., Color, Size)'),
|
|
100
|
+
})
|
|
101
|
+
.optional()
|
|
102
|
+
.describe('Product data updates'),
|
|
103
|
+
condition: z
|
|
104
|
+
.enum([
|
|
105
|
+
'NEW',
|
|
106
|
+
'LIKE_NEW',
|
|
107
|
+
'NEW_OTHER',
|
|
108
|
+
'NEW_WITH_DEFECTS',
|
|
109
|
+
'USED_EXCELLENT',
|
|
110
|
+
'USED_VERY_GOOD',
|
|
111
|
+
'USED_GOOD',
|
|
112
|
+
'USED_ACCEPTABLE',
|
|
113
|
+
'FOR_PARTS_OR_NOT_WORKING',
|
|
114
|
+
])
|
|
115
|
+
.optional()
|
|
116
|
+
.describe('Item condition'),
|
|
117
|
+
conditionDescription: z
|
|
118
|
+
.string()
|
|
119
|
+
.optional()
|
|
120
|
+
.describe('Condition description (for used items)'),
|
|
121
|
+
},
|
|
122
|
+
outputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
sku: { type: 'string' },
|
|
126
|
+
status: { type: 'string' },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
annotations: {
|
|
130
|
+
title: 'Update Inventory Item',
|
|
131
|
+
readOnlyHint: false,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
74
134
|
{
|
|
75
135
|
name: 'ebay_get_offers',
|
|
76
|
-
description: 'Get
|
|
136
|
+
description: 'Get offers for a specific SKU. Requires a valid SKU — eBay API returns error 25707 without one.',
|
|
77
137
|
inputSchema: {
|
|
78
|
-
sku: z
|
|
138
|
+
sku: z
|
|
139
|
+
.string()
|
|
140
|
+
.describe('The seller-defined SKU (required — eBay API returns error 25707 without it)'),
|
|
79
141
|
marketplaceId: z.nativeEnum(MarketplaceId).optional().describe('Filter by marketplace ID'),
|
|
80
142
|
limit: z.number().optional().describe('Number of offers to return'),
|
|
81
143
|
},
|