Command Palette

Search for a command to run...

UnylyUnyly
Весь каталог

Tomsk Transport

БесплатноНе проверен

MCP server providing real-time public transport data for Tomsk, including routes, schedules, stops with lazy OSM enrichment, nearby stop search, and a routing p

GitHubEmbed

Описание

MCP server providing real-time public transport data for Tomsk, including routes, schedules, stops with lazy OSM enrichment, nearby stop search, and a routing prompt for LLMs.

README

MCP-сервер, предоставляющий LLM актуальные данные о маршрутах общественного транспорта Томска. Дипломная работа: «Разработка MCP-сервера для интеграции больших языковых моделей в транспортные информационные системы».

Возможности

  • Tools
    • get_stops(route_id) — упорядоченный список остановок маршрута. Каждая остановка содержит координаты (lat/lon), расстояние и оценку времени до следующей (distance_to_next_m, estimated_travel_time_s при средней скорости 20 км/ч), плюс OSM-теги доступности (wheelchair/shelter/bench). Если в БД лежат только конечные, сервер лениво подтягивает полную топологию маршрута из Overpass API (OpenStreetMap) и кэширует — см. ниже.
    • get_routes_schedules(route_id) — расписание маршрута в markdown (распознано через OCR из первоисточника); первый вызов скачивает источник, прогоняет через OCR-пайплайн и кэширует, последующие — отдают из БД (TTL по умолчанию 24 ч)
    • find_nearby_stops(address, radius_m=500) — геокодит адрес через OSM Nominatim и возвращает остановки в радиусе с дистанцией в метрах
  • Resource transport://routes — список всех активных маршрутов
  • Prompt find_route_prompt(from_location, to_location) — шаблон, инструктирующий LLM последовательно использовать tools/resource для построения маршрута

Покрытие маршрутов

Поддерживаются 44 маршрута Томска от трёх источников расписаний:

Источник Кол-во Формат первоисточника Парсер
пассажир.online (xn--80aasi5akda.online) 3 + 6 DOCX в cloud.mail.ru / изображения / PDF EtvDocxScheduleParser (для 112С/Б/Д) + OCR-пайплайн (для пригородных)
rasptomsk.ru 17 JPG/PNG/PDF OCR-пайплайн
tomskavtotrans.ru 18 WordPress-страница с набором <img> HTML-индексатор + vstack + OCR-пайплайн

Полный список — в src/transport_mcp/db/seeds/_catalog.py.

route_id Маршрут Особенности
112S/B/D Томск — Серебряный бор / Борики / Дзержинское DOCX-парсер (точное извлечение таблиц python-docx)
26, 29 Кольцевая Алтайская — Авангард, Спичфабрика — Карандашная фабрика OCR JPG
4, 5, 11, 12, 13, 14, 19, 20, 23, 30, 33, 36, 37, 38, 53 Муниципальные маршруты с rasptomsk.ru OCR (включая PDF для 19)
118510 Пригородные маршруты ТомскАвтоТранса HTML→vstack→OCR
101, 133, 134, 301, 401, 514 Пригородные маршруты ЕТВ Multi-file (несколько файлов на маршрут, склейка)

Архитектура

tools/, resources/, prompts/        # FastMCP обвязка
        ↓
services/                            # бизнес-логика
        ↓
repositories/   parsers/   downloaders/         services/ocr_service
        ↓            ↓           ↓                        ↓
domain/      OCR-table /     httpx →            rapidocr-onnxruntime
             DOCX            cloud.mail.ru,     (CPU, ONNX, RU)
                             rasptomsk.ru,
                             tomskavtotrans.ru
        ↓
db/  (aiosqlite, SQLite)

OCR-пайплайн (для 38 маршрутов):

URL → Downloader → bytes (JPG/PNG/PDF) → OcrEngine.recognise()
                                          │
                                          ▼
                                     list[OcrBox]
                                          │
                                          ▼
                              OcrTableScheduleParser
                              (кластеризация колонок по X,
                               строк по Y → markdown-таблицы)
                                          │
                                          ▼
                                       markdown

