ebay-mcp-remote-edition 4.3.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- ...(price.currencyID && { '@_currencyID': price.currencyID }),
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] = {
@@ -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 all offers
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 !== undefined) {
369
- if (typeof sku !== 'string') {
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');
@@ -1,12 +1,13 @@
1
- import { getBaseUrl } from '../../config/environment.js';
2
1
  import axios from 'axios';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
3
4
  /**
4
5
  * Commerce Media API (v1_beta) - Upload and manage images via eBay Picture Services
5
6
  * Based on: https://developer.ebay.com/api-docs/commerce/media/resources/image/from_url/methods
6
7
  */
7
8
  export class MediaApi {
8
9
  client;
9
- basePath = '/commerce/media/v1';
10
+ basePath = '/commerce/media/v1_beta';
10
11
  constructor(client) {
11
12
  this.client = client;
12
13
  }
@@ -15,14 +16,18 @@ export class MediaApi {
15
16
  }
16
17
  getMediaBaseUrl() {
17
18
  const env = this.client.getConfig().environment;
18
- return getBaseUrl(env);
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
- * 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
24
+ * Flow:
25
+ * 1. Download image from URL
26
+ * 2. Validate dimensions (min 500px, max 4800px)
27
+ * 3. Resize if needed (maintain aspect ratio)
28
+ * 4. Optimize and convert to JPEG
29
+ * 5. Upload to eBay via multipart/form-data
30
+ * 6. Return hosted URL
26
31
  *
27
32
  * Supported formats: JPG, GIF, PNG, BMP, TIFF, AVIF, HEIC, WEBP
28
33
  * Max file size: 10MB per image
@@ -38,15 +43,89 @@ export class MediaApi {
38
43
  const token = await this.getAccessToken();
39
44
  const baseUrl = this.getMediaBaseUrl();
40
45
  try {
41
- // Step 1: Create image from URL
42
- const body = { imageUrl };
46
+ // Try direct createImageFromUrl endpoint first (eBay downloads server-side)
47
+ // This avoids the need to download+process+upload ourselves
48
+ const requestBody = { imageUrl };
43
49
  if (description) {
44
- Object.assign(body, { description });
50
+ requestBody.description = description;
45
51
  }
46
- const createResponse = await axios.post(`${baseUrl}${this.basePath}/image/from_url`, body, {
52
+ const response = await axios.post(`${baseUrl}${this.basePath}/image/create_image_from_url`, requestBody, {
47
53
  headers: {
48
54
  Authorization: `Bearer ${token}`,
49
55
  'Content-Type': 'application/json',
56
+ Accept: 'application/json',
57
+ },
58
+ timeout: 30000,
59
+ });
60
+ const data = response.data;
61
+ const imageId = data.id;
62
+ if (imageId) {
63
+ return await this.getImage(imageId);
64
+ }
65
+ // Fallback: if no image ID returned, try download+upload method
66
+ throw new Error('No image ID returned from createImageFromUrl endpoint');
67
+ }
68
+ catch (error) {
69
+ if (axios.isAxiosError(error)) {
70
+ const status = error.response?.status;
71
+ const data = error.response?.data;
72
+ const message = typeof data === 'object' && data !== null && 'errors' in data
73
+ ? data.errors?.[0]?.longMessage ||
74
+ data.errors?.[0]?.message ||
75
+ error.message
76
+ : error.message;
77
+ throw new Error(`Failed to upload image from URL (status ${status}): ${message}`, {
78
+ cause: error,
79
+ });
80
+ }
81
+ throw new Error(`Failed to upload image from URL: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
82
+ }
83
+ }
84
+ /**
85
+ * Upload an image from a local file to eBay Picture Services.
86
+ *
87
+ * Endpoint: POST /commerce/media/v1/image/create_image_from_file
88
+ * Content-Type: multipart/form-data
89
+ *
90
+ * Supported formats: JPG, GIF, PNG, BMP, TIFF, AVIF, HEIC, WEBP
91
+ * Max file size: 10MB per image
92
+ *
93
+ * @param filePath - Local file path of the image to upload
94
+ * @param description - Optional description for the image
95
+ * @returns Object with image ID and eBay-hosted image URL
96
+ */
97
+ async createImageFromFile(filePath, description) {
98
+ if (!filePath || typeof filePath !== 'string') {
99
+ throw new Error('filePath is required and must be a string');
100
+ }
101
+ if (!fs.existsSync(filePath)) {
102
+ throw new Error(`File not found: ${filePath}`);
103
+ }
104
+ const token = await this.getAccessToken();
105
+ const baseUrl = this.getMediaBaseUrl();
106
+ try {
107
+ const fileBuffer = fs.readFileSync(filePath);
108
+ // Build multipart/form-data body manually
109
+ const boundary = `----FormBoundary${Date.now()}`;
110
+ const fileName = path.basename(filePath);
111
+ // Build the multipart body
112
+ let body = '';
113
+ // File part
114
+ body += `--${boundary}\r\n`;
115
+ body += `Content-Disposition: form-data; name="imageFile"; filename="${fileName}"\r\n`;
116
+ body += `Content-Type: application/octet-stream\r\n\r\n`;
117
+ // Description part (optional)
118
+ if (description) {
119
+ body += `--${boundary}\r\n`;
120
+ body += `Content-Disposition: form-data; name="description"\r\n\r\n`;
121
+ body += `${description}\r\n`;
122
+ }
123
+ body += `--${boundary}--\r\n`;
124
+ const multipartBody = Buffer.concat([Buffer.from(body, 'utf-8'), Buffer.from(fileBuffer)]);
125
+ const createResponse = await axios.post(`${baseUrl}${this.basePath}/image/create_image_from_file`, multipartBody, {
126
+ headers: {
127
+ Authorization: `Bearer ${token}`,
128
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
50
129
  Prefer: 'return=representation',
51
130
  },
52
131
  timeout: 30000,
@@ -59,7 +138,7 @@ export class MediaApi {
59
138
  if (!imageId) {
60
139
  throw new Error('No image ID returned from create endpoint');
61
140
  }
62
- // Step 2: Fetch image details to get the eBay-hosted URL
141
+ // Fetch image details to get the eBay-hosted URL
63
142
  return await this.getImage(imageId);
64
143
  }
65
144
  catch (error) {
@@ -71,12 +150,53 @@ export class MediaApi {
71
150
  data.errors?.[0]?.message ||
72
151
  error.message
73
152
  : error.message;
74
- throw new Error(`Failed to upload image from URL (status ${status}): ${message}`, {
153
+ throw new Error(`Failed to upload image from file (status ${status}): ${message}`, {
75
154
  cause: error,
76
155
  });
77
156
  }
78
- throw new Error(`Failed to upload image from URL: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
157
+ throw new Error(`Failed to upload image from file: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
158
+ }
159
+ }
160
+ /**
161
+ * Upload a processed image buffer to eBay Picture Services.
162
+ *
163
+ * @param buffer - Processed image buffer
164
+ * @param metadata - Image metadata
165
+ * @param token - OAuth access token
166
+ * @param baseUrl - Media API base URL
167
+ * @param description - Optional description
168
+ * @returns Object with image ID and eBay-hosted image URL
169
+ */
170
+ async uploadProcessedImage(buffer, metadata, token, baseUrl, description) {
171
+ // Build multipart/form-data body
172
+ const boundary = `----FormBoundary${Date.now()}`;
173
+ const fileName = `image_${Date.now()}.jpg`;
174
+ let body = `--${boundary}\r\n`;
175
+ body += `Content-Disposition: form-data; name="imageFile"; filename="${fileName}"\r\n`;
176
+ body += `Content-Type: image/jpeg\r\n\r\n`;
177
+ if (description) {
178
+ body += `--${boundary}\r\n`;
179
+ body += `Content-Disposition: form-data; name="description"\r\n\r\n`;
180
+ body += `${description}\r\n`;
181
+ }
182
+ body += `--${boundary}--\r\n`;
183
+ const multipartBody = Buffer.concat([Buffer.from(body, 'utf-8'), buffer]);
184
+ const createResponse = await axios.post(`${baseUrl}${this.basePath}/image/create_image_from_file`, multipartBody, {
185
+ headers: {
186
+ Authorization: `Bearer ${token}`,
187
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
188
+ Prefer: 'return=representation',
189
+ },
190
+ timeout: 30000,
191
+ });
192
+ const responseData = createResponse.data;
193
+ const imageId = typeof responseData.id === 'string'
194
+ ? responseData.id
195
+ : createResponse.headers.location?.split('/').pop();
196
+ if (!imageId) {
197
+ throw new Error('No image ID returned from create endpoint');
79
198
  }
199
+ return await this.getImage(imageId);
80
200
  }
81
201
  /**
82
202
  * Get image details including the eBay-hosted URL.
@@ -3,6 +3,14 @@ export class TradingApi {
3
3
  constructor(client) {
4
4
  this.client = client;
5
5
  }
6
+ /**
7
+ * Transform an item for Trading API XML serialization.
8
+ * Ensures required fields like Currency and Country are always present.
9
+ * Used by publish_offer flow to pre-transform inventory items before publishing.
10
+ */
11
+ transformItemForXML(item) {
12
+ return this.client.transformItemForXML(item);
13
+ }
6
14
  async getActiveListings(page = 1, entriesPerPage = 50) {
7
15
  const result = await this.client.execute('GetMyeBaySelling', {
8
16
  ActiveList: {
@@ -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
- if (!this.config.redirectUri) {
132
- throw new Error('Redirect URI is required for authorization code exchange');
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: this.config.redirectUri,
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.EBAY_PRODUCTION_RUNAME ||
273
- process.env.EBAY_PRODUCTION_REDIRECT_URI ||
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,
@@ -745,7 +745,7 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
745
745
  serverLogger.info(`[${prefix || 'root'}/authorize] Credential check`, {
746
746
  environment,
747
747
  envSource,
748
- ruName: ebayConfig.redirectUri,
748
+ ruName: ebayConfig.ruName || ebayConfig.redirectUri,
749
749
  ruNameDetectedEnv: credCheck.detectedEnv,
750
750
  credentialValid: credCheck.valid,
751
751
  });
@@ -770,7 +770,7 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
770
770
  serverLogger.info(`[${prefix || 'root'}/authorize] Redirecting to eBay OAuth`, {
771
771
  state: stateRecord.state,
772
772
  environment,
773
- ruName: ebayConfig.redirectUri,
773
+ ruName: ebayConfig.ruName || ebayConfig.redirectUri,
774
774
  expectedCallbackUrl: getExpectedOAuthCallbackUrl(serverUrl),
775
775
  });
776
776
  res.redirect(oauthUrl);
@@ -874,7 +874,7 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
874
874
  serverLogger.info(`[${prefix || 'root'}/oauth/start] Redirecting to eBay OAuth`, {
875
875
  state: stateRecord.state,
876
876
  environment,
877
- ruName: ebayConfig.redirectUri,
877
+ ruName: ebayConfig.ruName || ebayConfig.redirectUri,
878
878
  expectedCallbackUrl: getExpectedOAuthCallbackUrl(serverUrl),
879
879
  returnTo,
880
880
  });
@@ -1132,7 +1132,7 @@ async function handleOAuthCallback(req, res, serverUrl) {
1132
1132
  environment,
1133
1133
  tokenBaseUrl: getBaseUrl(environment),
1134
1134
  clientIdPrefix: ebayConfig.clientId ? `${ebayConfig.clientId.slice(0, 12)}...` : '(missing)',
1135
- ruName: ebayConfig.redirectUri ?? '(missing)',
1135
+ ruName: (ebayConfig.ruName || ebayConfig.redirectUri) ?? '(missing)',
1136
1136
  });
1137
1137
  const userId = randomUUID();
1138
1138
  const api = await createUserScopedApi(userId, environment);
@@ -0,0 +1,83 @@
1
+ import { z } from 'zod';
2
+ export const browseTools = [
3
+ {
4
+ name: 'ebay_get_suggestions',
5
+ description: 'Get listing/product suggestions based on search query. Returns relevant item summaries with pricing and shipping info from the eBay Browse API.',
6
+ inputSchema: {
7
+ query: z.string().describe('Search query for product suggestions'),
8
+ marketplaceId: z.string().default('EBAY_US').describe('Marketplace ID (default: EBAY_US)'),
9
+ limit: z.number().default(20).describe('Number of suggestions to return (default: 20)'),
10
+ },
11
+ outputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ success: { type: 'boolean' },
15
+ data: { type: 'object' },
16
+ },
17
+ },
18
+ annotations: {
19
+ title: 'Get Suggestions',
20
+ readOnlyHint: true,
21
+ },
22
+ _meta: {
23
+ category: 'browse',
24
+ version: '1.0.0',
25
+ },
26
+ },
27
+ {
28
+ name: 'ebay_search_products',
29
+ description: 'Search the eBay product catalog by query. Supports category filtering, sorting, and price filtering. Uses the eBay Browse API.',
30
+ inputSchema: {
31
+ query: z.string().describe('Search query for products'),
32
+ marketplaceId: z.string().describe('Marketplace ID (e.g., EBAY_US)'),
33
+ categoryId: z.string().optional().describe('Filter by category ID'),
34
+ limit: z.number().default(20).describe('Results to return (1-200, default: 20)'),
35
+ sort: z
36
+ .enum(['bestMatch', 'newlyListed'])
37
+ .optional()
38
+ .describe('Sort order (default: bestMatch)'),
39
+ filter: z
40
+ .string()
41
+ .optional()
42
+ .describe('Filter expression (e.g., price:[300..800],priceCurrency:USD)'),
43
+ },
44
+ outputSchema: {
45
+ type: 'object',
46
+ properties: {
47
+ success: { type: 'boolean' },
48
+ data: { type: 'object' },
49
+ },
50
+ },
51
+ annotations: {
52
+ title: 'Search Products',
53
+ readOnlyHint: true,
54
+ },
55
+ _meta: {
56
+ category: 'browse',
57
+ version: '1.0.0',
58
+ },
59
+ },
60
+ {
61
+ name: 'ebay_get_item_specifics',
62
+ description: 'Get required and optional item specifics (aspects) for a given category. Use this to understand what fields are needed for listing in a category. This is an alias to ebay_get_item_aspects_for_category.',
63
+ inputSchema: {
64
+ categoryTreeId: z.string().describe('Category tree ID'),
65
+ categoryId: z.string().describe('Category ID'),
66
+ },
67
+ outputSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ success: { type: 'boolean' },
71
+ data: { type: 'object' },
72
+ },
73
+ },
74
+ annotations: {
75
+ title: 'Get Item Specifics',
76
+ readOnlyHint: true,
77
+ },
78
+ _meta: {
79
+ category: 'browse',
80
+ version: '1.0.0',
81
+ },
82
+ },
83
+ ];
@@ -18,12 +18,13 @@ import { marketingTools } from './marketing.js';
18
18
  import { analyticsTools } from './analytics.js';
19
19
  import { metadataTools } from './metadata.js';
20
20
  import { taxonomyTools } from './taxonomy.js';
21
+ import { browseTools } from './browse.js';
21
22
  import { communicationTools } from './communication.js';
22
23
  import { otherApiTools } from './other.js';
23
24
  import { developerTools } from './developer.js';
24
25
  import { tradingTools } from './trading.js';
25
26
  // Export individual categories
26
- export { tokenManagementTools, accountTools, inventoryTools, fulfillmentTools, marketingTools, analyticsTools, metadataTools, taxonomyTools, communicationTools, otherApiTools, developerTools, tradingTools, };
27
+ export { tokenManagementTools, accountTools, inventoryTools, fulfillmentTools, marketingTools, analyticsTools, metadataTools, taxonomyTools, browseTools, communicationTools, otherApiTools, developerTools, tradingTools, };
27
28
  // Export all tools as a single array
28
29
  export const allTools = [
29
30
  ...tokenManagementTools,
@@ -34,6 +35,7 @@ export const allTools = [
34
35
  ...analyticsTools,
35
36
  ...metadataTools,
36
37
  ...taxonomyTools,
38
+ ...browseTools,
37
39
  ...communicationTools,
38
40
  ...otherApiTools,
39
41
  ...developerTools,
@@ -71,11 +71,73 @@ export const inventoryTools = [
71
71
  properties: {},
72
72
  },
73
73
  },
74
+ {
75
+ name: 'ebay_update_inventory_item',
76
+ description: 'Update an existing inventory item by SKU. Supports partial updates — only provide the fields you want to change. Fetches existing data, merges your updates, then saves back. Use this for price changes, quantity updates, title edits, etc.\n\nIf the SKU does not exist, returns an error. Create it first with ebay_create_inventory_item.',
77
+ inputSchema: {
78
+ sku: z.string().describe('The seller-defined SKU to update (must already exist)'),
79
+ availability: z
80
+ .object({
81
+ shipToLocationAvailability: z
82
+ .object({
83
+ quantity: z.number().describe('New quantity'),
84
+ })
85
+ .optional(),
86
+ })
87
+ .optional()
88
+ .describe('Availability updates'),
89
+ product: z
90
+ .object({
91
+ title: z.string().optional().describe('New title (max 80 characters)'),
92
+ brand: z.string().optional().describe('Brand name'),
93
+ description: z.string().optional().describe('HTML description'),
94
+ imageUrls: z.array(z.string()).optional().describe('Array of image URLs'),
95
+ mpn: z.string().optional().describe('Manufacturer Part Number'),
96
+ aspects: z
97
+ .record(z.string(), z.array(z.string()))
98
+ .optional()
99
+ .describe('Item aspects/attributes (e.g., Color, Size)'),
100
+ })
101
+ .optional()
102
+ .describe('Product data updates'),
103
+ condition: z
104
+ .enum([
105
+ 'NEW',
106
+ 'LIKE_NEW',
107
+ 'NEW_OTHER',
108
+ 'NEW_WITH_DEFECTS',
109
+ 'USED_EXCELLENT',
110
+ 'USED_VERY_GOOD',
111
+ 'USED_GOOD',
112
+ 'USED_ACCEPTABLE',
113
+ 'FOR_PARTS_OR_NOT_WORKING',
114
+ ])
115
+ .optional()
116
+ .describe('Item condition'),
117
+ conditionDescription: z
118
+ .string()
119
+ .optional()
120
+ .describe('Condition description (for used items)'),
121
+ },
122
+ outputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ sku: { type: 'string' },
126
+ status: { type: 'string' },
127
+ },
128
+ },
129
+ annotations: {
130
+ title: 'Update Inventory Item',
131
+ readOnlyHint: false,
132
+ },
133
+ },
74
134
  {
75
135
  name: 'ebay_get_offers',
76
- description: 'Get all offers for the seller',
136
+ description: 'Get offers for a specific SKU. Requires a valid SKU — eBay API returns error 25707 without one.',
77
137
  inputSchema: {
78
- sku: z.string().optional().describe('Filter by SKU'),
138
+ sku: z
139
+ .string()
140
+ .describe('The seller-defined SKU (required — eBay API returns error 25707 without it)'),
79
141
  marketplaceId: z.nativeEnum(MarketplaceId).optional().describe('Filter by marketplace ID'),
80
142
  limit: z.number().optional().describe('Number of offers to return'),
81
143
  },