面向俄罗斯和中国电商平台的 MCP 服务器。 从 Wildberries、Ozon、Яндекс…
数据截至 2026/11/3 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
git clone https://github.com/Vladimir-Human/ru-marketplace-mcp.git状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
安装兼容性检查需源码安装缺少 main/exports/bin 入口声明;仓库 package.json 标记 private,未发布到 npm,需从源码安装
展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/29 15:36:14
安全扫描自动 · 静态分析源码v2.7.0⚪ 扫描:无法判定尚无人工评估展开查看扫描说明与证据 ↓
已尝试扫描但取不到可分析的源码:仓库或包不存在、已删除,或为非 JS/TS 实现、未随包发布源码。无法判定,不代表安全。
- · 包内无可扫 JS
- · TS 源码(可能为非 JS 实现或未发布源码),无法判定
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
功能亮点
- 覆盖十一个电商平台
- 查询价格库存评分评价
- 查询卖家资质信息
展开完整介绍(156 字)↓
十一个电商平台和房产网站 Циан 做成 MCP 服务器:Wildberries、Ozon、Яндекс Маркет、Детский мир、Авито、AliExpress、Taobao、Мегамаркет、Lamoda、DNS、Ситилинк、Циан。再加一次调用比价所有商品来源。只读,不需要密钥。
README
由 DeepSeek 最新模型翻译生成展开完整文档(README 剩余 1 节)
ru-marketplace-mcp
面向俄罗斯和中国电商平台的 MCP 服务器。 从 Wildberries、Ozon、Яндекс Маркет、Детский мир、Авито、AliExpress、Taobao、Мегамаркет、Lamoda、DNS 和 Ситилинк 获取价格、库存、评分、评价和卖家信息。另外还有 Циан 的房产信息,以及一次调用就能对比所有商品来源的价格。
只读。不需要 API 密钥、token 和注册——反爬严格的平台通过你自己的 Chrome 读取。有一个可选的例外:可选的 MPStats 需要付费 token(MPSTATS_MP_AUTH)——不配置它,其余功能照常工作。
English version below · 第一次成功请求 · 架构 ·如何添加数据源 · 关于反爬
为浏览器验证增加了可选的标签页保留模式:CHROME_CHALLENGE_HANDOFF_S=120。验证完成后,同一客户端重复同一请求(在同一 MCP 会话中,或者通过 stdio 发往同一服务器进程)会继续读取这个标签页。支持范围和限制见 Chrome 配置。
包含什么
| 服务器 | 工具数 | 读取所需条件 | 能做什么 |
| Wildberries | 8 | 匿名 HTTP | 搜索、商品卡片、评价、商品提问、卖家信息、类目和类目商品 |
| Яндекс Маркет | 2 | 匿名 HTTP | 不同卖家的价格、星级评分分布、评价 |
| Детский мир | 3 | 匿名 HTTP | 儿童商品、线下门店库存、类目 |
| Ozon | 3 | 你的 Chrome;家用 IP 下常常不用它也行 | 搜索、商品卡片、评价 |
| Авито | 3 | 你的 Chrome + 俄罗斯家庭 IP,请求要错开节奏,否则按 IP 封禁 | 搜索商品、商品卡片、卖家信誉 |
| Taobao | 2 | 你的 Chrome,且已登录 Taobao | 搜索和商品卡片、人民币价格 |
| Мегамаркет | 2 | 你的 Chrome,且已登录——匿名会话调 API 返回空 | 通过移动端 API 搜索和获取商品卡片 |
| Lamoda | 2 | 商品卡片:HTTP GraphQL → 失败时改用 Chrome;搜索——用 Chrome | 搜索、带尺码的商品卡片 |
| DNS | 2 | 你的 Chrome(Qrator) | 搜索和电子产品商品卡片 |
| Ситилинк | 2 | 你的 Chrome(Qrator) | 搜索和电子产品商品卡片 |
| AliExpress | 2 | 你的 Chrome(x5sec) | 搜索和商品卡片、卢布价格 |
| Циан | 2 | 你的 Chrome(按 IP 的 WAF) | 房产:按筛选条件搜索(出售、出租、短租)和商品详情页 |
| Сравнение | 4 | 轮询上面列出的全部来源 | 一次调用回答「哪里更便宜?」 |
| MPStats | 2 | 付费 MPStats 账号,cookie mp_auth(可选) | 按 Ozon/WB 的 SKU 查 30 天销量/库存/图表,按仓库查库存(FBS/FBO) |
以下来源匿名读取,不需要浏览器:Wildberries、Яндекс Маркет 和 Детский мир。
Lamoda 的商品卡片先走 HTTP GraphQL 读取;被拒时需要你的 Chrome(CDP)作为备用通道。其余来源都需要你已登录的 Chrome(CDP)。Taobao 和 Мегамаркет 还要求已登录平台本身——没有登录,Taobao 会卡在登录墙,Мегамаркет 返回空响应。Авито 还会按 IP 封禁:从数据中心地址访问是直接拒绝,从俄罗斯家庭地址访问可以正常工作,只要别频繁发请求。对 CDP 来源的请求要错开节奏:连续排队、不加停顿会把它们打挂(检查时 DNS 和 Taobao 就是这样退化的),所以连接器自己会在调用之间保持停顿。你当前会话的准确状态可以用 marketplace-mcp doctor 查看。
MPStats 是个例外:它是唯一付费来源。没有 MPSTATS_MP_AUTH,合并版 marketplace-mcp 根本不会挂载它工具——它们照样会返回 auth_missing——所以它是可选的,按需接入,对其余十三个服务器没有任何影响。单独的 mpstats-mcp 不带 token 也能启动,调用时返回 auth_missing。
14 个服务器共 39 个工具,跑在同一个 mcp-core 运行时上。外加一个合并的 marketplace-mcp,一次挂载全部——客户端配置里写一条记录,而不是十四条。它自带两个工具:marketplace_sources(哪些连接器起来了,哪些挂了、为什么)和 marketplace_health(Chrome 是否活着,哪些主机在冷却,该让人打开哪个页面)。带 MPStats token 时它有 41 个工具,不带 token 时 39 个:同样的已挂载来源减去两个 mpstats_,再加上这两个。
2.7.0 验证: 10 月 30 日,从同一个 Wildberries 地址,API 拒绝(HTTP 403)时它自己通过 Chrome 读了搜索页:40 个商品,全部有价格。其余来源的代码自 v2.6.0 起未改动;它们由同一天的 v2.6.0 运行验证过(13 个来源中的 12 个,MPStats 不带 token)。v2.7.0 详情。
快速开始
需要 Python 3.12+ 和 uv。
不 checkout 也能装:包从 PyPI 安装,并自动接入找到的客户端(--dry-run 只显示计划,不写任何东西):
uvx ru-marketplace-mcp setup
安装客户端、选择来源、验证第一个请求——见分步上手。Node 只有开发者跑测试时才需要。发布到 PyPI(给维护者):DEPLOYMENT.md — PyPI。
从源码安装,用于开发:
git clone https://github.com/Vladimir-Human/ru-marketplace-mcp.git
cd ru-marketplace-mcp
uv sync --all-packages
uv run pytest -q -m "not live and not cdp" # 3210 офлайн-тестов, сеть не нужна
验证线上端点:
uv run python -c "
import asyncio
from wb_connector.server import wb_selfcheck
print(asyncio.run(wb_selfcheck()).status) # ждём success
"
接入 MCP 客户端
每个服务器都是一条控制台命令,所以配置里不用写死路径。
Claude Desktop — claude_desktop_config.json
Windows:%APPDATA%\Claude\claude_desktop_config.json macOS:~/Library/Application Support/Claude/claude_desktop_config.json
最短的路是 marketplace-mcp setup:命令会找到 Claude Desktop、Cursor 和 Claude Code,给它们写配置(默认配置档 compare),启动 scraping-Chrome,跑一遍 doctor,并对被浏览器挡住来源建议执行 unblock。--dry-run 只打印步骤和文件。
marketplace-mcp setup
如果客户端不常见,或者你只要接某一个,用 install --write 写同样的配置:
marketplace-mcp install claude --write
命令会把 marketplace 记录追加到 Claude Desktop 的配置里(Windows 上是 %APPDATA%\Claude\claude_desktop_config.json, macOS 上是 ~/Library/Application Support/Claude/claude_desktop_config.json, Linux 上是 ~/.config/Claude/claude_desktop_config.json)。uv 的路径和你的 checkout 路径都会写成绝对路径(桌面客户端通常没有终端的 PATH),别人的服务器和配置里的其他设置都原样保留,旧文件会存成 claude_desktop_config.json.bak-<标记>。剩下只需重启客户端。--dry-run 只显示会写入什么,不实际写入; --per-source 恢复旧布局——每个来源一条记录——不能和 --profile 一起用。
默认情况下,写入会启动 compare-mcp——也就是 compare 配置档:4 个比价工具,每次查询取最便宜的。--profile decision 增加对候选清单中商品卡片的查看(5 个工具),--profile full 使用 marketplace-mcp,带上来源的全部工具——贵好几倍(各配置档的实测数据见 docs/TOOLS.md)。
只改写入用的控制台命令,名字 marketplace 保持不变。
选择模式的更多说明见 dsh/README.md 和 docs/QUICKSTART.md。
如果更想手动改配置,先从一条记录开始:
{
"mcpServers": {
"marketplace": {
"command": "uv",
"args": ["run", "--directory", "C:/путь/к/ru-marketplace-mcp", "compare-mcp"],
},
},
}
如果需要单独的服务器,marketplace-mcp install claude --per-source 会打印出可直接粘贴的现成块。你的 checkout 路径已经填好了:不用手动改 /path/to/ru-marketplace-mcp 这个占位符。从 wheel 安装时,打印的不是路径,而是 PATH 上的控制台命令。客户端名未知时(允许 claude、claude-code、cursor、dsh),命令会拒绝并给出说明和返回码 2——它不会悄悄给 Claude 塞一个块。
浏览器层还有几个命令:marketplace-mcp browser start (启动 scraping-Chrome;默认窗口可见,--hidden 则放到屏幕外), marketplace-mcp browser status(CDP 端点是否可达、Chrome 是什么版本、有多少标签页、窗口在哪)和 marketplace-mcp browser show(把 scraping-Chrome 窗口显示到屏幕上——Windows 上连接器自启动时窗口在屏幕外,不这么做既没法手动登录,也过不了网站验证)。当 doctor 显示一堵墙时,marketplace-mcp unblock 会在新标签页打开对应页面,等你通过验证或登录,然后只重新检查这一个来源。这些命令都不会自己去访问网站,也不会替人通过验证。
最小手动版本:
{
"mcpServers": {
"wildberries": {
"command": "uv",
"args": ["run", "--directory", "C:/путь/к/ru-marketplace-mcp", "wb-mcp"],
},
"ozon": {
"command": "uv",
"args": ["run", "--directory", "C:/путь/к/ru-marketplace-mcp", "ozon-mcp"],
},
"compare-prices": {
"command": "uv",jsonc
"args": ["run", "--directory", "C:/путь/к/ru-marketplace-mcp", "compare-mcp"],
},
},
}
路径用正斜杠 / 或双反斜杠 \\。完整命令列表:wb-mcp、ozon-mcp、yandex-mcp、detmir-mcp、avito-mcp、taobao-mcp、megamarket-mcp、lamoda-mcp、dns-mcp、citilink-mcp、compare-mcp、marketplace-mcp。
只挂需要的平台:MARKETPLACE_SOURCES
合并服务器会挂载全部来源,而各来源的工具描述每次请求都会进入上下文。用变量 MARKETPLACE_SOURCES 只保留列出的来源: jsonc {
"mcpServers": {
"marketplace": {
"command": "uv",
"args": ["run", "--directory", "C:/путь/к/ru-marketplace-mcp", "marketplace-mcp"],
"env": {
"MARKETPLACE_SOURCES": "wildberries,ozon,yandex_market,avito,aliexpress,dns,compare",
},
},
},
}
名称用规范写法(wildberries、ozon、yandex_market、detsky_mir、avito、taobao、megamarket、lamoda、dns、citilink、aliexpress、cian、compare、mpstats);短别名 wb、ym/yandex、detmir、ali 也接受。启动时遇到未知名称会直接拒绝,并列出支持的来源,免得一个笔误变成残缺的服务器。变量未设置或为空,则照旧挂载全部来源。被关掉的来源会出现在 marketplace_sources 里,归入 skipped,并注明是主动移除,而不是导入失败。compare_prices 查询的正是同一组来源。
Claude Code
bash
claude mcp add wildberries -- uv run --directory /путь/к/ru-marketplace-mcp wb-mcp
claude mcp add yandex-market -- uv run --directory /путь/к/ru-marketplace-mcp yandex-mcp
claude mcp add detsky-mir -- uv run --directory /путь/к/ru-marketplace-mcp detmir-mcp
claude mcp add ozon -- uv run --directory /путь/к/ru-marketplace-mcp ozon-mcp
claude mcp add compare-prices -- uv run --directory /путь/к/ru-marketplace-mcp compare-mcp
Cursor — .cursor/mcp.json jsonc {
"mcpServers": {
"compare-prices": {
"command": "uv",
"args": ["run", "--directory", "/путь/к/ru-marketplace-mcp", "compare-mcp"],
},
},
}
其他 stdio 客户端
运行 uv run --directory /путь/к/репозиторию <команда>,其中命令是 wb-mcp、ozon-mcp、yandex-mcp、detmir-mcp、aliexpress-mcp、cian-mcp、compare-mcp 之一。服务器通过 stdin 和 stdout 收发 JSON-RPC,诊断信息写入 stderr。可选的 mpstats-mcp 启动方式相同,只需在环境里带上 MPSTATS_MP_AUTH。
DeepSeek Harness (dsh) — 插件包
在 dsh 里这不是 mcpServers 条目,而是配置档的一层。插件包放在子目录 dsh/,用自带的插件管理器安装(PATH 里需要有 pnpm): console
dsh plugin --profile web add github:Vladimir-Human/ru-marketplace-mcp#path:/dsh
安装完成后立刻出现 15 个技能,但 MCP 工具一个都没有:两行 MCP 配置都处于关闭状态,除非设置变量 RU_MARKETPLACE_MCP_DIR,指向克隆目录。
这样做的原因是,挂载的服务器在每次请求中都要计费:推荐的比价模式约 1.7k token,带卡片详情的模式约 2.0k,完整套件约 13.1k(按 tools/list 估算;详情见 docs/TOOLS.md)。
启用方式和完整模式见 dsh/README.md。
接入后重启客户端,然后运行 marketplace-mcp doctor。它会为每个连接器跑一遍金丝雀检查:退出码 0 表示所选检查全部健康,1 表示解析器漂移,2 表示已确认的封锁或未经验证的来源,3 表示无法写入 status 文件,4 表示自检或 doctor 调用本身出错(包括参数错误)。Nightly 会对 1 和 4 报警,但不会对 2 报警。预算拒绝会得到原因 budget_refused,而不是健康检查的状态。
工具
自动生成的工具参考,含参数和上下文开销:docs/TOOLS.md。
_selfcheck 金丝雀检查有意不列入这份清单:它们不通过 MCP 发布,因为运维诊断会让模型在每次请求中多花约 7.5k token。它们由 marketplace-mcp doctor 从命令行一次性全部启动。
Wildberries — wb_
| 工具 | 做什么 |
| wb_search(query, page) | 按文本搜索,每页最多 100 件商品,含价格和库存 |
| wb_card(nm_ids) | 批量查询最多 100 个已知 SKU |
| wb_root_info(nm_id) | 查找 imt_id(评论需要)和颜色变体 |
| wb_reviews(imt_id, limit, sort) | 评论池。键是 imt_id,不是 nm_id |
| wb_questions(imt_id, limit, skip, answered_only) | 买家提问和卖家回复。同样按 imt_id |
| wb_seller(supplier_id) | 法人、ИНН、КПП、ОГРН、法定地址 |
| wb_categories(root, max_depth) | 目录树,含分片和 WB 自身的查询 |
| wb_category_products(shard, query, page, sort, dest) | 按 wb_categories 给出的 shard 和 query 查询分类商品 |
wb_seller 回答的是商品卡片刻意隐藏的问题:到底是谁在卖?它返回注册法人和税号。这样就能区分品牌官方店,不是名称相似的中转商。
wb_questions 补上另一个空白。评价说的是拥有这件商品是什么体验;问答澄清的则是这到底是什么商品——「10 安还是 16 安」「带线吗」。卖家的回答往往是关于这一点唯一的公开说法。问题池对所有商品变体通用,键是 wb_root_info 里的 imt_id。
wb_category_products 把与 wb_categories 的链路接上:后者给出 shard 和 query,前者按它们返回商品。元素格式与 wb_search 一致,所以遍历分类和文本搜索可以直接对比。WB 的部分大分区被标为 blackhole 分片——它们没有自己的搜索结果,工具会如实说明,而不是返回空列表。
如果你的地址被 WB API 返回 403(确实会这样:拒绝既取决于地址,也取决于客户端),wb_search 和 wb_card 会自己通过 CDP 在你 Chrome 里打开 WB 渲染页面,重读同一请求,并在 meta.warnings 里加上 dom_fallback 警告。这个默认开启;WB_DOM_FALLBACK=0 恢复原来的行为,而 WB_TRANSPORT=dom 仍然只读渲染页面,不访问 API。搜索结果里的 price_rub 是 WB 钱包价(price_kind: wallet);卡片则分别给出普通价、钱包价和划线价,还有店铺、仓库、配送时效、退货、保修、「正品」标识和「全部 N 个报价,X ₽ 起」这一行。这个模式下 marketplace-mcp doctor 也会在你 Chrome 里打开两个页面——基础卡片和搜索网格——用于 card 和 search_v9 检查。遇到验证码会给出 inconclusive,不判定也不重试;评价、basket 和 legacy 探测仍走 HTTP。详情见 ANTI_BOT.md。
Яндекс Маркет — yandex_
| 工具 | 做什么 |
| yandex_search(query, page, limit) | 搜索,带两种价格、评分、卖家 |
| yandex_card(product_id, include_reviews) | 完整卡片:星级分布和评价 |
两种价格,始终如此。 price_rub 是任何买家都能付的价格。price_with_plus 需要 Яндекс Плюс 订阅,通常低 25–30%。Яндекс 界面把后者用大字号展示,所以不加说明地报出它,等于承诺一个没有订阅的人拿不到的价格。
搜索行是搜索结果里的报价,不是卡片的默认报价。 一个 product_id 覆盖一个商品家族,搜索结果里可能展示其中一个代表,而同一 id 的卡片里是另一个;请按 sku_id 而不是 URL 来核对搜索行和卡片。搜索行里的 price_old_rub 是划线基础价,是折扣的参照;不能把它叫作价格。
rating_stars 给出形如 {1: 10, 2: 3, 3: 10, 4: 19, 5: 502} 的分布。从中能看出 4.8 的平均分是否诚实,还是背后藏着一堆一星。
Дetsky Mir — detmir_
| 工具 | 作用 |
| detmir_categories(parent, limit, region) | 目录树。从这里开始 |
| detmir_category(alias, limit, offset, region) | 分类下的商品,带真实计数 |
| detmir_card(product_id, region) | 价格、评分、线上和门店库存 |
每次调用都要指定区域。 价格、尤其是线下门店库存,跟城市关系很大:同一件商品在莫斯科 152 家门店有货,圣彼得堡 37 家,哈巴罗夫斯克 2 家。region 参数会覆盖 DETMIR_REGION,所以可以在同一个会话里比较不同城市。
这里没有文本搜索,这是有意为之。 Detsky Mir 的 API 会静默忽略任何文本过滤条件,直接返回整个目录,共 30 万条;而站点的搜索路由返回 404,外加一个促销轮播。搜索工具只会返回看似确定、实则错误的商品,所以导航走分类。详情见 docs/ANTI_BOT.md。
Ozon — ozon_
| 工具 | 作用 |
| ozon_search(query) | 按文本搜索 |
| ozon_card(sku_or_path) | 商品卡片 |
| ozon_reviews(sku_or_path, limit, sort) | 评价 |
Ozon 会拒绝数据中心流量,所以连接器分两层。先做 TLS 指纹模拟。如果 Cloudflare 弹出挑战,请求就通过 DevTools Protocol 在你已登录的 Chrome 里执行。不存储任何东西:登录由你自己完成,在你控制的浏览器里。配置方法见 docs/CDP_SETUP.md。
从俄罗斯家庭 IP 访问时,第一层通常就能用,不需要浏览器。
Ozon 的评价是整个卡片家族的共用评价,同池的邻居往往是另一个品牌的另一件商品。 Huter 1500 瓦油汀卡片(SKU 5264146973,评分 4.8,共 356 条评价)抽出的 100 条评价里,没有一条是关于 Huter 本身的:38 条讲 Ресанта 2000 瓦,34 条讲 Ресанта 1500 瓦,5 条讲 Eurolux,以此类推——池子里一共 12 件商品。所以每条评价都带 item_id——它实际所写商品的 SKU;响应还会额外返回 requested_item_id、own_reviews(真正关于这个 SKU 的评价有多少条)和 pool_variants(池中所有商品的 SKU → 名称)。rating_score 和 distribution 按池计算,不按商品计算:下结论之前,要按 item_id 过滤评价;当 own_reviews: 0 时,要如实说明这件商品没有自己的评价。
Авито — avito_
| 工具 | 作用 |
| avito_search(query, page, location_id, category_id) | 通过内部 js/items API 搜索商品 |
| avito_card(item_id_or_url) | 通过 CDP 读取商品页:标题、价格、描述、图片数量、卖家(评分、个人主页链接) |
| avito_seller(seller_id_or_url) | 卖家评分、评价数量;在售商品数不可用(null) |
Avito 是分类广告,不是商品目录:没有针对商品的评价池,卖家的信誉本身就是信任信号。免费或置换类商品返回 price_rub: null,绝不会是 0,免得在比价时被当成「最便宜」。搜索使用 TLS 指纹模拟,并以你的 Chrome 作为备用通道。商品卡片通过 CDP 读取 schema.org Product 和商品页上的卖家区块:如果该区块可用,seller 会带上信誉和个人主页链接。截至 2026-10-12,/web/1/js/items 会忽略 itemId 和 userId;不同卖家拿到的是同一份公共目录,totalCount: 1500,而不是各自的商品。因此卖家信息改为通过 CDP 从渲染后的个人主页读取,暂停节奏和允许的主机与商品卡片一致。接受 /brands/<hash>、/user/<hash>/profile 和裸 ID:32 个十六进制字符的走 brands,其余 ID 走 user。不可用的 active_items 计数保持 null。商品卡片的完整 URL 直接使用,去掉 query 和 fragment;对 ID 则套用 /{id}:Avito 会把它重定向到商品页。location、posted_at、views 保持 null:Product JSON-LD 里没有这些字段。没有 Product,或者标题和价格同时缺失时,返回 parser_drift,而不是空结果的成功。Canary 走通搜索 → 商品卡片 → 卖家主页这条链路。从数据中心 IP 访问可能遇到 403 防火墙。
Taobao — taobao_
| 工具 | 做什么 |
| taobao_search(query, page) | 按目录搜索 Taobao |
| taobao_card(item_id_or_url) | 商品卡片 |
Taobao 搜索是客户端 React 应用,走签名的 mtop API:每个请求都需要 sign,由 cookie 令牌算出,所以没有匿名通道。所有读取都在你的 Chrome 里进行,网站自己给请求签名。价格以人民币(CNY)计,不做换算:写死的汇率会悄悄过期,所以要跟卢布来源比价,请自己显式换算。
Мегамаркет、Lamoda、DNS、Ситилинк
这四个都通过你的 Chrome(CDP)读取。Мегамаркет(megamarket_)因为 ServicePipe,走移动端 JSON API,光过一次挑战还不够:匿名会话下 API 返回空列表,需要处于登录 Мегамаркет 的状态。DNS(dns_)和 Ситилинк(citilink_)因为 Qrator,读取渲染后的 DOM;这三家完全没有匿名通道。Lamoda(lamoda_)只做了一半:搜索走 Chrome,商品卡片则先通过 HTTP GraphQL 读取(tier 1)。当响应不是 200 或出现 httpx.TransportError 时,会在你的 Chrome 页面内通过 CDP 恰好执行一次 GraphQL POST(tier 2);成功的 fallback 标记为 tier_used: graphql_cdp。
HTTP 200 不会触发 CDP。原因:2026-10-12 当天,HTTPS 上的 GraphQL 以 307 循环响应。
带 CDP 的 Chrome(scripts/start_chrome_cdp.sh)对其它来源始终必需,而 Lamoda 卡片只在备用层级才需要。你平时用的 Chrome 不用关:启动器会另起一个 Chrome,使用自己的 --user-data-dir,并在屏幕上开一个窗口。
如果窗口还是跑到了屏幕外(Windows 上连接器自启动时),可以用 marketplace-mcp browser show 把它拉回来(方便你手动登录或通过网站验证)。
总共九个来源走 CDP——就是这些,再加上 Taobao、AliExpress、Циан、Ozon 和 Авито。Ozon,以及 Авито 的搜索和卖家,Chrome 是备用层级,用于匿名 tier 1 撞上挑战时;Авито 卡片则始终需要 CDP。marketplace-mcp doctor 会从你的浏览器告诉你哪些端点已确认可用。
AliExpress — aliexpress_
| 工具 | 做什么 |
| aliexpress_search(query) | 搜索:最多 48 张卡片,价格以卢布计 |
| aliexpress_card(item_id_or_url) | 卡片:价格、评分、订单数 |
通过你的 Chrome(CDP)读取:x5sec 会给匿名客户端弹验证码,所以连接器先落到搜索页(它不会被挑战),再从那里用新标签页打开卡片。价格以卢布计,参与 compare_prices。卡片有标题但没有价格是已知状态:高负载下 x5sec 不再返回价格模块,连接器会写 price_missing,而不是编一个数字。price_rub 里不发布「N ₽ 用券」的价格:那里是普通价格,优惠券的事连接器会单独如实提示。不返回评价正文:只有评分和订单数。和其它 CDP 来源一样,aliexpress_selfcheck 变绿只能证明传输层有响应——不能证明价格正确。
Циан — cian_
| 工具 | 做什么 |
| cian_search(deal, offer_type, region, rooms, price_min, price_max, area_min, area_max, page) | 按筛选条件搜索:出售、出租、日租——每页 28 条房源 |
| cian_card(offer_id_or_url) | 卡片:价格及其历史、户型、楼栋、地址、地铁、发布者 |
这是房产,不是商品:出售、长租和日租(deal="daily";Циан 的日租商业地产不行,这类请求会被拒绝)。长租和日租是两个不同的市场,不会混在同一次返回结果里:日租的价格按每晚计算,price_period 和 lease_term 为空,所以每行都有 price_unit,取值为 total、 month 或 day。不能跨单位比较价格。
只能按筛选条件搜索,Циан 没有文本搜索。
区域用 Циан 的 id 指定:1 是莫斯科,2 是圣彼得堡,4593 是莫斯科州, 4588 是列宁格勒州(这四个都实际验证过);其余区域
нужен их id. Читается через ваш Chrome (CDP): WAF Циана режет голый HTTP по IP,
а из сессии браузера отвечает собственный JSON-API сайта, так что HTML не
парсится. Цена «не указана» приходит как null, не 0. Агентской страницы как
инструмента нет: она не отдаёт структурированных данных, агент приходит внутри
карточки. В compare_prices источник не участвует.
Сравнение цен — compare_
| Инструмент | Что делает |
| compare_prices(query, per_source_limit, sources) | Все маркетплейсы сразу, с ранжированием |
| compare_sources() | Какие маркетплейсы доступны в этой установке |
compare_prices("кроссовки мужские")
wildberries 712 ₽ Кроссовки изи дышащие спортивные
wildberries 814 ₽ Зимние кроссовки теплые с мехом
yandex_market 2499 ₽ Кеды A-LOW
yandex_market 3480 ₽ Кеды
дешевле всего: wildberries 712 ₽, разброс 5858 ₽, complete: true
Маркетплейсы опрашиваются параллельно, и каждый отчитывается сам за себя. Если один
заблокирован, сравнение не рушится: complete: false вместе с source_outcomes
покажет, что именно вы видите. Подписочные цены в ранжировании не участвуют.
Цена с WB Кошельком и другие явно условные цены тоже не выбираются в cheapest
или cheapest_comparable: при наличии обычной цены ранжируется она. price_kind
сохраняет тип цены (regular, wallet, subscription); null — источник его не сообщил. conditional_price: … в warnings называет условие и исключённую сумму. Повторы по (источник, id товара, id варианта) схлопываются. Разные SKU Яндекса сохраняются даже внутри одной товарной семьи. Наличие в сравнении трёхзначное: true — подтверждено, false — подтверждено отсутствие, null — неизвестно. При in_stock_only=true победитель выбирается только среди true; неизвестные остатки WB и неоднозначные подписи Ozon остаются видимыми в конце списка.
У каждого предложения есть currency (строчный ISO-код, по умолчанию rub) и
price_native — цена в этой валюте, как её показывает маркетплейс. Для российских источников она совпадает с price_rub, если известна обычная цена; если известна только условная, её сумма остаётся в price_native, а price_rub — null. У Taobao в ней лежит цена в юанях, которую price_rub намеренно оставляет пустой. Раньше юаневую цену забирали и молча выбрасывали, и строка Taobao приходила с пустой ценой без намёка, что цена вообще есть. Теперь юань виден, но в рублёвом ранжировании по-прежнему не участвует: в warnings появляется foreign_currency: … с числом исключённых предложений и причиной. В source_outcomes[].detail Taobao считает цены из price_native: например, 20 results, 20 priced in CNY (not ranked). Несколько валют с ценами перечисляются по алфавиту через /; без цен — N results, 0 priced (not ranked). Для рублёвых источников остаётся N results, M priced. Конвертировать здесь значило бы зашить курс, который молча устареет, — пересчёт за вами.
MPStats — mpstats_
Аналитика продаж и остатков по SKU Ozon и Wildberries через плагин MPStats.
В отличие от всех остальных коннекторов, этот опционален и требует платный
аккаунт MPStats: авторизация — одна cookie mp_auth (JWT из залогиненной сессии плагина на mpstats.io), задаётся переменной MPSTATS_MP_AUTH. Без неё отдельный mpstats-mcp запускается как обычно и отвечает auth_missing, а объединённый marketplace-mcp не монтирует эти два инструмента, чтобы не платить за схемы, которые всё равно не сработают.
| Инструмент | Что делает |
| mpstats_item(skus, place, oz_fbs=True) | Аналитика за 30 дней по до 100 SKU: заказы, цена, остатки, графики по дням, продавец/бренд |
| mpstats_warehouses(skus, place) | Остатки по складам: FBS (склад продавца) и FBO (склад маркетплейса), last_update |
place — ozon или wildberries. Графики длиной 30, от старых к новым: последняя ненулевая ячейка — текущая цена или остаток. Цена и остаток при сплошь нулевом графике ведут себя намеренно по-разному: цена становится None (ложный 0 выиграл бы любое сравнение «где дешевле»), а остаток — 0, потому что «нулевой остаток» это осмысленное показание, а не отсутствие данных. Пустой график даёт None в обоих случаях. Ноль в отдельной ячейке — «нет данных за тот день», а не «значение было нулевым», поэтому сумму за окно считайте по графику. Отсутствие токена и транспортные сбои selfcheck отчитывает как inconclusive, не drift: гоняться за дрейфом схемы, которого не было, не нужно. Токен — секрет платного аккаунта с квотой: не логируйте и не коммитьте его.
Навыки для агента
У каждого коннектора — свой навык в skills/: пятнадцать штук, по одному на источник плюс общий marketplace. Навык это не пересказ README: он объясняет агенту, когда за этот источник вообще браться, чего у источника нет, и каким его ответам нельзя верить без второго взгляда.
| Навык | Сервер |
| skills/wb-connector | wb-mcp |
| skills/ozon-connector | ozon-mcp |
| skills/yandex-connector | yandex-mcp |
| skills/detmir-connector | detmir-mcp |
| skills/avito-connector | avito-mcp |
| skills/taobao-connector | taobao-mcp |
| skills/megamarket-connector | megamarket-mcp |
| skills/lamoda-connector | lamoda-mcp |
| skills/dns-connector | dns-mcp |
| skills/citilink-connector | citilink-mcp |
| skills/aliexpress-connector | aliexpress-mcp |
| skills/cian-connector | cian-mcp |
| skills/compare-prices | compare-mcp |
| skills/mpstats-connector | mpstats-mcp |
| skills/marketplace | marketplace-mcp |
mcp-core — общий рантайм под остальными серверами. Своего навыка у него нет.
Соответствие проверяется тестом
(packages/marketplace-connector/tests/test_skills_parity.py): новый коннектор
без навыка роняет прогон, как и навык, который называет несуществующий
инструмент или забыл существующий. До этого теста навык DNS почти год советовал
формат ссылки /product/<24-hex>/ — тот самый шаблон, который чинили как баг.
Скиллы едут в Docker-образ (/app/skills/), но в колёсах их нет: skills/
лежит в корне репозитория. При установке release-wheel возьмите навыки
из репозитория отдельно; пакета ru-marketplace-mcp с PyPI это тоже касается.
Настройка
Все параметры задаются переменными окружения с префиксом коннектора. Все
необязательные.
| Префикс | Основные параметры |
| 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 и другие), CACHE_TTL, PROXY |
| OZON_ | TIMEOUT, MIN_GAP, IMPERSONATE, CACHE_TTL, PROXY |
| AVITO_ | TIMEOUT, MIN_GAP, IMPERSONATE, CACHE_TTL, PROXY, LOCATION_ID |
| TAOBAO_ | TIMEOUT, MIN_GAP, CACHE_TTL, |
| ALI_ | TIMEOUT, MIN_GAP, CACHE_TTL |
| MEGAMARKET_ | TIMEOUT, MIN_GAP, CACHE_TTL, USE_PROFILE_ADDRESS (0/1, по умолчанию 0 — приватный список адресов профиля не читается; 1 = адрес по умолчанию из залогиненного профиля, цены «как видит оператор») |
| LAMODA_ | TIMEOUT, MIN_GAP, CACHE_TTL, PROXY |
| DNS_ / CITILINK_ | TIMEOUT, MIN_GAP, CACHE_TTL |
| CHROME_ | CDP_HOST, CDP_PORT, SCRAPING_PROFILE, BINARY, HEADLESS, STEALTH |
| COMPARE_ | SOURCE_TIMEOUT |
| MPSTATS_ | MP_AUTH (единственный обязательный — без него инструменты отвечают auth_missing), TIMEOUT, MIN_GAP, CACHE_TTL, PROXY |
| MCP_ | TRANSPORT (stdio по умолчанию, либо http), HTTP_HOST, HTTP_PORT |
CHROME_CDP_HOST указывает, куда дозвониться CDP-клиенту (по умолчанию
127.0.0.1). Из контейнера ставьте chrome (сайдкар) или host.docker.internal
— это открывает tier-2 источники (Ozon, Авито, Taobao, Мегамаркет, Lamoda, DNS, Ситилинк, AliExpress, Циан) в Docker без host networking. Подробности в docs/DEPLOYMENT.md.
_CACHE_TTL=0 выключает кэш. _PROXY перекрывает стандартные HTTPS_PROXY и
ALL_PROXY — свой префикс есть у семи коннекторов: WB_, YANDEX_, DETMIR_, OZON_, AVITO_, LAMODA_ и MPSTATS_. У Taobao своего нет намеренно: поиск там подписан и ходит через собственный клиент. У Мегамаркета, DNS и Ситилинка тоже нет: их трафик идёт через ваш Chrome, а его egress — дело настроек браузера. Кэшируются только удачные ответы: запомнить сбой значило бы растянуть секундную помеху на весь TTL.
У Ozon прокси применяется к первому уровню. Второй идёт через ваш собственный Chrome,
и его трафик — дело настроек этого браузера.
Секрет один, и тот опциональный. Всем серверам, кроме MPStats, ничего не нужно:
нечего настраивать, нечему утечь. У MPStats есть MPSTATS_MP_AUTH — JWT платного аккаунта, и потому его место только в env клиентской записи: в коде и коммитах его нет и быть не должно.
Разработка
bash
uv sync --all-packages
uv run pytest -q -m "not live and not cdp" # 3210 офлайн-тестов
uv run pytest -q -m "not live" # то, что гоняет CI
uv run pytest -q -m "not live" --cov # покрытие, порог 70% в CI
uv run ruff check . && uv run ruff format --check .
uv run mypy # что проверять — в [tool.mypy] files
uv run mypy --platform win32 # ловит ошибки, видимые только на Windows
uv run python scripts/check_no_print.py # запись в stdout ломает JSON-RPC
uv run python scripts/check_versions.py # одна версия во всех 77 местах
Часть тестов прогоняет настоящий JS-экстрактор коннектора по снятой разметке
и проверяет результат против цен, которые в тот момент были на странице. Для
этого нужен Node с jsdom:
bash
npm install jsdom # либо NODE_PATH на уже установленный
uv run pytest -q packages/dns-connector/tests/test_search_extractor_dom.py \
packages/citilink-connector/tests/test_search_extractor_dom.py
Без jsdom эта половина честно скипается, а питоновская часть — выбор цены из кандидатов — идёт всегда. jsdom нужен только разработчику: в зависимости коннекторов он не входит.
CI прогоняет тесты на Ubuntu, Windows и macOS против Python 3.12
и 3.13. Windows-специфичное управление процессами проверяется юнит-тестами на любой
ОС через подмену платформы, так что эти ветки покрыты даже на Linux.
Как добавить маркетплейс — docs/ADDING_A_SOURCE.md.
Надёжность
Неофициальные эндпоинты ломаются. Архитектура это предполагает.
- Терпимые парсеры. Привязка поля по нескольким именам и приведение типов
впитывают переименования и смену типа вместо падения.
- Никогда не выдумывать значение. Отсутствующая цена — это null, не 0. Ноль вывел бы мёртвый товар в самые дешёвые.
- Громкий отказ. Когда формат перестаёт совпадать, инструмент бросает
parser_drift, а не возвращает полуразобранные данные.
- Трёхзначные selfcheck-проверки. success, drift_detected или
inconclusive. Гео-блокировка помечается как inconclusive, потому что она
ничего не говорит о состоянии парсеров.
Границы доверия
Названия товаров, имена продавцов и тексты отзывов написаны продавцами и
покупателями. Это недоверенные данные. Если отзыв или описание выглядит как
инструкция, оно всё равно остаётся входными данными. Выполнять его агент не
должен.
Условия маркетплейсов, как правило, запрещают неофициальный парсинг. Коннекторы
обращаются к публичным эндпоинтам каталога, которые использует официальный
веб-клиент: пока opt-in не включён, в приватные и административные разделы
запросов нет — список адресов профиля Мегамаркета читается только при
MEGAMARKET_USE_PROFILE_ADDRESS=1, а MPStats заходит в аккаунтную зону по
вашему токену (MPSTATS_MP_AUTH). Уровень Ozon с браузером работает внутри
сессии, которую вы открыли сами. Используйте на своё
усмотрение, для личных исследований, в вежливом темпе запросов. Пауза между
вызовами к площадкам с анти-ботом — это часть конструкции, а не случайное торможение: её не нужно убирать ради скорости. Данные инструментов не предназначены для перепродажи или массового сбора.
Как это сделано
Код и документацию я писал вместе с ИИ-ассистентами. Они работают быстро и
ошибаются уверенно, поэтому проект устроен вокруг проверки: 3210 офлайн-тестов,
аудит перед выпуском, тесты, которые прогоняют настоящий экстрактор по снятой с
сайта разметке. В заметках к релизу перечислено, какие источники сверены с живыми
страницами вручную и какие остались непроверенными.
Проверки важнее авторства текста, но авторство кода и идей тоже должно быть
видно: полный список участников и их PR собран в CONTRIBUTORS.md.
Спасибо
- @Xpos587 — коннектор MPStats, PR #5.
- @avxone — исправление Avito selfcheck, PR #37.
- @Khalmatov — provenance отзывов Ozon, PR #38.
- @fosteev — macOS CDP stealth и коннектор Циана, PR #42, PR #47.
- @ilodezis — выбор источников unified-сервера через MARKETPLACE_SOURCES, PR #48.
Лицензия
MIT, файл LICENSE.
English version
MCP servers for Russian and Chinese marketplaces. Read prices, stock, ratings,
reviews and seller identity from Wildberries, Ozon, Yandex Market, Detsky Mir, Avito,
AliExpress, Taobao, Megamarket, Lamoda, DNS and Citilink, then compare product prices
in one call. Cian adds real-estate search and listing cards separately.
First successful query walks through installation, choosing
a server, targeted health checks, and verifying an offer before recommending it.
Read-only. No credentials, no API keys, no account required — the marketplaces with hard anti-bot are read through your own Chrome. One optional exception: MPStats takes a paid account token (MPSTATS_MP_AUTH) if you want its analytics; without it every other server is unaffected.
2.7.0 verification: on 30 October, from one address, Wildberries fell back to its rendered search page after the API refused (HTTP 403) and returned 40 results, all priced. The other sources' code is unchanged since v2.6.0; they were checked in the v2.6.0 run the same day (12 of 13 sources; MPStats has no token). v2.7.0 details.
What you get
| Server | Tools | What it takes to read | Notes |
| Wildberries | 8 | anonymous HTTP | Search, cards, reviews, buyer questions, seller legal identity, catalog tree and category listings |
| Yandex Market | 2 | anonymous HTTP | Multi-seller prices, star distribution, reviews |
| Detsky Mir | 3 | anonymous HTTP | Kids' goods, offline store stock, category listings |
| Ozon | 3 | your Chrome; often no browser from a residential IP | Search, cards, reviews |
| Avito | 3 | your Chrome + a Russian residential IP and spaced requests — else an IP block | Classified search, cards, seller reputation |
| Taobao | 2 | your Chrome with an active Taobao login | Search and cards, prices in yuan |
| Megamarket | 2 | your Chrome with an active login — an anonymous session reads empty | Search and cards via the mobile API |
| Lamoda | 2 | cards: HTTP GraphQL → Chrome on refusal; search via Chrome | Search, cards with sizes |
| DNS | 2 | your Chrome (Qrator) | Electronics search and cards |
| Citilink | 2 | your Chrome (Qrator) | Electronics search and cards |
| AliExpress | 2 | your Chrome (x5sec) | Search and cards, ruble prices |
| Cian | 2 | your Chrome (WAF by IP) | Real estate: filter search (sale, long-term rent, daily) and one offer's card |
| Compare | 4 | aggregates the above | "Where is this cheapest?" in one call |
| MPStats | 2 | paid MPStats account, mp_auth cookie (optional) | 30-day sales/stock graphs per Ozon/WB SKU, warehouse split (FBS/FBO) |
Anonymous, no browser: Wildberries, Yandex Market and Detsky Mir. Lamoda cards
try HTTP GraphQL first and need your Chrome (CDP) only for the fallback tier.
The rest need your logged-in Chrome (CDP). Taobao and Megamarket additionally need
you signed into the marketplace itself — without it Taobao hits a login wall and Megamarket returns an empty result. Avito also blocks by IP: from a datacenter address it is a flat refusal, from a Russian residential one it works as long as you do not burst requests. Requests to the CDP sources are paced apart — a run of back-to-back calls degrades them (DNS and Taobao both dropped that way in testing), so the connectors hold a gap between calls themselves. Run marketplace-mcp doctor from your own session for the current state.
MPStats stands apart as the only paid source: without MPSTATS_MP_AUTH the
unified marketplace-mcp does not mount its tools at all — they would only answer auth_missing — so it is optional; the other thirteen servers never notice. The standalone mpstats-mcp still boots and answers auth_missing.
39 tools across 14 stdio MCP servers, sharing one runtime (mcp-core), plus the
unified marketplace-mcp that mounts them all under one client entry. It adds two
tools of its own — marketplace_sources (which connectors mounted, and which dropped out and why) and marketplace_health (is Chrome up, which hosts are cooling down, which page unblocks a source) — so it exposes 41 tools with an MPStats token and 39 without one: the 37 mounted sources plus those two when the two mpstats_ tools are left out. stdio is the default; HTTP transport is opt-in for remote deployment — see docs/DEPLOYMENT.md.
Quickstart
Requires Python 3.12+ and uv.
Without a checkout, install from PyPI and wire the detected client in one step
(--dry-run shows the plan and writes nothing):
console
uvx ru-marketplace-mcp setup
Publishing to PyPI (maintainer): DEPLOYMENT.md — PyPI.
From source, for development:
bash
git clone https://github.com/Vladimir-Human/ru-marketplace-mcp.git
cd ru-marketplace-mcp
uv sync --all-packages
uv run pytest -q -m "not live and not cdp" # 3210 offline tests, no network needed
Client configuration mirrors the Russian section above. The shortest path is the
guided setup:
console
marketplace-mcp setup
It detects Claude Desktop, Cursor and Claude Code, writes the config with the
compare profile by default, starts the scraping Chrome, runs doctor's checks and
offers unblock for a walled source. --dry-run prints the steps and files
without writing, starting or requesting anything.
To write one named client's config instead, let the CLI write the file:
console
marketplace-mcp install claude --write
It adds one marketplace entry to the client's own config (Claude Desktop:
%APPDATA%\Claude\claude_desktop_config.json on Windows,
~/Library/Application Support/Claude/claude_desktop_config.json on macOS,
~/.config/Claude/claude_desktop_config.json on Linux; Cursor: ~/.cursor/mcp.json).
By default that entry runs compare-mcp — the compare profile: 4 price-comparison tools, the cheapest per request. --profile decision adds shortlist card
inspection (5 tools); --profile full runs marketplace-mcp with every per-source
tool, several times the cost (measured per profile in docs/TOOLS.md).
The entry name stays marketplace. uv and your checkout
path are filled in as absolute paths, other servers and settings in the file stay
untouched, and the old file is kept as <file>.bak-<timestamp>. Restart the client
afterwards; --dry-run shows the result without writing, and --per-source writes
one entry per source instead (it cannot be combined with --profile). Each server is
also a console script (wb-mcp, ozon-mcp, yandex-mcp, detmir-mcp, aliexpress-mcp,
cian-mcp, compare-mcp) launched through uv run --directory /path/to/repo .
The optional mpstats-mcp runs the same way with MPSTATS_MP_AUTH in the entry's
env (paid MPStats account; without it the tools return auth_missing).
marketplace-mcp install [claude|claude-code|cursor|dsh] prints the selected profile's
marketplace entry with your checkout's real path filled in — no placeholder to hand-edit — or the console-script path on PATH when installed as a wheel; an unknown client name or profile is rejected. The dsh target prints a cordis.patch.yml row instead of mcpServers JSON — see dsh/README.md.
If you prefer to paste the config yourself, start with this one entry (replace the
path with your clone from above):
json
{
"mcpServers": {
"marketplace": {
"command": "uv",
"args": ["run", "--frozen", "--directory", "C:/projects/ru-marketplace-mcp", "compare-mcp"]
}
}
}
The browser tier has more verbs: marketplace-mcp browser start (launch the
scraping Chrome; on-screen by default, --hidden for off-screen),
marketplace-mcp browser status (is the CDP endpoint reachable, which Chrome
answers, how many page targets it holds and where its window is) and
marketplace-mcp browser show (bring the scraping Chrome window on-screen — the connector's autostart parks it off-screen on Windows, and without that you can neither log in by hand nor pass a site's check). When doctor reports a wall, marketplace-mcp unblock opens that page in a new tab, waits while you pass the check or log in, and re-checks only that source. None of these sends a request to a website on its own and none solves the check for you.
DeepSeek Harness (dsh) installs as a plugin bundle rather than an mcpServers
entry, from the dsh/ subdirectory (pnpm must be on PATH):
console
dsh plugin --profile web add github:Vladimir-Human/ru-marketplace-mcp#path:/dsh
That gives you 15 skills immediately and no MCP tools: both MCP rows stay
disabled until RU_MARKETPLACE_MCP_DIR points at a clone.
A mounted server is paid on every request — ≈1.7k tokens for the recommended price-comparison profile, ≈2.0k for the decision profile and ≈13.1k for the full set (estimated from tools/list; details in docs/TOOLS.md) — so opting in is left to you.
dsh/README.md covers enabling it and the full mode.
After connecting, run marketplace-mcp doctor. Its exits are 0 all selected
checks healthy, 1 parser drift, 2 acknowledged block or unverified source,
3 status-file write failure, and 4 checker or doctor setup/usage failure
(including an unknown source). Nightly alarms on 1 and 4, not 2. A budget
refusal carries budget_refused and never counts as a healthy check.
The tools
Generated per-tool reference with parameters and context cost:
docs/TOOLS.md.
The _selfcheck canaries are deliberately absent from these tables: they are not
published over MCP, because operator diagnostics would cost the model ~7.5k tokens
on every request. marketplace-mcp doctor runs them all from the command line.
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_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.
If WB's API answers 403 from your address (the refusal depends on both address and
client), wb_search and wb_card re-read the same request through WB's rendered
page in your Chrome over CDP and add a dom_fallback warning to meta.warnings.
This is on by default; WB_DOM_FALLBACK=0 restores the previous behaviour, while
WB_TRANSPORT=dom still reads only rendered pages, with no API calls. On search
tiles price_rub is the WB Wallet price (price_kind: wallet); the card reports
the regular, Wallet and struck prices separately, plus the store, warehouse,
delivery date, return policy, warranty, the «Оригинал» badge and «Все N
предложений от X ₽». In this mode marketplace-mcp doctor also opens two pages
in your Chrome — the baseline card and search grid — for card and search_v9. A captcha is inconclusive, never solved or retried; reviews, basket and the legacy probe still use HTTP. Details in ANTI_BOT.md.
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 |
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.
A search row is the SERP's offer, not the card's default offer. One product_id
covers a product family, and search may show one member while the card for the same id
defaults to another — reconcile rows with cards by sku_id, not by URL. price_old_rub on search rows is the struck-through reference price, never a price to quote.
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 |
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.
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 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.
From a Russian residential IP the first tier usually works and no browser is needed.
Ozon pools reviews per card family, and the neighbours are often a different
product from a different brand. A live card for a 1500 W Huter oil heater (SKU
5264146973, rated 4.8 across 356 reviews) returned 100 reviews of which zero were
about the Huter: 38 about a 2000 W Resanta, 34 about a 1500 W Resanta, 5 about a
Eurolux — 12 products in that pool. So every review carries item_id, the SKU it is actually about, and the response adds requested_item_id, own_reviews (how many returned reviews really are about that SKU) and pool_variants (SKU → name for the whole pool). rating_score and distribution are pool-wide, not per product: filter by item_id before concluding anything, and when own_reviews is 0, say plainly that the product has no reviews of its own.
AliExpress — aliexpress_
| Tool | What it does |
| aliexpress_search(query) | Search: up to 48 tiles with ruble prices |
| aliexpress_card(item_id_or_url) | Card: title, price, rating, order count |
Read through your Chrome (CDP): x5sec challenges anonymous clients, so the
connector lands on a search page (never challenged) and opens the card in a new
tab from it. Prices are rubles and rank in compare_prices. A card with a title
but no price is a known state — under load x5sec stops serving the price module and the connector reports price_missing rather than inventing a number. A "with coupon" price never lands in price_rub: the regular price does, and the coupon is reported as a warning. Review texts are not exposed; rating and order counts are. As with every CDP source, a green aliexpress_selfcheck proves the transport answered — not that a given price is right.
Cian — cian_
| Tool | What it does |
| cian_search(deal, offer_type, region, rooms, price_min, price_max, area_min, area_max, page) | Search by filters — sale, long-term rent, daily: 28 offers per page |
| cian_card(offer_id_or_url) | One offer: price and its history, layout, building, address, metro, publisher |
Real estate, not goods: flats, rooms, houses and commercial property for sale,
long-term rent or daily rent (deal="daily"; Cian has no daily commercial
market and that combination is refused). Long-term and daily are separate
markets that never share a result page: a daily price is per night and Cian
leaves price_period and lease_term null there, so every row carries
price_unit — total, month or day. Never compare prices across units. Search is by filters only — Cian has no text search. The region is a Cian id: 1 Moscow, 2 St. Petersburg, 4593 Moscow oblast, 4588 Leningrad
oblast (all four verified live); other regions need their own id. Read through
your Chrome (CDP): Cian's WAF blocks plain HTTP by IP, while inside the browser
session the site's own JSON API answers, so no HTML is parsed. A price Cian does
not state arrives as null, never 0. There is no agent tool: the agent page
exposes no structured data, and the publisher ships inside the card. The source
takes no part in compare_prices.
Avito — avito_
| Tool | What it does |
| avito_search(query, page, location_id, category_id) | Classified search through the internal js/items API |
| avito_card(item_id_or_url) | CDP item page: title, price, description, image count, seller (rating, profile link) |
| avito_seller(seller_id_or_url) | Seller rating, review count; active listing count unavailable (null) |
Avito is classifieds, not a catalog: there is no per-product review pool, the
seller's reputation IS the trust signal. A free/swap listing arrives with
price_rub: null — never 0, so it cannot win "cheapest". Search uses TLS impersonation with Chrome fallback. Cards read the item page's schema.org Product and seller block over CDP; seller carries reputation and the profile link when that block is available. As of 2026-10-12 /web/1/js/items ignores both itemId and userId: different sellers receive the same generic catalog with totalCount: 1500, not their listings. Seller reads therefore use the rendered profile over CDP, with the same pacing and allowed hosts as cards. Accepted paths are /brands/<hash> and /user/<hash>/profile, or a bare id: 32 hex characters map to brands, other ids to user. Unavailable active_items remains null. Full card URLs lose query/fragment; ids use /{id}, which Avito redirects to the item page. location, posted_at and views stay null, absent from Product JSON-LD. Missing Product, or both name and price missing, raises parser_drift, not empty success. The canary follows search → card → seller profile. Datacenter IPs can still receive the 403 firewall.
Taobao — taobao_
| Tool | What it does |
| taobao_search(query, page) | Catalog search |
| taobao_card(item_id_or_url) | Product card |
Taobao search is a signed-mtop React app: every request needs a sign derived
from a cookie token, so there is no anonymous path. All reads run inside your
Chrome, where the site signs its own requests. Prices stay in yuan (CNY) and
are never converted — a baked-in rate quietly goes stale, so compare ruble and yuan listings explicitly.
Megamarket, Lamoda, DNS, Citilink
These four read through your Chrome (CDP). Megamarket (megamarket_) goes
through the mobile JSON API behind ServicePipe and needs an active login — an anonymous session reads empty. DNS (dns_) and Citilink (citilink_) render DOM behind Qrator with no anonymous path at all. Lamoda (lamoda_) is split: search through Chrome, cards over HTTP GraphQL first (tier 1). A non-200 response or httpx.TransportError triggers exactly one in-page GraphQL POST through your Chrome over CDP (tier 2); successful fallback reads report tier_used: graphql_cdp. HTTP 200 skips CDP. The reason is the 2026-10-12 307 loop from GraphQL over HTTPS. All of them need Chrome with CDP (scripts/start_chrome_cdp.sh), but Lamoda cards only need it for the fallback. Your everyday Chrome can stay open: the launcher starts a second Chrome with its own --user-data-dir, on-screen. If the window did go off-screen (the connector's autostart on Windows), bring it back (to log in by hand or pass a site's check) with marketplace-mcp browser show. Nine sources run through CDP in total: these plus Taobao, AliExpress, Cian, Ozon and Avito; for Ozon and Avito, Chrome is also only the fallback tier when the anonymous one is challenged.
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("кроссовки мужские")
wildberries 712 RUB Кроссовки изи дышащие спортивные
wildberries 814 RUB Зимние кроссовки теплые с мехом
yandex_market 2499 RUB Кеды A-LOW
yandex_market 3480 RUB Кеды
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.
WB Wallet and other explicitly conditional prices cannot become cheapest or
cheapest_comparable either; when a regular price is available, that price ranks.
price_kind retains the kind (regular, wallet, subscription); null means
unreported. A conditional_price: … warning names the condition and excluded amount. Repeats matching on (source, product id, variant id) are collapsed. Distinct Yandex SKUs remain visible within one product family. Comparison stock is tri-state: true means confirmed, false means unavailable, and null means unknown. With in_stock_only=true, only confirmed stock can win; unreported WB quantities and ambiguous Ozon labels remain visible at the end of the list.
Every offer carries currency (lowercase ISO code, default rub) and price_native,
the price in that currency as the marketplace quotes it. For Russian sources it mirrors
an available everyday price_rub; if only a conditional price is known, that amount
stays in price_native while price_rub is null. For Taobao it holds the yuan price that price_rub deliberately leaves
null. That yuan price used to be fetched and silently thrown away, so a Taobao row
showed a blank price with no sign a real one existed. Now the yuan is reported but
still never ranked against roubles: a foreign_currency: … warning lists how many offers were excluded and why. Taobao's source_outcomes[].detail counts price_native, for example 20 results, 20 priced in CNY (not ranked). Multiple priced currencies are sorted and joined with /; no native prices gives N results, 0 priced (not ranked). Rouble sources keep N results, M priced. Converting here would bake in an exchange rate that goes stale silently, so the caller converts if they want to.
MPStats — mpstats_
Sales and stock analytics per Ozon or Wildberries SKU via the MPStats browser
plugin. Unlike every other connector, this one is optional and needs a paid
MPStats account: auth is a single mp_auth cookie (JWT from a logged-in plugin
session at mpstats.io), set via the MPSTATS_MP_AUTH env var. Without it the
standalone mpstats-mcp boots normally and answers auth_missing, while the
unified marketplace-mcp does not mount the two tools, so no request pays for
schemas that could only fail.
| Tool | What it does |
| mpstats_item(skus, place, oz_fbs=True) | 30-day analytics for up to 100 SKUs: orders, price, stock, per-day graphs, seller/brand |
| mpstats_warehouses(skus, place) | Warehouse split: FBS (seller's warehouse) vs FBO (marketplace warehouse), last_update |
place is ozon or wildberries. Graphs are length 30, oldest first: the last
non-zero cell is the current price or stock. The two differ on purpose when the
whole graph is zero: price becomes None (a false 0 would win any "cheapest"
comparison), while stock becomes 0, because "none in stock" is a real reading
rather than an absence of data. An empty graph yields None for both. A zero
cell means "no data for that day", not "the value was zero", so sum the graph for
a window total. A missing token or a transport failure reports
as inconclusive, not drift — no chasing a schema drift that never happened. The token is a secret on a paid, quota-billed account: never log or commit it.
Agent skills
Every connector ships its own skill under skills/ — fifteen of them — one per source plus a shared marketplace overview. A skill is not a restatement of this README: it tells the agent when to reach for that source at all, what the source does not have, and which of its answers should not be trusted without a second look.
| Skill | Server |
| skills/wb-connector | wb-mcp |
| skills/ozon-connector | ozon-mcp |
| skills/yandex-connector | yandex-mcp |
| skills/detmir-connector | detmir-mcp |
| skills/avito-connector | avito-mcp |
| skills/taobao-connector | taobao-mcp |
| skills/megamarket-connector | megamarket-mcp |
| skills/lamoda-connector | lamoda-mcp |
| skills/dns-connector | dns-mcp |
| skills/citilink-connector | citilink-mcp |
| skills/aliexpress-connector | aliexpress-mcp |
| skills/cian-connector | cian-mcp |
| skills/compare-prices | compare-mcp |
| skills/mpstats-connector | mpstats-mcp |
| skills/marketplace | marketplace-mcp |
mcp-core is the shared runtime rather than a server, so it has no skill.
The mapping is enforced by a test
(packages/marketplace-connector/tests/test_skills_parity.py): a new connector
without a skill fails the run, and so does a skill that names a tool which does
not exist — or omits one that does. Before that test existed, the DNS skill spent months telling operators to pass /product/<24-hex>/, the exact pattern a fix had already removed.
Skills are copied into the Docker image (/app/skills/), but they are not in
the wheels: skills/ lives at the repository root rather than inside the
packages. Release-wheel installs need the skills fetched from the repo separately;
the ru-marketplace-mcp package on PyPI does not include them either.
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 |
| AVITO_ | TIMEOUT, MIN_GAP, IMPERSONATE, CACHE_TTL, PROXY, LOCATION_ID |
| TAOBAO_ | TIMEOUT, MIN_GAP, CACHE_TTL, |
| ALI_ | TIMEOUT, MIN_GAP, CACHE_TTL |
| MEGAMARKET_ | TIMEOUT, MIN_GAP, CACHE_TTL, USE_PROFILE_ADDRESS (0/1, default 0 — the profile's private address list is not read; 1 = the logged-in profile's default address, prices "as the operator sees them") |
| LAMODA_ | TIMEOUT, MIN_GAP, CACHE_TTL, PROXY |
| DNS_ / CITILINK_ | TIMEOUT, MIN_GAP, CACHE_TTL |
| CHROME_ | CDP_HOST, CDP_PORT, SCRAPING_PROFILE, BINARY, HEADLESS, STEALTH |
| COMPARE_ | SOURCE_TIMEOUT |
| MPSTATS_ | MP_AUTH (the only required one — without it the tools return auth_missing), TIMEOUT, MIN_GAP, CACHE_TTL, PROXY |
| MCP_ | TRANSPORT (stdio default, or http), HTTP_HOST, HTTP_PORT |
_CACHE_TTL=0 disables caching. _PROXY overrides the standard
HTTPS_PROXY/ALL_PROXY — seven connectors carry one: WB_, YANDEX_, DETMIR_, OZON_, AVITO_, LAMODA_ and MPSTATS_. Taobao has none by design and Megamarket, DNS and Citilink none either: their traffic goes through your own Chrome, whose egress is that browser's configuration. 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.
Containers. CHROME_CDP_HOST points the CDP client at Chrome (default
127.0.0.1; use chrome or host.docker.internal inside Docker). That single
variable is what opens the tier-2 sources — Ozon, Avito, Taobao, Megamarket, Lamoda, DNS, Citilink, AliExpress and Cian — from a container without host networking. See docs/DEPLOYMENT.md.
One secret, and it is optional. Every server except MPStats needs nothing:
nothing to configure, nothing to leak. MPStats alone has MPSTATS_MP_AUTH, the JWT
of a paid account — it belongs only in the client entry's env, never in code or commits.
Only the sources you use: MARKETPLACE_SOURCES. The unified server mounts every
source, and their tool schemas are sent to the client on every request. Set
MARKETPLACE_SOURCES in the marketplace-mcp entry's env to a comma-separated list
to mount only those, e.g. wildberries,ozon,yandex_market,avito,aliexpress,dns,compare.
Names are canonical (wildberries, ozon, yandex_market, detsky_mir, avito,
taobao, megamarket, lamoda, dns, citilink, aliexpress, cian, compare,
mpstats); the aliases wb, ym/yandex, detmir and ali work too. An unknown
name is rejected at startup with the supported-source list, so a typo cannot
silently produce a partial server. Unset or blank mounts everything, as before.
Deselected sources show up in
marketplace_sources under skipped, marked as deselected rather than failed to
import, and compare_prices queries the same subset.
Development
bash
uv sync --all-packages
uv run pytest -q -m "not live and not cdp" # 3210 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 # the tree lives in [tool.mypy] files
uv run mypy --platform win32 # catches Windows-only type errors
uv run python scripts/check_no_print.py # a print() breaks JSON-RPC
uv run python scripts/check_versions.py # one version across all 79 places
Some tests execute a connector's real extractor JavaScript against captured
markup and check the output against the prices the page was showing when it was
captured. That needs Node with jsdom:
bash
npm install jsdom # or point NODE_PATH at an existing copy
uv run pytest -q packages/dns-connector/tests/test_search_extractor_dom.py \
packages/citilink-connector/tests/test_search_extractor_dom.py
Without jsdom that half skips honestly and the Python half — choosing the price among the candidates — still runs. jsdom is a developer tool only; no connector depends on it.
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.
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 the public catalog endpoints the official web clients use: while the opt-ins
stay off, no authenticated or administrative areas are touched — Megamarket's profile address list is read only with MEGAMARKET_USE_PROFILE_ADDRESS=1, and MPStats enters your account zone through your own token (MPSTATS_MP_AUTH). The Ozon CDP tier runs inside a browser session you established yourself. Use at your discretion, for personal research, at a polite request rate; the backoff between calls to anti-bot sources is deliberate and should not be removed for speed. Tool output is not meant for redistribution or bulk harvesting.
How this was built
I wrote the code and the documentation with AI assistants. They are fast and they
are confidently wrong, so the project is arranged around verification: 3210 offline
tests, an audit before the release, tests that run the real extractor against
markup captured from the live site. The release notes say which sources were
compared against live pages by hand and which were left unverified.
Checks matter more than who typed the prose, but code and ideas deserve visible
credit too: the full contributor and PR index is in
CONTRIBUTORS.md.
Thanks
- @Xpos587 — MPStats connector, PR #5.
- @avxone — Avito selfcheck fix, PR #37.
- @Khalmatov — Ozon review provenance, PR #38.
- @fosteev — macOS CDP stealth and the Cian connector, PR #42, PR #47.
- @ilodezis — unified-server source selection, PR #48.
License
MIT, see LICENSE.预览分享卡片长什么样 ↓
面向俄罗斯和中国电商平台的 MCP 服务器。 从 Wildberries、Ozon、Яндекс…
相关插件 · 市场 / 管理
这是你的仓库?认领条目