rzd-api
MCP server providing access to Russian Railways ticket API, enabling train search, station lookup, and trip information.
README
RZD API for Bun
Типизированный асинхронный клиент на Bun/TypeScript и MCP-сервер для
неофициального API ticket.rzd.ru. Проект не связан с
ОАО «РЖД»; внутренние endpoint и схемы ответов могут меняться без предупреждения.
Возможности
- поиск прямых поездов в одну сторону и туда-обратно;
- поиск станций и разрешение названий в коды;
- календарь поездов и минимальные цены;
- вагоны, места, схемы, изображения и станции маршрута;
- поиск полностью свободного купе по диапазону дат;
- MCP через STDIO и Streamable HTTP;
- retries, таймауты и LRU-кэш станций.
Установка
Требуется Bun 1.2 или новее.
bun install
TypeScript API
import { RzdClient } from "rzd-api";
const client = new RzdClient();
try {
const routes = await client.searchTickets(
"Москва",
"Санкт-Петербург",
"2026-09-01",
{ adults: 1 },
);
console.log(routes);
} finally {
client.close();
}
Основные методы: searchTickets, findStations, resolveStationCode,
getCarriages, getTrainAvailability, getMinimalPrices, getCarScheme,
getCarImages, getRouteStations, searchFullCompartments.
Полное купе
searchFullCompartments и MCP-инструмент search_full_compartments перебирают
диапазон дат (не более 31 дня) и разделяют два разных ответа:
confirmed— API вернул нужные места внутри одного купе (FreePlacesByCompartments), указаны номер вагона, номер купе и сами места. Один физический вагон приходит несколькими записями — нижние и верхние полки одного купе тарифицируются отдельно, — поэтому записи вагона сначала сливаются по номеру, иначе купе из четырёх мест выглядит как два раза по два;candidates— свободных мест в вагоне достаточно, но одно купе не подтверждено:places_not_in_one_compartment(места в разных купе) илиcompartment_layout_missing(в ответе нет разбивки по купе).
Суммарное число свободных мест никогда не переводит вагон в confirmed. Инвариант
закреплён тестами на фикстурах в tests/fixtures/, поэтому изменение парсера не может
незаметно вернуть более смелую формулировку.
Номера мест возвращаются вместе с их маркерами: 36Ж — место в женском купе,
6С — в смешанном. Купе определяет число, маркер сохраняется, потому что именно
его пассажир увидит в билете.
Поле checkedAt содержит момент проверки по московскому времени: наличие мест
устаревает за минуты. Даты, которые не удалось проверить, попадают в errors,
а не молча превращаются в «мест нет».
Готовая инструкция для ассистента лежит в skills/find-full-compartment/.
Схема вагона
getCarScheme возвращает imageUrls — абсолютные ссылки на чертёж вагона
(SVG). MCP-инструмент get_car_scheme с include_image: true вкладывает сам
чертёж в ответ; по умолчанию выключено.
Чертёж отдаётся в PNG, а не в SVG: модели принимают png, jpeg, gif и webp, а
SVG клиент может только сохранить в файл. Растеризацией занимается
@resvg/resvg-wasm; шрифт для номеров мест (сабсет DejaVu Sans, 14 КБ) зашит в
src/scheme-font.ts, иначе resvg не нарисует текст. Если растеризатор не
поднялся, возвращается исходный SVG.
Сам чертёж — шаблон: все полки почти белые, номера на них тоже белые, потому что
сайт перекрашивает места под статус. Поэтому free_places заливаются синим, а
selected_places красным — те же два цвета, что в легенде ticket.rzd.ru.
Номера на незакрашенных полках перекрашиваются в тёмный, на закрашенных
остаются белыми. Без этого картинка нечитаема.
Рисуется весь вагон целиком, около 60 КБ и 300 мс. Кадрирование до одного купе не делается намеренно: resvg режет уже отрисованное, поэтому зум в купе заставил бы его сначала нарисовать вагон в 21000 пикселей шириной — это 17 секунд.
Ссылки строятся от schemeImageBaseUrl (RZD_SCHEME_IMAGE_BASE_URL,
по умолчанию публичный ticket.rzd.ru) и никогда от RZD_BASE_URL: иначе
адрес приватного proxy попадёт в каждый ответ ассистента. Байты, наоборот,
запрашиваются через RZD_BASE_URL, чтобы работать из закрытой сети. Инвариант
закреплён тестом в tests/car-scheme.test.ts.
MCP
Локальный STDIO:
bun run mcp
Streamable HTTP на loopback:
bun run mcp:http
curl http://127.0.0.1:8000/health
При публикации на non-loopback адресе нужен Bearer-токен длиной не менее 32 символов:
MCP_TRANSPORT=streamable-http \
MCP_HOST=0.0.0.0 \
MCP_AUTH_TOKEN="replace-with-a-random-token-at-least-32-characters" \
bun run src/mcp.ts
Endpoint: http://localhost:8000/mcp. Переменные окружения:
MCP_PORT, MCP_RATE_LIMIT_PER_MINUTE, MCP_ALLOWED_HOSTS.
Подключение к Codex:
codex mcp add rzd -- bun run /absolute/path/to/rzd-api/src/mcp.ts
Docker
export MCP_AUTH_TOKEN="replace-with-a-random-token-at-least-32-characters"
docker compose up -d
Vercel
Проект содержит Vercel Functions без web-фреймворка:
https://<project>.vercel.app/— страница с краткой документацией;https://<project>.vercel.app/mcp— публичный Streamable HTTP MCP;https://<project>.vercel.app/health— healthcheck.
Страница собирается в src/landing.ts из того же registerMcpTools, что
регистрирует инструменты в MCP: новый инструмент без русского описания роняет
сборку страницы, поэтому таблица не может разойтись с сервером.
Vercel entrypoint использует Elysia, официальный mcp-handler и Bun Runtime.
Маршруты принадлежат самому приложению; Vercel rewrites не используются.
Endpoint РЖД можно переопределить переменными окружения RZD_BASE_URL и
RZD_B2B_BASE_URL. Значения не должны храниться в репозитории.
Под serverless клиент отказывает быстро: при выставленной VERCEL таймаут
становится 8 секунд, повтор один. Иначе зависший upstream съедает всё время
функции, платформа обрывает вызов, и клиент получает не ошибку, а мёртвое
соединение — по нему невозможно понять, что случилось. Переопределяется
RZD_TIMEOUT_MS и RZD_RETRY_TOTAL.
vercel deploy
Ограничение нагрузки
Публичный /mcp открыт без авторизации, поэтому лимит стоит на эдже Vercel, а не
в коде: serverless-инстанс держит счётчики в памяти, масштабируется горизонтально
и теряет их на каждом холодном старте, так что лимит в процессе — это «N на
инстанс». Эдж к тому же отбивает запрос до вызова функции.
./scripts/firewall-rate-limit.sh # применить: 60 запросов в минуту с IP
./scripts/firewall-rate-limit.sh check # показать живое правило
Скрипт идемпотентен: существующее правило редактируется на месте, отсутствующее
создаётся, черновик публикуется. Значения переопределяются переменными
RATE_LIMIT_REQUESTS, RATE_LIMIT_WINDOW, RATE_LIMIT_PATH, RULE_NAME.
Ключ лимита — IP клиента. Vercel сам перезаписывает x-forwarded-for и не
пропускает внешние значения, поэтому подделать адрес нельзя.
Учтите, что лимит считает входящие запросы, а не исходящие: один вызов
search_full_compartments за месяц — это десятки обращений к API РЖД. Сам вызов
ограничен с трёх сторон: диапазон не длиннее 31 дня, поезд не открывается, если
ни одна его купейная группа не дотягивает до нужного числа мест (группа не может
содержать меньше мест, чем любой её вагон, поэтому пропуска подтверждённого купе
не будет), и весь обход ограничен бюджетом maxRequests — по умолчанию 150.
К первому подтверждённому купе прикладывается чертёж вагона с залитыми синим
местами — и картинкой в ответе, и ссылкой image.url внутри JSON. Ссылка нужна
потому, что не всякий клиент показывает image-блоки: ChatGPT их не видит и,
оставшись без изображения, иллюстрирует ответ фотографиями чужих вагонов из
интернета. Отключается include_image: false.
Ответ инструмента несёт чертёж тремя способами, потому что клиенты различаются
в том, что показывают: сама картинка блоком image с аннотацией
audience: ["user"], штатный resource_link со ссылкой на неё и та же ссылка
полем image.url первым в JSON — последним оно быть не может, длинный ответ
клиент обрезает с хвоста.
Ссылка ведёт на собственный endpoint:
GET /scheme/552/PcFirstStorey.png?free=33,34,35,36
Он рисует ту же схему и кэшируется на сутки. Специально узкий — числовой номер
схемы, известная раскладка, не больше 36 мест, — чтобы не превратиться в
универсальный прокси. Адрес сервера задаётся RZD_PUBLIC_BASE_URL.
Диапазон, начинающийся в прошлом, не отвергается, а подрезается сегодняшним днём:
«найди купе на август» приходит целым месяцем и в середине августа, и отказывать
из-за прошедших дат бессмысленно. Фактическое начало видно в dateFrom ответа.
Обход ограничен и по времени — maxSeconds, по умолчанию 9 секунд. Serverless-
вызов всё равно обрывается платформой через несколько секунд, а оборванный вызов
не сообщает клиенту ничего и выглядит как недоступный сервис. Лучше вернуть
найденное и назвать даты, до которых не дошли.
Обход останавливается, как только набрано maxResults подтверждённых купе — по
умолчанию три ближайших варианта, а не весь месяц. Даты, до которых он не дошёл,
возвращаются в unchecked, а не выдаются за пустые: с них можно продолжить
поиск. Что не поместилось в ответ, посчитано в omitted, потраченные запросы —
в requests.
Логи
Каждый вызов инструмента и каждый запрос наружу пишутся строкой JSON в stdout — это то, что собирает и показывает Vercel:
{"at":"2026-08-06T11:25:35.095Z","upstream":"suggests","status":200,"ms":504,"attempt":0}
{"at":"2026-08-06T11:25:37.421Z","tool":"search_full_compartments","ms":2331,"ok":true,"blocks":["text","image"]}
По upstream видно, какие endpoint РЖД дёргались и сколько отвечали, по tool —
дошёл ли клиент до инструмента вообще. Без этого зависший upstream и клиент,
который инструмент не вызывал, выглядят снаружи одинаково.
Адрес из RZD_BASE_URL в лог не попадает: пишется только путь под ним. Лог —
типовое место, куда утекают секреты, поэтому на это есть тест.
Разработка
bun run check
Безопасность
Проверка TLS-сертификата включена: ticket.rzd.ru предъявляет публично
доверенный сертификат. Отключить её можно только явно, переменной
RZD_INSECURE_TLS=1 — это нужно, если запрос идёт через собственный proxy с
самоподписанным сертификатом. Не передавайте клиенту секреты или учётные данные.
Лицензия
MIT
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。