ebay-mcp-remote-edition 4.3.1 → 4.5.1
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 +150 -15
- package/build/api/trading/trading.js +8 -0
- package/build/auth/oauth.js +9 -3
- package/build/config/environment.js +8 -9
- package/build/index.js +0 -0
- package/build/scripts/update-api-status-doc.js +19 -1
- package/build/server-http.js +30 -5
- 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 +479 -31
- package/build/tools/schemas.js +2 -2
- package/build/utils/api-status-feed.js +4 -1
- 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 +27 -37
|
@@ -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 {
|
|
1
|
+
import { processImageForUpload, } from '../../utils/image-processor.js';
|
|
2
2
|
import axios from 'axios';
|
|
3
|
+
import * as fs from 'fs';
|
|
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,16 +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
|
-
*
|
|
25
|
-
* 2. GET /commerce/media/v1/image/{imageId} to retrieve the hosted URL
|
|
24
|
+
* Primary flow: Pass URL directly to eBay's createImageFromUrl endpoint
|
|
25
|
+
* (eBay downloads and processes server-side — no local processing needed).
|
|
26
26
|
*
|
|
27
|
-
*
|
|
27
|
+
* Fallback: If eBay rejects the image (e.g., too small for 500px minimum),
|
|
28
|
+
* download → enlarge with Sharp → upload via createImageFromFile.
|
|
29
|
+
*
|
|
30
|
+
* Supported source formats: JPG, GIF, PNG, BMP, TIFF, AVIF, HEIC, WEBP
|
|
28
31
|
* Max file size: 10MB per image
|
|
29
32
|
*
|
|
30
33
|
* @param imageUrl - Public URL of the image to upload
|
|
@@ -38,20 +41,20 @@ export class MediaApi {
|
|
|
38
41
|
const token = await this.getAccessToken();
|
|
39
42
|
const baseUrl = this.getMediaBaseUrl();
|
|
40
43
|
try {
|
|
41
|
-
//
|
|
44
|
+
// Primary: Pass URL directly to eBay — they handle server-side download
|
|
42
45
|
const body = { imageUrl };
|
|
43
46
|
if (description) {
|
|
44
|
-
|
|
47
|
+
body.description = description;
|
|
45
48
|
}
|
|
46
|
-
const createResponse = await axios.post(`${baseUrl}${this.basePath}/image/
|
|
49
|
+
const createResponse = await axios.post(`${baseUrl}${this.basePath}/image/create_image_from_url`, body, {
|
|
47
50
|
headers: {
|
|
48
51
|
Authorization: `Bearer ${token}`,
|
|
49
52
|
'Content-Type': 'application/json',
|
|
53
|
+
Accept: 'application/json',
|
|
50
54
|
Prefer: 'return=representation',
|
|
51
55
|
},
|
|
52
56
|
timeout: 30000,
|
|
53
57
|
});
|
|
54
|
-
// Extract image ID from response body or Location header
|
|
55
58
|
const responseData = createResponse.data;
|
|
56
59
|
const imageId = typeof responseData.id === 'string'
|
|
57
60
|
? responseData.id
|
|
@@ -59,9 +62,79 @@ export class MediaApi {
|
|
|
59
62
|
if (!imageId) {
|
|
60
63
|
throw new Error('No image ID returned from create endpoint');
|
|
61
64
|
}
|
|
62
|
-
// Step 2: Fetch image details to get the eBay-hosted URL
|
|
63
65
|
return await this.getImage(imageId);
|
|
64
66
|
}
|
|
67
|
+
catch (primaryError) {
|
|
68
|
+
// Fallback: if eBay rejects (e.g., image too small), download → Sharp enlarge → upload via file
|
|
69
|
+
if (axios.isAxiosError(primaryError)) {
|
|
70
|
+
const status = primaryError.response?.status;
|
|
71
|
+
// Only fallback on errors that suggest image quality/size issues
|
|
72
|
+
if (status && (status === 400 || status === 500)) {
|
|
73
|
+
try {
|
|
74
|
+
console.log(`[MediaApi] Direct URL upload failed (status ${status}), falling back to Sharp processing for: ${imageUrl}`);
|
|
75
|
+
// Download the image
|
|
76
|
+
const downloadResponse = await axios.get(imageUrl, {
|
|
77
|
+
responseType: 'arraybuffer',
|
|
78
|
+
timeout: 30000,
|
|
79
|
+
maxContentLength: 10 * 1024 * 1024, // 10MB max
|
|
80
|
+
});
|
|
81
|
+
const imageBuffer = Buffer.from(downloadResponse.data);
|
|
82
|
+
// Process with Sharp (enlarge if too small, convert to JPEG)
|
|
83
|
+
const processed = await processImageForUpload(imageBuffer);
|
|
84
|
+
// Upload via file endpoint
|
|
85
|
+
return await this.uploadProcessedImage(processed.buffer, processed.metadata, token, baseUrl, description);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// If fallback also fails, throw the original error (more actionable)
|
|
89
|
+
throw primaryError;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Re-throw non-retryable errors as-is
|
|
94
|
+
if (axios.isAxiosError(primaryError)) {
|
|
95
|
+
const status = primaryError.response?.status;
|
|
96
|
+
const data = primaryError.response?.data;
|
|
97
|
+
const message = typeof data === 'object' && data !== null && 'errors' in data
|
|
98
|
+
? data.errors?.[0]?.longMessage ||
|
|
99
|
+
data.errors?.[0]?.message ||
|
|
100
|
+
primaryError.message
|
|
101
|
+
: primaryError.message;
|
|
102
|
+
throw new Error(`Failed to upload image from URL (status ${status}): ${message}`, {
|
|
103
|
+
cause: primaryError,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`Failed to upload image from URL: ${primaryError instanceof Error ? primaryError.message : 'Unknown error'}`, { cause: primaryError });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Upload an image from a local file to eBay Picture Services.
|
|
111
|
+
*
|
|
112
|
+
* Endpoint: POST /commerce/media/v1/image/create_image_from_file
|
|
113
|
+
* Content-Type: multipart/form-data
|
|
114
|
+
*
|
|
115
|
+
* Supported formats: JPG, GIF, PNG, BMP, TIFF, AVIF, HEIC, WEBP
|
|
116
|
+
* Max file size: 10MB per image
|
|
117
|
+
*
|
|
118
|
+
* @param filePath - Local file path of the image to upload
|
|
119
|
+
* @param description - Optional description for the image
|
|
120
|
+
* @returns Object with image ID and eBay-hosted image URL
|
|
121
|
+
*/
|
|
122
|
+
async createImageFromFile(filePath, description) {
|
|
123
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
124
|
+
throw new Error('filePath is required and must be a string');
|
|
125
|
+
}
|
|
126
|
+
if (!fs.existsSync(filePath)) {
|
|
127
|
+
throw new Error(`File not found: ${filePath}`);
|
|
128
|
+
}
|
|
129
|
+
const token = await this.getAccessToken();
|
|
130
|
+
const baseUrl = this.getMediaBaseUrl();
|
|
131
|
+
try {
|
|
132
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
133
|
+
// Process image — validate dimensions, enlarge to min 500px if too small,
|
|
134
|
+
// convert to JPEG, and optimize. Uses sharp library.
|
|
135
|
+
const processed = await processImageForUpload(fileBuffer);
|
|
136
|
+
return await this.uploadProcessedImage(processed.buffer, processed.metadata, token, baseUrl, description);
|
|
137
|
+
}
|
|
65
138
|
catch (error) {
|
|
66
139
|
if (axios.isAxiosError(error)) {
|
|
67
140
|
const status = error.response?.status;
|
|
@@ -71,13 +144,69 @@ export class MediaApi {
|
|
|
71
144
|
data.errors?.[0]?.message ||
|
|
72
145
|
error.message
|
|
73
146
|
: error.message;
|
|
74
|
-
throw new Error(`Failed to upload image from
|
|
147
|
+
throw new Error(`Failed to upload image from file (status ${status}): ${message}`, {
|
|
75
148
|
cause: error,
|
|
76
149
|
});
|
|
77
150
|
}
|
|
78
|
-
throw new Error(`Failed to upload image from
|
|
151
|
+
throw new Error(`Failed to upload image from file: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
|
|
79
152
|
}
|
|
80
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Upload a processed image buffer to eBay Picture Services.
|
|
156
|
+
*
|
|
157
|
+
* @param buffer - Processed image buffer
|
|
158
|
+
* @param metadata - Image metadata
|
|
159
|
+
* @param token - OAuth access token
|
|
160
|
+
* @param baseUrl - Media API base URL
|
|
161
|
+
* @param description - Optional description
|
|
162
|
+
* @returns Object with image ID and eBay-hosted image URL
|
|
163
|
+
*/
|
|
164
|
+
async uploadProcessedImage(buffer, metadata, token, baseUrl, description) {
|
|
165
|
+
// Build multipart/form-data body correctly:
|
|
166
|
+
// --boundary\r\n
|
|
167
|
+
// Content-Disposition: imageFile\r\n
|
|
168
|
+
// Content-Type: image/jpeg\r\n\r\n
|
|
169
|
+
// [IMAGE BINARY DATA]
|
|
170
|
+
// --boundary\r\n
|
|
171
|
+
// Content-Disposition: description\r\n\r\n
|
|
172
|
+
// [description text]
|
|
173
|
+
// --boundary--\r\n
|
|
174
|
+
const boundary = `----FormBoundary${Date.now()}`;
|
|
175
|
+
const fileName = `image_${Date.now()}.jpg`;
|
|
176
|
+
const parts = [];
|
|
177
|
+
// Image file part — headers + binary data
|
|
178
|
+
const imageHeaders = `--${boundary}\r\n` +
|
|
179
|
+
`Content-Disposition: form-data; name="imageFile"; filename="${fileName}"\r\n` +
|
|
180
|
+
`Content-Type: image/jpeg\r\n\r\n`;
|
|
181
|
+
parts.push(Buffer.from(imageHeaders, 'utf-8'));
|
|
182
|
+
parts.push(buffer);
|
|
183
|
+
// Description part (optional)
|
|
184
|
+
if (description) {
|
|
185
|
+
const descPart = `--${boundary}\r\n` +
|
|
186
|
+
`Content-Disposition: form-data; name="description"\r\n\r\n` +
|
|
187
|
+
`${description}\r\n`;
|
|
188
|
+
parts.push(Buffer.from(descPart, 'utf-8'));
|
|
189
|
+
}
|
|
190
|
+
// Closing boundary
|
|
191
|
+
parts.push(Buffer.from(`--${boundary}--\r\n`, 'utf-8'));
|
|
192
|
+
const multipartBody = Buffer.concat(parts);
|
|
193
|
+
const createResponse = await axios.post(`${baseUrl}${this.basePath}/image/create_image_from_file`, multipartBody, {
|
|
194
|
+
headers: {
|
|
195
|
+
Authorization: `Bearer ${token}`,
|
|
196
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
197
|
+
Prefer: 'return=representation',
|
|
198
|
+
},
|
|
199
|
+
timeout: 30000,
|
|
200
|
+
});
|
|
201
|
+
const responseData = createResponse.data;
|
|
202
|
+
const imageId = typeof responseData.id === 'string'
|
|
203
|
+
? responseData.id
|
|
204
|
+
: createResponse.headers.location?.split('/').pop();
|
|
205
|
+
if (!imageId) {
|
|
206
|
+
throw new Error('No image ID returned from create endpoint');
|
|
207
|
+
}
|
|
208
|
+
return await this.getImage(imageId);
|
|
209
|
+
}
|
|
81
210
|
/**
|
|
82
211
|
* Get image details including the eBay-hosted URL.
|
|
83
212
|
*
|
|
@@ -99,9 +228,15 @@ export class MediaApi {
|
|
|
99
228
|
timeout: 30000,
|
|
100
229
|
});
|
|
101
230
|
const data = response.data;
|
|
231
|
+
let imageUrl = data.imageUrl;
|
|
232
|
+
// eBay Media API returns $_1.JPG thumbnail URL. Convert to full-size (s-l1600.jpg)
|
|
233
|
+
// which is required for listing images (500px minimum).
|
|
234
|
+
if (imageUrl?.includes('$_1.JPG')) {
|
|
235
|
+
imageUrl = imageUrl.replace('$_1.JPG', 's-l1600.jpg');
|
|
236
|
+
}
|
|
102
237
|
return {
|
|
103
238
|
id: data.id,
|
|
104
|
-
imageUrl:
|
|
239
|
+
imageUrl: imageUrl || '',
|
|
105
240
|
description: typeof data.description === 'string' ? data.description : undefined,
|
|
106
241
|
};
|
|
107
242
|
}
|
|
@@ -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/index.js
CHANGED
|
File without changes
|
|
@@ -32,7 +32,25 @@ function buildMarkdown(items) {
|
|
|
32
32
|
async function main() {
|
|
33
33
|
const { items, error } = await getApiStatusFeed({ limit: DEFAULT_LIMIT });
|
|
34
34
|
if (error && items.length === 0) {
|
|
35
|
-
|
|
35
|
+
// Feed is temporarily unavailable — write a fallback doc instead of failing
|
|
36
|
+
const fallback = [
|
|
37
|
+
'# eBay API Status (cached)',
|
|
38
|
+
'',
|
|
39
|
+
'The eBay API Status RSS feed is currently unavailable. Last attempt:',
|
|
40
|
+
`*${new Date().toISOString()}*`,
|
|
41
|
+
'',
|
|
42
|
+
`**Error:** ${error}`,
|
|
43
|
+
'',
|
|
44
|
+
'Full list: [developer.ebay.com/support/api-status](https://developer.ebay.com/support/api-status).',
|
|
45
|
+
'',
|
|
46
|
+
'---',
|
|
47
|
+
'',
|
|
48
|
+
'*Generated by [ebay-mcp-remote-edition](https://github.com/mrnajiboy/ebay-mcp-remote-edition) API status sync.*',
|
|
49
|
+
].join('\n');
|
|
50
|
+
writeFileSync(OUT_PATH, fallback, 'utf8');
|
|
51
|
+
console.warn(`Feed unavailable (${error}), wrote fallback to ${OUT_PATH}`);
|
|
52
|
+
process.exitCode = 0;
|
|
53
|
+
return;
|
|
36
54
|
}
|
|
37
55
|
const markdown = buildMarkdown(items);
|
|
38
56
|
writeFileSync(OUT_PATH, markdown, 'utf8');
|
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
|
});
|
|
@@ -966,17 +966,42 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
966
966
|
{ src: `${iconBaseUrl}/48x48.png`, mimeType: 'image/png', sizes: ['48x48'] },
|
|
967
967
|
],
|
|
968
968
|
});
|
|
969
|
+
// Configurable tool execution timeout (60s default — most tools complete in <10s)
|
|
970
|
+
// Longer than individual API timeouts (30s) to allow for token refresh + API chaining
|
|
971
|
+
const TOOL_TIMEOUT_MS = Number(process.env.MCP_TOOL_TIMEOUT_MS ?? 60_000);
|
|
969
972
|
const tools = getToolDefinitions();
|
|
970
973
|
for (const toolDef of tools) {
|
|
971
974
|
try {
|
|
972
975
|
server.registerTool(toolDef.name, { description: toolDef.description, inputSchema: toolDef.inputSchema }, async (args) => {
|
|
976
|
+
const startTime = Date.now();
|
|
973
977
|
try {
|
|
974
|
-
|
|
978
|
+
// Wrap tool execution with timeout to prevent indefinite hangs
|
|
979
|
+
const result = await Promise.race([
|
|
980
|
+
executeTool(api, toolDef.name, args),
|
|
981
|
+
new Promise((_, reject) => {
|
|
982
|
+
setTimeout(() => reject(new Error(`Tool ${toolDef.name} timed out after ${TOOL_TIMEOUT_MS}ms`)), TOOL_TIMEOUT_MS);
|
|
983
|
+
}),
|
|
984
|
+
]);
|
|
985
|
+
const duration = Date.now() - startTime;
|
|
986
|
+
serverLogger.info(`[tool-exec] ${toolDef.name}`, {
|
|
987
|
+
userId,
|
|
988
|
+
environment,
|
|
989
|
+
durationMs: duration,
|
|
990
|
+
status: 'success',
|
|
991
|
+
});
|
|
975
992
|
return {
|
|
976
993
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
977
994
|
};
|
|
978
995
|
}
|
|
979
996
|
catch (error) {
|
|
997
|
+
const duration = Date.now() - startTime;
|
|
998
|
+
serverLogger.warn(`[tool-exec] ${toolDef.name}`, {
|
|
999
|
+
userId,
|
|
1000
|
+
environment,
|
|
1001
|
+
durationMs: duration,
|
|
1002
|
+
status: 'error',
|
|
1003
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1004
|
+
});
|
|
980
1005
|
return {
|
|
981
1006
|
content: [
|
|
982
1007
|
{
|
|
@@ -1132,7 +1157,7 @@ async function handleOAuthCallback(req, res, serverUrl) {
|
|
|
1132
1157
|
environment,
|
|
1133
1158
|
tokenBaseUrl: getBaseUrl(environment),
|
|
1134
1159
|
clientIdPrefix: ebayConfig.clientId ? `${ebayConfig.clientId.slice(0, 12)}...` : '(missing)',
|
|
1135
|
-
ruName: ebayConfig.redirectUri ?? '(missing)',
|
|
1160
|
+
ruName: (ebayConfig.ruName || ebayConfig.redirectUri) ?? '(missing)',
|
|
1136
1161
|
});
|
|
1137
1162
|
const userId = randomUUID();
|
|
1138
1163
|
const api = await createUserScopedApi(userId, environment);
|