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.
- 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 +55 -8
- package/build/auth/oauth.js +37 -10
- package/build/auth/scopeUtils.js +5 -0
- package/build/config/credentialFile.js +16 -0
- package/build/config/environment.js +2 -1
- package/build/index.js +5 -0
- package/build/mcp/runtime.js +17 -6
- package/build/schemas/inventory-management/inventory.js +21 -2
- package/build/scripts/diagnostics.js +21 -15
- package/build/skills/content.js +5 -6
- package/build/tools/categories/account.js +11 -2
- 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 +5 -5
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
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
-
import {
|
|
3
|
+
import { closeSync, existsSync, fsyncSync, openSync, readFileSync, writeFileSync } from 'fs';
|
|
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
|
*/
|
|
@@ -19,17 +18,39 @@ export const APP_ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS = 60;
|
|
|
19
18
|
/** Tagged failure returned when credential persistence cannot write to disk. */
|
|
20
19
|
export class CredentialStoreError extends Data.TaggedError('CredentialStoreError') {
|
|
21
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Process-wide write mutexes keyed by absolute `.env` path so concurrent
|
|
23
|
+
* DotEnvCredentialStore instances cannot interleave read-modify-write on the
|
|
24
|
+
* same file (e.g. user-token write racing app-token write).
|
|
25
|
+
*/
|
|
26
|
+
const writeLocksByEnvPath = new Map();
|
|
27
|
+
const writeLockForEnvPath = (envPath) => {
|
|
28
|
+
const existing = writeLocksByEnvPath.get(envPath);
|
|
29
|
+
if (existing) {
|
|
30
|
+
return existing;
|
|
31
|
+
}
|
|
32
|
+
const created = Effect.runSync(Effect.makeSemaphore(1));
|
|
33
|
+
writeLocksByEnvPath.set(envPath, created);
|
|
34
|
+
return created;
|
|
35
|
+
};
|
|
22
36
|
/**
|
|
23
37
|
* Credential store that merges token updates into the project .env file.
|
|
38
|
+
*
|
|
39
|
+
* Writes are serialized through a one-permit semaphore so concurrent Effect
|
|
40
|
+
* callers cannot interleave read-modify-write on the same `.env` path.
|
|
24
41
|
*/
|
|
25
42
|
export class DotEnvCredentialStore {
|
|
26
43
|
getEnvPath;
|
|
27
|
-
constructor(getEnvPath = () =>
|
|
44
|
+
constructor(getEnvPath = () => CREDENTIAL_ENV_PATH) {
|
|
28
45
|
this.getEnvPath = getEnvPath;
|
|
29
46
|
}
|
|
30
47
|
/**
|
|
31
48
|
* Merges credential updates into the project `.env` file.
|
|
32
49
|
*
|
|
50
|
+
* Concurrent writes are serialized so one call cannot overwrite another
|
|
51
|
+
* caller's keys. After the file is written, the store fsyncs the file
|
|
52
|
+
* descriptor when the path exists so crash/kill is less likely to drop tokens.
|
|
53
|
+
*
|
|
33
54
|
* @param updates - Environment variable names and values to persist.
|
|
34
55
|
* @returns An Effect that succeeds after `.env` is written.
|
|
35
56
|
*
|
|
@@ -40,18 +61,44 @@ export class DotEnvCredentialStore {
|
|
|
40
61
|
*/
|
|
41
62
|
write(updates) {
|
|
42
63
|
const envPath = this.getEnvPath();
|
|
43
|
-
|
|
64
|
+
const writeLock = writeLockForEnvPath(envPath);
|
|
65
|
+
return writeLock.withPermits(1)(Effect.try({
|
|
44
66
|
try: () => {
|
|
45
|
-
|
|
46
|
-
const
|
|
67
|
+
// Drop non-string values so a missing refresh_token cannot persist as empty.
|
|
68
|
+
const sanitizedUpdates = {};
|
|
69
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
70
|
+
if (typeof value === 'string') {
|
|
71
|
+
sanitizedUpdates[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const existingEnv = existsSync(envPath)
|
|
75
|
+
? dotenv.parse(readFileSync(envPath, 'utf-8'))
|
|
76
|
+
: {};
|
|
77
|
+
const safeEnvContent = stringify({ ...existingEnv, ...sanitizedUpdates });
|
|
47
78
|
writeFileSync(envPath, safeEnvContent, 'utf-8');
|
|
79
|
+
// Durability: best-effort fsync. Windows and mocked FS may reject fsync
|
|
80
|
+
// on a read-only open; never fail the write after a successful writeFileSync.
|
|
81
|
+
if (existsSync(envPath)) {
|
|
82
|
+
try {
|
|
83
|
+
const fd = openSync(envPath, 'r+');
|
|
84
|
+
try {
|
|
85
|
+
fsyncSync(fd);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
closeSync(fd);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Ignore fsync failures — tokens are already on disk for the write path.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
48
95
|
},
|
|
49
96
|
catch: (cause) => new CredentialStoreError({
|
|
50
97
|
envPath,
|
|
51
98
|
message: `Failed to write credential updates to ${envPath}`,
|
|
52
99
|
cause,
|
|
53
100
|
}),
|
|
54
|
-
});
|
|
101
|
+
}));
|
|
55
102
|
}
|
|
56
103
|
}
|
|
57
104
|
/**
|
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) {
|
|
@@ -265,6 +283,15 @@ export class EbayOAuthClient {
|
|
|
265
283
|
Authorization: `Basic ${credentials}`,
|
|
266
284
|
},
|
|
267
285
|
});
|
|
286
|
+
// Authorization-code exchange must return a refresh token; without it we
|
|
287
|
+
// cannot persist durable user credentials and must not claim success.
|
|
288
|
+
const refreshToken = tokenData.refresh_token;
|
|
289
|
+
if (typeof refreshToken !== 'string' || refreshToken.length === 0) {
|
|
290
|
+
return yield* Effect.fail(new EbayOAuthError({
|
|
291
|
+
operation: 'exchangeCodeForToken',
|
|
292
|
+
message: 'eBay token response did not include a non-empty refresh_token; cannot complete authorization code exchange',
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
268
295
|
this.userTokens = createStoredUserTokensFromResponse({
|
|
269
296
|
config: this.config,
|
|
270
297
|
tokenData,
|
|
@@ -273,13 +300,13 @@ export class EbayOAuthClient {
|
|
|
273
300
|
yield* this.credentialStore
|
|
274
301
|
.write({
|
|
275
302
|
EBAY_USER_ACCESS_TOKEN: tokenData.access_token,
|
|
276
|
-
EBAY_USER_REFRESH_TOKEN:
|
|
303
|
+
EBAY_USER_REFRESH_TOKEN: refreshToken,
|
|
277
304
|
})
|
|
278
305
|
.pipe(Effect.mapError((cause) => mapCredentialStoreError('exchangeCodeForToken', 'Failed to persist exchanged user tokens', cause)));
|
|
279
306
|
this.config = {
|
|
280
307
|
...this.config,
|
|
281
308
|
accessToken: tokenData.access_token,
|
|
282
|
-
refreshToken:
|
|
309
|
+
refreshToken: refreshToken,
|
|
283
310
|
};
|
|
284
311
|
return tokenData;
|
|
285
312
|
});
|
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/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { validateEnvironmentConfig } from './config/environment.js';
|
|
4
4
|
import { createEbayMcpRuntime } from './mcp/runtime.js';
|
|
5
|
+
import { runDiagnostics } from './scripts/diagnostics.js';
|
|
5
6
|
import { runSetup } from './scripts/setup.js';
|
|
6
7
|
import { runSkillsWizard } from './scripts/skills.js';
|
|
7
8
|
import { getErrorMessage } from './utils/errors.js';
|
|
@@ -29,6 +30,10 @@ if (args.includes('setup')) {
|
|
|
29
30
|
if (args.includes('skills')) {
|
|
30
31
|
await runCliCommand('Skills install', runSkillsWizard);
|
|
31
32
|
}
|
|
33
|
+
if (args.includes('diagnose')) {
|
|
34
|
+
const exportReport = args.includes('--export') || args.includes('-e');
|
|
35
|
+
await runCliCommand('Diagnostics', () => runDiagnostics(exportReport));
|
|
36
|
+
}
|
|
32
37
|
/**
|
|
33
38
|
* eBay API MCP Server
|
|
34
39
|
* Provides access to eBay APIs through Model Context Protocol
|
package/build/mcp/runtime.js
CHANGED
|
@@ -6,26 +6,36 @@ 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) {
|
|
13
|
+
// JSON.stringify(undefined) returns undefined (not a string), which produces an
|
|
14
|
+
// invalid MCP content block and triggers client -32602 on empty-body 201/204s.
|
|
15
|
+
const successDocument = result === undefined || result === null ? { status: 'success' } : result;
|
|
13
16
|
return {
|
|
14
17
|
content: [
|
|
15
18
|
{
|
|
16
19
|
type: 'text',
|
|
17
|
-
text: JSON.stringify(
|
|
20
|
+
text: JSON.stringify(successDocument, null, 2),
|
|
18
21
|
},
|
|
19
22
|
],
|
|
20
23
|
};
|
|
21
24
|
}
|
|
22
25
|
function formatToolFailure(error) {
|
|
23
|
-
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
|
+
}
|
|
24
34
|
return {
|
|
25
35
|
content: [
|
|
26
36
|
{
|
|
27
37
|
type: 'text',
|
|
28
|
-
text: JSON.stringify(
|
|
38
|
+
text: JSON.stringify(payload, null, 2),
|
|
29
39
|
},
|
|
30
40
|
],
|
|
31
41
|
isError: true,
|
|
@@ -53,9 +63,10 @@ function registerTool(server, api, entry, logToolExecution, ui) {
|
|
|
53
63
|
? buildUiToolResult(entry.ui, result)
|
|
54
64
|
: formatToolSuccess(result);
|
|
55
65
|
}), Effect.catchAll((error) => {
|
|
56
|
-
const errorMessage = getErrorMessage(error);
|
|
57
66
|
if (logToolExecution) {
|
|
58
|
-
toolLogger.error(`Tool ${definition.name} failed`, {
|
|
67
|
+
toolLogger.error(`Tool ${definition.name} failed`, {
|
|
68
|
+
error: getEbayErrorDetails(error).message,
|
|
69
|
+
});
|
|
59
70
|
}
|
|
60
71
|
return Effect.succeed(formatToolFailure(error));
|
|
61
72
|
})));
|