ebay-mcp 1.14.3 → 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.
- package/README.de.md +8 -8
- package/README.es.md +8 -8
- package/README.fr.md +8 -8
- package/README.ja.md +8 -8
- package/README.ko.md +8 -8
- package/README.md +25 -10
- package/README.pt-BR.md +8 -8
- package/README.ru.md +8 -8
- package/README.zh-CN.md +8 -8
- package/build/api/client.js +8 -2
- package/build/api/listing-management/offers.js +78 -23
- package/build/auth/credentialSession.js +2 -3
- package/build/auth/oauth.js +26 -8
- package/build/auth/scopeUtils.js +5 -0
- package/build/config/credentialFile.js +16 -0
- package/build/config/environment.js +2 -1
- package/build/mcp/runtime.js +13 -5
- package/build/schemas/inventory-management/inventory.js +21 -2
- package/build/skills/content.js +5 -6
- package/build/tools/categories/account.js +1 -1
- package/build/tools/categories/inventory.js +25 -8
- package/build/tools/categories/taxonomy.js +2 -2
- package/build/utils/errors.js +84 -0
- package/package.json +2 -2
package/README.ru.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>MCP-сервер eBay — дайте Claude, Cursor и любому ИИ-ассистенту полный доступ к Sell API eBay.
|
|
8
|
+
<strong>MCP-сервер eBay — дайте Claude, Cursor и любому ИИ-ассистенту полный доступ к Sell API eBay. 299 инструмента для инвентаря, заказов, маркетинга и аналитики, работающих локально с вашими собственными ключами.</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center"><sub>Неофициальный проект с открытым исходным кодом — не связан с eBay Inc., не авторизован и не одобрен ею.</sub></p>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
<p align="center">
|
|
23
|
-
<img src="https://img.shields.io/badge/tools-
|
|
23
|
+
<img src="https://img.shields.io/badge/tools-299-8957e5?logo=ebay&logoColor=white" alt="299 инструмента API eBay" />
|
|
24
24
|
<img src="https://img.shields.io/badge/Sell%20API%20coverage-100%25-success" alt="100% покрытие Sell API eBay" />
|
|
25
25
|
<img src="https://img.shields.io/badge/Model%20Context%20Protocol-compatible-000000" alt="совместим с Model Context Protocol" />
|
|
26
26
|
<img src="https://img.shields.io/badge/tests-1%2C000%2B%20passing-3fb950?logo=vitest&logoColor=white" alt="более 1000 пройденных тестов" />
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
> **Примечание:** английский [README.md](README.md) — источник истины для актуальных продуктовых формулировок (семейства инструментов, вывод API из эксплуатации, заметки о версиях). Этот перевод может отставать.
|
|
49
49
|
|
|
50
|
-
**eBay MCP** — это локальный сервер [Model Context Protocol](https://modelcontextprotocol.io), который подключает ИИ-ассистентов —Claude Desktop, Claude Code, Cursor, Cline, Windsurf, Zed, Continue.dev, Roo Code и Amazon Q— напрямую к **Sell API eBay**. Он предоставляет **
|
|
50
|
+
**eBay MCP** — это локальный сервер [Model Context Protocol](https://modelcontextprotocol.io), который подключает ИИ-ассистентов —Claude Desktop, Claude Code, Cursor, Cline, Windsurf, Zed, Continue.dev, Roo Code и Amazon Q— напрямую к **Sell API eBay**. Он предоставляет **299 инструмента**, охватывающих **100% поверхности Sell API eBay** (270 уникальных эндпоинтов): управление инвентарём, обработка заказов, маркетинг продвигаемых объявлений, аналитика и инструменты разработчика. Всё работает на вашей машине через STDIO или локальный HTTP — **без облачного ретранслятора**, и ваши учётные данные eBay никогда не покидают компьютер.
|
|
51
51
|
|
|
52
52
|
> **Отказ от ответственности:** Неофициальный сторонний проект — **не связан с eBay Inc. и не одобрен ею.** Предоставляется «как есть», без гарантий. Вы сами отвечаете за соблюдение [Лицензионного соглашения API eBay](https://developer.ebay.com/join/api-license-agreement) и [требований к обработке данных](https://developer.ebay.com/api-docs/static/data-handling-update.html), за сохранность учётных данных и за соблюдение лимитов запросов. Протестируйте в песочнице перед продакшеном. См. [LICENSE](LICENSE), [SECURITY.md](SECURITY.md) и [EBAY_COMPLIANCE.md](EBAY_COMPLIANCE.md).
|
|
53
53
|
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
|
|
72
72
|
## Возможности
|
|
73
73
|
|
|
74
|
-
- **
|
|
74
|
+
- **299 инструмента API eBay** — 100% покрытие Sell API eBay: инвентарь, заказы, маркетинг, аналитика, метаданные, таксономия и инструменты разработчика.
|
|
75
75
|
- **9 ИИ-клиентов с автонастройкой** — Claude Desktop, Cursor, Zed, Cline, Continue.dev, Windsurf, Roo Code, Claude Code CLI и Amazon Q Developer.
|
|
76
76
|
- **Встроенный OAuth 2.0** — полное управление пользовательскими токенами с автоматическим обновлением и умным переходом от пользовательских токенов (10–50 тыс. запросов/день) к клиентским учётным данным (1 тыс. запросов/день).
|
|
77
77
|
- **Надёжность по умолчанию** — автоматический повтор с экспоненциальной задержкой при лимитах `429` и понятный, последовательный вывод ошибок.
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
| Валидация входных данных | Схемы Zod + типы TypeScript на каждом инструменте | Нет — вы проверяете свои полезные данные сами |
|
|
94
94
|
| Настройка | Один мастер (`npm run setup`) | Авторизация, заголовки и маркетплейс на каждый вызов |
|
|
95
95
|
| Поддержка ИИ-клиентов | 9 клиентов с автонастройкой | Неприменимо |
|
|
96
|
-
| Покрытие API |
|
|
96
|
+
| Покрытие API | 299 инструмента на 100% Sell API, готовы к вызову | Вы строите каждый запрос по документации |
|
|
97
97
|
| Размещение | Работает локально, без облачного ретранслятора | Ваша собственная инфраструктура |
|
|
98
98
|
|
|
99
99
|
## Настройка с помощью ИИ в один клик
|
|
@@ -236,7 +236,7 @@ EBAY_MCP_UI=on # интерактивные предста
|
|
|
236
236
|
|
|
237
237
|
## Доступные инструменты
|
|
238
238
|
|
|
239
|
-
**
|
|
239
|
+
**299 инструмента**, 100% покрытие Sell API, сгруппированы по категориям. Каждая ссылка ведёт к определениям инструментов и их обработчикам в [`src/tools/categories/`](src/tools/categories/):
|
|
240
240
|
|
|
241
241
|
| Категория | Что вы можете делать |
|
|
242
242
|
| --- | --- |
|
|
@@ -299,7 +299,7 @@ EBAY_MCP_UI=on # интерактивные предста
|
|
|
299
299
|
|
|
300
300
|
### Что такое MCP-сервер eBay?
|
|
301
301
|
|
|
302
|
-
Локальный сервер [Model Context Protocol](https://modelcontextprotocol.io), предоставляющий **
|
|
302
|
+
Локальный сервер [Model Context Protocol](https://modelcontextprotocol.io), предоставляющий **299 инструмента**, охватывающих **100% Sell API eBay** (270 эндпоинтов), ИИ-ассистентам — инвентарь, обработка заказов, маркетинг, аналитика и инструменты разработчика.
|
|
303
303
|
|
|
304
304
|
### Это официальный продукт eBay?
|
|
305
305
|
|
|
@@ -319,7 +319,7 @@ EBAY_MCP_UI=on # интерактивные предста
|
|
|
319
319
|
|
|
320
320
|
### Сколько API и инструментов eBay он охватывает?
|
|
321
321
|
|
|
322
|
-
|
|
322
|
+
299 инструмента на 270 уникальных эндпоинтах — 100% Sell API eBay.
|
|
323
323
|
|
|
324
324
|
### Это бесплатно и с открытым исходным кодом?
|
|
325
325
|
|
package/README.zh-CN.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>eBay MCP 服务器 — 让 Claude、Cursor 以及任意 AI 助手完全访问 eBay 的 Sell API。
|
|
8
|
+
<strong>eBay MCP 服务器 — 让 Claude、Cursor 以及任意 AI 助手完全访问 eBay 的 Sell API。299 个工具覆盖库存、订单、营销与分析,使用你自己的密钥在本地运行。</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center"><sub>非官方开源项目 — 与 eBay Inc. 无任何关联,未获其授权或认可。</sub></p>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
<p align="center">
|
|
23
|
-
<img src="https://img.shields.io/badge/tools-
|
|
23
|
+
<img src="https://img.shields.io/badge/tools-299-8957e5?logo=ebay&logoColor=white" alt="299 个 eBay API 工具" />
|
|
24
24
|
<img src="https://img.shields.io/badge/Sell%20API%20coverage-100%25-success" alt="100% 覆盖 eBay Sell API" />
|
|
25
25
|
<img src="https://img.shields.io/badge/Model%20Context%20Protocol-compatible-000000" alt="兼容 Model Context Protocol" />
|
|
26
26
|
<img src="https://img.shields.io/badge/tests-1%2C000%2B%20passing-3fb950?logo=vitest&logoColor=white" alt="超过 1000 个通过的测试" />
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
> **说明:** 英文 [README.md](README.md) 是最新产品说明(工具族、API 下线、版本说明)的权威来源。本翻译可能滞后。
|
|
49
49
|
|
|
50
|
-
**eBay MCP** 是一个本地的 [Model Context Protocol](https://modelcontextprotocol.io) 服务器,可将 AI 助手 —Claude Desktop、Claude Code、Cursor、Cline、Windsurf、Zed、Continue.dev、Roo Code 和 Amazon Q— 直接连接到 **eBay 的 Sell API**。它提供 **
|
|
50
|
+
**eBay MCP** 是一个本地的 [Model Context Protocol](https://modelcontextprotocol.io) 服务器,可将 AI 助手 —Claude Desktop、Claude Code、Cursor、Cline、Windsurf、Zed、Continue.dev、Roo Code 和 Amazon Q— 直接连接到 **eBay 的 Sell API**。它提供 **299 个工具**,覆盖 **eBay Sell API 的 100% 范围**(270 个唯一端点),涵盖库存管理、订单履行、推广刊登营销、分析和开发者工具。一切都在你的机器上通过 STDIO 或本地 HTTP 运行 — **没有云端中继**,你的 eBay 凭据永远不会离开你的电脑。
|
|
51
51
|
|
|
52
52
|
> **免责声明:** 非官方第三方项目 — **与 eBay Inc. 无关联,也未获其认可。** 按"原样"提供,不附带任何担保。你需自行负责遵守 [eBay API 许可协议](https://developer.ebay.com/join/api-license-agreement) 和 [数据处理要求](https://developer.ebay.com/api-docs/static/data-handling-update.html)、妥善保管你的凭据并遵守速率限制。在投入生产前请先在沙盒中测试。参见 [LICENSE](LICENSE)、[SECURITY.md](SECURITY.md) 和 [EBAY_COMPLIANCE.md](EBAY_COMPLIANCE.md)。
|
|
53
53
|
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
|
|
72
72
|
## 功能特性
|
|
73
73
|
|
|
74
|
-
- **
|
|
74
|
+
- **299 个 eBay API 工具** — 100% 覆盖 eBay Sell API,涵盖库存、订单、营销、分析、元数据、分类法和开发者工具。
|
|
75
75
|
- **自动配置 9 个 AI 客户端** — Claude Desktop、Cursor、Zed、Cline、Continue.dev、Windsurf、Roo Code、Claude Code CLI 和 Amazon Q Developer。
|
|
76
76
|
- **内置 OAuth 2.0** — 完整的用户令牌管理与自动刷新,并能从用户令牌(每天 1 万–5 万次请求)智能回退到客户端凭据(每天 1 千次请求)。
|
|
77
77
|
- **默认具备韧性** — 遇到 `429` 速率限制时自动重试并采用指数退避,错误提示清晰一致。
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
| 输入校验 | 每个工具都有 Zod 模式 + TypeScript 类型 | 无 — 由你校验自己的载荷 |
|
|
94
94
|
| 配置 | 一个向导(`npm run setup`) | 每次调用都要处理认证、请求头和站点 |
|
|
95
95
|
| AI 客户端支持 | 自动配置 9 个客户端 | 不适用 |
|
|
96
|
-
| API 覆盖 |
|
|
96
|
+
| API 覆盖 | 299 个工具覆盖 100% Sell API,开箱即用 | 你需根据文档构建每个请求 |
|
|
97
97
|
| 托管 | 本地运行,无云端中继 | 你自己的基础设施 |
|
|
98
98
|
|
|
99
99
|
## AI 一键配置
|
|
@@ -236,7 +236,7 @@ EBAY_MCP_UI=on # 交互式 MCP Apps 视图(测试版)
|
|
|
236
236
|
|
|
237
237
|
## 可用工具
|
|
238
238
|
|
|
239
|
-
**
|
|
239
|
+
**299 个工具**,100% Sell API 覆盖,按类别组织。每个链接都指向 [`src/tools/categories/`](src/tools/categories/) 中的工具定义及其处理程序:
|
|
240
240
|
|
|
241
241
|
| 类别 | 你可以做什么 |
|
|
242
242
|
| --- | --- |
|
|
@@ -299,7 +299,7 @@ EBAY_MCP_UI=on # 交互式 MCP Apps 视图(测试版)
|
|
|
299
299
|
|
|
300
300
|
### 什么是 eBay MCP 服务器?
|
|
301
301
|
|
|
302
|
-
一个本地的 [Model Context Protocol](https://modelcontextprotocol.io) 服务器,向 AI 助手提供 **
|
|
302
|
+
一个本地的 [Model Context Protocol](https://modelcontextprotocol.io) 服务器,向 AI 助手提供 **299 个工具**,覆盖 **eBay Sell API 的 100%**(270 个端点)—— 库存、订单履行、营销、分析和开发者工具。
|
|
303
303
|
|
|
304
304
|
### 这是 eBay 的官方产品吗?
|
|
305
305
|
|
|
@@ -319,7 +319,7 @@ EBAY_MCP_UI=on # 交互式 MCP Apps 视图(测试版)
|
|
|
319
319
|
|
|
320
320
|
### 它覆盖多少 eBay API 和工具?
|
|
321
321
|
|
|
322
|
-
|
|
322
|
+
299 个工具,覆盖 270 个唯一端点 —— eBay Sell API 的 100%。
|
|
323
323
|
|
|
324
324
|
### 它是免费且开源的吗?
|
|
325
325
|
|
package/build/api/client.js
CHANGED
|
@@ -6,7 +6,7 @@ import { getErrorMessage } from '../utils/errors.js';
|
|
|
6
6
|
import { httpRequestEffect, isHttpError } from '../utils/http.js';
|
|
7
7
|
import { isRecord } from '../utils/typeGuards.js';
|
|
8
8
|
import { apiLogger, logRequest, logResponse, logErrorResponse } from '../utils/logger.js';
|
|
9
|
-
import { Effect } from 'effect';
|
|
9
|
+
import { Cause, Effect, Exit } from 'effect';
|
|
10
10
|
/** Sleep for a retry backoff delay without exposing timers to callers. */
|
|
11
11
|
const sleep = (delayMs) => Effect.promise(() => new Promise((resolve) => {
|
|
12
12
|
setTimeout(resolve, delayMs);
|
|
@@ -96,7 +96,13 @@ export class EbayApiClient {
|
|
|
96
96
|
* Core request boundary shared by every verb.
|
|
97
97
|
*/
|
|
98
98
|
async request(method, endpoint, options) {
|
|
99
|
-
|
|
99
|
+
// Preserve the typed request error for endpoint adapters. Plain runPromise
|
|
100
|
+
// rejects with a FiberFailure whose generic message hides eBay response data.
|
|
101
|
+
const exit = await Effect.runPromiseExit(this.requestEffect(method, endpoint, options));
|
|
102
|
+
if (Exit.isSuccess(exit)) {
|
|
103
|
+
return exit.value;
|
|
104
|
+
}
|
|
105
|
+
throw Cause.squash(exit.cause);
|
|
100
106
|
}
|
|
101
107
|
/**
|
|
102
108
|
* Build the request Effect that owns auth, logging, retry, and transport errors.
|
|
@@ -1,6 +1,30 @@
|
|
|
1
|
-
import { buildEndpointParams, optionalNonNegativeNumberEffect, optionalPositiveNumberEffect, optionalStringEffect, requestDeleteEffect, requestGetEffect, requestPostEffect, requestPutEffect, requireObjectEffect, requireStringEffect, } from '../../api/shared/request.js';
|
|
2
|
-
import { Effect } from 'effect';
|
|
1
|
+
import { EndpointInputError, buildEndpointParams, optionalNonNegativeNumberEffect, optionalPositiveNumberEffect, optionalStringEffect, requestDeleteEffect, requestGetEffect, requestPostEffect, requestPutEffect, requireObjectEffect, requireStringEffect, } from '../../api/shared/request.js';
|
|
2
|
+
import { Effect, Either } from 'effect';
|
|
3
3
|
import { INVENTORY_BASE_PATH } from './shared.js';
|
|
4
|
+
/** Maximum number of SKU entries accepted by one batch lookup. */
|
|
5
|
+
export const MAX_BATCH_GET_OFFERS_SKUS = 25;
|
|
6
|
+
/** Maximum number of simultaneous eBay requests made by a batch lookup. */
|
|
7
|
+
export const BATCH_GET_OFFERS_CONCURRENCY = 3;
|
|
8
|
+
// Keep the shared request implementation private so the existing and batch methods cannot drift.
|
|
9
|
+
const getOffersEffect = (client, input) => {
|
|
10
|
+
const path = `${INVENTORY_BASE_PATH}/offer`;
|
|
11
|
+
return Effect.gen(function* () {
|
|
12
|
+
const validatedInput = yield* requireObjectEffect(input, 'input');
|
|
13
|
+
const format = yield* optionalStringEffect(validatedInput.format, 'format');
|
|
14
|
+
const limit = yield* optionalPositiveNumberEffect(validatedInput.limit, 'limit');
|
|
15
|
+
const marketplaceId = yield* optionalStringEffect(validatedInput.marketplaceId, 'marketplaceId');
|
|
16
|
+
const offset = yield* optionalNonNegativeNumberEffect(validatedInput.offset, 'offset');
|
|
17
|
+
const sku = yield* requireStringEffect(validatedInput.sku, 'sku');
|
|
18
|
+
const params = buildEndpointParams({
|
|
19
|
+
format: { wireName: 'format', value: format },
|
|
20
|
+
limit: { wireName: 'limit', value: limit === undefined ? undefined : String(limit) },
|
|
21
|
+
marketplaceId: { wireName: 'marketplace_id', value: marketplaceId },
|
|
22
|
+
offset: { wireName: 'offset', value: offset === undefined ? undefined : String(offset) },
|
|
23
|
+
sku: { wireName: 'sku', value: sku },
|
|
24
|
+
});
|
|
25
|
+
return yield* requestGetEffect(client, path, params);
|
|
26
|
+
});
|
|
27
|
+
};
|
|
4
28
|
/** Inventory API — offers methods closed over a shared client. */
|
|
5
29
|
export const createInventoryOffersMethods = (client) => ({
|
|
6
30
|
/**
|
|
@@ -46,9 +70,9 @@ export const createInventoryOffersMethods = (client) => ({
|
|
|
46
70
|
});
|
|
47
71
|
},
|
|
48
72
|
/**
|
|
49
|
-
* Retrieves offers
|
|
73
|
+
* Retrieves offers for one seller-defined SKU.
|
|
50
74
|
*
|
|
51
|
-
* @param input -
|
|
75
|
+
* @param input - Required SKU plus optional offer filters and pagination controls.
|
|
52
76
|
* @returns An Effect that succeeds with eBay's Offers response.
|
|
53
77
|
*
|
|
54
78
|
* @example
|
|
@@ -60,25 +84,56 @@ export const createInventoryOffersMethods = (client) => ({
|
|
|
60
84
|
*
|
|
61
85
|
* @see https://developer.ebay.com/api-docs/sell/inventory/resources/offer/methods/getOffers
|
|
62
86
|
*/
|
|
63
|
-
getOffers: (input
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
getOffers: (input) => getOffersEffect(client, input),
|
|
88
|
+
/**
|
|
89
|
+
* Retrieves offers for up to 25 SKUs, preserving first-occurrence order.
|
|
90
|
+
*
|
|
91
|
+
* Exact duplicates are requested once. At most three requests run concurrently, and request
|
|
92
|
+
* failures are returned beside their SKU instead of failing the whole batch.
|
|
93
|
+
*/
|
|
94
|
+
getOffersBySkus: (input) => Effect.gen(function* () {
|
|
95
|
+
const validatedInput = yield* requireObjectEffect(input, 'input');
|
|
96
|
+
if (!Array.isArray(validatedInput.skus)) {
|
|
97
|
+
return yield* Effect.fail(new EndpointInputError({ parameter: 'skus', message: 'skus must be an array' }));
|
|
98
|
+
}
|
|
99
|
+
if (validatedInput.skus.length === 0 ||
|
|
100
|
+
validatedInput.skus.length > MAX_BATCH_GET_OFFERS_SKUS) {
|
|
101
|
+
return yield* Effect.fail(new EndpointInputError({
|
|
102
|
+
parameter: 'skus',
|
|
103
|
+
message: `skus must contain between 1 and ${MAX_BATCH_GET_OFFERS_SKUS} entries`,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
for (const sku of validatedInput.skus) {
|
|
107
|
+
if (typeof sku !== 'string' || sku.trim().length === 0 || sku.length > 50) {
|
|
108
|
+
return yield* Effect.fail(new EndpointInputError({
|
|
109
|
+
parameter: 'skus',
|
|
110
|
+
message: 'each SKU must be a non-empty string of at most 50 characters',
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const format = yield* optionalStringEffect(validatedInput.format, 'format');
|
|
115
|
+
const marketplaceId = yield* optionalStringEffect(validatedInput.marketplaceId, 'marketplaceId');
|
|
116
|
+
const uniqueSkus = [...new Set(validatedInput.skus)];
|
|
117
|
+
const results = yield* Effect.forEach(uniqueSkus, (sku) => Effect.map(Effect.either(getOffersEffect(client, {
|
|
118
|
+
sku,
|
|
119
|
+
format,
|
|
120
|
+
marketplaceId,
|
|
121
|
+
})), (outcome) => Either.isRight(outcome)
|
|
122
|
+
? { sku, status: 'success', response: outcome.right }
|
|
123
|
+
: {
|
|
124
|
+
sku,
|
|
125
|
+
status: 'failure',
|
|
126
|
+
error: { type: outcome.left._tag, message: outcome.left.message },
|
|
127
|
+
}), { concurrency: BATCH_GET_OFFERS_CONCURRENCY });
|
|
128
|
+
const successCount = results.filter((result) => result.status === 'success').length;
|
|
129
|
+
return {
|
|
130
|
+
requestedSkuCount: validatedInput.skus.length,
|
|
131
|
+
uniqueSkuCount: uniqueSkus.length,
|
|
132
|
+
successCount,
|
|
133
|
+
failureCount: results.length - successCount,
|
|
134
|
+
results,
|
|
135
|
+
};
|
|
136
|
+
}),
|
|
82
137
|
/**
|
|
83
138
|
* Creates one offer.
|
|
84
139
|
*
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import dotenv from 'dotenv';
|
|
2
2
|
import stringify from 'dotenv-stringify';
|
|
3
3
|
import { closeSync, existsSync, fsyncSync, openSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
-
import {
|
|
4
|
+
import { CREDENTIAL_ENV_PATH } from '../config/credentialFile.js';
|
|
5
5
|
import { Data, Effect } from 'effect';
|
|
6
|
-
import process from 'node:process';
|
|
7
6
|
/**
|
|
8
7
|
* Default lifetime for stored eBay user access tokens when eBay omits an expiry.
|
|
9
8
|
*/
|
|
@@ -42,7 +41,7 @@ const writeLockForEnvPath = (envPath) => {
|
|
|
42
41
|
*/
|
|
43
42
|
export class DotEnvCredentialStore {
|
|
44
43
|
getEnvPath;
|
|
45
|
-
constructor(getEnvPath = () =>
|
|
44
|
+
constructor(getEnvPath = () => CREDENTIAL_ENV_PATH) {
|
|
46
45
|
this.getEnvPath = getEnvPath;
|
|
47
46
|
}
|
|
48
47
|
/**
|
package/build/auth/oauth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createAppAccessTokenExpiry, createStoredUserTokens, createStoredUserTokensFromResponse, DotEnvCredentialStore, isTokenExpired, } from '../auth/credentialSession.js';
|
|
1
|
+
import { createAppAccessTokenExpiry, createStoredUserTokens, createStoredUserTokensFromResponse, CredentialStoreError, DotEnvCredentialStore, isTokenExpired, } from '../auth/credentialSession.js';
|
|
2
2
|
import { getBaseUrl, getDefaultScopes } from '../config/environment.js';
|
|
3
3
|
import { LocaleEnum } from '../types/ebayEnums.js';
|
|
4
4
|
import { getErrorMessage } from '../utils/errors.js';
|
|
@@ -13,6 +13,7 @@ const mapCredentialStoreError = (operation, message, cause) => new EbayOAuthErro
|
|
|
13
13
|
message: `${message}: ${cause.message}`,
|
|
14
14
|
cause,
|
|
15
15
|
});
|
|
16
|
+
const isCredentialPersistenceError = (error) => error.cause instanceof CredentialStoreError;
|
|
16
17
|
/**
|
|
17
18
|
* Manages eBay OAuth 2.0 authentication
|
|
18
19
|
* Loads tokens exclusively from validated runtime config
|
|
@@ -70,12 +71,20 @@ export class EbayOAuthClient {
|
|
|
70
71
|
}));
|
|
71
72
|
if (Either.isLeft(refreshed)) {
|
|
72
73
|
const error = refreshed.left;
|
|
73
|
-
|
|
74
|
-
error
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
if (isCredentialPersistenceError(error)) {
|
|
75
|
+
authLogger.error('Access token refreshed but could not be persisted', {
|
|
76
|
+
error: getErrorMessage(error, String(error)),
|
|
77
|
+
hint: 'The refreshed token remains available for this process, but the credential file must be writable to survive a restart',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
authLogger.error('Failed to refresh access token', {
|
|
82
|
+
error: getErrorMessage(error, String(error)),
|
|
83
|
+
hint: 'The configured EBAY_USER_REFRESH_TOKEN may be invalid or expired',
|
|
84
|
+
});
|
|
85
|
+
// Clear tokens only when the refresh itself failed.
|
|
86
|
+
this.userTokens = null;
|
|
87
|
+
}
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
90
|
});
|
|
@@ -135,10 +144,19 @@ export class EbayOAuthClient {
|
|
|
135
144
|
}
|
|
136
145
|
const refreshed = yield* Effect.either(this.refreshUserToken());
|
|
137
146
|
if (Either.isLeft(refreshed)) {
|
|
147
|
+
if (isCredentialPersistenceError(refreshed.left) &&
|
|
148
|
+
this.userTokens &&
|
|
149
|
+
!this.isUserAccessTokenExpired(this.userTokens)) {
|
|
150
|
+
authLogger.error('User token refreshed but could not be persisted', {
|
|
151
|
+
error: getErrorMessage(refreshed.left, String(refreshed.left)),
|
|
152
|
+
hint: 'Using the valid refreshed token for this process only',
|
|
153
|
+
});
|
|
154
|
+
return this.userTokens.userAccessToken;
|
|
155
|
+
}
|
|
138
156
|
authLogger.error('Failed to refresh user token, falling back to app access token', {
|
|
139
157
|
error: getErrorMessage(refreshed.left, String(refreshed.left)),
|
|
140
158
|
});
|
|
141
|
-
// Clear
|
|
159
|
+
// Clear tokens only when no valid refreshed access token remains.
|
|
142
160
|
this.userTokens = null;
|
|
143
161
|
}
|
|
144
162
|
else if (this.userTokens) {
|
package/build/auth/scopeUtils.js
CHANGED
|
@@ -126,6 +126,11 @@ export const getRequiredScopesForTool = (toolName) => {
|
|
|
126
126
|
minimumScope: 'https://api.ebay.com/oauth/api_scope/sell.account',
|
|
127
127
|
description: 'Requires write access to account settings',
|
|
128
128
|
},
|
|
129
|
+
ebay_update_fulfillment_policy: {
|
|
130
|
+
requiredScopes: ['https://api.ebay.com/oauth/api_scope/sell.account'],
|
|
131
|
+
minimumScope: 'https://api.ebay.com/oauth/api_scope/sell.account',
|
|
132
|
+
description: 'Requires write access to account settings',
|
|
133
|
+
},
|
|
129
134
|
// Marketing Tools
|
|
130
135
|
ebay_get_campaigns: {
|
|
131
136
|
requiredScopes: [
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the credential `.env` file from an installed source or build module URL.
|
|
5
|
+
*
|
|
6
|
+
* @param moduleUrl - URL of a module in `src/config` or `build/config`.
|
|
7
|
+
* @returns Absolute path to the package-root credential file.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* resolveCredentialEnvPath(import.meta.url);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export const resolveCredentialEnvPath = (moduleUrl) => join(dirname(fileURLToPath(moduleUrl)), '../../.env');
|
|
15
|
+
/** Package-root credential file used for both runtime reads and token writes. */
|
|
16
|
+
export const CREDENTIAL_ENV_PATH = resolveCredentialEnvPath(import.meta.url);
|
|
@@ -2,6 +2,7 @@ import { config } from 'dotenv';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
|
+
import { CREDENTIAL_ENV_PATH } from '../config/credentialFile.js';
|
|
5
6
|
import { getToolGatingConfigError } from '../config/toolFamilies.js';
|
|
6
7
|
import { getErrorMessage } from '../utils/errors.js';
|
|
7
8
|
import { getVersion } from '../utils/version.js';
|
|
@@ -13,7 +14,7 @@ const __dirname = dirname(__filename);
|
|
|
13
14
|
// Load .env from the package root (two levels up from src/config/), not process.cwd().
|
|
14
15
|
// MCP servers inherit cwd from the host (e.g. Claude Code's project dir), so
|
|
15
16
|
// process.cwd() may point to an unrelated project with a different .env.
|
|
16
|
-
config({ path:
|
|
17
|
+
config({ path: CREDENTIAL_ENV_PATH, quiet: true });
|
|
17
18
|
const writeConfigDiagnostic = (message) => {
|
|
18
19
|
process.stderr.write(`[eBay MCP] ${message}\n`);
|
|
19
20
|
};
|
package/build/mcp/runtime.js
CHANGED
|
@@ -6,7 +6,7 @@ import { isReadOnlyModeEnabled, isReadOnlyTool } from '../mcp/readOnlyFilter.js'
|
|
|
6
6
|
import { createToolGatingController, DYNAMIC_MODE_INSTRUCTIONS, registerMetaTools, toolNamesInFamilies, } from '../mcp/toolGating.js';
|
|
7
7
|
import { buildUiToolResult, createUiBridge } from '../mcp/uiBridge.js';
|
|
8
8
|
import { getToolEntries } from '../tools/registry.js';
|
|
9
|
-
import {
|
|
9
|
+
import { getEbayErrorDetails } from '../utils/errors.js';
|
|
10
10
|
import { serverLogger, toolLogger } from '../utils/logger.js';
|
|
11
11
|
import { Effect } from 'effect';
|
|
12
12
|
function formatToolSuccess(result) {
|
|
@@ -23,12 +23,19 @@ function formatToolSuccess(result) {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
function formatToolFailure(error) {
|
|
26
|
-
const
|
|
26
|
+
const { message, status, errors } = getEbayErrorDetails(error);
|
|
27
|
+
const payload = { error: message };
|
|
28
|
+
if (status !== undefined) {
|
|
29
|
+
payload.status = status;
|
|
30
|
+
}
|
|
31
|
+
if (errors !== undefined) {
|
|
32
|
+
payload.details = errors;
|
|
33
|
+
}
|
|
27
34
|
return {
|
|
28
35
|
content: [
|
|
29
36
|
{
|
|
30
37
|
type: 'text',
|
|
31
|
-
text: JSON.stringify(
|
|
38
|
+
text: JSON.stringify(payload, null, 2),
|
|
32
39
|
},
|
|
33
40
|
],
|
|
34
41
|
isError: true,
|
|
@@ -56,9 +63,10 @@ function registerTool(server, api, entry, logToolExecution, ui) {
|
|
|
56
63
|
? buildUiToolResult(entry.ui, result)
|
|
57
64
|
: formatToolSuccess(result);
|
|
58
65
|
}), Effect.catchAll((error) => {
|
|
59
|
-
const errorMessage = getErrorMessage(error);
|
|
60
66
|
if (logToolExecution) {
|
|
61
|
-
toolLogger.error(`Tool ${definition.name} failed`, {
|
|
67
|
+
toolLogger.error(`Tool ${definition.name} failed`, {
|
|
68
|
+
error: getEbayErrorDetails(error).message,
|
|
69
|
+
});
|
|
62
70
|
}
|
|
63
71
|
return Effect.succeed(formatToolFailure(error));
|
|
64
72
|
})));
|
|
@@ -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().
|
|
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
|
*/
|
package/build/skills/content.js
CHANGED
|
@@ -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. `
|
|
44
|
-
'3. `
|
|
45
|
-
'4. `
|
|
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'),
|
|
@@ -105,7 +105,7 @@ export const accountEntries = [
|
|
|
105
105
|
}),
|
|
106
106
|
defineTool({
|
|
107
107
|
name: 'ebay_update_fulfillment_policy',
|
|
108
|
-
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',
|
|
109
109
|
inputSchema: updateFulfillmentPolicyInputSchema.shape,
|
|
110
110
|
outputSchema: zodToJsonSchema(createFulfillmentPolicyOutputSchema, {
|
|
111
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:
|
|
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
|
|
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(
|
|
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
|
|
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',
|