Подключение MCP

shopmcp.ru предоставляет remote MCP-сервер по адресу:

https://shopmcp.ru/mcp

Транспорт: Streamable HTTP (JSON-RPC 2.0 поверх HTTPS).

Claude Code

claude mcp add --transport http shopmcp https://shopmcp.ru/mcp

Claude Desktop / Cursor

{
  "mcpServers": {
    "shopmcp": {
      "url": "https://shopmcp.ru/mcp"
    }
  }
}

Модель

Outlet — единая сущность «точка получения товара». Четыре типа:

У каждого outlet'а есть opaque id — строка вида shp_BWxlbnRhbXNrLXR2ZXJza2F5YS0x. LLM получает её в поле id через find_outlets и передаёт её как параметр outlet_id в search_products/get_product. Префиксы: shp_ (store), ntw_ (network), dlv_ (delivery), mkt_ (marketplace).

Инструменты

list_retailers(query?, has_offline?, has_delivery?)

Полный справочник активных торговых сетей. Каждый элемент включает promotions[] — сетевые акции этой сети.

find_outlets(area, retailer_ids?, kinds?)

Точки получения товара в зоне поиска. Возвращает массив outlet'ов разных типов — LLM выбирает подходящий по контексту запроса (имя, описание, pricing, расстояние, capabilities) и забирает id.

area — либо точка с радиусом, либо маршрут:

area = {
  type: "radius",
  center: { type:"address", text:"..." } | { type:"coords", lat, lon },
  radius_m: 1500
}
// или
area = {
  type: "along_path",
  from: Location, to: Location,
  max_extra_m: 800   // насколько outlet может уклоняться от прямой "от-до"
}

Внутри shopmcp комбинирует два источника: статичный индекс физических точек (rstar по координатам — store/network) и live-запрос у парсера (delivery/marketplace). Для kind=network outlet'ов physical locations группируются в один outlet с массивом locations[]. Результаты сортируются по расстоянию, обрезаются до 30.

