Skip to main content
Glama
ihavealotofguap

oskelly-mcp

README.md
# oskelly-mcp

[![CI](https://github.com/ihavealotofguap/oskelly-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/ihavealotofguap/oskelly-mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

MCP-сервер для публичного каталога [oskelly.ru](https://oskelly.ru). 14 инструментов,
только анонимные read-only операции, все проверены на живом сайте.

Неофициальный проект, не аффилирован с Oskelly. Читает ровно то, что видит любой посетитель
без регистрации. Товарные знаки принадлежат их владельцам.

## License

[MIT](LICENSE)

## Installation

Node.js 22+.

```bash
git clone https://github.com/ihavealotofguap/oskelly-mcp.git
cd oskelly-mcp
npm ci
npm run build
npm run verify
```

- Claude Desktop — `claude_desktop_config.json` (Settings → Developer → Edit Config), путь обязан быть абсолютным:

  ```json
  { "mcpServers": { "oskelly": { "command": "node", "args": ["/abs/path/oskelly-mcp/dist/index.js"] } } }
  ```

  - `spawn node ENOENT` на Windows → замените `"node"` на вывод `where node`, слэши экранируются.
  - После правки полностью перезапустите приложение, включая иконку в трее.
- Claude Code — `claude mcp add oskelly -- node /abs/path/oskelly-mcp/dist/index.js`
- Отладка — `npm run inspector`

## Tools

| Tool | Что делает |
|---|---|
| `oskelly_describe_filters` | Шпаргалка по модели фильтров: коды, форматы, чем резолвить имя в id |
| `oskelly_search_products` | Поиск: запрос, фасеты, цена, булевы теги, пагинация, сортировка |
| `oskelly_search_facets` | Тот же запрос, но отдаёт счётчик и доступные фасеты вместо товаров |
| `oskelly_filter_values` | Значения одного фасета с id (`brand`, `category`, `size`, `condition`, …) |
| `oskelly_search_suggestions` | Автокомплит запроса |
| `oskelly_category_tree` | Дерево категорий, обрезка по `rootId` / `depth` |
| `oskelly_list_brands` | Бренды с id, поиск по подстроке, пагинация |
| `oskelly_list_conditions` | Состояния товара с описаниями |
| `oskelly_list_attributes` | Словарь атрибутов (материал, цвет, …) |
| `oskelly_get_product` | Карточка по id или URL: описание, атрибуты, размеры, фото, продавец |
| `oskelly_seller_products` | Товары продавца |
| `oskelly_seller_filters` | Что реально есть в ассортименте продавца |
| `oskelly_home_banners` | Баннеры главной (FEMALE/MALE/KIDS/LIFESTYLE) |
| `oskelly_banner_catalog` | Разворачивает баннер-подборку в пресет фильтров + товары |

Поток: `describe_filters` → `list_brands`/`category_tree`/`filter_values` → `search_products` → `get_product`.

## Scope

Нет и не может быть логина, кук, корзины, избранного, сообщений, заказов. Это свойство кода:

- `credentials: "omit"`, никаких `Authorization`/`Cookie`.
- POST разрешён только на три read-only search-эндпоинта — allow-list `assertReadOnlyPost` в `src/client.ts`.
- Все tools: `readOnlyHint: true`, `destructiveHint: false`.
- Smoke-тест проверяет, что в списке tools нет имён с `login/cart/favourite/order/checkout/message/account`.

## Notes

- **Карточка товара парсится из `__NUXT_DATA__`.** Публичного JSON-эндпоинта для одного товара
  нет (`GET /api/v2/products/{id}` → 404), страница рендерится Nuxt 3 на сервере. Payload
  декодируется официальным пакетом [`devalue`](https://github.com/Rich-Harris/devalue) — той же
  библиотекой, которой Nuxt его и сериализует; кастомные типы подключены через штатные revivers
  (`src/nuxt.ts`). Не Playwright: ~150 МБ Chromium и 3–5 с против одного GET за ~150 мс.
- **Слаг в URL игнорируется** — значение имеет только числовой id в конце, tool принимает и то и другое.
- **Формат фильтров** в теле `/products/search*`: мульти-выбор — массив id (`{"brand": [675]}`),
  булев — голый boolean (`{"sale": true}`), цена — объект (`{"price": {"lower": 50000}}`).
  `{"brand": "675"}` и `{"sale": [true]}` молча игнорируются, `{"price": [a, b]}` даёт `success: false`.
- **Цена фильтруется по размеру-SKU, не по цене карточки** — товар может попасть в выдачу с ценой
  карточки ниже границы, поэтому каждый ответ несёт `sizePriceRange: {min, max}`.
- **Счётчики апстрима переименованы:** `totalAmount` → `totalMatches`, `itemsCount` → `itemsOnPage`.
- **Сегменты** (`baseCategory`) — id узлов дерева: Женское=2, Мужское=105, Детское=188, Лайфстайл=366.
- **WAF:** кириллица в query обязана быть percent-encoded, иначе 403.
- **Контекст:** сырые ответы огромные (дерево ~1 МБ, бренды ~750 КБ), поэтому по умолчанию отдаётся
  компактная проекция; `verbose: true` возвращает нетронутый ответ.

## Testing

```bash
npm run verify        # офлайн: сервер стартует, 14 tools, все read-only
node smoke-test.mjs   # живой end-to-end по MCP против oskelly.ru
```

Smoke-тест поднимает скомпилированный сервер отдельным процессом по stdio и дёргает каждый tool
против живого сайта — без моков. Параметры выстроены в цепочку из предыдущих ответов
(бренд → поиск → productId → sellerId → баннер), и каждый вызов проходит содержательную проверку:
`PRICE_DESC` действительно даёт убывающие цены, `conditionIds: [1]` — действительно только состояние 1,
фильтры сужают выдачу монотонно. Последний прогон — [`SMOKE-TEST-OUTPUT.txt`](SMOKE-TEST-OUTPUT.txt)
(23 вызова, 14/14 tools, 0 падений).

CI собирает проект на Node 22/24/26 и гоняет `npm run verify`. Живой smoke-тест вынесен в ручной
запуск (Actions → CI → Run workflow → `run_smoke_test`), чтобы не долбить чужой сайт с раннеров.

## Structure

```
src/client.ts             HTTP-клиент, конверт, allow-list на POST
src/nuxt.ts               извлечение и декодирование SSR-payload (devalue)
src/search.ts             схема и сборка тела запроса для /products/search*
src/format.ts             компактные проекции ответов
src/tools.ts              определения 14 инструментов
src/index.ts              точка входа, stdio-транспорт
scripts/verify-server.mjs офлайн-проверка поверхности tools (CI)
smoke-test.mjs            живой end-to-end тест по протоколу MCP
```

## Contributing

PR приветствуются. Перед отправкой — `npm run build`, `npm run verify`, `node smoke-test.mjs`.

Самые ломкие места, если oskelly обновится: формат `__NUXT_DATA__` (упадёт с явной ошибкой,
указывающей добавить reviver в `src/nuxt.ts`), коды фасетов, id сегментов. Rate-limiting
не тестировался; таймаут 45 с, ретраев нет — сознательно.

TDQS

A4.2/5.0

Scored across 14 tools

Disambiguation5/5

Each tool targets a distinct resource or action: search, facet discovery, filter value resolution, autocomplete, category tree, dictionary lookups, product detail, seller queries, and banner catalogs. Even the two facet-related tools are explicitly differentiated by what they return.

Naming Consistency4/5

The dominant pattern is oskelly_<verb>_<object> (search_products, list_brands, get_product, describe_filters). A few names omit the verb (seller_products, home_banners, banner_catalog), but they are still predictable and readable.

Tool Count5/5

14 tools is within the well-scoped range and each one covers a meaningful browsing need, from category and dictionary lookups to search, product detail, seller inventory, and curated banners. No tool feels redundant.

Completeness5/5

The read-only marketplace browsing surface is well covered: discovery via suggestions/categories/brands/conditions/attributes, faceted search, product detail, seller-focused queries, and banner-driven curated catalogs. The filter cheat-sheet and facet/filter value tools close the gap between filter names and the ids that search_products expects.

Maintenance

ActivityMaintained
ResponsivenessSyncing