Rzd Api
БесплатноНе проверенMCP server providing access to Russian Railways ticket API, enabling train search, station lookup, and trip information.
Описание
MCP server providing access to Russian Railways ticket API, enabling train search, station lookup, and trip information.
README
Типизированный асинхронный клиент на 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
Установка Rzd Api
У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.
▸ github.com/slavb18/rzd-apiFAQ
Rzd Api MCP бесплатный?
Да, Rzd Api MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Rzd Api?
Нет, Rzd Api работает без API-ключей и переменных окружения.
Rzd Api — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить Rzd Api в Claude Desktop, Claude Code или Cursor?
Открой Rzd Api на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Похожие MCP
GitHub
PRs, issues, code search, CI status
автор: GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
автор: mcpdotdirectCompare Rzd Api with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории development
