Content
# ru-marketplace-mcp
[](https://github.com/Vladimir-Human/ru-marketplace-mcp/actions/workflows/ci.yml)
[](https://www.python.org/downloads/)
[](LICENSE)
[](https://modelcontextprotocol.io)
**MCP servers for Russian marketplaces.** Prices, availability, ratings, reviews, and
seller details from Wildberries, Ozon, Yandex Market, and Detsky Mir. Plus,
price comparison across all sources with a single call.
Read-only. No API keys, tokens, or registration required.
[English version below](#english-version) · [Architecture](docs/ARCHITECTURE.md) ·
[How to add a source](docs/ADDING_A_SOURCE.md) · [About anti-bot](docs/ANTI_BOT.md)
---
## What's inside
| Server | Tools | Access | Capabilities |
|---|---|---|---|
| **Wildberries** | 9 | anonymous HTTP | Search, product cards, reviews, product questions, seller details, catalog, and category products |
| **Yandex Market** | 3 | anonymous HTTP | Prices from different sellers, rating breakdown by stars, reviews |
| **Detsky Mir** | 4 | anonymous HTTP | Children's products, availability in offline stores, categories |
| **Ozon** | 4 | TLS impersonation, then your Chrome | Search, product cards, reviews |
| **Comparison** | 2 | queries all listed | "Where is it cheaper?" with a single call |
A total of 22 tools in 5 stdio servers on the shared `mcp-core` runtime.
## Quick Start
**Python 3.12+** and [uv](https://docs.astral.sh/uv/) are required.
```bash
git clone https://github.com/Vladimir-Human/ru-marketplace-mcp.git
cd ru-marketplace-mcp
uv sync --all-packages
uv run pytest -q # 406 offline tests, no network needed
```
Live endpoint check:
```bash
uv run python -c "
import asyncio
from wb_connector.server import wb_selfcheck
print(asyncio.run(wb_selfcheck()).status) # waiting for success
"
```
## Connecting to an MCP Client
Each server is a console command, so paths are not hardcoded in the config.
<details open>
<summary><b>Claude Desktop</b> — <code>claude_desktop_config.json</code></summary>
Windows: `%APPDATA%\Claude\claude_desktop_config.json`
macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
```jsonc
{
"mcpServers": {
"wildberries": {
"command": "uv",
"args": ["run", "--directory", "C:/path/to/ru-marketplace-mcp", "wb-mcp"]
},
"yandex-market": {
"command": "uv",
"args": ["run", "--directory", "C:/path/to/ru-marketplace-mcp", "yandex-mcp"]
},
"detsky-mir": {
"command": "uv",
"args": ["run", "--directory", "C:/path/to/ru-marketplace-mcp", "detmir-mcp"]
},
"ozon": {
"command": "uv",
"args": ["run", "--directory", "C:/path/to/ru-marketplace-mcp", "ozon-mcp"]
},
"compare-prices": {
"command": "uv",
"args": ["run", "--directory", "C:/path/to/ru-marketplace-mcp", "compare-mcp"]
}
}
}
```
Use forward slashes `/` or double backslashes `\\` for paths.
</details>
<details>
<summary><b>Claude Code</b></summary>
```bash
claude mcp add wildberries -- uv run --directory /path/to/ru-marketplace-mcp wb-mcp
claude mcp add yandex-market -- uv run --directory /path/to/ru-marketplace-mcp yandex-mcp
claude mcp add detsky-mir -- uv run --directory /path/to/ru-marketplace-mcp detmir-mcp
claude mcp add ozon -- uv run --directory /path/to/ru-marketplace-mcp ozon-mcp
claude mcp add compare-prices -- uv run --directory /path/to/ru-marketplace-mcp compare-mcp
```
</details>
<details>
<summary><b>Cursor</b> — <code>.cursor/mcp.json</code></summary>
```jsonc
{
"mcpServers": {
"compare-prices": {
"command": "uv",
"args": ["run", "--directory", "/path/to/ru-marketplace-mcp", "compare-mcp"]
}
}
}
```
</details>
<details>
<summary><b>Another stdio client</b></summary>
Run `uv run --directory /path/to/repository <command>`, where the command is one of
`wb-mcp`, `ozon-mcp`, `yandex-mcp`, `detmir-mcp`, `compare-mcp`. Servers communicate
via JSON-RPC over stdin and stdout, and diagnostics are written to stderr.
</details>
After connecting, restart the client and ask the agent to call `wb_selfcheck`. It
will check all endpoint families and respond with `success`, `drift_detected`, or
`inconclusive`.
## Tools
### Wildberries — `wb_*`
| Tool | Capabilities |
|---|---|
| `wb_search(query, page)` | Text search, up to 100 products per page with prices and stock |
| `wb_card(nm_ids)` | Batch request for up to 100 known SKUs |
| `wb_root_info(nm_id)` | Finds `imt_id` (required for reviews) and color variants |
| `wb_reviews(imt_id, limit, sort)` | Reviews pool. Key is `imt_id`, not `nm_id` |
| `wb_questions(imt_id, limit, skip, answered_only)` | Buyer questions and seller answers. Also by `imt_id` |
| `wb_seller(supplier_id)` | Legal entity, INN, KPP, OGRN, legal address |
| `wb_categories(root, max_depth)` | Catalog tree with shards and WB queries |
| `wb_category_products(shard, query, page, sort, dest)` | Category products by `shard` and `query` from `wb_categories` |
| `wb_selfcheck()` | Canary for format drift |
`wb_seller` answers the question that the product card hides: who is actually
selling? Returns the registered legal entity and tax numbers. This distinguishes
an official brand store from a reseller with a similar name.
`wb_questions` fills another gap. Reviews tell you what it's like to own a product;
questions clarify what the product actually is — "10 or 16 amps," "is the cable
included?" The seller's response is often the only public statement about this.
The pool is shared across all product variants, keyed by `imt_id` from `wb_root_info`.
`wb_category_products` closes the loop with `wb_categories`: the former returns
`shard` and `query`, and the latter returns products by them. The format of
elements matches `wb_search`, so category traversal and text search are directly
comparable. Some large sections of WB are marked with the shard `blackhole` —
they have no listings, and the tool honestly reports this instead of an empty list.
### Yandex Market — `yandex_*`
| Tool | Capabilities |
|---|---|
| `yandex_search(query, page, limit)` | Search with both prices, ratings, and sellers |
| `yandex_card(product_id, include_reviews)` | Full product card: rating breakdown and reviews |
| `yandex_selfcheck()` | Canary for format drift |
**Two prices, always.** `price_rub` is what any buyer pays. `price_with_plus`
requires a Yandex Plus subscription and is usually 25–30% lower. The Yandex
interface displays the second price prominently, so calling it without
qualification means promising a price that a non-subscriber won't get.
`rating_stars` provides a distribution like `{1: 10, 2: 3, 3: 10, 4: 19, 5: 502}`.
From this, you can see if the average rating of 4.8 is honest or if it's hiding a
batch of ones.
### Detsky Mir — `detmir_*`
| Tool | Capabilities |
|---|---|
| `detmir_categories(parent, limit, region)` | Catalog tree. Start here |
| `detmir_category(alias, limit, offset, region)` | Category products with a real counter |
| `detmir_card(product_id, region)` | Price, rating, online and in-store availability |
| `detmir_selfcheck()` | Canary for format drift |
**Region is set for each call.** Prices and especially offline store availability
strongly depend on the city: the same product was available in 152 Moscow stores,
37 in St. Petersburg, and 2 in Khabarovsk. The `region` parameter overrides
`DETMIR_REGION`, so you can compare cities in one session.
**No text search here, and that's intentional.** The Detsky Mir API silently
ignores any text filters and returns the entire catalog of 300,000 items, while
the site's search route returns a 404 with a promo carousel. A search tool would
confidently return incorrect products, so navigation is done through categories.
See [docs/ANTI_BOT.md](docs/ANTI_BOT.md) for details.
### Ozon — `ozon_*`
| Tool | Capabilities |
|---|---|
| `ozon_search(query)` | Text search |
| `ozon_card(sku_or_path)` | Product card |
| `ozon_reviews(sku_or_path, limit, sort)` | Reviews |
| `ozon_selfcheck()` | Canary for format drift |
Ozon rejects data center traffic, so the connector is two-tiered. First, TLS
impersonation. If Cloudflare issues a challenge, the request is made within your
logged-in Chrome via the DevTools Protocol. Nothing is stored: you perform the
login yourself, in the browser you control. Setup is described in
[docs/CDP_SETUP.md](docs/CDP_SETUP.md).
From a Russian home IP, the first tier usually works, and no browser is needed.
### Price Comparison — `compare_*`
| Tool | What it does |
|---|---|
| `compare_prices(query, per_source_limit, sources)` | All marketplaces at once, with ranking |
| `compare_sources()` | Which marketplaces are available in this installation |
```
compare_prices("men's sneakers")
wildberries 712 ₽ Breathe easy sports sneakers
wildberries 814 ₽ Warm winter sneakers with fur
yandex_market 2499 ₽ A-LOW sneakers
yandex_market 3480 ₽ Sneakers
cheapest: wildberries 712 ₽, spread 5858 ₽, complete: true
```
Marketplaces are queried in parallel, and each reports on its own. If one
is blocked, the comparison doesn't break: `complete: false` along with `source_outcomes`
will show what you see. Subscription prices don't participate in ranking.
## Configuration
All parameters are set with environment variables with a connector prefix. All
are optional.
| Prefix | Main parameters |
|---|---|
| `WB_` | `TIMEOUT`, `MIN_GAP`, `DEFAULT_DEST`, `NET_RETRIES`, `MAX_BODY_BYTES`, `CACHE_TTL`, `PROXY` |
| `YANDEX_` | `TIMEOUT`, `MIN_GAP`, `CACHE_TTL`, `PROXY` |
| `DETMIR_` | `REGION` (`RU-MOW`, `RU-SPE` and others), `CACHE_TTL`, `PROXY` |
| `OZON_` | `TIMEOUT`, `MIN_GAP`, `IMPERSONATE`, `CACHE_TTL`, `PROXY` |
| `CHROME_` | `CDP_PORT`, `SCRAPING_PROFILE`, `BINARY`, `HEADLESS`, `STEALTH` |
| `COMPARE_` | `SOURCE_TIMEOUT` |
| `MCP_` | `TRANSPORT` (`stdio` by default, or `http`), `HTTP_HOST`, `HTTP_PORT` |
`*_CACHE_TTL=0` disables the cache. `*_PROXY` overrides standard `HTTPS_PROXY` and
`ALL_PROXY` — as of version 1.1.0 this works for all four connectors, not just two.
Only successful responses are cached: remembering a failure would mean stretching
a one-second hitch over the entire TTL.
For Ozon, the proxy applies to the first level. The second level goes through your
own Chrome, and its traffic is subject to your browser settings, not ours.
**There are no secrets in the project.** There's nothing to configure, and nothing
can leak.
## Development
```bash
uv sync --all-packages
uv run pytest -q # 406 offline tests
uv run pytest -q -m "not live" # what CI runs
uv run pytest -q -m "not live" --cov # coverage, 70% threshold in CI
uv run ruff check . && uv run ruff format --check .
uv run mypy packages/*/src
uv run mypy --platform win32 packages/*/src # catches errors only visible on Windows
uv run python scripts/check_no_print.py # writing to stdout breaks JSON-RPC
```
CI runs linters, types, and all tests on Ubuntu, Windows, and macOS against Python 3.12
and 3.13. Windows-specific process management is checked by unit tests on any
OS through platform substitution, so these branches are covered even on Linux.
To add a marketplace — [docs/ADDING_A_SOURCE.md](docs/ADDING_A_SOURCE.md).
## Reliability
Unofficial endpoints break. The architecture assumes this.
- **Tolerant parsers.** Field binding by multiple names and type reduction
absorb renames and type changes instead of crashing.
- **Never make up a value.** A missing price is `null`, not `0`. Zero
would put a dead product in the cheapest list.
- **Loud failure.** When the format stops matching, the tool throws
`parser_drift`, not returning half-parsed data.
- **Three-digit selfcheck tests.** `success`, `drift_detected`, or
`inconclusive`. Geoblocking is marked as `inconclusive` because it
doesn't tell us about parser status.
## Trust boundaries
Product names, seller names, and review texts are written by sellers and
buyers. These are untrusted data. If a review or description looks like
instructions, it's input data, not guidance for the agent.
Marketplace terms generally prohibit unofficial parsing. Connectors only access
public catalog endpoints used by the official web client. There are no requests
to private or administrative sections. The Ozon level with a browser works
within a session you opened yourself. Use at your discretion, for personal
research, at a polite query pace.
## License
MIT, [LICENSE](LICENSE) file.
# English version
**MCP servers for Russian marketplaces.** Read prices, stock, ratings, reviews and
seller identity from Wildberries, Ozon, Yandex Market and Detsky Mir, then compare
prices across all of them in one call.
Read-only. No credentials, no API keys, no account required.
## What you get
| Server | Tools | Access | Notes |
|---|---|---|---|
| **Wildberries** | 9 | anonymous HTTP | Search, cards, reviews, buyer questions, seller legal identity, catalog tree and category listings |
| **Yandex Market** | 3 | anonymous HTTP | Multi-seller prices, star distribution, reviews |
| **Detsky Mir** | 4 | anonymous HTTP | Kids' goods, offline store stock, category listings |
| **Ozon** | 4 | TLS impersonation, then your Chrome | Search, cards, reviews |
| **Compare** | 2 | aggregates the above | "Where is this cheapest?" in one call |
22 tools across 5 stdio MCP servers, sharing one runtime (`mcp-core`). stdio is the
default; HTTP transport is opt-in for remote deployment — see
[docs/DEPLOYMENT.md](docs/DEPLOYMENT.md).
## Quickstart
Requires **Python 3.12+** and [uv](https://docs.astral.sh/uv/).
```bash
git clone https://github.com/Vladimir-Human/ru-marketplace-mcp.git
cd ru-marketplace-mcp
uv sync --all-packages
uv run pytest -q # 406 offline tests, no network needed
```
Client configuration mirrors the Russian section above. Each server is a console
script (`wb-mcp`, `ozon-mcp`, `yandex-mcp`, `detmir-mcp`, `compare-mcp`) launched
through `uv run --directory /path/to/repo <script>`.
After connecting, ask your agent to run `wb_selfcheck`. It probes every endpoint
family and reports `success`, `drift_detected`, or `inconclusive`.
## The tools
### Wildberries — `wb_*`
| Tool | What it does |
|---|---|
| `wb_search(query, page)` | Text search, up to 100 products/page with prices and stock |
| `wb_card(nm_ids)` | Batch lookup for up to 100 known SKUs |
| `wb_root_info(nm_id)` | Resolves `imt_id` (needed for reviews) plus colour variants |
| `wb_reviews(imt_id, limit, sort)` | Review pool, keyed by `imt_id`, not `nm_id` |
| `wb_questions(imt_id, limit, skip, answered_only)` | Buyer questions with seller answers, also keyed by `imt_id` |
| `wb_seller(supplier_id)` | Registered entity, INN, KPP, OGRN, legal address |
| `wb_categories(root, max_depth)` | Catalog tree with WB's own shard/query selectors |
| `wb_category_products(shard, query, page, sort, dest)` | Products in a category, using those selectors |
| `wb_selfcheck()` | Drift canary |
`wb_seller` answers the question a listing hides: who actually ships this? It returns
the registered legal entity and tax ids, which is how you distinguish an official
brand store from a reseller trading under a lookalike name.
`wb_questions` covers a different gap. Reviews describe what owning the product is
like; questions clarify what it actually is — "10A or 16A?", "is the cable
included?" — and the seller's reply is often the only public statement of that fact.
One pool per `imt_id`, shared across every variant.
`wb_category_products` closes the loop `wb_categories` opens: that tool hands back
WB's `shard` and `query`, and this one fetches the products behind them. Items use
the same shape as `wb_search`, so a category walk and a text search are directly
comparable. Several of WB's largest sections carry the shard `blackhole` and have no
feed at all; the tool says so instead of returning an empty list.
### Yandex Market — `yandex_*`
| Tool | What it does |
|---|---|
| `yandex_search(query, page, limit)` | Search with both prices, ratings, sellers |
| `yandex_card(product_id, include_reviews)` | Full detail plus star breakdown and reviews |
| `yandex_selfcheck()` | Drift canary |
**Two prices, always.** `price_rub` is what anyone pays. `price_with_plus` needs a
paid Yandex Plus subscription and runs 25–30% lower. Yandex leads with the subscriber
price, so quoting it uncritically misstates the real cost.
`rating_stars` gives the distribution, for example `{1: 10, 2: 3, 3: 10, 4: 19, 5: 502}`.
That reveals whether a 4.8 average is earned or hides a cluster of complaints.
### Detsky Mir — `detmir_*`
| Tool | What it does |
|---|---|
| `detmir_categories(parent, limit, region)` | Catalog tree, start here |
| `detmir_category(alias, limit, offset, region)` | Products in a category, with real totals |
| `detmir_card(product_id, region)` | Price, rating, online and offline store stock |
| `detmir_selfcheck()` | Drift canary |
**Region is per call.** Prices and especially offline availability swing by city —
one item sat in 152 Moscow stores, 37 in St Petersburg, 2 in Khabarovsk. The
`region` parameter overrides `DETMIR_REGION`, so one session can compare cities.
**There is no text search, deliberately.** Detsky Mir's API silently ignores every
text filter and returns its entire 300k-item catalog; the website's search route
answers 404 and renders a promo carousel. A search tool would return confidently
wrong products, so discovery goes through categories instead. See
[docs/ANTI_BOT.md](docs/ANTI_BOT.md).
### Ozon — `ozon_*`
| Tool | What it does |
|---|---|
| `ozon_search(query)` | Text search |
| `ozon_card(sku_or_path)` | Product detail |
| `ozon_reviews(sku_or_path, limit, sort)` | Reviews |
| `ozon_selfcheck()` | Drift canary |
Ozon rejects datacenter traffic, so this connector is two-tier: TLS impersonation
first, then a fetch inside your own logged-in Chrome over the DevTools Protocol when
Cloudflare challenges. Nothing is stored; you log in yourself, in a browser you
control. Setup: [docs/CDP_SETUP.md](docs/CDP_SETUP.md).
From a Russian residential IP the first tier usually works and no browser is needed.
### Cross-marketplace — `compare_*`
| Tool | What it does |
|---|---|
| `compare_prices(query, per_source_limit, sources)` | Every marketplace at once, ranked |
| `compare_sources()` | Which marketplaces this install can query |
```
compare_prices("mens shoes")
wildberries 712 RUB Easy breathing sports shoes
wildberries 814 RUB Warm winter shoes with fur
yandex_market 2499 RUB A-LOW shoes
yandex_market 3480 RUB Shoes
cheapest: wildberries 712 RUB, spread 5858 RUB, complete: true
```
Sources are queried concurrently and each reports its own outcome. One marketplace
being blocked never sinks the comparison: `complete: false` plus `source_outcomes`
tells you exactly what you are looking at. Subscription prices never win the ranking.
## Configuration
Every setting is an environment variable with a per-connector prefix. All optional.
| Prefix | Common knobs |
|---|---|
| `WB_` | `TIMEOUT`, `MIN_GAP`, `DEFAULT_DEST`, `NET_RETRIES`, `MAX_BODY_BYTES`, `CACHE_TTL`, `PROXY` |
| `YANDEX_` | `TIMEOUT`, `MIN_GAP`, `CACHE_TTL`, `PROXY` |
| `DETMIR_` | `REGION` (`RU-MOW`, `RU-SPE`, and others), `CACHE_TTL`, `PROXY` |
| `OZON_` | `TIMEOUT`, `MIN_GAP`, `IMPERSONATE`, `CACHE_TTL`, `PROXY` |
| `CHROME_` | `CDP_PORT`, `SCRAPING_PROFILE`, `BINARY`, `HEADLESS`, `STEALTH` |
| `COMPARE_` | `SOURCE_TIMEOUT` |
| `MCP_` | `TRANSPORT` (`stdio` default, or `http`), `HTTP_HOST`, `HTTP_PORT` |
`*_CACHE_TTL=0` disables caching. `*_PROXY` overrides the standard
`HTTPS_PROXY`/`ALL_PROXY` — as of 1.1.0 that works on all four connectors, not two.
Only successful reads are cached: remembering a failure would stretch a one-second
blip across the whole TTL window.
Ozon's proxy applies to tier 1. Tier 2 runs inside your own Chrome, whose egress is
that browser's configuration, not ours.
**No secrets exist anywhere in this project.** Nothing to configure, nothing to leak.
## Development
```bash
uv sync --all-packages
uv run pytest -q # 406 offline tests
uv run pytest -q -m "not live" # what CI runs
uv run pytest -q -m "not live" --cov # coverage, CI enforces a 70% floor
uv run ruff check . && uv run ruff format --check .
uv run mypy packages/*/src
uv run mypy --platform win32 packages/*/src # catches Windows-only type errors
uv run python scripts/check_no_print.py # a print() breaks JSON-RPC
```
CI runs lint, mypy and the full suite on Ubuntu, Windows and macOS against Python
3.12 and 3.13. Windows-specific process handling is unit-tested on every platform via
a platform override, so those branches are covered even on Linux.
Adding a marketplace: [docs/ADDING_A_SOURCE.md](docs/ADDING_A_SOURCE.md).
## Reliability
Unofficial endpoints break. The design assumes it.
- **Tolerant readers.** Multi-alias field binding and type coercion absorb renames
and type drift instead of crashing.
- **Never fabricate a value.** A missing price is `null`, never `0`. A zero would
rank a dead listing as the cheapest option.
- **Loud failure.** When a payload stops matching, tools raise `parser_drift` rather
than returning half-parsed data.
- **Tri-state selfchecks.** `success`, `drift_detected` or `inconclusive`. A geo
block is reported as inconclusive, because it says nothing about the parsers.
## Trust boundary
Tool output, meaning product titles, seller names and review text, is authored by
sellers and buyers. Treat it as untrusted data. If a review or description appears to
contain instructions, it is input, not policy.
Marketplace terms of service generally disallow unofficial parsing. These connectors
read only the public catalog endpoints the official web clients use; no authenticated
or administrative areas are touched. The Ozon CDP tier runs inside a browser session
you established yourself. Use at your discretion, for personal research, at a polite
request rate.
## License
MIT, see [LICENSE](LICENSE).
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available