Skip to main content
Glama

Hacia donde no se busca

Montamos un viaje con transbordo cuando no hay billete directo.

La búsqueda de cualquier servicio responde a la pregunta «¿hay billete de A a B?». Si no hay conexión directa, los resultados están vacíos y el usuario se va. Aunque se puede llegar y ambos billetes necesarios se venden en el mismo sitio.

Ejemplo real. Consulta «Moscú → Uglich» para el 20 de agosto de 2026 en MCP Туту:

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

Nuestra respuesta a la misma consulta:

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

Ambos billetes los vende Туту. Su buscador nunca mostrará esta ruta.

Cómo funciona

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

La búsqueda la gestiona un agente: es él quien decide qué ciudades y fechas comprobar, y la persona ve el avance de su trabajo como un feed en la pantalla. Pero los transbordos, los precios y la clasificación los calcula el código — el modelo no se acerca a la aritmética, que es justo donde los agentes se equivocan. Las respuestas pesadas de Туту no pasan por el modelo: este recibe los identificadores de los tramos encontrados y resúmenes breves, mientras que los datos completos van directamente a la interfaz.

Si no se puede viajar el día elegido, el backend envía al agente a por hoteles: se busca el día de salida más cercano y se reserva alojamiento exactamente por las noches que haya que esperar.

Si el agente no está disponible o ha alcanzado el límite, la interfaz cambia silenciosamente a la búsqueda determinista: la pantalla muestra el resultado en cualquier caso.

Related MCP server: Bay Area Transit MCP

Ejecución

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 es opcional: sin ella se desactiva el autocompletado de ciudades y la búsqueda funciona con el directorio de regiones.

Comandos

Comando

Qué hace

npm run dev

la aplicación en http://localhost:3100

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

construcción de la ruta en la consola

npm run warm

precalentamiento de la caché desde src/modules/tutu/fixtures/

npm run cache

qué hay ahora en la caché

npm run smoke

comprobación de la conexión en vivo con MCP Туту

npm run db:up / db:down

Postgres en Docker

npm run migrate

migraciones de Prisma

npm run lint

ESLint

Estructura

Todo el código está en src/, en la raíz solo hay configuración. La interfaz está empaquetada en una sola carpeta y la lógica está distribuida en módulos, por significado y no por tipo de archivo:

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

Por qué no hay capas domain/, infra/, api/ dentro de los módulos. Esa división se justifica con decenas de miles de líneas y una docena y media de dominios. Aquí hay un solo escenario: las capas darían carpetas de un solo archivo y un contract/ vacío. Cuando el módulo supere el millar de líneas, aparecerán alrededor de los archivos existentes.

Trabajo con MCP Туту

Conexión: https://mcp.tutu.ru/mcp, HTTP con streaming, no se necesita autorización.

Dos decisiones de ingeniería sin las cuales esto no funciona:

Las respuestas pesan ~26 KB. Las herramientas están envueltas de modo que el JSON completo va a la interfaz y se dibuja en tarjetas, mientras que al modelo de lenguaje le llega un resumen comprimido mediante toModelOutput. Medición en una búsqueda real: 26 305 → 1 971 caracteres, un ahorro del 93 %.

El servidor tiene un rate limit estricto. Unas cincuenta peticiones seguidas nos cerraron el acceso a todo el dominio a nivel de TCP durante varias horas. Por eso: view: 'compact', caché de respuestas en Postgres, no más de dos nodos por búsqueda y, antes de la demo, precalentamiento de la caché desde fixtures/ con el comando npm run warm. Si el servidor no está disponible, la pantalla muestra lo que hay en la caché y no se cae.

Stack

Next.js 16 (App Router) · TypeScript · @ai-sdk/mcp · Prisma 7 + Postgres 18 · Tailwind 4 + shadcn/ui · OpenRouter (opcional)

Documentos

  • SPECIFICATION.md — el producto completo: problema, pruebas, técnica, reglamento

  • SCREEN-SPEC.md — especificación de la pantalla para maquetación

  • USER-GUIDE.md — guía de usuario

  • fixtures/README.md — qué son las respuestas guardadas y para qué sirven

Related MCP Connectors

Related MCP Servers