Для multi-file источников (пассажир.online 101/133/134/...) и tomskavtotrans (несколько <img> на странице) скачанные изображения склеиваются вертикально через utils.image_join.vstack_images до подачи в OCR — парсеру это выглядит как одна высокая картинка.

Lazy-обогащение остановками из OSM. OCR-расписания публикуют только конечные остановки маршрута, поэтому для не-legacy маршрутов в БД по умолчанию лежит лишь 2 точки (по seed-у из terminal_coordinates.json). При первом вызове get_stops(route_id) для такого маршрута StopsService идёт в Overpass API (OpenStreetMap), достаёт relation маршрута по ref и упорядоченный список stop-нод, делает upsert в таблицы stops+route_stops и записывает метку в overpass_sync. На последующих вызовах данные отдаются из БД мгновенно. Логика:

get_stops(route_id)
        ↓
StopsService.list_for_route
        ├── route exists? → нет → ToolError
        ├── stops в БД ≥ OVERPASS_MIN_STOPS (=3)? → вернуть
        ├── route в LEGACY_SEED_ONLY (112С/Б/Д/26/29)? → вернуть seed как есть
        ├── overpass_sync свежий (TTL=168ч)? → вернуть существующий
        └── per-route asyncio.Lock + double-check
                ↓
            OverpassRouteFetcher.fetch_route_stops(ref)
                ├── OK     → upsert stops + route_stops, mark_ok
                ├── empty  → mark_skipped (нет в OSM)
                └── error  → mark_failed (back-off через TTL)
        ↓
StopsRepository.list_by_route
        ↓ (distance/eta считаются налету через haversine)
list[Stop]

Расширяемость: добавление нового маршрута — это одна запись в db/seeds/_catalog.py (URL источника, конечные, source_kind). Координаты конечных подтягиваются из data/terminal_coordinates.json (заполняется однократно через scripts/seed_coordinates.py). Промежуточные остановки подтянутся из OSM автоматически при первом запросе. Сервисы и tools не меняются.

Запуск

uv sync --extra dev                                  # установка зависимостей (включая OCR)
Copy-Item .env.example .env                          # вписать переменные окружения (опционально)
uv run transport-mcp-seed                            # инициализация БД + seed всех маршрутов
uv run transport-mcp                                 # сервер на http://127.0.0.1:8000/mcp

Первый вызов get_routes_schedules для OCR-маршрута займёт ~10-30 секунд: rapidocr-onnxruntime загружает свои модели (~50 МБ) при первом инференсе, далее — мгновенно из кэша.

Подключение к Claude Desktop

В claude_desktop_config.json:

{
  "mcpServers": {
    "tomsk-transport": {
      "url": "http://127.0.0.1:8000/mcp"
    }
  }
}

Подключение к Claude Code

claude mcp add --transport http tomsk-transport http://127.0.0.1:8000/mcp

Проверка через MCP Inspector

npx @modelcontextprotocol/inspector
# Подключиться к http://127.0.0.1:8000/mcp transport=Streamable HTTP
# Вызовите: get_stops("112S")  — seed (промежуточные внесены вручную),
#           get_stops("12")    — lazy-fetch из Overpass (1й вызов ~5-10с, далее мгновенно),
#           get_stops("442")   — пригородный Северск, тоже из Overpass,
#           get_routes_schedules("4"|"19"|"119"|"133"),
#           find_nearby_stops("проспект Ленина 30", 600);
# прочитайте transport://routes; вызовите prompt find_route_prompt.

Тесты

uv run pytest                                       # 116 unit/E2E тестов, ~25 сек
uv run pytest -m ocr_live                           # 4 live OCR-теста на реальных изображениях, ~30 сек

Live OCR-тесты по умолчанию отключены (addopts = ["-m", "not ocr_live"]). Они скачивают модели rapidocr и стучатся в карьерные сайты — запускать вручную для регрессии. Запросы к Overpass API в тестах не делаются — tests/conftest.py выставляет OVERPASS_ENABLED=false, реальный сетевой клиент проверяется через pytest-httpx с записанной фикстурой (tests/fixtures/overpass_route_12.json).

Ручная сверка OCR

