ebay-mcp 1.14.2 → 1.15.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.
@@ -55,7 +55,7 @@ const productIdentifierSchema = z.object({
55
55
  ktype: z.string().optional(),
56
56
  });
57
57
  const productSchema = z.object({
58
- title: z.string().optional(),
58
+ title: z.string().max(80).optional(),
59
59
  aspects: z.record(z.array(z.string())).optional(),
60
60
  brand: z.string().optional(),
61
61
  description: z.string().optional(),
@@ -244,7 +244,7 @@ export const offerResponseSchema = offerSchema.extend({
244
244
  */
245
245
  export const getOffersInputSchema = z.object({
246
246
  format: z.string().optional().describe('Filter offers by listing format'),
247
- sku: z.string().optional().describe('Filter offers by SKU'),
247
+ sku: z.string().min(1).describe('Seller-defined SKU whose offers to return'),
248
248
  marketplaceId: z.nativeEnum(MarketplaceId).optional().describe('Filter offers by marketplace'),
249
249
  limit: z.number().optional().describe('Number of offers to return'),
250
250
  offset: z.number().optional().describe('Number of offers to skip'),
@@ -263,6 +263,25 @@ export const getOffersOutputSchema = z.object({
263
263
  total: z.number().optional(),
264
264
  warnings: z.array(errorSchema).optional(),
265
265
  });
266
+ /** Validates the aggregate response from the bounded multi-SKU offer lookup. */
267
+ export const getOffersBySkusOutputSchema = z.object({
268
+ requestedSkuCount: z.number(),
269
+ uniqueSkuCount: z.number(),
270
+ successCount: z.number(),
271
+ failureCount: z.number(),
272
+ results: z.array(z.union([
273
+ z.object({
274
+ sku: z.string(),
275
+ status: z.enum(['success']),
276
+ response: getOffersOutputSchema,
277
+ }),
278
+ z.object({
279
+ sku: z.string(),
280
+ status: z.enum(['failure']),
281
+ error: z.object({ type: z.string(), message: z.string() }),
282
+ }),
283
+ ])),
284
+ });
266
285
  /**
267
286
  * Validates the Inventory Management API create offer request payload.
268
287
  */
@@ -3,11 +3,12 @@
3
3
  *
4
4
  * Usage:
5
5
  * npm run diagnose
6
- * npx ebay-mcp --diagnose
6
+ * ebay-mcp diagnose
7
+ * ebay-mcp diagnose --export
7
8
  */
8
9
  import chalk from 'chalk';
9
10
  import { writeFileSync } from 'fs';
10
- import { join, dirname } from 'path';
11
+ import { join, dirname, resolve } from 'path';
11
12
  import { fileURLToPath } from 'url';
12
13
  import { runSecurityChecks, displaySecurityResults } from '../scripts/securityChecker.js';
13
14
  import { validateSetup, displayRecommendations } from '../scripts/setupValidator.js';
@@ -235,7 +236,7 @@ async function generateDiagnosticReport(exportPath) {
235
236
  /**
236
237
  * Main diagnostics function
237
238
  */
238
- async function runDiagnostics(exportReport = false) {
239
+ export const runDiagnostics = async (exportReport = false) => {
239
240
  displayHeader();
240
241
  displaySystemInfo();
241
242
  await displayUpdateStatus();
@@ -295,16 +296,21 @@ async function runDiagnostics(exportReport = false) {
295
296
  console.log(chalk.bold.green('✅ Diagnostics complete!\n'));
296
297
  console.log(chalk.gray('For more help, visit:'));
297
298
  console.log(chalk.blue.underline(' https://github.com/YosefHayim/ebay-mcp#troubleshooting\n'));
299
+ };
300
+ // Direct script entry (`node build/scripts/diagnostics.js` / `npm run diagnose`).
301
+ // Guarded so importing from the bin does not auto-run diagnostics.
302
+ const entryPath = process.argv[1] ? resolve(process.argv[1]) : undefined;
303
+ const modulePath = resolve(fileURLToPath(import.meta.url));
304
+ if (entryPath && modulePath === entryPath) {
305
+ const cliArgs = process.argv.slice(2);
306
+ const exportReport = cliArgs.includes('--export') || cliArgs.includes('-e');
307
+ void Effect.runPromise(Effect.either(Effect.tryPromise({
308
+ try: () => runDiagnostics(exportReport),
309
+ catch: (error) => error,
310
+ }))).then((result) => {
311
+ if (Either.isLeft(result)) {
312
+ console.error(chalk.red('\n❌ Diagnostics failed:'), getErrorMessage(result.left));
313
+ process.exitCode = 1;
314
+ }
315
+ });
298
316
  }
299
- // CLI handler
300
- const args = process.argv.slice(2);
301
- const exportReport = args.includes('--export') || args.includes('-e');
302
- void Effect.runPromise(Effect.either(Effect.tryPromise({
303
- try: () => runDiagnostics(exportReport),
304
- catch: (error) => error,
305
- }))).then((result) => {
306
- if (Either.isLeft(result)) {
307
- console.error(chalk.red('\n❌ Diagnostics failed:'), getErrorMessage(result.left));
308
- process.exitCode = 1;
309
- }
310
- });
@@ -40,12 +40,10 @@ export const buildUsingDoc = (snapshot) => ({
40
40
  body: [
41
41
  '**Publish a fixed-price listing (REST Inventory model).**',
42
42
  '1. One-time account setup: `ebay_opt_in_to_program` (business-policy management), then `ebay_create_payment_policy`, `ebay_create_return_policy`, `ebay_create_fulfillment_policy` for reusable policy IDs, and `ebay_create_inventory_location` for a `merchantLocationKey`.',
43
- '2. `ebay_create_or_replace_inventory_item` (SKU + product details + availability).',
44
- '3. `ebay_create_offer` (SKU, marketplace, price, the policy IDs, location).',
45
- '4. `ebay_publish_offer` (offerId) → a live `listingId`. Use `ebay_bulk_publish_offer` for many at once.',
46
- '',
47
- '**Pick the right category & required item specifics first.**',
48
- '`ebay_get_default_category_tree_id` (marketplace) → `ebay_get_category_suggestions` (query) → `ebay_get_item_aspects_for_category` to learn the required aspects to put on the inventory item.',
43
+ '2. Pick the category and fetch its current item-specific requirements: `ebay_get_default_category_tree_id` (marketplace) → `ebay_get_category_suggestions` (query) → `ebay_get_item_aspects_for_category`.',
44
+ '3. Add every required aspect from that response to `ebay_create_or_replace_inventory_item` (SKU + product details + availability). Requirements vary by category and marketplace; do not treat examples from another category as permanent rules.',
45
+ '4. `ebay_create_offer` (SKU, marketplace, price, the policy IDs, location).',
46
+ '5. Check the aspects again when the category changes and before `ebay_get_listing_fees`, then `ebay_publish_offer` (offerId) → a live `listingId`. Use `ebay_bulk_publish_offer` for many at once.',
49
47
  '',
50
48
  '**Fulfill an order.**',
51
49
  '`ebay_get_orders` (filter unfulfilled) → `ebay_get_order` (detail) → `ebay_create_shipping_fulfillment` (tracking number + carrier).',
@@ -68,6 +66,7 @@ export const buildUsingDoc = (snapshot) => ({
68
66
  body: [
69
67
  '- **Two listing models.** REST Inventory (`inventory_item` → `offer` → `publish`) vs legacy Trading (XML). Pick one per SKU and stay in it.',
70
68
  '- **Offers need policies + a location first.** A "policy not found" error usually means the one-time account setup was skipped.',
69
+ '- **Item specifics are category-specific.** Use `ebay_get_item_aspects_for_category` for the selected category instead of relying on a fixed fallback value or requirements learned from another category.',
71
70
  '- **`search` and `fetch` are ChatGPT-connector only** — ignore them when driving the Sell API directly.',
72
71
  '- **Write actions are real.** `publish`, `issue_refund`, `end_listing`, and any `bulk_*` change live seller data — confirm intent before calling.',
73
72
  ].join('\n'),
@@ -63,7 +63,16 @@ export const accountEntries = [
63
63
  }),
64
64
  defineTool({
65
65
  name: 'ebay_create_fulfillment_policy',
66
- description: 'Create a new fulfillment policy.\n\nRequired OAuth Scope: sell.account\nMinimum Scope: https://api.ebay.com/oauth/api_scope/sell.account',
66
+ description: 'Create a new fulfillment policy.\n\n' +
67
+ 'shippingServiceCode must be a marketplace-valid eBay service enum (not a free-form carrier nickname). ' +
68
+ 'Invalid codes return "Please select a valid shipping service". eBay may cite DomesticItemShippingService[0] ' +
69
+ 'and [1] for a single bad entry — that is remote validation, not client array duplication. ' +
70
+ 'Discover valid codes via Trading GeteBayDetails (ShippingServiceDetails). ' +
71
+ 'Known-good EBAY_US FLAT_RATE sketch: policy.name, policy.marketplaceId=EBAY_US, ' +
72
+ 'policy.categoryTypes=[{name:ALL_EXCLUDING_MOTORS_VEHICLES}], policy.handlingTime={unit:DAY,value:1}, ' +
73
+ 'policy.shippingOptions=[{costType:FLAT_RATE,optionType:DOMESTIC,shippingServices:[{shippingServiceCode:USPSPriority,shippingCost:{currency:USD,value:"5.99"}}]}]. ' +
74
+ 'Avoid invented codes like USPSPriorityMail. Sandbox may accept a smaller code set than production.\n\n' +
75
+ 'Required OAuth Scope: sell.account\nMinimum Scope: https://api.ebay.com/oauth/api_scope/sell.account',
67
76
  inputSchema: createFulfillmentPolicyInputSchema.shape,
68
77
  outputSchema: zodToJsonSchema(createFulfillmentPolicyOutputSchema, {
69
78
  name: 'CreateFulfillmentPolicyResponse',
@@ -96,7 +105,7 @@ export const accountEntries = [
96
105
  }),
97
106
  defineTool({
98
107
  name: 'ebay_update_fulfillment_policy',
99
- description: 'Update an existing fulfillment policy',
108
+ description: 'Update an existing fulfillment policy.\n\nRequired OAuth Scope: sell.account\nMinimum Scope: https://api.ebay.com/oauth/api_scope/sell.account',
100
109
  inputSchema: updateFulfillmentPolicyInputSchema.shape,
101
110
  outputSchema: zodToJsonSchema(createFulfillmentPolicyOutputSchema, {
102
111
  name: 'UpdateFulfillmentPolicyResponse',
@@ -4,7 +4,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema';
4
4
  import { defineTool } from '../../tools/defineTool.js';
5
5
  import { mapInventoryItemsToTable, mapInventoryItemToCard, mapLocationsToTable, mapOffersToTable, mapOfferToCard, } from '../../tools/ui/maps.js';
6
6
  import { MarketplaceId } from '../../types/ebayEnums.js';
7
- import { bulkInventoryItemResponseSchema, bulkOfferResponseSchema, bulkPublishResponseSchema, createInventoryItemOutputSchema, createInventoryLocationOutputSchema, createOfferOutputSchema, getInventoryItemGroupOutputSchema, getInventoryItemOutputSchema, getInventoryItemsOutputSchema, getInventoryLocationsOutputSchema, getOffersOutputSchema, getProductCompatibilityOutputSchema, offerResponseSchema, publishOfferOutputSchema, } from '../../schemas/inventory-management/inventory.js';
7
+ import { bulkInventoryItemResponseSchema, bulkOfferResponseSchema, bulkPublishResponseSchema, createInventoryItemOutputSchema, createInventoryLocationOutputSchema, createOfferOutputSchema, getInventoryItemGroupOutputSchema, getInventoryItemOutputSchema, getInventoryItemsOutputSchema, getInventoryLocationsOutputSchema, getOffersInputSchema, getOffersOutputSchema, getOffersBySkusOutputSchema, getProductCompatibilityOutputSchema, inventoryItemSchema, offerResponseSchema, publishOfferOutputSchema, } from '../../schemas/inventory-management/inventory.js';
8
8
  const emptyOutputSchema = {
9
9
  type: 'object',
10
10
  properties: {},
@@ -23,7 +23,7 @@ const inventoryPaginationInputSchema = z.object({
23
23
  offset: z.number().optional().describe('Number of records to skip'),
24
24
  });
25
25
  const createOrReplaceInventoryItemInputSchema = skuInputSchema.extend({
26
- body: generatedBodySchema('Generated InventoryItem request body'),
26
+ body: inventoryItemSchema,
27
27
  });
28
28
  const bulkCreateOrReplaceInventoryItemInputSchema = z.object({
29
29
  body: generatedBodySchema('Generated BulkInventoryItem request body'),
@@ -52,12 +52,14 @@ const createInventoryLocationInputSchema = merchantLocationKeyInputSchema.extend
52
52
  const updateInventoryLocationInputSchema = merchantLocationKeyInputSchema.extend({
53
53
  body: generatedBodySchema('Generated InventoryLocation request body'),
54
54
  });
55
- const getOffersInputSchema = z.object({
55
+ const getOffersBySkusInputSchema = z.object({
56
+ skus: z
57
+ .array(z.string().min(1).max(50))
58
+ .min(1)
59
+ .max(25)
60
+ .describe('1-25 seller SKUs; exact duplicates are queried once in first-occurrence order'),
56
61
  format: z.string().optional().describe('Filter by listing format'),
57
- limit: z.number().optional().describe('Number of offers to return'),
58
62
  marketplaceId: z.nativeEnum(MarketplaceId).optional().describe('Filter by marketplace ID'),
59
- offset: z.number().optional().describe('Number of offers to skip'),
60
- sku: z.string().optional().describe('Filter by SKU'),
61
63
  });
62
64
  const offerIdInputSchema = z.object({
63
65
  offerId: z.string().describe('The offer ID'),
@@ -145,7 +147,12 @@ export const inventoryEntries = [
145
147
  name: 'CreateOrReplaceInventoryItemResponse',
146
148
  $refStrategy: 'none',
147
149
  }),
148
- handler: (api, args) => Effect.runPromise(api.inventory.createOrReplaceInventoryItem(args)),
150
+ handler: (api, args) => Effect.runPromise(api.inventory.createOrReplaceInventoryItem({
151
+ ...args,
152
+ // The generated OpenAPI type incorrectly models `product.aspects` as a
153
+ // string; the endpoint wire contract and validation schema use a map.
154
+ body: args.body,
155
+ })),
149
156
  }),
150
157
  defineTool({
151
158
  name: 'ebay_delete_inventory_item',
@@ -302,7 +309,7 @@ export const inventoryEntries = [
302
309
  }),
303
310
  defineTool({
304
311
  name: 'ebay_get_offers',
305
- description: 'Get all offers for the seller',
312
+ description: 'Get offers for one seller-defined SKU. To enumerate offers across SKUs, call ebay_get_inventory_items first, then call this tool once per SKU.',
306
313
  inputSchema: getOffersInputSchema.shape,
307
314
  outputSchema: zodToJsonSchema(getOffersOutputSchema, {
308
315
  name: 'GetOffersResponse',
@@ -311,6 +318,16 @@ export const inventoryEntries = [
311
318
  handler: (api, args) => Effect.runPromise(api.inventory.getOffers(args)),
312
319
  ui: { archetype: 'table', map: mapOffersToTable },
313
320
  }),
321
+ defineTool({
322
+ name: 'ebay_get_offers_by_skus',
323
+ description: 'Get offers for 1-25 SKUs. Exact duplicates are queried once, at most 3 eBay requests run concurrently, and every SKU has its own success or failure result.',
324
+ inputSchema: getOffersBySkusInputSchema.shape,
325
+ outputSchema: zodToJsonSchema(getOffersBySkusOutputSchema, {
326
+ name: 'GetOffersBySkusResponse',
327
+ $refStrategy: 'none',
328
+ }),
329
+ handler: (api, args) => Effect.runPromise(api.inventory.getOffersBySkus(args)),
330
+ }),
314
331
  defineTool({
315
332
  name: 'ebay_get_offer',
316
333
  description: 'Get a specific offer by ID',
@@ -65,14 +65,14 @@ export const taxonomyEntries = [
65
65
  }),
66
66
  defineTool({
67
67
  name: 'ebay_get_item_aspects_for_category',
68
- description: 'Get item aspects for a specific category',
68
+ description: 'Identify required and recommended item specifics for a category before creating an inventory item or checking listing fees',
69
69
  inputSchema: getItemAspectsForCategorySchema.shape,
70
70
  outputSchema: {
71
71
  type: 'object',
72
72
  properties: {
73
73
  aspects: { type: 'array' },
74
74
  },
75
- description: 'Item aspects for category',
75
+ description: 'Required and recommended item specifics for the category',
76
76
  },
77
77
  handler: (api, args) => Effect.runPromise(api.taxonomy.getItemAspectsForCategory(args)),
78
78
  }),
@@ -1,3 +1,4 @@
1
+ import { Cause, Runtime } from 'effect';
1
2
  /**
2
3
  * Normalize an unknown thrown value into a human-readable message string.
3
4
  *
@@ -18,3 +19,86 @@
18
19
  * ```
19
20
  */
20
21
  export const getErrorMessage = (error, fallback = 'Unknown error') => error instanceof Error ? error.message : fallback;
22
+ const asRecord = (value) => typeof value === 'object' && value !== null ? value : undefined;
23
+ const nextErrorNode = (node) => {
24
+ const fiberCause = node[Runtime.FiberFailureCauseId];
25
+ if (fiberCause !== undefined) {
26
+ return Cause.squash(fiberCause);
27
+ }
28
+ return node.cause;
29
+ };
30
+ const firstEbayErrorMessage = (errors) => {
31
+ const first = asRecord(errors[0]);
32
+ if (!first) {
33
+ return;
34
+ }
35
+ const detail = first.longMessage ?? first.message;
36
+ return typeof detail === 'string' ? detail : undefined;
37
+ };
38
+ const KIND_IS_GUIDANCE = {
39
+ missingCredentials: true,
40
+ localRateLimit: true,
41
+ tokenAcquisition: true,
42
+ missingAccessToken: true,
43
+ tokenRefresh: true,
44
+ remoteRateLimit: true,
45
+ httpStatus: false,
46
+ transport: false,
47
+ };
48
+ const isGuidanceKind = (kind) => Object.hasOwn(KIND_IS_GUIDANCE, kind) && KIND_IS_GUIDANCE[kind];
49
+ const collectEbayErrorNode = (node, acc) => {
50
+ const message = typeof node.message === 'string' && node.message.length > 0 ? node.message : undefined;
51
+ if (message !== undefined && !acc.messageLocked) {
52
+ acc.message = message;
53
+ if (typeof node.kind === 'string' && isGuidanceKind(node.kind)) {
54
+ acc.messageLocked = true;
55
+ }
56
+ }
57
+ if (typeof node.status === 'number') {
58
+ acc.status = node.status;
59
+ }
60
+ const data = asRecord(node.data);
61
+ if (data && Array.isArray(data.errors)) {
62
+ acc.errors = data.errors;
63
+ const detail = firstEbayErrorMessage(data.errors);
64
+ if (detail && !acc.messageLocked) {
65
+ acc.message = detail;
66
+ }
67
+ return;
68
+ }
69
+ // Trading XML failures store their structured entries directly on `cause`.
70
+ if (acc.errors === undefined && Array.isArray(node.cause) && node.cause.length > 0) {
71
+ acc.errors = node.cause;
72
+ }
73
+ };
74
+ /**
75
+ * Recovers status, structured eBay errors, and the best message from nested
76
+ * tagged errors and Effect FiberFailures.
77
+ *
78
+ * @param error - Failure caught at an external boundary.
79
+ * @returns Structured details suitable for an MCP error result.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * const details = getEbayErrorDetails(error);
84
+ * ```
85
+ */
86
+ export const getEbayErrorDetails = (error) => {
87
+ const acc = { message: getErrorMessage(error) };
88
+ const seen = new Set();
89
+ let current = error;
90
+ while (current !== undefined && current !== null && !seen.has(current)) {
91
+ seen.add(current);
92
+ const node = asRecord(current);
93
+ if (!node) {
94
+ break;
95
+ }
96
+ collectEbayErrorNode(node, acc);
97
+ current = nextErrorNode(node);
98
+ }
99
+ return {
100
+ message: acc.message,
101
+ ...(acc.status === undefined ? {} : { status: acc.status }),
102
+ ...(acc.errors === undefined ? {} : { errors: acc.errors }),
103
+ };
104
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ebay-mcp",
3
- "version": "1.14.2",
4
- "description": "Model Context Protocol (MCP) server that exposes 100% of eBay's Sell APIs (322 tools across 270 endpoints) to AI assistants like Claude, Cursor, and Cline, covering inventory, order fulfillment, marketing, analytics, and developer tools with OAuth authentication and refresh-token support.",
3
+ "version": "1.15.0",
4
+ "description": "Model Context Protocol (MCP) server that exposes 100% of eBay's Sell APIs (299 tools across 270 endpoints) to AI assistants like Claude, Cursor, and Cline, covering inventory, order fulfillment, marketing, analytics, and developer tools with OAuth authentication and refresh-token support.",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
7
7
  "types": "build/index.d.ts",
@@ -109,10 +109,10 @@
109
109
  "test": "vitest run",
110
110
  "test:coverage": "vitest run --coverage",
111
111
  "test:integration": "vitest run --config vitest.integration.config.cts",
112
- "setup": "tsx src/scripts/setup.ts",
113
- "skills": "tsx src/scripts/skills.ts",
112
+ "setup": "node build/scripts/setup.js",
113
+ "skills": "node build/scripts/skills.js",
114
114
  "sync": "tsx src/scripts/devSync.ts",
115
- "diagnose": "tsx src/scripts/diagnostics.ts",
115
+ "diagnose": "node build/scripts/diagnostics.js",
116
116
  "typecheck": "tsc --noEmit",
117
117
  "typecheck:ui": "tsc -p ui/tsconfig.json --noEmit",
118
118
  "check": "tsc --noEmit && tsc -p ui/tsconfig.json --noEmit && biome check \"{src,ui}/**/*.{ts,tsx,js,json,md,css,html}\"",