Skip to main content
Glama

Туда, куда не ищется

Собираем поездку через пересадку, когда прямого билета нет.

Поиск любого сервиса отвечает на вопрос «есть ли билет из А в Б». Если прямого сообщения нет, выдача пустая — и человек уходит. Хотя доехать можно, и оба нужных билета продаются там же.

Живой пример. Запрос «Москва → Углич» на 20 августа 2026 в MCP Туту:

search_multitransport → 0 вариантов
search_bus            → 0 вариантов
search_rail           → 0 вариантов

Наш ответ на тот же запрос:

Москва → Ярославль   автобус 09:00 → 13:00    863 ₽
пересадка в Ярославле — 40 минут
Ярославль → Углич    автобус 13:40 → 16:25    616 ₽
                                       итого  1 479 ₽, на месте в 16:25

Оба билета продаёт Туту. Их поиск этого маршрута не покажет никогда.

Как это работает

запрос «Москва → Плёс»
   ↓
MCP Туту: прямой поиск → 0 вариантов, регион «Ивановская область»
   ↓
узлы-кандидаты: справочник «регион → центр» + предложения агента
   ↓
для каждого узла: сначала плечо «узел → цель», затем «откуда → узел»
   ↓
код сводит стыковки: отбрасывает рейсы, на которые не успеть, считает запас и сумму
   ↓
карточки маршрутов

Поиском управляет агент: он сам решает, какие города и даты проверять, и человек видит ход его работы лентой на экране. Но стыковки, цены и ранжирование считает код — модель к арифметике не допущена, именно там агенты ошибаются. Тяжёлые ответы Туту через модель не проходят: она получает идентификаторы найденных плеч и короткие сводки, а полные данные уходят прямо в интерфейс.

Если уехать в выбранный день нельзя, бэкенд досылает агента за гостиницами: ищется ближайший день отъезда и подбирается ночлег ровно на столько ночей, сколько ждать.

Если агент недоступен или упёрся в лимит, интерфейс молча переключается на детерминированный поиск — экран показывает результат в любом случае.

Related MCP server: Bay Area Transit MCP

Запуск

npm install
cp .env.example .env          # вписать OPENROUTER_API_KEY (необязательно)
npm run db:up                 # Postgres в Docker
npm run migrate               # схема базы
npm run warm                  # прогрев кэша реальными ответами Туту
npm run dev                   # http://localhost:3100

OPENROUTER_API_KEY опционален: без него подсказка городов отключается, поиск работает на справочнике регионов.

Команды

Команда

Что делает

npm run dev

приложение на http://localhost:3100

npm run route -- Москва Углич 2026-08-20

сборка маршрута в консоли

npm run warm

прогрев кэша из src/modules/tutu/fixtures/

npm run cache

что сейчас в кэше

npm run smoke

проверка живой связки с MCP Туту

npm run db:up / db:down

Postgres в Docker

npm run migrate

миграции Prisma

npm run lint

ESLint

Структура

Весь код в src/, в корне только конфигурация. Интерфейс собран одной папкой, логика разложена по модулям — по смыслу, а не по типу файла:

src/
  app/                роутинг Next: страница и три API — тонкие, только вызовы модулей
    api/agent/          агентный поиск потоком
    api/route/          детерминированный поиск (страховка)
    api/stay/           гостиницы

  frontend/           всё, что видно в браузере
    components/route/   форма, календарь, подсказки, лента, карточки, гостиницы
    hooks/              чтение потока агента, определение города
    design.ts           палитра и подписи из макета
    format.ts, geo.ts   форматирование и расстояния
    config.ts           константы интерфейса

  modules/            логика, зависимости однонаправленные: agent → routing → tutu
    tutu/               клиент MCP, кэш, типы, прокси, снятые ответы (fixtures)
    routing/            сборка маршрутов, справочники регионов и городов
    agent/              цикл, инструменты, промпт, ошибки, ретраи, состояние

  lib/                клиент Prisma

Почему внутри модулей нет слоёв domain/, infra/, api/. Такая нарезка оправдана на десятках тысяч строк и полутора десятках доменов. Здесь один сценарий: слои дали бы папки по одному файлу и пустой contract/. Когда модуль перерастёт тысячу строк, они появятся вокруг существующих файлов.

Работа с MCP Туту

Подключение: https://mcp.tutu.ru/mcp, streamable HTTP, авторизация не нужна.

Два инженерных решения, без которых это не работает:

Ответы весят ~26 КБ. Инструменты обёрнуты так, что полный JSON уходит в интерфейс и рисуется карточками, а в языковую модель — сжатая выжимка через toModelOutput. Замер на реальном поиске: 26 305 → 1 971 символ, экономия 93 %.

У сервера жёсткий rate limit. Около полусотни запросов подряд закрыли нам доступ ко всему домену на уровне TCP на несколько часов. Поэтому: view: 'compact', кэш ответов в Postgres, не больше двух узлов на поиск, а перед демо — прогрев кэша из fixtures/ командой npm run warm. Если сервер недоступен, экран показывает то, что есть в кэше, и не падает.

Стек

Next.js 16 (App Router) · TypeScript · @ai-sdk/mcp · Prisma 7 + Postgres 18 · Tailwind 4 + shadcn/ui · OpenRouter (опционально)

Документы

  • SPECIFICATION.md — продукт целиком: проблема, доказательства, техника, регламент

  • SCREEN-SPEC.md — спецификация экрана для вёрстки

  • USER-GUIDE.md — руководство пользователя

  • fixtures/README.md — что за сохранённые ответы и зачем

Related MCP Connectors

Related MCP Servers