uv run python scripts\manual_ocr_check.py

Прогоняет OCR-пайплайн по списку маршрутов из ROUTES_TO_CHECK, складывает распознанный markdown в data/ocr_manual_check/route_<id>.md. Удобно для сверки с оригинальными скриншотами.

Структура

src/transport_mcp/
├── server.py                    # composition root, регистрация tools/resources/prompts
├── config.py                    # pydantic-settings (.env)
├── exceptions.py
├── logging_setup.py             # лог в stderr
├── domain/                      # Pydantic-модели (Route, Stop, Schedule, ...)
├── db/
│   ├── connection.py
│   ├── migrations.py
│   ├── schema.sql
│   ├── seed.py                  # composition root для transport-mcp-seed
│   └── seeds/
│       ├── route_112s/112b/112d/26/29.py   # ручные seed-модули (legacy)
│       └── _catalog.py                      # каталог 39 OCR-маршрутов
├── repositories/                # routes_repo, stops_repo, schedule_repo, cache_repo, overpass_sync_repo
├── services/
│   ├── routes_service.py
│   ├── stops_service.py          # БД-first, fallback на Overpass под per-route Lock
│   ├── schedule_service.py       # TTL+lock, поддержка multi-URL источников
│   ├── geocoding_service.py      # OSM Nominatim + Overpass (для find_nearby_stops)
│   ├── overpass_client.py        # OverpassRouteFetcher: relation+stop-nodes по ref
│   ├── route_osm_ref.py          # mapping route_id → OSM ref + LEGACY_SEED_ONLY
│   └── ocr_service.py            # rapidocr-onnxruntime + PDF через pypdfium2
├── parsers/
│   ├── base.py                  # ScheduleParser ABC
│   ├── etv_docx_parser.py       # DOCX-парсер для 112С/Б/Д (python-docx)
│   ├── ocr_table_parser.py      # универсальный OCR-парсер
│   ├── rasptomsk_ocr_parser.py  # тонкий wrapper для обратной совместимости тестов
│   ├── rasptomsk_specs.py       # DayBlockSpec для 26 и 29
│   ├── registry.py              # ScheduleSource, SourceRegistry
│   └── route_registry.py        # массовая регистрация из _catalog.py
├── downloaders/
│   ├── base.py                  # FileDownloader ABC
│   ├── cloud_mail_ru.py         # cloud.mail.ru public weblinks (пассажир.online)
│   ├── http_direct.py           # обычный GET (rasptomsk.ru)
│   ├── tomskavtotrans.py        # HTML-индексатор страницы + vstack
│   └── local_cache.py           # дисковый кэш SHA-256 (декоратор)
├── tools/                       # get_stops, get_routes_schedules, find_nearby_stops
├── resources/                   # transport://routes
├── prompts/                     # find_route_prompt
└── utils/
    ├── geo.py                   # haversine
    ├── transport_constants.py   # AVG_BUS_SPEED_MPS (для ETA до следующей остановки)
    └── image_join.py            # vstack_images для multi-file/HTML-источников

scripts/
├── seed_coordinates.py          # one-time массовое геокодирование через Nominatim
└── manual_ocr_check.py          # сверка OCR с оригиналами

data/
├── transport.db                 # SQLite БД
├── cache/                       # дисковый кэш скачанных файлов (SHA-256(url))
├── terminal_coordinates.json    # координаты конечных, заполняется seed_coordinates.py
└── ocr_manual_check/            # выводы ручной сверки

