ebay-mcp-remote-edition 3.2.0 → 3.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.
@@ -1,31 +1,11 @@
1
+ import axios from 'axios';
1
2
  import { getBaseUrl } from '../../config/environment.js';
3
+ import { buildResolvedBrowseQueryPlan } from './query-utils.js';
2
4
  function round(value) {
3
5
  return Math.round(value * 100) / 100;
4
6
  }
5
- function buildQueryTerms(request) {
6
- const terms = new Set();
7
- const addTerms = (values, maxTerms = values.length) => {
8
- for (const value of values) {
9
- if (!value)
10
- continue;
11
- const normalized = value.trim();
12
- if (!normalized)
13
- continue;
14
- terms.add(normalized);
15
- if (terms.size >= maxTerms) {
16
- break;
17
- }
18
- }
19
- };
20
- addTerms([request.item.name], 1);
21
- addTerms(request.item.canonicalArtists, 3);
22
- addTerms(request.item.relatedAlbums, 5);
23
- addTerms(request.item.variation, 7);
24
- addTerms([request.validation.validationType], 8);
25
- return Array.from(terms);
26
- }
27
- export function buildEbayValidationQuery(request) {
28
- return buildQueryTerms(request).join(' ').replace(/\s+/g, ' ').trim();
7
+ export function buildEbayValidationQueries(request) {
8
+ return buildResolvedBrowseQueryPlan(request).queryPlan.map((candidate) => candidate.query);
29
9
  }
30
10
  function deriveTrend(current, previous, fallback) {
31
11
  if (current === null || previous === null || previous <= 0) {
@@ -48,8 +28,31 @@ function median(values) {
48
28
  }
49
29
  return sorted[middle];
50
30
  }
31
+ function getAxiosFailureDebug(error) {
32
+ if (!axios.isAxiosError(error)) {
33
+ return {
34
+ responseStatus: null,
35
+ responseBodyExcerpt: null,
36
+ };
37
+ }
38
+ const responseStatus = error.response?.status ?? null;
39
+ const rawBody = error.response?.data;
40
+ if (rawBody === undefined) {
41
+ return {
42
+ responseStatus,
43
+ responseBodyExcerpt: null,
44
+ };
45
+ }
46
+ const bodyText = typeof rawBody === 'string' ? rawBody : JSON.stringify(rawBody, null, 2);
47
+ return {
48
+ responseStatus,
49
+ responseBodyExcerpt: bodyText.slice(0, 500),
50
+ };
51
+ }
51
52
  export async function getEbayValidationSignals(api, request) {
52
- const ebayQuery = buildEbayValidationQuery(request);
53
+ const { queryPlan, queryResolution } = buildResolvedBrowseQueryPlan(request);
54
+ const queryCandidates = queryPlan.map((candidate) => candidate.query);
55
+ const ebayQuery = queryCandidates[0] ?? '';
53
56
  const fallbackTrend = request.validation.currentMetrics.marketPriceTrend || 'Stable';
54
57
  const emptyResult = {
55
58
  avgWatchersPerListing: null,
@@ -59,6 +62,13 @@ export async function getEbayValidationSignals(api, request) {
59
62
  competitionLevel: null,
60
63
  marketPriceTrend: fallbackTrend,
61
64
  ebayQuery,
65
+ queryCandidates,
66
+ selectedQuery: ebayQuery || undefined,
67
+ selectedQueryTier: ebayQuery ? 1 : null,
68
+ queryDiagnostics: [],
69
+ selectionReason: queryCandidates.length
70
+ ? 'Cleaned browse candidates were generated, but none have been evaluated yet.'
71
+ : 'No valid browse query candidates were generated after sanitization and semantic filtering.',
62
72
  sampleSize: 0,
63
73
  soldVelocity: {
64
74
  day1Sold: request.validation.currentMetrics.day1Sold,
@@ -68,45 +78,93 @@ export async function getEbayValidationSignals(api, request) {
68
78
  day5Sold: request.validation.currentMetrics.day5Sold,
69
79
  daysTracked: request.validation.currentMetrics.daysTracked,
70
80
  },
81
+ queryResolution,
71
82
  };
72
- if (!ebayQuery) {
83
+ if (queryCandidates.length === 0) {
73
84
  return emptyResult;
74
85
  }
75
86
  try {
76
87
  const environment = api.getAuthClient().getConfig().environment;
77
- const browseUrl = `${getBaseUrl(environment)}/buy/browse/v1/item_summary/search`;
78
- const response = await api.getAuthClient().getWithFullUrl(browseUrl, {
79
- q: ebayQuery,
80
- limit: 25,
81
- sort: 'newlyListed',
82
- });
83
- const itemSummaries = response.itemSummaries ?? [];
84
- const prices = itemSummaries
85
- .map((item) => Number(item.price?.value ?? Number.NaN))
86
- .filter((value) => Number.isFinite(value) && value > 0);
87
- const shipping = itemSummaries
88
- .map((item) => Number(item.shippingOptions?.[0]?.shippingCost?.value ?? Number.NaN))
89
- .filter((value) => Number.isFinite(value) && value >= 0);
90
- const marketPriceUsd = median(prices);
91
- const avgShippingCostUsd = shipping.length > 0
92
- ? shipping.reduce((sum, value) => sum + value, 0) / shipping.length
93
- : null;
94
- const totalListings = typeof response.total === 'number' && Number.isFinite(response.total)
95
- ? response.total
96
- : itemSummaries.length;
88
+ const browseUrl = new URL('/buy/browse/v1/item_summary/search', getBaseUrl(environment)).toString();
89
+ let selectedResult = emptyResult;
90
+ let bestScore = -1;
91
+ const queryDiagnostics = [];
92
+ for (const [index, query] of queryCandidates.entries()) {
93
+ const response = await api.getAuthClient().getWithFullUrl(browseUrl, {
94
+ q: query,
95
+ limit: 25,
96
+ sort: 'newlyListed',
97
+ });
98
+ const itemSummaries = response.itemSummaries ?? [];
99
+ const prices = itemSummaries
100
+ .map((item) => Number(item.price?.value ?? Number.NaN))
101
+ .filter((value) => Number.isFinite(value) && value > 0);
102
+ const shipping = itemSummaries
103
+ .map((item) => Number(item.shippingOptions?.[0]?.shippingCost?.value ?? Number.NaN))
104
+ .filter((value) => Number.isFinite(value) && value >= 0);
105
+ const marketPriceUsd = median(prices);
106
+ const avgShippingCostUsd = shipping.length > 0
107
+ ? shipping.reduce((sum, value) => sum + value, 0) / shipping.length
108
+ : null;
109
+ const totalListings = typeof response.total === 'number' && Number.isFinite(response.total)
110
+ ? response.total
111
+ : itemSummaries.length;
112
+ const attemptScore = Math.max(itemSummaries.length, totalListings);
113
+ queryDiagnostics.push({
114
+ query,
115
+ tier: index + 1,
116
+ family: queryPlan[index]?.family,
117
+ itemSummaryCount: itemSummaries.length,
118
+ totalListings,
119
+ });
120
+ if (attemptScore <= bestScore) {
121
+ if (itemSummaries.length >= 5 || totalListings >= 5) {
122
+ break;
123
+ }
124
+ continue;
125
+ }
126
+ bestScore = attemptScore;
127
+ selectedResult = {
128
+ avgWatchersPerListing: null,
129
+ preOrderListingsCount: totalListings,
130
+ marketPriceUsd: marketPriceUsd === null ? null : round(marketPriceUsd),
131
+ avgShippingCostUsd: avgShippingCostUsd === null ? null : round(avgShippingCostUsd),
132
+ competitionLevel: totalListings,
133
+ marketPriceTrend: deriveTrend(marketPriceUsd, request.validation.currentMetrics.marketPriceUsd, fallbackTrend),
134
+ ebayQuery: query,
135
+ queryCandidates,
136
+ selectedQuery: query,
137
+ selectedQueryTier: index + 1,
138
+ queryDiagnostics: [...queryDiagnostics],
139
+ selectionReason: itemSummaries.length >= 5 || totalListings >= 5
140
+ ? 'Selected because this cleaned browse candidate produced sufficient listing depth.'
141
+ : attemptScore > 0
142
+ ? 'Selected as the strongest cleaned browse fallback after higher-priority candidates returned weaker results.'
143
+ : 'All cleaned browse candidates seen so far returned zero results; keeping the highest-priority candidate for traceability.',
144
+ sampleSize: itemSummaries.length,
145
+ soldVelocity: emptyResult.soldVelocity,
146
+ queryResolution,
147
+ };
148
+ if (itemSummaries.length >= 5 || totalListings >= 5) {
149
+ break;
150
+ }
151
+ }
97
152
  return {
98
- avgWatchersPerListing: null,
99
- preOrderListingsCount: totalListings,
100
- marketPriceUsd: marketPriceUsd === null ? null : round(marketPriceUsd),
101
- avgShippingCostUsd: avgShippingCostUsd === null ? null : round(avgShippingCostUsd),
102
- competitionLevel: totalListings,
103
- marketPriceTrend: deriveTrend(marketPriceUsd, request.validation.currentMetrics.marketPriceUsd, fallbackTrend),
104
- ebayQuery,
105
- sampleSize: itemSummaries.length,
106
- soldVelocity: emptyResult.soldVelocity,
153
+ ...selectedResult,
154
+ queryDiagnostics,
155
+ selectionReason: selectedResult.selectionReason ??
156
+ 'Browse selection completed without a stronger candidate than the highest-priority cleaned query.',
107
157
  };
108
158
  }
109
- catch {
110
- return emptyResult;
159
+ catch (error) {
160
+ const failureDebug = getAxiosFailureDebug(error);
161
+ return {
162
+ ...emptyResult,
163
+ selectionReason: 'Browse query execution failed before result selection could complete; returning the cleaned fallback candidate set for debug traceability.',
164
+ errorMessage: error instanceof Error ? error.message : String(error),
165
+ responseStatus: failureDebug.responseStatus,
166
+ responseBodyExcerpt: failureDebug.responseBodyExcerpt,
167
+ queryResolution,
168
+ };
111
169
  }
112
170
  }