Фильтры: retailer_ids (UUID'ы), kinds (например ["network"] чтобы получить только сети с общим каталогом).

search_products(outlet_id, queries[], in_stock_only?, extra_fields?)

Поиск товаров в одном pricing-контексте по нескольким текстовым запросам сразу. Удобно для сборки корзины: queries: ["молоко 3.2%", "хлеб бородинский", "сыр гауда"]. Ответ — { outlet_id, results: { "молоко 3.2%": {products, ...}, ... } }. Лимит — 30 товаров на каждый запрос.

По умолчанию листинг содержит минимум: id, title, price, currency, in_stock + promotions. Для kind=network добавляется stock_by_location[], для kind=marketplacedelivery[]. Остальное — через extra_fields: ["image", "rating", "brand", ...].

Контекст outlet'а и retailer'а в ответе не дублируется — LLM помнит из find_outlets. Если потеряла (очистила историю) — вызывает find_outlets повторно по координатам.

get_product(outlet_id, product_id, fields?)

Полная карточка одного товара. По умолчанию все поля что отдал парсер: description, images, categories, attributes, nutrition, rating/votes. fields: [...] ограничивает выдачу. Ответ — { outlet_id, product }.

Долгоживущие ссылки: LLM сохранила (outlet_id, product_id) в чате — через месяц зовёт и получает актуальную цену.

Типы

type Location =
  | { type: "address", text: string }
  | { type: "coords",  lat: number, lon: number }

type Area =
  | { type: "radius",     center: Location, radius_m: number }
  | { type: "along_path", from: Location, to: Location, max_extra_m: number }

type Outlet = {
  id: string,             // opaque shopmcp-id, передавай как outlet_id в search_products/get_product
  kind: "store" | "network" | "delivery" | "marketplace",
  retailer_id: string,
  name: string,
  description?: string,
  // Физические точки. store=1, network=N, delivery/marketplace=0.
  locations?: OutletLocation[],
  // Pricing. Три состояния fee-полей: отсутствует (неизвестно),
  // "0" (явно бесплатно), "99.00" (конкретное значение).
  delivery_fee?: string,    // курьерская доставка (одна цена для любой корзины)
  delivery_tiers?: DeliveryTier[],  // ступенчатые пороги: «199 от 1000, 0 от 4099»
  service_fee?: string,     // сервисный сбор платформы
  pickup_fee?: string,      // сборка готового заказа для самовывоза
  min_order?: string,
  max_weight_kg?: string,   // max вес заказа в кг
  fee_note?: string,        // динамические условия в свободной форме
  min_days?: number, max_days?: number,  // срок в целых днях
  delivery_time?: string,                // или свободный формат: «от 40 минут», «2-3 часа»
  promotions?: Promotion[]
}

type OutletLocation = {
  id?: string,                     // нужен для network (привязка stock_by_location)
  address?: string, lat?: number, lon?: number,
  hours?: string, phone?: string,
  walkin?: boolean, pickup?: boolean,
  promotions?: Promotion[],        // локальные акции этой точки
  distance_m?: number               // sorted ascending
}

// Ступенчатый тариф доставки. LLM для конкретной корзины берёт последний
// tier у которого cart_sum >= min_order.
type DeliveryTier = {
  price: string,       // "0" — явно бесплатно
  min_order: string
}

// Только для kind=network — наличие/сроки per-магазин (поле product.stock_by_location[]).
type LocationStock = {
  location: string,                // ссылка на OutletLocation.id
  in_stock: boolean,
  stock_qty?: string,
  min_days?: number, max_days?: number   // срок поставки если in_stock=false
}

// Только для kind=marketplace — варианты получения per-SKU (поле product.delivery[]).
type DeliveryVariant = {
  type: "pickup" | "partner_pickup" | "courier",
  name: string,
  description?: string,
  delivery_fee?: string, service_fee?: string, fee_note?: string,
  min_days?: number, max_days?: number
}

Сценарии

Молоко в магазине у дома

  1. find_outlets({ area: { type:"radius", center:{ type:"coords", lat, lon }, radius_m: 1500 }, kinds:["store"] })
  2. Берём id ближайшего outlet'а (у store одна locations[0] с адресом).
  3. search_products({ outlet_id, queries: ["молоко 3.2%"] })
  4. (Опц.) get_product(outlet_id, product_id) — полная карточка с description/nutrition.

Холодильник в сети с общим каталогом

  1. find_outlets({ area: { type:"radius", center, radius_m: 3000 }, kinds:["network"] }) → одна Эльдорадо с 3 locations в Москве.
  2. search_products({ outlet_id, queries: ["холодильник Bosch"] })ОДИН запрос, цены общие. Парсер вернёт stock_by_location[] per-магазин.
  3. LLM сама решает, какую location рекомендовать — это компромисс между наличием (stock_by_location), distance_m, часами, capabilities и количеством товаров из корзины, попавших в эту точку. Например, «оливье из 7 продуктов» — лучше один магазин подальше, где все 7 есть, чем ближний, где только 4.

Сравнение цен «магазин рядом vs распред-центр»

  1. find_outlets({ area: { type:"radius", center, radius_m: 1500 }, kinds:["delivery"] }) → 2 варианта.
  2. Сравниваем fee_note/delivery_fee/min_days/min_order.
  3. Параллельно зовём search_products для каждого outlet_id, складываем корзину и сравниваем итог.

По дороге с работы домой

  1. find_outlets({ area: { type:"along_path", from:{type:"address", text:"офис, ул. Льва Толстого 16"}, to:{type:"address", text:"дом, Тверская 1"}, max_extra_m: 500 }, kinds:["store","network"] })
  2. Получаем outlet'ы (физические + network), чьи locations лежат в коридоре до 500 м от прямой маршрута, отсортированные по distance_m.
  3. Берём ближайший по пути id и зовём search_products.

Через месяц

LLM из сохранённой переписки берёт outlet_id + product_id, зовёт get_product(outlet_id, product_id) → получает свежую цену.