tomsk-transport

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 prompt for LLMs.

Category
访问服务器

README

Transport MCP Server (Tomsk)

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 точек туда-обратно).

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选