Замечания

  1. Гибридная архитектура парсеров. 112С/Б/Д используют точный DOCX-парсер (python-docx → таблицы напрямую, 100% точность). Все остальные маршруты — OCR-пайплайн поверх rapidocr-onnxruntime (CPU, ONNX, поддержка русского). Это компромисс между качеством (DOCX даёт идеальные таблицы) и охватом (OCR покрывает любые форматы первоисточника).

  2. Координаты остановок. Конечные 38 OCR-маршрутов геокодируются один раз через OSM Nominatim (scripts/seed_coordinates.py); результат лежит в data/terminal_coordinates.json. Точки, которые Nominatim не нашёл, заполнены вручную как fallback. Для 112С/Б/Д координаты всех промежуточных остановок внесены вручную в seed-модулях. Для остальных маршрутов промежуточные остановки подтягиваются автоматически из OpenStreetMap через Overpass API при первом обращении к get_stops (см. замечание 7).

  3. Кэш. Скачанные документы хранятся в data/cache/ по SHA-256(url). Сгенерированный markdown — в таблице schedule_documents с TTL 24 ч (поле cache_meta.last_fetched_at).

  4. rapidocr-onnxruntime вместо PaddleOCR. PaddlePaddle 3.x имеет известный баг с oneDNN на Windows (OneDnnContext does not have the input Filter), который не выключается ни флагами, ни enable_mkldnn=False. ONNX Runtime упаковка тех же моделей PaddleOCR работает стабильно, занимает ~50 МБ вместо ~700 МБ paddlepaddle и даёт сравнимое качество распознавания.

  5. Дополнительные распознанные колонки. На длинных расписаниях (например, маршрут 26 на rasptomsk.ru) первоисточник физически разбит на несколько столбцов на странице. OCR-парсер ожидает 6 колонок по спецификации DayBlockSpec (3 блока × 2 направления), а распознаёт 7-8. Лишние колонки выводятся в секцию ## Дополнительные распознанные колонки со списком времён — это страховка против молчаливой потери данных. LLM-клиент видит и основные блоки, и дополнительные, и трактует их в контексте запроса пользователя.

  6. Маршрут 19 на rasptomsk.ru опубликован в виде PDF. OCR-движок определяет PDF по магическим байтам и рендерит каждую страницу через pypdfium2 в изображение перед распознаванием.

  7. Overpass API как источник остановок. Yandex Schedules API не покрывает городской транспорт Томска (только междугороднее автобусное и ж/д сообщение), Yandex Maps публичный API остановок маршрута не отдаёт. Поэтому источник промежуточных остановок — Overpass API (OpenStreetMap): бесплатный, без ключа. Запрос идёт по area["name"="Томская область"]["admin_level"="4"] (не ["name"="Томск"]["admin_level"="6"] — в OSM городские маршруты Томска относятся к области, а не к городу). Сам Overpass-запрос отсекает рекурсию по way-геометрии и явно резолвит только stop-nodes через node(r.routes); out; — иначе запрос для admin_level=4 не укладывается в server-side timeout. Метки синхронизации хранятся в таблице overpass_sync (TTL=168 ч / 7 дней), при недоступности OSM get_stops тихо возвращает имеющиеся в БД остановки. Поведение настраивается через env-переменные OVERPASS_ENABLED, OVERPASS_URL, OVERPASS_AREA_NAME, OVERPASS_ADMIN_LEVEL, OVERPASS_TIMEOUT_S, OVERPASS_SYNC_TTL_HOURS, OVERPASS_MIN_STOPS.

  8. Пригородные маршруты, не размеченные в OSM. Маршруты 118, 131, 141, 308, 514 в OpenStreetMap не размечены — для них get_stops всегда возвращает только 2 конечные остановки из seed. Это ограничение источника данных, не проекта. По состоянию на дату аудита 34 из 39 не-legacy маршрутов реально находятся в OSM и через Overpass отдают полный список остановок (для маршрута 12, например, 42+37 точек туда-обратно).

from github.com/atBuba/transport-MCP

Установка Tomsk Transport

У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.

▸ github.com/atBuba/transport-MCP

FAQ

Tomsk Transport MCP бесплатный?

Да, Tomsk Transport MCP бесплатный — установка в пару кликов через Unyly без оплаты.

Нужен ли API-ключ для Tomsk Transport?

Нет, Tomsk Transport работает без API-ключей и переменных окружения.

Tomsk Transport — hosted или self-hosted?

Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.

Как установить Tomsk Transport в Claude Desktop, Claude Code или Cursor?

Открой Tomsk Transport на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.

Похожие MCP

Compare Tomsk Transport with

Не уверен что выбрать?

Найди свой стек за 60 секунд

Автор?

Embed-бейдж для README

Похожее

Все в категории development