yandex-market-mcp
# yandex-market-mcp
**English** · [Русский](README.ru.md)
An [MCP](https://modelcontextprotocol.io) server that gives LLM agents live, honestly-labelled data from
[Yandex Market](https://market.yandex.ru): search with real pages, the site's sort orders and price windows;
seller cards with the three prices kept apart; all sellers' offers of a product; reviews at the right level;
buyers' questions; seller details with the legal entity.
> Buyer side: reads the public storefront pages, no Yandex account needed. Looking for your **seller
> cabinet** (the official Partner API)? That is a different tool, e.g.
> [dontsovcmc/mcp-server-yandex-market-seller](https://github.com/dontsovcmc/mcp-server-yandex-market-seller).
## Why another Yandex Market server
Market's pages are full of numbers that look like one thing and are another. Scrapers that take the big
number on the page return data a buyer never pays. This server is built around not doing that:
| Market trap | What this server does |
|---|---|
| The big green price on tiles, cards and in schema.org data is the price **with the Yandex Pay card**; the plain price is visible only in the «Детали цены» popup | `price_rub` = without the card (from the site's own price-details popup, cross-checked with the cart price), `price_with_pay_card_rub` = with the card, `price_before_discount_rub` = crossed-out. A missing plain price is `null` with a reason — the Pay price is never substituted |
| «Скидка 41%» is counted from the Pay-card price | `discount_percent_vs_pay_price` (the site's number) next to `discount_percent_without_card` |
| The search price filter and price sorting work on the Pay-card price | Every answer says so (`price_filter.basis`, `sort_basis`) |
| The rating on a card belongs to **the seller's card**, which can pool several different products (one seller's card pools seven Fiskars axes, 745 ratings) | `rating_of_this_card` with `pools_other_products`, checked against the SKUs of the reviews shown on the card; `get_reviews(scope="variant")` returns only this product's reviews |
| «Отзывы и оценки у всех продавцов» is a **model group** that merges different products (JBL Tune 520BT + 720BT + 770NC) | `model_group_rating` with an explicit scope; every model review says which product it is about |
| Page 1 of "popular" is almost entirely ads | `sponsored` on every tile, `sponsored_on_page`, optional filtering; the instructions steer depth to price sorting and windows |
| One product from several sellers = several tiles; `model_id` merges different products | Tiles deduplicated by offer id; `same_sku_several_offers` groups sellers of one SKU; the instructions say never to dedupe by model id |
| Promo codes and «−200 ₽ с доставкой по клику» look like price cuts | `conditional_offers` with threshold, code and end date — never subtracted; a promo that exists in the data but is not shown on the card is not offered |
| Cross-border offers, markdown («Уценка»), «Версия: для других стран», «Оригинал» | `cross_border` with the site's duty rule (order above 200 € or 31 kg), `markdown` with the seller's condition text, `version_on_card`, and «Оригинал» explained as "the seller provided documents" |
| Market picks the city from the IP; `lr` and `yandex_gid` are ignored, choosing an address needs a login | No fake `city` parameter: every answer echoes the region from Market's own page, and a loud note appears if it is not the expected one (VPN) |
| «6.9K оценок», «72K» | Numbers with `approximate: true` (exact counts are used where the page has them) |
| Every normal page contains an empty `CaptchaService` widget; real captchas and "empty frames" (a page with no data) do happen | Captchas are recognised by URL and SmartCaptcha markers only; empty frames and foreign pages are retried once, then reported as errors, never as data |
Every response carries `fetched_at` (UTC) and `city`.
## Tools
| Tool | What it returns |
|---|---|
| `search_products(query, page, sort, price_min, price_max, include_sponsored)` | Page 1 = 8 tiles, later pages 16; sort `popular` (default) / `price_asc` / `price_desc` / `rating`; `total_found`, `page_count`, `has_more`, `positions`. Per item: offer / card / model / SKU ids, title, URL, three prices and discounts, seller and `business_id`, `sponsored`, rating of the seller's card with count and `low_evidence`, badges, signals («Уценка», «Из-за рубежа»), stock, delivery dates |
| `get_product(product, include_specs, include_description, include_seller_legal)` | One seller's card (`product` = card id or `/card/` URL; specs on, description and legal entity off by default): three prices + `conditional_offers` + split flag; seller (rating, exact ratings count, orders, years on Market, status; legal entity on request); delivery options with dates and prices for the IP city; rating of the card with stars and the pooling check; «Все N предложений от X»; badges with their meaning; cross-border / markdown / version flags; stock; characteristics; description |
| `get_offers(model_id, sku_id, sort)` | All sellers' offers of one product as tiles (sort `price_asc` by default, also `popular` / `price_desc` / `rating` / `delivery`) and the labelled model-group rating |
| `get_reviews(product, scope, sort, page)` | 10 per page; `scope` `card` (default) / `variant` / `model`; sort `helpful` (default) / `newest` / `best` / `worst`. Date (ISO; «3 января», «Вчера» resolved), stars, pros, cons, comment, votes, photo count, the product each review is about |
| `get_questions(product, page)` | 10 per page; questions with the answers shown on the site, exact dates, `by_seller` |
| `get_seller(business_id)` | Rating, exact ratings count and star split, orders, years on Market, status; legal entity (name, INN, OGRN, address, cross-border flag) |
| `compare_products(products)` | Up to 10 cards side by side: three prices, card rating with the pooling flag, seller, fastest delivery, «все предложения от» |
Author names, user ids and order numbers never appear in answers; names in sellers' greetings («Здравствуйте, Анна!»,
«Анна, добрый день!»), e-mails and phone numbers in texts are masked.
## Requirements
Measured on 2026-09-25 (Linux, Python 3.13, a Russian home IP):
| | |
|---|---|
| Python | ≥ 3.10, with [uv](https://docs.astral.sh/uv/) (or pip) |
| Browser | **Not needed.** Plain HTTP with a Chrome TLS fingerprint ([curl_cffi](https://github.com/lexiforest/curl_cffi)) gets the same pages a browser does |
| Display | Not needed |
| Docker | Not needed |
| Disk | ≈ 85 MB for the environment (curl_cffi 38 MB, the rest is the MCP SDK and pydantic) |
| Memory | Peak RSS of the server process 110–113 MB during a full run of all tools (two runs; pages are 1.3–2.6 MB of HTML) |
| Cold start | 0.4–0.5 s until the MCP client sees the tools (0.7 s via `uvx` with a warm uv cache); no warm-up requests |
| Request time | A page answers in 0.6–1.2 s; requests are spaced 4–5 s apart, so a call takes ≈ 1 s when idle, ≈ 5 s back-to-back; `get_product` with the legal entity 14–16 s (3 requests); `compare_products` ≈ 5 s per card |
| Network | A Russian home or residential IP. Market maps the city from it; datacenter IPs abroad have been reported to get a captcha at once |
## Install
Claude Code:
```bash
claude mcp add yandex-market -- uvx --from git+https://github.com/SZhukovWork/yandex-market-mcp yandex-market-mcp
```
Any MCP client (`claude_desktop_config.json`, `.mcp.json`, …):
```json
{
"mcpServers": {
"yandex-market": {
"command": "uvx",
"args": ["--from", "git+https://github.com/SZhukovWork/yandex-market-mcp", "yandex-market-mcp"]
}
}
}
```
From a checkout: `uv venv && uv pip install -e . && .venv/bin/yandex-market-mcp`.
## Configuration (environment variables)
| Variable | Default | Meaning |
|---|---|---|
| `YM_EXPECTED_REGION` | `54` (Екатеринбург) | Region id you expect Market to assign to your IP (213 Москва, 2 Санкт-Петербург). If the page says otherwise, answers carry a `WARNING` note — usually a VPN. It does not change the city: nothing can, without logging in |
| `YM_MIN_INTERVAL` | `4.0` | Seconds between requests (never below 3). The IP is usually shared with your own browsing |
| `YM_CAPTCHA_COOLDOWN` | `1800` | After a captcha the server does not query the site for this many seconds, also across restarts |
| `YM_RATE_LIMIT_COOLDOWN` | `600` | The same pause after HTTP 403 / 429 persisted through one retry |
| `YM_PROXY` | — | Proxy URL, e.g. `http://user:pass@host:3128` (the city then follows the proxy's IP) |
| `YM_CACHE_DIR` | `~/.cache/yandex-market-mcp` (honours `XDG_CACHE_HOME`) | Where the cookies (`session.json`, 0600; directory 0700) and the pause marker live |
`yandex-market-mcp status` shows whether a pause is active; `yandex-market-mcp unblock` clears it (for example
after you solved the captcha in your own browser).
## What the numbers mean
- **`price_rub`** — the price without the Yandex Pay card for an anonymous buyer in the reported city. Use it for
comparisons and the landed price. **`price_with_pay_card_rub`** applies only when paying with the Pay card.
**`price_before_discount_rub`** is the crossed-out «Обычная цена» and can be inflated (Fiskars X21 was seen at
«79 567 → 33 915» while honest sellers ask about 9 000).
- **`conditional_offers`** — promo codes and offers with conditions (minimum order, delivery mode, shop
subscribers). They are listed, never subtracted.
- **`rating_of_this_card`** — the seller's card; `pools_other_products: true` means the reviews shown on the card
are partly about other products of the seller. **`model_group_rating`** — all sellers of a Market "model",
which can merge different products. There is no separate rating of one variant on Market; its reviews are
available with `get_reviews(scope="variant")`. `low_evidence` = fewer than 10 ratings.
- **Delivery** — dates and prices for the IP city without an address, as the card shows them; «Доставка
Маркета» options are often 0 ₽, sellers' couriers can cost more. A missing price means unknown.
- **`stock_left`** — units the offer has according to the page.
- **`all_offers.from_pay_card_price_rub`** — «Все N предложений от X»: X is the lowest price with the Pay card.
## Limitations
- **City = the IP's city.** Market ignores `lr` / `yandex_gid`; choosing an address requires a login. Prices and
delivery for another city are not available. Whether prices depend on the city at all was not verified.
- **Search depth: at most 30 pages (≈ 470 items) per query, sort and price window.** Page 1 has 8 tiles, later
pages 16. With more results the answer carries `depth_cap`: split the price range into windows.
- **`get_offers` shows at most 16 offers per sort order.** Market's server-rendered offers page ignores `page`
(checked: page 2 returns page 1 again). Call it with other sorts to see more offers.
- **No price history** — Market removed it in 2022 and the pages do not carry it.
- **Anonymous prices only.** The site says «Войдите, и станет дешевле»: prices after login, personal discounts,
split amounts, Plus points and cashback are not available (split is a yes/no flag; the raw
`isPersonalDiscount` flag is passed as `personal_discount_flag_raw`, its meaning unconfirmed).
- **Search totals count loose matches and drift**: the same query reported 2308 and, an hour later, 419
results; a nonsense query «finds» thousands. Check titles; do not treat `total_found` as a catalogue size.
- **Legal entity of a seller** comes from the site's seller-info popup, which the page loads with a POST to
`/api/render-lazy` — a path that robots.txt disallows. Only `get_seller` and
`get_product(include_seller_legal=true)` make that request; everything else reads regular pages.
- **Customs duty** — Market's rule for cross-border orders (duty above 200 € or 31 kg per order) is passed on; a
fixed «Пошлина N ₽» is reported only when a tile shows one.
- **Unofficial**: the server reads the state Market embeds in its pages. Market changes it without notice;
parsers are isolated in `parse.py` and covered by tests on recorded pages. Implausible pages become errors.
- **Captcha**: the server never solves or bypasses it. It stops and pauses (30 min by default).
## Roadmap
- Optional account mode (personal prices after login) and the cart — later.
- Checkout, payment and address changes are out of scope.
## Development
```bash
uv venv && uv pip install -e '.[dev]'
.venv/bin/pytest # offline tests on recorded, anonymised pages
.venv/bin/pytest -m live -s # every tool over MCP stdio against the live site (14 requests, ~1.5 min)
```
Please run the live test rarely: the IP is shared with your own browsing, and a captcha pauses the server for
30 minutes.
## Disclaimer & credits
Not affiliated with Yandex. The server reads Market's internal page state, not an official API; use it for
personal price research at a human request rate and respect Market's terms of use. Ideas for recognising real
captchas (URL and SmartCaptcha markers, not the substring "captcha"), "empty frame" pages and retrying an empty
redirect come from [Vladimir-Human/ru-marketplace-mcp](https://github.com/Vladimir-Human/ru-marketplace-mcp)
(MIT); no code was copied.
License: MIT.
TDQS
Scored across 7 tools
Each tool targets a distinct resource and action: search, single product card, all offers, comparison, reviews, questions, and seller info. Even the overlapping get_product and get_offers are clearly separated by scope (one seller vs. all sellers).
All tool names follow a consistent verb_noun snake_case pattern: search_products, get_product, get_offers, compare_products, get_reviews, get_questions, get_seller. No mixed conventions or vague verbs.
Seven tools is well-scoped for a marketplace research API. Each tool covers a distinct user need without redundancy or bloat.
The set covers the full product research lifecycle: discovery (search), deep dive (product, offers, compare), social proof (reviews, questions), and seller trust (seller info). No obvious dead ends or missing core operations for a read-only marketplace tool.