ebay-mcp-remote-edition 4.2.0 → 4.3.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/client-trading.js +221 -1
- package/build/api/index.js +3 -0
- package/build/api/media/media.js +124 -0
- package/build/api/trading/trading.js +2 -1
- package/build/server-http.js +51 -1
- package/build/tools/definitions/inventory.js +24 -3
- package/build/tools/definitions/trading.js +97 -4
- package/build/tools/index.js +100 -8
- package/build/tools/schemas.js +5 -3
- package/build/validation/recommendation.js +21 -0
- package/build/validation/run-validation.js +2 -0
- package/build/validation/threshold-calibration.js +311 -0
- package/package.json +16 -16
|
@@ -42,14 +42,234 @@ export class TradingApiClient {
|
|
|
42
42
|
getBaseUrl() {
|
|
43
43
|
return this.baseUrl;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Transform a Trading API item object into a structure that fast-xml-parser
|
|
47
|
+
* can serialize into valid eBay Trading API XML.
|
|
48
|
+
*
|
|
49
|
+
* Handles nested objects like PrimaryCategory, ShippingDetails, ReturnPolicy,
|
|
50
|
+
* PicturesDetails, and ItemSpecifics that require special XML structure.
|
|
51
|
+
*/
|
|
52
|
+
transformItemForXML(item) {
|
|
53
|
+
const transformed = {};
|
|
54
|
+
for (const [key, value] of Object.entries(item)) {
|
|
55
|
+
if (value === undefined || value === null)
|
|
56
|
+
continue;
|
|
57
|
+
switch (key) {
|
|
58
|
+
case 'PrimaryCategory':
|
|
59
|
+
if (typeof value === 'object') {
|
|
60
|
+
transformed[key] = { CategoryID: value.CategoryID };
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
transformed[key] = value;
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
case 'StartPrice':
|
|
67
|
+
transformed[key] = this.transformPrice(value);
|
|
68
|
+
break;
|
|
69
|
+
case 'Currency':
|
|
70
|
+
transformed[key] = value;
|
|
71
|
+
break;
|
|
72
|
+
case 'ListingDuration': {
|
|
73
|
+
// Normalize listing duration - map invalid/common values to valid ones
|
|
74
|
+
const duration = value;
|
|
75
|
+
if (duration === 'GMS' || duration === 'GN') {
|
|
76
|
+
// GMS (Good Month) and GN (Good 'til Cancelled) may not be available
|
|
77
|
+
transformed[key] = 'Days_30';
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
transformed[key] = value;
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case 'ShippingDetails':
|
|
85
|
+
transformed[key] = this.transformShippingDetails(value);
|
|
86
|
+
break;
|
|
87
|
+
case 'ReturnPolicy':
|
|
88
|
+
transformed[key] = this.transformReturnPolicy(value);
|
|
89
|
+
break;
|
|
90
|
+
case 'PicturesDetails':
|
|
91
|
+
transformed[key] = this.transformPicturesDetails(value);
|
|
92
|
+
break;
|
|
93
|
+
case 'ItemSpecifics':
|
|
94
|
+
transformed[key] = this.transformItemSpecifics(value);
|
|
95
|
+
break;
|
|
96
|
+
case 'PaymentMethods':
|
|
97
|
+
// Ensure array of strings becomes array of PaymentMethod elements
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
transformed[key] = value;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
transformed[key] = value;
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
transformed[key] = value;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Ensure Currency is always present - extract from StartPrice if not provided
|
|
111
|
+
if (!transformed.Currency && item.StartPrice) {
|
|
112
|
+
const sp = item.StartPrice;
|
|
113
|
+
if (typeof sp === 'object' && sp.currencyID) {
|
|
114
|
+
transformed.Currency = sp.currencyID;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
transformed.Currency = 'USD';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return transformed;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Transform price fields that may have currency attributes.
|
|
124
|
+
* eBay Trading API expects: <StartPrice currencyID="USD">34.99</StartPrice>
|
|
125
|
+
*/
|
|
126
|
+
transformPrice(price) {
|
|
127
|
+
if (typeof price === 'string') {
|
|
128
|
+
return price;
|
|
129
|
+
}
|
|
130
|
+
// Use fast-xml-parser attribute convention: @_<attrName> for attributes, #text for content
|
|
131
|
+
return {
|
|
132
|
+
'#text': String(price.value),
|
|
133
|
+
...(price.currencyID && { '@_currencyID': price.currencyID }),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Transform ShippingDetails into proper nested XML structure.
|
|
138
|
+
*/
|
|
139
|
+
transformShippingDetails(sd) {
|
|
140
|
+
const transformed = {};
|
|
141
|
+
if (sd.HandlingTime !== undefined) {
|
|
142
|
+
transformed.HandlingTime = sd.HandlingTime;
|
|
143
|
+
}
|
|
144
|
+
if (sd.ShippingServiceOptions) {
|
|
145
|
+
const options = sd.ShippingServiceOptions;
|
|
146
|
+
const serviceOption = {};
|
|
147
|
+
if (options.ShippingServicePriority) {
|
|
148
|
+
serviceOption.ShippingServicePriority = options.ShippingServicePriority;
|
|
149
|
+
}
|
|
150
|
+
if (options.ShippingServiceID) {
|
|
151
|
+
serviceOption.ShippingServiceID = options.ShippingServiceID;
|
|
152
|
+
}
|
|
153
|
+
if (options.ShippingServiceCost) {
|
|
154
|
+
serviceOption.ShippingServiceCost = this.transformPrice(options.ShippingServiceCost);
|
|
155
|
+
}
|
|
156
|
+
if (options.ShippingType) {
|
|
157
|
+
serviceOption.ShippingType = options.ShippingType;
|
|
158
|
+
}
|
|
159
|
+
transformed.ShippingServiceOptions = serviceOption;
|
|
160
|
+
}
|
|
161
|
+
if (sd.InternationalShippingServiceOption) {
|
|
162
|
+
const intlOptions = sd.InternationalShippingServiceOption;
|
|
163
|
+
transformed.InternationalShippingServiceOption = intlOptions.map((option) => {
|
|
164
|
+
const transformedOption = {};
|
|
165
|
+
if (option.ShippingServicePriority) {
|
|
166
|
+
transformedOption.ShippingServicePriority = option.ShippingServicePriority;
|
|
167
|
+
}
|
|
168
|
+
if (option.ShippingServiceID) {
|
|
169
|
+
transformedOption.ShippingServiceID = option.ShippingServiceID;
|
|
170
|
+
}
|
|
171
|
+
if (option.ShippingServiceCost) {
|
|
172
|
+
transformedOption.ShippingServiceCost = this.transformPrice(option.ShippingServiceCost);
|
|
173
|
+
}
|
|
174
|
+
if (option.ShippingType) {
|
|
175
|
+
transformedOption.ShippingType = option.ShippingType;
|
|
176
|
+
}
|
|
177
|
+
if (option.Country) {
|
|
178
|
+
transformedOption.Country = Array.isArray(option.Country)
|
|
179
|
+
? option.Country
|
|
180
|
+
: [option.Country];
|
|
181
|
+
}
|
|
182
|
+
return transformedOption;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return transformed;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Transform ReturnPolicy into proper nested XML structure.
|
|
189
|
+
* Maps user-friendly values to eBay Trading API enum values.
|
|
190
|
+
*/
|
|
191
|
+
transformReturnPolicy(rp) {
|
|
192
|
+
const transformed = {};
|
|
193
|
+
if (rp.ReturnsAcceptedOption !== undefined) {
|
|
194
|
+
const val = rp.ReturnsAcceptedOption;
|
|
195
|
+
// Map user-friendly values to eBay enum values
|
|
196
|
+
if (val.toLowerCase() === 'yes') {
|
|
197
|
+
transformed.ReturnsAcceptedOption = 'ReturnsAccepted';
|
|
198
|
+
}
|
|
199
|
+
else if (val.toLowerCase() === 'no') {
|
|
200
|
+
transformed.ReturnsAcceptedOption = 'ReturnsNotAccepted';
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
transformed.ReturnsAcceptedOption = val;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (rp.ReturnsWithinOption !== undefined) {
|
|
207
|
+
const val = rp.ReturnsWithinOption;
|
|
208
|
+
// Map user-friendly values to eBay enum values (Days30 → Days_30)
|
|
209
|
+
transformed.ReturnsWithinOption = val.replace(/^Days(\d+)$/, 'Days_$1');
|
|
210
|
+
}
|
|
211
|
+
if (rp.Description !== undefined) {
|
|
212
|
+
transformed.Description = rp.Description;
|
|
213
|
+
}
|
|
214
|
+
if (rp.RefundOption !== undefined) {
|
|
215
|
+
transformed.RefundOption = rp.RefundOption;
|
|
216
|
+
}
|
|
217
|
+
return transformed;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Transform PicturesDetails into proper nested XML structure.
|
|
221
|
+
*/
|
|
222
|
+
transformPicturesDetails(pd) {
|
|
223
|
+
const transformed = {};
|
|
224
|
+
if (pd.GalleryType) {
|
|
225
|
+
transformed.GalleryType = pd.GalleryType;
|
|
226
|
+
}
|
|
227
|
+
if (pd.PictureURL) {
|
|
228
|
+
const urls = pd.PictureURL;
|
|
229
|
+
transformed.PictureURL = Array.isArray(urls) ? urls : [urls];
|
|
230
|
+
}
|
|
231
|
+
return transformed;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Transform ItemSpecifics into proper NameValueList XML structure.
|
|
235
|
+
* eBay expects:
|
|
236
|
+
* <ItemSpecifics>
|
|
237
|
+
* <NameValueList><Name>Brand</Name><Value>Nike</Value></NameValueList>
|
|
238
|
+
* </ItemSpecifics>
|
|
239
|
+
*/
|
|
240
|
+
transformItemSpecifics(specifics) {
|
|
241
|
+
if (!Array.isArray(specifics))
|
|
242
|
+
return specifics;
|
|
243
|
+
// eBay expects: <ItemSpecifics><NameValueList><Name>...</Name><Value>...</Value></NameValueList>...</ItemSpecifics>
|
|
244
|
+
// fast-xml-parser XMLBuilder: when ItemSpecifics is in isArray list, it creates one <ItemSpecifics> per array item.
|
|
245
|
+
// So we wrap each spec in { NameValueList: { Name, Value } } and return as a single object.
|
|
246
|
+
// The caller (transformItemForXML) sets transformed['ItemSpecifics'] = this result.
|
|
247
|
+
// To get a single <ItemSpecifics> with multiple <NameValueList>, we need to NOT return an array
|
|
248
|
+
// but instead return the NameValueList array directly, and let the XMLBuilder handle it.
|
|
249
|
+
// Actually: the cleanest approach is to return the array of NameValueList objects,
|
|
250
|
+
// and since transformItemForXML assigns it to transformed['ItemSpecifics'], and
|
|
251
|
+
// ItemSpecifics IS in the isArray list, XMLBuilder will emit multiple <ItemSpecifics>.
|
|
252
|
+
// FIX: Return a single object with NameValueList array inside.
|
|
253
|
+
return {
|
|
254
|
+
NameValueList: specifics.map((spec) => ({
|
|
255
|
+
Name: spec.name,
|
|
256
|
+
Value: Array.isArray(spec.value) ? spec.value : [spec.value],
|
|
257
|
+
})),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
45
260
|
async execute(callName, params) {
|
|
46
261
|
const token = await this.restClient.getOAuthClient().getAccessToken();
|
|
47
262
|
const requestTag = `${callName}Request`;
|
|
48
263
|
const responseTag = `${callName}Response`;
|
|
264
|
+
// Transform Item field for proper XML serialization
|
|
265
|
+
const transformedParams = { ...params };
|
|
266
|
+
if (transformedParams.Item && typeof transformedParams.Item === 'object') {
|
|
267
|
+
transformedParams.Item = this.transformItemForXML(transformedParams.Item);
|
|
268
|
+
}
|
|
49
269
|
const xmlObj = {};
|
|
50
270
|
xmlObj[requestTag] = {
|
|
51
271
|
'@_xmlns': 'urn:ebay:apis:eBLBaseComponents',
|
|
52
|
-
...
|
|
272
|
+
...transformedParams,
|
|
53
273
|
};
|
|
54
274
|
const xmlBody = `<?xml version="1.0" encoding="utf-8"?>\n${this.builder.build(xmlObj)}`;
|
|
55
275
|
apiLogger.debug(`Trading API ${callName}`, { xmlBody });
|
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';
|
|
@@ -41,6 +42,7 @@ export class EbaySellerApi {
|
|
|
41
42
|
translation;
|
|
42
43
|
edelivery;
|
|
43
44
|
developer;
|
|
45
|
+
media;
|
|
44
46
|
trading;
|
|
45
47
|
constructor(config, context) {
|
|
46
48
|
this.client = new EbayApiClient(config, context);
|
|
@@ -63,6 +65,7 @@ export class EbaySellerApi {
|
|
|
63
65
|
this.translation = new TranslationApi(this.client);
|
|
64
66
|
this.edelivery = new EDeliveryApi(this.client);
|
|
65
67
|
this.developer = new DeveloperApi(this.client);
|
|
68
|
+
this.media = new MediaApi(this.client);
|
|
66
69
|
const tradingClient = new TradingApiClient(this.client);
|
|
67
70
|
this.trading = new TradingApi(tradingClient);
|
|
68
71
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { getBaseUrl } from '../../config/environment.js';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
/**
|
|
4
|
+
* Commerce Media API (v1_beta) - Upload and manage images via eBay Picture Services
|
|
5
|
+
* Based on: https://developer.ebay.com/api-docs/commerce/media/resources/image/from_url/methods
|
|
6
|
+
*/
|
|
7
|
+
export class MediaApi {
|
|
8
|
+
client;
|
|
9
|
+
basePath = '/commerce/media/v1';
|
|
10
|
+
constructor(client) {
|
|
11
|
+
this.client = client;
|
|
12
|
+
}
|
|
13
|
+
async getAccessToken() {
|
|
14
|
+
return await this.client.getOAuthClient().getAccessToken();
|
|
15
|
+
}
|
|
16
|
+
getMediaBaseUrl() {
|
|
17
|
+
const env = this.client.getConfig().environment;
|
|
18
|
+
return getBaseUrl(env);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Upload an image from a public URL to eBay Picture Services.
|
|
22
|
+
*
|
|
23
|
+
* Steps:
|
|
24
|
+
* 1. POST /commerce/media/v1/image/from_url to create the image
|
|
25
|
+
* 2. GET /commerce/media/v1/image/{imageId} to retrieve the hosted URL
|
|
26
|
+
*
|
|
27
|
+
* Supported formats: JPG, GIF, PNG, BMP, TIFF, AVIF, HEIC, WEBP
|
|
28
|
+
* Max file size: 10MB per image
|
|
29
|
+
*
|
|
30
|
+
* @param imageUrl - Public URL of the image to upload
|
|
31
|
+
* @param description - Optional description for the image
|
|
32
|
+
* @returns Object with image ID and eBay-hosted image URL
|
|
33
|
+
*/
|
|
34
|
+
async createImageFromUrl(imageUrl, description) {
|
|
35
|
+
if (!imageUrl || typeof imageUrl !== 'string') {
|
|
36
|
+
throw new Error('imageUrl is required and must be a string');
|
|
37
|
+
}
|
|
38
|
+
const token = await this.getAccessToken();
|
|
39
|
+
const baseUrl = this.getMediaBaseUrl();
|
|
40
|
+
try {
|
|
41
|
+
// Step 1: Create image from URL
|
|
42
|
+
const body = { imageUrl };
|
|
43
|
+
if (description) {
|
|
44
|
+
Object.assign(body, { description });
|
|
45
|
+
}
|
|
46
|
+
const createResponse = await axios.post(`${baseUrl}${this.basePath}/image/from_url`, body, {
|
|
47
|
+
headers: {
|
|
48
|
+
Authorization: `Bearer ${token}`,
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
Prefer: 'return=representation',
|
|
51
|
+
},
|
|
52
|
+
timeout: 30000,
|
|
53
|
+
});
|
|
54
|
+
// Extract image ID from response body or Location header
|
|
55
|
+
const responseData = createResponse.data;
|
|
56
|
+
const imageId = typeof responseData.id === 'string'
|
|
57
|
+
? responseData.id
|
|
58
|
+
: createResponse.headers.location?.split('/').pop();
|
|
59
|
+
if (!imageId) {
|
|
60
|
+
throw new Error('No image ID returned from create endpoint');
|
|
61
|
+
}
|
|
62
|
+
// Step 2: Fetch image details to get the eBay-hosted URL
|
|
63
|
+
return await this.getImage(imageId);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (axios.isAxiosError(error)) {
|
|
67
|
+
const status = error.response?.status;
|
|
68
|
+
const data = error.response?.data;
|
|
69
|
+
const message = typeof data === 'object' && data !== null && 'errors' in data
|
|
70
|
+
? data.errors?.[0]?.longMessage ||
|
|
71
|
+
data.errors?.[0]?.message ||
|
|
72
|
+
error.message
|
|
73
|
+
: error.message;
|
|
74
|
+
throw new Error(`Failed to upload image from URL (status ${status}): ${message}`, {
|
|
75
|
+
cause: error,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Failed to upload image from URL: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get image details including the eBay-hosted URL.
|
|
83
|
+
*
|
|
84
|
+
* @param imageId - The image ID returned from createImageFromUrl
|
|
85
|
+
* @returns Image details including hosted URL
|
|
86
|
+
*/
|
|
87
|
+
async getImage(imageId) {
|
|
88
|
+
if (!imageId || typeof imageId !== 'string') {
|
|
89
|
+
throw new Error('imageId is required and must be a string');
|
|
90
|
+
}
|
|
91
|
+
const token = await this.getAccessToken();
|
|
92
|
+
const baseUrl = this.getMediaBaseUrl();
|
|
93
|
+
try {
|
|
94
|
+
const response = await axios.get(`${baseUrl}${this.basePath}/image/${imageId}`, {
|
|
95
|
+
headers: {
|
|
96
|
+
Authorization: `Bearer ${token}`,
|
|
97
|
+
Accept: 'application/json',
|
|
98
|
+
},
|
|
99
|
+
timeout: 30000,
|
|
100
|
+
});
|
|
101
|
+
const data = response.data;
|
|
102
|
+
return {
|
|
103
|
+
id: data.id,
|
|
104
|
+
imageUrl: data.imageUrl,
|
|
105
|
+
description: typeof data.description === 'string' ? data.description : undefined,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (axios.isAxiosError(error)) {
|
|
110
|
+
const status = error.response?.status;
|
|
111
|
+
const data = error.response?.data;
|
|
112
|
+
const message = typeof data === 'object' && data !== null && 'errors' in data
|
|
113
|
+
? data.errors?.[0]?.longMessage ||
|
|
114
|
+
data.errors?.[0]?.message ||
|
|
115
|
+
error.message
|
|
116
|
+
: error.message;
|
|
117
|
+
throw new Error(`Failed to get image details (status ${status}): ${message}`, {
|
|
118
|
+
cause: error,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`Failed to get image details: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -51,7 +51,8 @@ export class TradingApi {
|
|
|
51
51
|
return items?.[0] || result;
|
|
52
52
|
}
|
|
53
53
|
async createListing(item) {
|
|
54
|
-
|
|
54
|
+
const transformed = this.client.transformItemForXML(item);
|
|
55
|
+
return await this.client.execute('AddFixedPriceItem', { Item: transformed });
|
|
55
56
|
}
|
|
56
57
|
async reviseListing(itemId, fields) {
|
|
57
58
|
if (!itemId)
|
package/build/server-http.js
CHANGED
|
@@ -31,6 +31,27 @@ console.log(`[auth-store] Active KV backend: ${authStore.backendName}`);
|
|
|
31
31
|
function getServerBaseUrl() {
|
|
32
32
|
return CONFIG.publicBaseUrl || `http://localhost:${CONFIG.port}`;
|
|
33
33
|
}
|
|
34
|
+
function getExpectedOAuthCallbackUrl(serverUrl = getServerBaseUrl()) {
|
|
35
|
+
return `${serverUrl.replace(/\/+$/, '')}/oauth/callback`;
|
|
36
|
+
}
|
|
37
|
+
function isLocalDevelopmentBaseUrl(baseUrl) {
|
|
38
|
+
if (!baseUrl) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const { hostname } = new URL(baseUrl);
|
|
43
|
+
const normalizedHost = hostname.toLowerCase();
|
|
44
|
+
return (normalizedHost === 'localhost' ||
|
|
45
|
+
normalizedHost === '127.0.0.1' ||
|
|
46
|
+
normalizedHost === '::1' ||
|
|
47
|
+
normalizedHost === 'ebay-local.test' ||
|
|
48
|
+
normalizedHost.endsWith('.localhost') ||
|
|
49
|
+
normalizedHost.endsWith('.test'));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
34
55
|
function htmlEscape(value) {
|
|
35
56
|
return value
|
|
36
57
|
.replace(/&/g, '&')
|
|
@@ -749,6 +770,8 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
749
770
|
serverLogger.info(`[${prefix || 'root'}/authorize] Redirecting to eBay OAuth`, {
|
|
750
771
|
state: stateRecord.state,
|
|
751
772
|
environment,
|
|
773
|
+
ruName: ebayConfig.redirectUri,
|
|
774
|
+
expectedCallbackUrl: getExpectedOAuthCallbackUrl(serverUrl),
|
|
752
775
|
});
|
|
753
776
|
res.redirect(oauthUrl);
|
|
754
777
|
}
|
|
@@ -848,6 +871,13 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
848
871
|
}
|
|
849
872
|
const stateRecord = await authStore.createOAuthState(environment, returnTo);
|
|
850
873
|
const oauthUrl = getOAuthAuthorizationUrl(ebayConfig.clientId, ebayConfig.redirectUri, environment, getHostedOauthScopes(environment), undefined, stateRecord.state);
|
|
874
|
+
serverLogger.info(`[${prefix || 'root'}/oauth/start] Redirecting to eBay OAuth`, {
|
|
875
|
+
state: stateRecord.state,
|
|
876
|
+
environment,
|
|
877
|
+
ruName: ebayConfig.redirectUri,
|
|
878
|
+
expectedCallbackUrl: getExpectedOAuthCallbackUrl(serverUrl),
|
|
879
|
+
returnTo,
|
|
880
|
+
});
|
|
851
881
|
res.redirect(oauthUrl);
|
|
852
882
|
}
|
|
853
883
|
catch (error) {
|
|
@@ -1066,6 +1096,8 @@ async function handleOAuthCallback(req, res, serverUrl) {
|
|
|
1066
1096
|
hasCode: !!code,
|
|
1067
1097
|
hasState: !!state,
|
|
1068
1098
|
oauthError,
|
|
1099
|
+
expectedCallbackUrl: getExpectedOAuthCallbackUrl(serverUrl),
|
|
1100
|
+
actualCallbackUrl: `${serverUrl}${req.originalUrl}`,
|
|
1069
1101
|
});
|
|
1070
1102
|
if (oauthError) {
|
|
1071
1103
|
res
|
|
@@ -1256,11 +1288,29 @@ async function main() {
|
|
|
1256
1288
|
const app = createApp();
|
|
1257
1289
|
const certPath = process.env.EBAY_LOCAL_TLS_CERT_PATH;
|
|
1258
1290
|
const keyPath = process.env.EBAY_LOCAL_TLS_KEY_PATH;
|
|
1259
|
-
const
|
|
1291
|
+
const localTlsEligible = isLocalDevelopmentBaseUrl(CONFIG.publicBaseUrl);
|
|
1292
|
+
const useHttps = CONFIG.publicBaseUrl.startsWith('https://') && localTlsEligible && !!certPath && !!keyPath;
|
|
1260
1293
|
const serverUrl = getServerBaseUrl();
|
|
1294
|
+
serverLogger.info('[startup] Public OAuth URL diagnostics', {
|
|
1295
|
+
publicBaseUrl: CONFIG.publicBaseUrl || '(unset)',
|
|
1296
|
+
serverUrl,
|
|
1297
|
+
expectedCallbackUrl: getExpectedOAuthCallbackUrl(serverUrl),
|
|
1298
|
+
localTlsEligible,
|
|
1299
|
+
hasLocalTlsCertPath: !!certPath,
|
|
1300
|
+
hasLocalTlsKeyPath: !!keyPath,
|
|
1301
|
+
useHttps,
|
|
1302
|
+
});
|
|
1303
|
+
if (CONFIG.publicBaseUrl.startsWith('https://') && !localTlsEligible && (certPath || keyPath)) {
|
|
1304
|
+
serverLogger.warn('[startup] Ignoring local TLS certificate settings for hosted base URL', {
|
|
1305
|
+
publicBaseUrl: CONFIG.publicBaseUrl,
|
|
1306
|
+
hasLocalTlsCertPath: !!certPath,
|
|
1307
|
+
hasLocalTlsKeyPath: !!keyPath,
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1261
1310
|
const onListening = () => {
|
|
1262
1311
|
const protocol = useHttps ? 'HTTPS' : 'HTTP';
|
|
1263
1312
|
console.log(`Server running at ${serverUrl} [${protocol}]`);
|
|
1313
|
+
console.log(`OAuth callback: ${getExpectedOAuthCallbackUrl(serverUrl)}`);
|
|
1264
1314
|
console.log(`MCP (default): ${serverUrl}/mcp`);
|
|
1265
1315
|
console.log(`MCP (sandbox): ${serverUrl}/sandbox/mcp`);
|
|
1266
1316
|
console.log(`MCP (production): ${serverUrl}/production/mcp`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MarketplaceId } from '../../types/ebay-enums.js';
|
|
1
|
+
import { MarketplaceId, Condition } from '../../types/ebay-enums.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { zodToJsonSchema } from '../../utils/zod-compat.js';
|
|
4
4
|
import { bulkInventoryItemRequestSchema, bulkMigrateRequestSchema, bulkOfferRequestSchema, bulkPriceQuantityRequestSchema, bulkPublishRequestSchema, inventoryItemGroupSchema, inventoryItemSchema, listingFeesRequestSchema, locationSchema, offerSchema, productCompatibilitySchema, } from '../schemas.js';
|
|
@@ -29,10 +29,31 @@ export const inventoryTools = [
|
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
name: 'ebay_create_inventory_item',
|
|
32
|
-
description: 'Create or replace an inventory item
|
|
32
|
+
description: 'Create or replace an inventory item.\\n\\nRequired OAuth Scope: sell.inventory\\nMinimum Scope: https://api.ebay.com/oauth/api_scope/sell.inventory',
|
|
33
33
|
inputSchema: {
|
|
34
34
|
sku: z.string().describe('The seller-defined SKU'),
|
|
35
|
-
inventoryItem: inventoryItemSchema.describe('Inventory item details'),
|
|
35
|
+
inventoryItem: inventoryItemSchema.describe('Inventory item details (availability, condition, etc.)'),
|
|
36
|
+
// Accept individual inventory fields as top-level fallback for LLMs that don't wrap in inventoryItem
|
|
37
|
+
availability: z
|
|
38
|
+
.any()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('[Fallback] Availability details if inventoryItem not provided'),
|
|
41
|
+
condition: z
|
|
42
|
+
.nativeEnum(Condition)
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('[Fallback] Item condition if inventoryItem not provided'),
|
|
45
|
+
conditionDescription: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('[Fallback] Condition description if inventoryItem not provided'),
|
|
49
|
+
product: z
|
|
50
|
+
.any()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('[Fallback] Product details if inventoryItem not provided'),
|
|
53
|
+
packageWeightAndSize: z
|
|
54
|
+
.any()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('[Fallback] Package weight/size if inventoryItem not provided'),
|
|
36
57
|
},
|
|
37
58
|
outputSchema: zodToJsonSchema(createInventoryItemOutputSchema, {
|
|
38
59
|
name: 'CreateInventoryItemResponse',
|
|
@@ -1,4 +1,88 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
// Schema for Trading API price fields that include currency
|
|
3
|
+
const PriceWithCurrencySchema = z.union([
|
|
4
|
+
z.string(),
|
|
5
|
+
z.object({
|
|
6
|
+
value: z.string().or(z.number()).describe('Price value'),
|
|
7
|
+
currencyID: z.string().optional().describe('Currency code, e.g., USD, EUR, GBP'),
|
|
8
|
+
}),
|
|
9
|
+
]);
|
|
10
|
+
// Schema for Trading API item specifics (NameValueList)
|
|
11
|
+
const ItemSpecificsSchema = z.array(z.object({
|
|
12
|
+
name: z.string().describe('Specific name, e.g., Brand, Colour, Size'),
|
|
13
|
+
value: z.union([z.string(), z.array(z.string())]).describe('Specific value(s)'),
|
|
14
|
+
}));
|
|
15
|
+
// Schema for Trading API item object
|
|
16
|
+
const TradingItemSchema = z.object({
|
|
17
|
+
// Required fields
|
|
18
|
+
Title: z.string().describe('Item title (max 80 characters)'),
|
|
19
|
+
PrimaryCategory: z
|
|
20
|
+
.object({
|
|
21
|
+
CategoryID: z.string().describe('eBay category ID, e.g., "15032"'),
|
|
22
|
+
})
|
|
23
|
+
.describe('Primary category for the listing'),
|
|
24
|
+
StartPrice: PriceWithCurrencySchema.describe('Starting price or fixed price'),
|
|
25
|
+
ConditionID: z.union([z.number(), z.string()]).describe('eBay condition ID, e.g., 1000 for New'),
|
|
26
|
+
Country: z.string().describe('ISO country code, e.g., US, GB, DE'),
|
|
27
|
+
Currency: z.string().optional().describe('Currency code, e.g., USD, EUR, GBP'),
|
|
28
|
+
DispatchTimeMax: z.union([z.number(), z.string()]).describe('Max dispatch time in days'),
|
|
29
|
+
ListingDuration: z
|
|
30
|
+
.string()
|
|
31
|
+
.describe('Listing duration — use Days_30 (recommended), Days_7, Days_10, Days_14, Days_21, Days_30, Days_60, Days_90. GMS/GN may not be available for all accounts.'),
|
|
32
|
+
ListingType: z.literal('FixedPriceItem').describe('Must be "FixedPriceItem"'),
|
|
33
|
+
Quantity: z.union([z.number(), z.string()]).describe('Number of items available'),
|
|
34
|
+
SKU: z.string().describe('Stock keeping unit / inventory identifier'),
|
|
35
|
+
// Optional fields
|
|
36
|
+
Subtitle: z.string().optional().describe('Item subtitle (max 35 characters)'),
|
|
37
|
+
Description: z.string().optional().describe('HTML description of the item'),
|
|
38
|
+
ItemSpecifics: ItemSpecificsSchema.optional().describe('Product specifics/attributes'),
|
|
39
|
+
PicturesDetails: z
|
|
40
|
+
.object({
|
|
41
|
+
GalleryType: z.string().optional().describe('Gallery type, e.g., Plus, Summary'),
|
|
42
|
+
PictureURL: z.array(z.string()).describe('Array of image URLs'),
|
|
43
|
+
})
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('Picture details for the listing'),
|
|
46
|
+
ShippingDetails: z
|
|
47
|
+
.object({
|
|
48
|
+
ShippingServiceOptions: z
|
|
49
|
+
.object({
|
|
50
|
+
ShippingServicePriority: z.string().optional().describe('Shipping priority (1-9)'),
|
|
51
|
+
ShippingServiceID: z.string().describe('eBay shipping service ID'),
|
|
52
|
+
ShippingServiceCost: PriceWithCurrencySchema.describe('Shipping cost'),
|
|
53
|
+
ShippingType: z.string().describe('Shipping type: Flat, FlatPlusHandling, or Calculated'),
|
|
54
|
+
})
|
|
55
|
+
.describe('Domestic shipping service options'),
|
|
56
|
+
InternationalShippingServiceOption: z
|
|
57
|
+
.array(z.object({
|
|
58
|
+
ShippingServicePriority: z.string().optional().describe('Shipping priority (1-9)'),
|
|
59
|
+
ShippingServiceID: z.string().describe('eBay shipping service ID'),
|
|
60
|
+
ShippingServiceCost: PriceWithCurrencySchema.describe('Shipping cost'),
|
|
61
|
+
ShippingType: z.string().describe('Shipping type'),
|
|
62
|
+
Country: z.array(z.string()).describe('Array of destination country codes'),
|
|
63
|
+
}))
|
|
64
|
+
.optional()
|
|
65
|
+
.describe('International shipping service options'),
|
|
66
|
+
HandlingTime: z.union([z.number(), z.string()]).optional().describe('Handling time in days'),
|
|
67
|
+
})
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Shipping details for the listing'),
|
|
70
|
+
ReturnPolicy: z
|
|
71
|
+
.object({
|
|
72
|
+
ReturnsAcceptedOption: z
|
|
73
|
+
.string()
|
|
74
|
+
.describe('Returns accepted: ReturnsAccepted or ReturnsNotAccepted'),
|
|
75
|
+
ReturnsWithinOption: z
|
|
76
|
+
.string()
|
|
77
|
+
.describe('Return window: Days_30, Days_60, Days_90 (with underscore)'),
|
|
78
|
+
Description: z.string().describe('Return policy description'),
|
|
79
|
+
RefundOption: z.string().optional().describe('Refund type: MoneyBack or Replacement'),
|
|
80
|
+
})
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('Return policy for the listing'),
|
|
83
|
+
PaymentMethods: z.array(z.string()).optional().describe('Accepted payment methods'),
|
|
84
|
+
ProxyItem: z.boolean().optional().describe('Whether this is a proxy item'),
|
|
85
|
+
});
|
|
2
86
|
export const tradingTools = [
|
|
3
87
|
{
|
|
4
88
|
name: 'ebay_get_active_listings',
|
|
@@ -19,11 +103,9 @@ export const tradingTools = [
|
|
|
19
103
|
},
|
|
20
104
|
{
|
|
21
105
|
name: 'ebay_create_listing',
|
|
22
|
-
description: 'Create a new fixed-price listing
|
|
106
|
+
description: 'Create a new fixed-price listing.\\n\\nUses the Trading API (AddFixedPriceItem). Requires complete item details.\\n\\nIMPORTANT: Photos are REQUIRED and MUST be hosted on eBay servers. External URLs (even HTTPS) are rejected. To get eBay-hosted photo URLs:\\n1. Upload photos via eBay Seller Hub UI\\n2. Use the eBay Image Hosting API (if enabled for your app)\\n3. Reference existing eBay-hosted URLs from previous listings\\n\\nRequired: User OAuth token.',
|
|
23
107
|
inputSchema: {
|
|
24
|
-
item:
|
|
25
|
-
.record(z.string(), z.unknown())
|
|
26
|
-
.describe('Item details object. Required fields: Title, PrimaryCategory.CategoryID, StartPrice, ConditionID, Country, Currency, DispatchTimeMax, ListingDuration, ListingType ("FixedPriceItem"), Quantity, SKU.'),
|
|
108
|
+
item: TradingItemSchema.describe('Trading API item object with eBay-specific field structure.'),
|
|
27
109
|
},
|
|
28
110
|
annotations: { readOnlyHint: false },
|
|
29
111
|
},
|
|
@@ -68,4 +150,15 @@ export const tradingTools = [
|
|
|
68
150
|
},
|
|
69
151
|
annotations: { readOnlyHint: false },
|
|
70
152
|
},
|
|
153
|
+
{
|
|
154
|
+
name: 'ebay_upload_images',
|
|
155
|
+
description: 'Upload images to eBay Picture Services using the Commerce Media API and get eBay-hosted image URLs for use in listings.\n\nUploads images from public URLs — eBay fetches and hosts them on their servers. The returned image URLs can be used in ebay_create_listing PicturesDetails.PictureURL array.\n\nSupports: JPG, GIF, PNG, BMP, TIFF, AVIF, HEIC, WEBP. Max 10MB per image.\n\nReturns a result for each uploaded image including the eBay-hosted URL and image ID.',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
imageUrls: z
|
|
158
|
+
.array(z.string().describe('Public URL of the image to upload'))
|
|
159
|
+
.describe('Array of image URLs to upload (1 or more)'),
|
|
160
|
+
description: z.string().optional().describe('Optional description for the uploaded images'),
|
|
161
|
+
},
|
|
162
|
+
annotations: { readOnlyHint: false },
|
|
163
|
+
},
|
|
71
164
|
];
|
package/build/tools/index.js
CHANGED
|
@@ -8,6 +8,37 @@ import { getAwaitingFeedbackSchema, getFeedbackRatingSummarySchema, getFeedbackS
|
|
|
8
8
|
import { bulkUpdateConversationSchema, getConversationSchema, getConversationsSchema, sendMessageSchema, updateConversationSchema, } from '../utils/communication/message.js';
|
|
9
9
|
import { findEligibleItemsSchema, getOffersToBuyersSchema, sendOfferToInterestedBuyersSchema, } from '../utils/communication/negotiation.js';
|
|
10
10
|
import { createDestinationSchema, createSubscriptionFilterSchema, createSubscriptionSchema, deleteDestinationSchema, deleteSubscriptionFilterSchema, deleteSubscriptionSchema, disableSubscriptionSchema, enableSubscriptionSchema, getConfigSchema, getDestinationSchema, getPublicKeySchema, getSubscriptionFilterSchema, getSubscriptionSchema, getSubscriptionsSchema, getTopicSchema, getTopicsSchema, testSubscriptionSchema, updateConfigSchema, updateDestinationSchema, updateSubscriptionSchema, } from '../utils/communication/notification.js';
|
|
11
|
+
/**
|
|
12
|
+
* Normalize enum values that LLMs often send in wrong formats
|
|
13
|
+
* (lowercase, plural, hyphenated) to eBay API uppercase snake_case format
|
|
14
|
+
*/
|
|
15
|
+
function normalizeEnumValue(value) {
|
|
16
|
+
return value.toUpperCase().replace(/-/g, '_').replace(/S$/, ''); // Remove trailing S (e.g., "DAYS" -> "DAY")
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Normalize time duration unit values
|
|
20
|
+
*/
|
|
21
|
+
function normalizeTimeUnit(value) {
|
|
22
|
+
const normalized = value.toUpperCase().replace(/-/g, '_');
|
|
23
|
+
const timeUnitMap = {
|
|
24
|
+
DAY: 'DAY',
|
|
25
|
+
DAYS: 'DAY',
|
|
26
|
+
BUSINESS_DAY: 'BUSINESS_DAY',
|
|
27
|
+
BUSINESS_DAYS: 'BUSINESS_DAY',
|
|
28
|
+
CALENDAR_DAY: 'CALENDAR_DAY',
|
|
29
|
+
HOUR: 'HOUR',
|
|
30
|
+
HOURS: 'HOUR',
|
|
31
|
+
MINUTE: 'MINUTE',
|
|
32
|
+
MINUTES: 'MINUTE',
|
|
33
|
+
SECOND: 'SECOND',
|
|
34
|
+
SECONDS: 'SECOND',
|
|
35
|
+
MONTH: 'MONTH',
|
|
36
|
+
MONTHS: 'MONTH',
|
|
37
|
+
YEAR: 'YEAR',
|
|
38
|
+
YEARS: 'YEAR',
|
|
39
|
+
};
|
|
40
|
+
return timeUnitMap[normalized] || normalized;
|
|
41
|
+
}
|
|
11
42
|
/**
|
|
12
43
|
* Get all tool definitions for the MCP server
|
|
13
44
|
*/
|
|
@@ -414,8 +445,17 @@ export async function executeTool(api, toolName, args) {
|
|
|
414
445
|
case 'ebay_get_return_policies':
|
|
415
446
|
return await api.account.getReturnPolicies(args.marketplaceId);
|
|
416
447
|
// Fulfillment Policy CRUD
|
|
417
|
-
case 'ebay_create_fulfillment_policy':
|
|
418
|
-
|
|
448
|
+
case 'ebay_create_fulfillment_policy': {
|
|
449
|
+
// Normalize handlingTime.unit to valid enum values (LLMs often send lowercase/plural)
|
|
450
|
+
const policy = args.policy;
|
|
451
|
+
if (policy?.handlingTime && typeof policy.handlingTime === 'object') {
|
|
452
|
+
const ht = policy.handlingTime;
|
|
453
|
+
if (typeof ht.unit === 'string') {
|
|
454
|
+
ht.unit = normalizeTimeUnit(ht.unit);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return await api.account.createFulfillmentPolicy(policy);
|
|
458
|
+
}
|
|
419
459
|
case 'ebay_get_fulfillment_policy':
|
|
420
460
|
return await api.account.getFulfillmentPolicy(args.fulfillmentPolicyId);
|
|
421
461
|
case 'ebay_get_fulfillment_policy_by_name':
|
|
@@ -495,8 +535,27 @@ export async function executeTool(api, toolName, args) {
|
|
|
495
535
|
return await api.inventory.getInventoryItems(args.limit, args.offset);
|
|
496
536
|
case 'ebay_get_inventory_item':
|
|
497
537
|
return await api.inventory.getInventoryItem(args.sku);
|
|
498
|
-
case 'ebay_create_inventory_item':
|
|
499
|
-
|
|
538
|
+
case 'ebay_create_inventory_item': {
|
|
539
|
+
// Handle both patterns: nested inventoryItem OR flat fields
|
|
540
|
+
let inventoryItemData = args.inventoryItem;
|
|
541
|
+
if (!inventoryItemData || typeof inventoryItemData !== 'object') {
|
|
542
|
+
// Merge fallback fields into inventoryItem
|
|
543
|
+
const fallbackFields = [
|
|
544
|
+
'availability',
|
|
545
|
+
'condition',
|
|
546
|
+
'conditionDescription',
|
|
547
|
+
'product',
|
|
548
|
+
'packageWeightAndSize',
|
|
549
|
+
];
|
|
550
|
+
inventoryItemData = {};
|
|
551
|
+
for (const field of fallbackFields) {
|
|
552
|
+
if (args[field] !== undefined) {
|
|
553
|
+
inventoryItemData[field] = args[field];
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return await api.inventory.createOrReplaceInventoryItem(args.sku, inventoryItemData);
|
|
558
|
+
}
|
|
500
559
|
case 'ebay_delete_inventory_item':
|
|
501
560
|
return await api.inventory.deleteInventoryItem(args.sku);
|
|
502
561
|
// Bulk Operations
|
|
@@ -540,10 +599,20 @@ export async function executeTool(api, toolName, args) {
|
|
|
540
599
|
return await api.inventory.getOffers(args.sku, args.marketplaceId, args.limit);
|
|
541
600
|
case 'ebay_get_offer':
|
|
542
601
|
return await api.inventory.getOffer(args.offerId);
|
|
543
|
-
case 'ebay_create_offer':
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
602
|
+
case 'ebay_create_offer': {
|
|
603
|
+
const offer = args.offer;
|
|
604
|
+
if (offer?.format) {
|
|
605
|
+
offer.format = normalizeEnumValue(offer.format);
|
|
606
|
+
}
|
|
607
|
+
return await api.inventory.createOffer(offer);
|
|
608
|
+
}
|
|
609
|
+
case 'ebay_update_offer': {
|
|
610
|
+
const offer = args.offer;
|
|
611
|
+
if (offer?.format) {
|
|
612
|
+
offer.format = normalizeEnumValue(offer.format);
|
|
613
|
+
}
|
|
614
|
+
return await api.inventory.updateOffer(args.offerId, offer);
|
|
615
|
+
}
|
|
547
616
|
case 'ebay_delete_offer':
|
|
548
617
|
return await api.inventory.deleteOffer(args.offerId);
|
|
549
618
|
case 'ebay_publish_offer':
|
|
@@ -1235,6 +1304,29 @@ export async function executeTool(api, toolName, args) {
|
|
|
1235
1304
|
return await api.trading.endListing(args.itemId, args.reason);
|
|
1236
1305
|
case 'ebay_relist_item':
|
|
1237
1306
|
return await api.trading.relistItem(args.itemId, args.modifications);
|
|
1307
|
+
case 'ebay_upload_images': {
|
|
1308
|
+
const imageUrls = args.imageUrls;
|
|
1309
|
+
const description = args.description;
|
|
1310
|
+
const results = [];
|
|
1311
|
+
for (const imageUrl of imageUrls) {
|
|
1312
|
+
try {
|
|
1313
|
+
const result = await api.media.createImageFromUrl(imageUrl, description);
|
|
1314
|
+
results.push({ success: true, id: result.id, imageUrl: result.imageUrl });
|
|
1315
|
+
}
|
|
1316
|
+
catch (e) {
|
|
1317
|
+
results.push({
|
|
1318
|
+
success: false,
|
|
1319
|
+
error: e instanceof Error ? e.message : String(e),
|
|
1320
|
+
sourceUrl: imageUrl,
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return {
|
|
1325
|
+
uploaded: results.filter((r) => r.success).length,
|
|
1326
|
+
failed: results.filter((r) => !r.success).length,
|
|
1327
|
+
results,
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1238
1330
|
default:
|
|
1239
1331
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
1240
1332
|
}
|
package/build/tools/schemas.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { RegionType, ShippingCostType, ShippingOptionType, DepositType, RefundMethod, ReturnMethod, ReturnShippingCostPayer, Condition, LengthUnit, WeightUnit, PricingVisibility, FormatType, LocationType, MerchantLocationStatus, DayOfWeek, ReasonForRefund, FundingModel, MessageReferenceType, FeedbackRating, ReportedItemType, TimeDurationUnit, } from '../types/ebay-enums.js';
|
|
3
3
|
/**
|
|
4
4
|
* Reusable Zod schemas for eBay API tool input validation
|
|
5
5
|
*
|
|
@@ -11,7 +11,9 @@ import { TimeDurationUnit, RegionType, ShippingCostType, ShippingOptionType, Dep
|
|
|
11
11
|
// ============================================================================
|
|
12
12
|
export const timeDurationSchema = z
|
|
13
13
|
.object({
|
|
14
|
-
unit: z
|
|
14
|
+
unit: z
|
|
15
|
+
.nativeEnum(TimeDurationUnit)
|
|
16
|
+
.describe('Time unit: DAY, BUSINESS_DAY, CALENDAR_DAY, HOUR, MINUTE, SECOND, MONTH, YEAR'),
|
|
15
17
|
value: z.number(),
|
|
16
18
|
})
|
|
17
19
|
.passthrough();
|
|
@@ -233,7 +235,7 @@ export const offerSchema = z
|
|
|
233
235
|
.object({
|
|
234
236
|
sku: z.string(),
|
|
235
237
|
marketplaceId: z.string(),
|
|
236
|
-
format: z.
|
|
238
|
+
format: z.string().describe('Listing format: FIXED_PRICE (recommended) or AUCTION'),
|
|
237
239
|
availableQuantity: z.number().optional(),
|
|
238
240
|
categoryId: z.string().optional(),
|
|
239
241
|
listingDescription: z.string().optional(),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { computeWeightedValidationScore, shouldApplyAgeAwareCalibration, } from './threshold-calibration.js';
|
|
1
2
|
function addHours(timestamp, hours) {
|
|
2
3
|
return new Date(new Date(timestamp).getTime() + hours * 60 * 60 * 1000).toISOString();
|
|
3
4
|
}
|
|
@@ -105,6 +106,14 @@ export function buildValidationRecommendation(request, signals) {
|
|
|
105
106
|
: signals.research.previousAlbumTitle !== null ||
|
|
106
107
|
signals.research.previousComebackFirstWeekSales !== null ||
|
|
107
108
|
signals.research.perplexityHistoricalContextScore > 0;
|
|
109
|
+
// ── Age-aware weighted scoring (threshold calibration) ─────────────────
|
|
110
|
+
// Computes a weighted composite score that adjusts momentum/velocity emphasis
|
|
111
|
+
// based on how many days the item has been tracked. New items (< 7 days)
|
|
112
|
+
// get 70% momentum weight (since velocity data is sparse), while established
|
|
113
|
+
// items get 70% velocity weight (since sales history is reliable).
|
|
114
|
+
const weightedScore = shouldApplyAgeAwareCalibration(request)
|
|
115
|
+
? computeWeightedValidationScore(request, signals.momentumScore)
|
|
116
|
+
: null;
|
|
108
117
|
let latestAiRecommendation = 'Continue watching until stronger market signal appears.';
|
|
109
118
|
let latestAiConfidence = 'Medium';
|
|
110
119
|
let monitoringNotes = `Baseline recommendation generated from current ${signals.effectiveContext.mode} validation state for ${subjectLabel}.`;
|
|
@@ -172,6 +181,16 @@ export function buildValidationRecommendation(request, signals) {
|
|
|
172
181
|
if (hasUsableHistoricalResearch && signals.research.historicalContextNotes.length > 0) {
|
|
173
182
|
monitoringNotes += ` Historical context (${signals.research.confidence}, score ${signals.research.perplexityHistoricalContextScore}/20): ${signals.research.historicalContextNotes}`;
|
|
174
183
|
}
|
|
184
|
+
// Append weighted scoring summary to monitoring notes if available
|
|
185
|
+
if (weightedScore) {
|
|
186
|
+
monitoringNotes +=
|
|
187
|
+
`\nAge-weighted score: ${weightedScore.compositeScore}/100 (${weightedScore.classification}). ` +
|
|
188
|
+
`Item type: ${weightedScore.isNewItem ? 'new (<7 days)' : 'established (>=7 days)'}. ` +
|
|
189
|
+
`Weights: momentum ${Math.round(weightedScore.weights.momentum * 100)}%, ` +
|
|
190
|
+
`velocity ${Math.round(weightedScore.weights.velocity * 100)}%. ` +
|
|
191
|
+
`Effective thresholds — momentum BUY/Watch: ${weightedScore.effectiveThresholds.momentumBuy}/${weightedScore.effectiveThresholds.momentumWatch}, ` +
|
|
192
|
+
`velocity BUY/Watch: ${weightedScore.effectiveThresholds.velocityBuy}/${weightedScore.effectiveThresholds.velocityWatch}.`;
|
|
193
|
+
}
|
|
175
194
|
return {
|
|
176
195
|
buyDecision: request.validation.buyDecision,
|
|
177
196
|
automationStatus: shouldAutoTrack ? 'Watching' : 'Paused',
|
|
@@ -181,5 +200,7 @@ export function buildValidationRecommendation(request, signals) {
|
|
|
181
200
|
latestAiRecommendation,
|
|
182
201
|
latestAiConfidence,
|
|
183
202
|
monitoringNotes,
|
|
203
|
+
// Weighted scoring metadata (used by downstream Airtable formulas)
|
|
204
|
+
weightedValidationScore: weightedScore ?? undefined,
|
|
184
205
|
};
|
|
185
206
|
}
|
|
@@ -429,6 +429,8 @@ export async function runValidation(api, input) {
|
|
|
429
429
|
chart,
|
|
430
430
|
research,
|
|
431
431
|
effectiveContext,
|
|
432
|
+
// Artist momentum score from Artist/Group table — passed via currentMetrics
|
|
433
|
+
momentumScore: effectiveRequest.validation.currentMetrics.artistMomentumScore ?? null,
|
|
432
434
|
});
|
|
433
435
|
const requestQueryResolution = buildProviderQueryResolutionDebug(effectiveRequest, Boolean(ebay.queryResolution?.queryContextUsed));
|
|
434
436
|
const mergedSignals = { effectiveContext, ebay, sold, terapeak, social, chart, research };
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Threshold Calibration — Age-Weighted Scoring
|
|
3
|
+
*
|
|
4
|
+
* Problem: Current validation thresholds (75/60/45) treat all items the same,
|
|
5
|
+
* regardless of whether they have eBay sales history or are brand new listings.
|
|
6
|
+
* New items (< 7 days tracked) get penalized because velocity data is missing,
|
|
7
|
+
* even when momentum signals are strong.
|
|
8
|
+
*
|
|
9
|
+
* Solution: Weighted scoring that adjusts momentum vs. velocity emphasis based
|
|
10
|
+
* on item age (days tracked):
|
|
11
|
+
* - New items (< 7 days): 70% momentum + 30% velocity
|
|
12
|
+
* - Established (>= 7 days): 30% momentum + 70% velocity
|
|
13
|
+
* - Smooth transition between 0-14 days
|
|
14
|
+
*
|
|
15
|
+
* Velocity thresholds by tier: 5 (buy) / 10 (watch) / 20 (skip)
|
|
16
|
+
* Momentum thresholds by tier: 75 (buy) / 60 (watch) / 45 (skip)
|
|
17
|
+
*/
|
|
18
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
19
|
+
const NEW_ITEM_THRESHOLD_DAYS = 7; // < 7 days = new item
|
|
20
|
+
const WEIGHT_TRANSITION_END = 14; // full transition completes at day 14
|
|
21
|
+
// Momentum thresholds (out of 100)
|
|
22
|
+
const MOMENTUM_THRESHOLDS = {
|
|
23
|
+
buy: 75,
|
|
24
|
+
watch: 60,
|
|
25
|
+
skip: 45,
|
|
26
|
+
};
|
|
27
|
+
// Velocity thresholds (total sold units across day1-day5)
|
|
28
|
+
const VELOCITY_THRESHOLDS = {
|
|
29
|
+
buy: 5,
|
|
30
|
+
watch: 10,
|
|
31
|
+
skip: 20,
|
|
32
|
+
};
|
|
33
|
+
// Default weights for new vs established
|
|
34
|
+
const NEW_ITEM_WEIGHTS = {
|
|
35
|
+
momentum: 0.7,
|
|
36
|
+
velocity: 0.3,
|
|
37
|
+
};
|
|
38
|
+
const ESTABLISHED_ITEM_WEIGHTS = {
|
|
39
|
+
momentum: 0.3,
|
|
40
|
+
velocity: 0.7,
|
|
41
|
+
};
|
|
42
|
+
// ── Core Functions ───────────────────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Calculate scoring weights based on item age (days tracked).
|
|
45
|
+
*
|
|
46
|
+
* Days 0-6: 70% momentum / 30% velocity (new item weights)
|
|
47
|
+
* Days 7-14: Linear transition from new → established weights
|
|
48
|
+
* Days 14+: 30% momentum / 70% velocity (established item weights)
|
|
49
|
+
*/
|
|
50
|
+
export function calculateAgeAwareWeights(daysTracked) {
|
|
51
|
+
if (daysTracked === null || daysTracked < 0) {
|
|
52
|
+
return {
|
|
53
|
+
weights: { ...NEW_ITEM_WEIGHTS },
|
|
54
|
+
isNewItem: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (daysTracked >= WEIGHT_TRANSITION_END) {
|
|
58
|
+
return {
|
|
59
|
+
weights: { ...ESTABLISHED_ITEM_WEIGHTS },
|
|
60
|
+
isNewItem: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (daysTracked < NEW_ITEM_THRESHOLD_DAYS) {
|
|
64
|
+
return {
|
|
65
|
+
weights: { ...NEW_ITEM_WEIGHTS },
|
|
66
|
+
isNewItem: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// Linear interpolation between NEW_ITEM_THRESHOLD_DAYS and WEIGHT_TRANSITION_END
|
|
70
|
+
const progress = (daysTracked - NEW_ITEM_THRESHOLD_DAYS) / (WEIGHT_TRANSITION_END - NEW_ITEM_THRESHOLD_DAYS);
|
|
71
|
+
const momentumWeight = NEW_ITEM_WEIGHTS.momentum +
|
|
72
|
+
(ESTABLISHED_ITEM_WEIGHTS.momentum - NEW_ITEM_WEIGHTS.momentum) * progress;
|
|
73
|
+
const velocityWeight = 1 - momentumWeight;
|
|
74
|
+
return {
|
|
75
|
+
weights: {
|
|
76
|
+
momentum: Math.round(momentumWeight * 100) / 100,
|
|
77
|
+
velocity: Math.round(velocityWeight * 100) / 100,
|
|
78
|
+
},
|
|
79
|
+
isNewItem: daysTracked < NEW_ITEM_THRESHOLD_DAYS,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Calculate effective thresholds adjusted by the current weights.
|
|
84
|
+
*
|
|
85
|
+
* When momentum weight is high (new items), the momentum threshold effectively
|
|
86
|
+
* matters more, so we lower the velocity bar proportionally, and vice versa.
|
|
87
|
+
*
|
|
88
|
+
* The formula scales thresholds by the inverse of their weight:
|
|
89
|
+
* effective_momentum_threshold = momentum_threshold * (1 - momentum_weight + momentum_weight * 0.5)
|
|
90
|
+
* effective_velocity_threshold = velocity_threshold * (1 - velocity_weight + velocity_weight * 0.5)
|
|
91
|
+
*
|
|
92
|
+
* This gives ~25% leeway on the dominant signal while keeping the threshold meaningful.
|
|
93
|
+
*/
|
|
94
|
+
export function calculateEffectiveThresholds(weights) {
|
|
95
|
+
const momentumScale = 1 - weights.momentum + weights.momentum * 0.5;
|
|
96
|
+
const velocityScale = 1 - weights.velocity + weights.velocity * 0.5;
|
|
97
|
+
return {
|
|
98
|
+
momentumBuy: Math.round(MOMENTUM_THRESHOLDS.buy * momentumScale * 10) / 10,
|
|
99
|
+
momentumWatch: Math.round(MOMENTUM_THRESHOLDS.watch * momentumScale * 10) / 10,
|
|
100
|
+
velocityBuy: Math.round(VELOCITY_THRESHOLDS.buy * velocityScale * 10) / 10,
|
|
101
|
+
velocityWatch: Math.round(VELOCITY_THRESHOLDS.watch * velocityScale * 10) / 10,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Classify a momentum score against effective thresholds.
|
|
106
|
+
*/
|
|
107
|
+
function classifyMomentum(score, thresholds) {
|
|
108
|
+
if (score === null)
|
|
109
|
+
return 'SKIP';
|
|
110
|
+
if (score >= thresholds.momentumBuy)
|
|
111
|
+
return 'BUY';
|
|
112
|
+
if (score >= thresholds.momentumWatch)
|
|
113
|
+
return 'WATCH';
|
|
114
|
+
return 'SKIP';
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Classify velocity (total sold units) against effective thresholds.
|
|
118
|
+
*/
|
|
119
|
+
function classifyVelocity(score, thresholds) {
|
|
120
|
+
if (score === null)
|
|
121
|
+
return 'WATCH'; // No velocity data = neutral, not automatic skip
|
|
122
|
+
if (score >= thresholds.velocityBuy)
|
|
123
|
+
return 'BUY';
|
|
124
|
+
if (score >= thresholds.velocityWatch)
|
|
125
|
+
return 'WATCH';
|
|
126
|
+
return 'SKIP';
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Extract momentum score from request validation context.
|
|
130
|
+
* Uses artist momentum score from the request's currentMetrics or item data.
|
|
131
|
+
*/
|
|
132
|
+
function extractMomentumScore(request) {
|
|
133
|
+
// Check if currentMetrics has a momentum field (may be added by Airtable formula)
|
|
134
|
+
const _metrics = request.validation.currentMetrics;
|
|
135
|
+
// Check artist-tier or momentum from the request
|
|
136
|
+
// The momentum score comes from Artist/Group table, not directly in currentMetrics
|
|
137
|
+
// It would be available as part of the artistTier context
|
|
138
|
+
return null; // Fallback — populated by caller if available
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Calculate total velocity (sold units) from current metrics.
|
|
142
|
+
*/
|
|
143
|
+
function extractVelocityScore(request) {
|
|
144
|
+
const m = request.validation.currentMetrics;
|
|
145
|
+
const day1 = m.day1Sold ?? 0;
|
|
146
|
+
const day2 = m.day2Sold ?? 0;
|
|
147
|
+
const day3 = m.day3Sold ?? 0;
|
|
148
|
+
const day4 = m.day4Sold ?? 0;
|
|
149
|
+
const day5 = m.day5Sold ?? 0;
|
|
150
|
+
const total = day1 + day2 + day3 + day4 + day5;
|
|
151
|
+
return total > 0 ? total : null;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Map classification to a 0-100 composite score.
|
|
155
|
+
*/
|
|
156
|
+
function classificationToScore(classification) {
|
|
157
|
+
switch (classification) {
|
|
158
|
+
case 'BUY':
|
|
159
|
+
return 80;
|
|
160
|
+
case 'WATCH':
|
|
161
|
+
return 50;
|
|
162
|
+
case 'SKIP':
|
|
163
|
+
return 20;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Main weighted scoring function.
|
|
168
|
+
*
|
|
169
|
+
* Takes the validation request, optional external momentum score, and computes
|
|
170
|
+
* an age-aware weighted composite score with buy/watch/skip classification.
|
|
171
|
+
*/
|
|
172
|
+
export function computeWeightedValidationScore(request, momentumScoreOverride) {
|
|
173
|
+
const daysTracked = request.validation.currentMetrics.daysTracked;
|
|
174
|
+
const { weights, isNewItem } = calculateAgeAwareWeights(daysTracked);
|
|
175
|
+
const thresholds = calculateEffectiveThresholds(weights);
|
|
176
|
+
const momentumScore = momentumScoreOverride ?? extractMomentumScore(request);
|
|
177
|
+
const velocityScore = extractVelocityScore(request);
|
|
178
|
+
const momentumClass = classifyMomentum(momentumScore, thresholds);
|
|
179
|
+
const velocityClass = classifyVelocity(velocityScore, thresholds);
|
|
180
|
+
// Composite: weighted combination of component scores
|
|
181
|
+
const momentumComponent = classificationToScore(momentumClass);
|
|
182
|
+
const velocityComponent = classificationToScore(velocityClass);
|
|
183
|
+
const compositeScore = Math.round(momentumComponent * weights.momentum + velocityComponent * weights.velocity);
|
|
184
|
+
// Final classification: most restrictive (unless either is BUY)
|
|
185
|
+
// If either component says BUY, composite is at least WATCH
|
|
186
|
+
// If both say BUY, composite is BUY
|
|
187
|
+
let classification;
|
|
188
|
+
if (momentumClass === 'BUY' && velocityClass === 'BUY') {
|
|
189
|
+
classification = 'BUY';
|
|
190
|
+
}
|
|
191
|
+
else if (momentumClass === 'BUY' || velocityClass === 'BUY') {
|
|
192
|
+
classification = 'WATCH'; // One strong signal + one neutral = watch
|
|
193
|
+
}
|
|
194
|
+
else if (momentumClass === 'SKIP' && velocityClass === 'SKIP') {
|
|
195
|
+
classification = 'SKIP';
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
classification = 'WATCH'; // Mixed signals = watch
|
|
199
|
+
}
|
|
200
|
+
// Override: if composite score is high enough, upgrade to BUY
|
|
201
|
+
if (compositeScore >= 70 && classification === 'WATCH') {
|
|
202
|
+
classification = 'BUY';
|
|
203
|
+
}
|
|
204
|
+
// Build reasoning
|
|
205
|
+
const reasoning = [];
|
|
206
|
+
reasoning.push(`Item age: ${daysTracked !== null ? `${daysTracked} days` : 'unknown'}, ` +
|
|
207
|
+
`type: ${isNewItem ? 'new' : 'established'}`);
|
|
208
|
+
reasoning.push(`Weights: momentum ${Math.round(weights.momentum * 100)}%, ` +
|
|
209
|
+
`velocity ${Math.round(weights.velocity * 100)}%`);
|
|
210
|
+
reasoning.push(`Effective thresholds — momentum BUY/Watch: ${thresholds.momentumBuy}/${thresholds.momentumWatch}, ` +
|
|
211
|
+
`velocity BUY/Watch: ${thresholds.velocityBuy}/${thresholds.velocityWatch}`);
|
|
212
|
+
if (momentumScore !== null) {
|
|
213
|
+
reasoning.push(`Momentum: ${momentumScore}/100 → ${momentumClass}`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
reasoning.push('Momentum: no data → treated as SKIP (neutral for new items)');
|
|
217
|
+
}
|
|
218
|
+
if (velocityScore !== null) {
|
|
219
|
+
reasoning.push(`Velocity: ${velocityScore} units sold (day1-day5) → ${velocityClass}`);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
reasoning.push('Velocity: no sales data → treated as neutral (WATCH)');
|
|
223
|
+
}
|
|
224
|
+
reasoning.push(`Composite score: ${compositeScore}/100 → ${classification}`);
|
|
225
|
+
return {
|
|
226
|
+
compositeScore,
|
|
227
|
+
momentumScore,
|
|
228
|
+
velocityScore,
|
|
229
|
+
classification,
|
|
230
|
+
momentumClassification: momentumClass,
|
|
231
|
+
velocityClassification: velocityClass,
|
|
232
|
+
weights,
|
|
233
|
+
daysTracked,
|
|
234
|
+
isNewItem,
|
|
235
|
+
effectiveThresholds: thresholds,
|
|
236
|
+
reasoning,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Convenience: check if the request qualifies for age-aware calibration.
|
|
241
|
+
*/
|
|
242
|
+
export function shouldApplyAgeAwareCalibration(request) {
|
|
243
|
+
// Apply when the item is in a watchable/tracking state
|
|
244
|
+
return (request.validation.autoCheckEnabled === true &&
|
|
245
|
+
request.validation.buyDecision === 'Watching' &&
|
|
246
|
+
request.validation.automationStatus === 'Watching');
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Compare old flat scoring vs new age-weighted scoring for analysis.
|
|
250
|
+
*/
|
|
251
|
+
export function calibrateAndCompare(itemLabel, daysTracked, momentumScore, velocityScore) {
|
|
252
|
+
// Old scoring: flat thresholds, 50/50 weight
|
|
253
|
+
const oldThresholds = {
|
|
254
|
+
momentumBuy: MOMENTUM_THRESHOLDS.buy,
|
|
255
|
+
momentumWatch: MOMENTUM_THRESHOLDS.watch,
|
|
256
|
+
velocityBuy: VELOCITY_THRESHOLDS.buy,
|
|
257
|
+
velocityWatch: VELOCITY_THRESHOLDS.watch,
|
|
258
|
+
};
|
|
259
|
+
const oldMomentumClass = classifyMomentum(momentumScore, oldThresholds);
|
|
260
|
+
const oldVelocityClass = classifyVelocity(velocityScore, oldThresholds);
|
|
261
|
+
const oldMomentumComp = classificationToScore(oldMomentumClass);
|
|
262
|
+
const oldVelocityComp = classificationToScore(oldVelocityClass);
|
|
263
|
+
const oldCompositeScore = Math.round((oldMomentumComp + oldVelocityComp) / 2);
|
|
264
|
+
let oldClassification;
|
|
265
|
+
if (oldMomentumClass === 'BUY' && oldVelocityClass === 'BUY')
|
|
266
|
+
oldClassification = 'BUY';
|
|
267
|
+
else if (oldMomentumClass === 'BUY' || oldVelocityClass === 'BUY')
|
|
268
|
+
oldClassification = 'WATCH';
|
|
269
|
+
else if (oldMomentumClass === 'SKIP' && oldVelocityClass === 'SKIP')
|
|
270
|
+
oldClassification = 'SKIP';
|
|
271
|
+
else
|
|
272
|
+
oldClassification = 'WATCH';
|
|
273
|
+
if (oldCompositeScore >= 70 && oldClassification === 'WATCH')
|
|
274
|
+
oldClassification = 'BUY';
|
|
275
|
+
// New scoring: age-weighted
|
|
276
|
+
const { weights, isNewItem } = calculateAgeAwareWeights(daysTracked);
|
|
277
|
+
const effectiveThresholds = calculateEffectiveThresholds(weights);
|
|
278
|
+
const newMomentumClass = classifyMomentum(momentumScore, effectiveThresholds);
|
|
279
|
+
const newVelocityClass = classifyVelocity(velocityScore, effectiveThresholds);
|
|
280
|
+
const newMomentumComp = classificationToScore(newMomentumClass);
|
|
281
|
+
const newVelocityComp = classificationToScore(newVelocityClass);
|
|
282
|
+
const newCompositeScore = Math.round(newMomentumComp * weights.momentum + newVelocityComp * weights.velocity);
|
|
283
|
+
let newClassification;
|
|
284
|
+
if (newMomentumClass === 'BUY' && newVelocityClass === 'BUY')
|
|
285
|
+
newClassification = 'BUY';
|
|
286
|
+
else if (newMomentumClass === 'BUY' || newVelocityClass === 'BUY')
|
|
287
|
+
newClassification = 'WATCH';
|
|
288
|
+
else if (newMomentumClass === 'SKIP' && newVelocityClass === 'SKIP')
|
|
289
|
+
newClassification = 'SKIP';
|
|
290
|
+
else
|
|
291
|
+
newClassification = 'WATCH';
|
|
292
|
+
if (newCompositeScore >= 70 && newClassification === 'WATCH')
|
|
293
|
+
newClassification = 'BUY';
|
|
294
|
+
return {
|
|
295
|
+
itemLabel,
|
|
296
|
+
daysTracked,
|
|
297
|
+
momentumScore,
|
|
298
|
+
velocityScore,
|
|
299
|
+
oldMomentumClass,
|
|
300
|
+
oldVelocityClass,
|
|
301
|
+
oldCompositeScore,
|
|
302
|
+
oldClassification,
|
|
303
|
+
newCompositeScore,
|
|
304
|
+
newClassification,
|
|
305
|
+
newWeights: weights,
|
|
306
|
+
isNewItem,
|
|
307
|
+
effectiveThresholds,
|
|
308
|
+
classificationChanged: oldClassification !== newClassification,
|
|
309
|
+
scoreDelta: newCompositeScore - oldCompositeScore,
|
|
310
|
+
};
|
|
311
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ebay-mcp-remote-edition",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"description": "Remote + Local MCP server for eBay APIs - provides access to eBay developer functionality through MCP (Model Context Protocol)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -69,21 +69,21 @@
|
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
71
71
|
"@upstash/redis": "^1.37.0",
|
|
72
|
-
"axios": "^1.
|
|
72
|
+
"axios": "^1.16.0",
|
|
73
73
|
"chalk": "^5.6.2",
|
|
74
74
|
"cors": "^2.8.6",
|
|
75
75
|
"dotenv-stringify": "^3.0.1",
|
|
76
76
|
"express": "^5.2.1",
|
|
77
|
-
"fast-xml-parser": "^5.
|
|
77
|
+
"fast-xml-parser": "^5.7.2",
|
|
78
78
|
"flatted": "^3.4.2",
|
|
79
79
|
"helmet": "^8.1.0",
|
|
80
|
-
"jose": "^6.2.
|
|
80
|
+
"jose": "^6.2.3",
|
|
81
81
|
"jsonwebtoken": "^9.0.3",
|
|
82
82
|
"playwright-core": "^1.59.1",
|
|
83
83
|
"prompts": "^2.4.2",
|
|
84
84
|
"update-notifier": "^7.3.1",
|
|
85
85
|
"winston": "^3.19.0",
|
|
86
|
-
"zod": "^4.3
|
|
86
|
+
"zod": "^4.4.3",
|
|
87
87
|
"zod-to-json-schema": "^3.25.2"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
@@ -91,27 +91,27 @@
|
|
|
91
91
|
"@types/cors": "^2.8.19",
|
|
92
92
|
"@types/express": "^5.0.6",
|
|
93
93
|
"@types/jsonwebtoken": "^9.0.10",
|
|
94
|
-
"@types/node": "^25.
|
|
94
|
+
"@types/node": "^25.6.0",
|
|
95
95
|
"@types/prompts": "^2.4.9",
|
|
96
96
|
"@types/supertest": "^7.2.0",
|
|
97
97
|
"@types/update-notifier": "^6.0.8",
|
|
98
|
-
"@vitest/coverage-v8": "^4.1.
|
|
99
|
-
"@vitest/eslint-plugin": "^1.6.
|
|
100
|
-
"@vitest/ui": "^4.1.
|
|
101
|
-
"eslint": "^10.
|
|
98
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
99
|
+
"@vitest/eslint-plugin": "^1.6.16",
|
|
100
|
+
"@vitest/ui": "^4.1.5",
|
|
101
|
+
"eslint": "^10.3.0",
|
|
102
102
|
"eslint-config-prettier": "^10.1.8",
|
|
103
103
|
"eslint-plugin-n": "^17.24.0",
|
|
104
|
-
"nock": "^14.0.
|
|
104
|
+
"nock": "^14.0.14",
|
|
105
105
|
"openapi-typescript": "^7.13.0",
|
|
106
|
-
"prettier": "^3.8.
|
|
106
|
+
"prettier": "^3.8.3",
|
|
107
107
|
"supertest": "^7.2.2",
|
|
108
|
-
"tsc-alias": "^1.8.
|
|
108
|
+
"tsc-alias": "^1.8.17",
|
|
109
109
|
"tsx": "^4.21.0",
|
|
110
110
|
"typescript": "^5.9.3",
|
|
111
|
-
"typescript-eslint": "^8.
|
|
112
|
-
"vitest": "^4.1.
|
|
111
|
+
"typescript-eslint": "^8.59.1",
|
|
112
|
+
"vitest": "^4.1.5"
|
|
113
113
|
},
|
|
114
|
-
"packageManager": "pnpm@10.33.
|
|
114
|
+
"packageManager": "pnpm@10.33.2",
|
|
115
115
|
"engines": {
|
|
116
116
|
"node": ">=18.0.0"
|
|
117
117
|
},
|