rail-interop-mcp
# Rail Interop Planner
MCP-інтерфейс до реєстру інфраструктури ERA RINF (Польща, Чехія) плюс два шари, яких немає в наявних інструментах перевірки сумісності маршруту (зокрема в офіційному ERA Route Compatibility Check): погодне попередження і рекомендація, як саме змінити поїзд, щоб маршрут став прохідним. Два MCP-сервери: власний `rail-interop-mcp` (маршрут, сумісність поїзда, погодний ризик) і наявний OpenWeather MCP (прогноз в опорних пунктах маршруту), об'єднані агентом на LangGraph.
Індивідуальна лабораторна робота курсу KSE Agentic AI Summer School, тема «MCP Integration». Автор: Тихон Дукарев.
`Python 3.12` · `uv` · `MIT`
## Архітектура
```
Користувач (CLI) → Агент LangGraph → MCP-клієнт ─ streamable-http ─→ rail-interop-mcp (окремий процес, Python) → data/rinf.sqlite (ERA RINF, PL+CZ)
└─ stdio ───────────→ mcp-openweather (окремий процес, Go) → api.openweathermap.org
```
```mermaid
flowchart LR
U[Користувач CLI] --> A[Агент LangGraph]
A --> C[MCP-клієнт]
C -- streamable-http --> S1["rail-interop-mcp\nвласний сервер"]
C -- stdio --> S2["OpenWeather MCP\nнаявний сервер"]
S1 --> D[("data/rinf.sqlite\nERA RINF, PL+CZ")]
S2 --> W[("OpenWeather API")]
```
Агент бачить дані лише через MCP: жодного прямого доступу до `data/rinf.sqlite` чи до OpenWeather API з коду агента. Власний сервер стартує першим у своєму терміналі (`uv run rail-interop-mcp --transport streamable-http`, `127.0.0.1:8765/mcp`); агент підключається за адресою з `RAIL_MCP_URL`, і той самий процес видно в MCP Inspector незалежно від агента. Сервер stateless: інструменти приймають маршрутний запит або явний список `section_ids`, жодного сесійного стану чи побічних ефектів; граф і індекси завантажуються один раз при старті (близько 0,1 с на 7883 точки). Обидва процеси друкують кожен виклик інструмента: сервер у stderr, агент - у консоль (discovery обох серверів, аргументи й скорочений результат кожного виклику).
## Як це працює: чотири питання
Агент відповідає на практичне питання «чи проїде цей конкретний поїзд цим маршрутом», ставлячи кожній ділянці маршруту чотири перевірки проти реєстру RINF:
1. **Чи збігається ширина колії?** Колісні пари поїзда (1435 мм) проти колії кожної ділянки: розбіжність - жорстка заборона.
2. **Чи витримає колія вагу?** Навантаження на вісь поїзда проти межі ділянки (типово 20-22,5 т): перевищення руйнує колію.
3. **Чи влізе поїзд за габаритом?** Профіль GA/GB/GC поїзда проти просвіту ділянки (мости, тунелі).
4. **Чи не задовгий поїзд?** Довжина проти ліміту менеджера інфраструктури (PL 750 м / CZ 740 м) - м'яке попередження.
Параметри, яких реєстр не знає (наприклад, ETCS у польській частині заповнений лише на 4,4%), чесно позначаються `unknown` - це прогалина даних, а не дозвіл і не заборона. Поверх статичного реєстру агент кладе два динамічні шари: **план тяги** (які локомотиви і де їх міняти через зміну системи струму) і **погодний ризик** на день запиту (вітер для порожніх вагонів, спека, мороз проти температурного класу ділянок) - при перевищенні порога агент сам перебудовує маршрут.
## Quick start
Передумови: [uv](https://docs.astral.sh/uv/), Python 3.12 (`uv` ставить сам), Go 1.24+ (для збірки OpenWeather MCP - `brew install go` або офіційний tarball), ключ [OpenWeather](https://openweathermap.org/api) (безкоштовний план, активація 10-60 хв) і ключ OpenRouter для LLM агента. Бінарник наявного сервера: `go install github.com/mschneider82/mcp-openweather@e032683574a0723591445462ef7104d360ad0889` (закріплений коміт, його звіряє doctor).
```bash
uv sync
cp .env.example .env # заповнити OWM_API_KEY, OWM_MCP_BIN, OPENROUTER_API_KEY
# Перевірка середовища (toolchain, ключі, зовнішні сервіси, без секретів у виводі)
uv run --python 3.12 --no-project scripts/doctor.py
# Термінал 1: власний сервер, окремий процес
uv run rail-interop-mcp --transport streamable-http
# Термінал 2: агент
uv run rail-agent --scenario A
# Тести (235, без мережі й ключів)
uv run pytest
# Discovery незалежно від агента (Термінал 1 має бути запущений)
npx -y @modelcontextprotocol/inspector --cli http://127.0.0.1:8765/mcp --method tools/list
```
## Демо-сценарії
Кожен сценарій - готовий профіль поїзда (`data/orders/*.yaml`) плюс маршрут; звіт українською пишеться в `out/`. Точні визначення - `rail_agent/scenarios.py`.
| Сценарій | Команда | Маршрут | Результат (реальний прогін) |
|---|---|---|---|
| A | `uv run rail-agent --scenario A` | Katowice - Ostrava, контейнерний поїзд | 81,4 км, PL-CZ через EU00072, увесь шлях `dc_3kv`, `feasible_with_conditions` (2 невизначені випадки габариту), погода спокійна (вітер до 7,7 м/с), низький ризик |
| B | `uv run rail-agent --scenario B` | Katowice - Breclav, той самий поїзд | 265,4 км, перехід `dc_3kv` → `ac_25kv_50hz` на лінії Přerov-Břeclav (90,1 км під 25 кВ), `feasible_with_conditions` (16 невизначених випадків через прогалини RINF на коротких ділянках), погода спокійна; `plan_traction` показує зміну локомотива ET22 → Vectron MS саме на стику (CZ35966) |
| B2 | `uv run rail-agent --scenario B2` | той самий маршрут, порожні платформи, знижений поріг вітру як демо штормового попередження IM | реальний прогноз вітру (3,9-8,0 м/с) перевищує знижений поріг 3,0 м/с → `overall=high`, `replan_recommended=true` → агент переплановує маршрут в об'їзд (373,8 км через EU00074) |
| C | `uv run rail-agent --scenario C` | той самий маршрут, поїзд 25 т/вісь | спершу `infeasible` (перевищення осьового навантаження), `adjust` пропонує і перевіряє нижче навантаження - фінальний звіт `feasible_with_conditions`, 81,4 км |
### Failure demo
Реалістичний збій наявного сервера і контрольована деградація (`docs/EXISTING_SERVER.md` розділ 5), без падіння всього потоку:
| Сценарій | Команда | Що відбувається |
|---|---|---|
| D1 | `uv run rail-agent --scenario D1` | шлях до бінарника OpenWeather MCP навмисно зламаний → з'єднання не встановлюється, discovery показує `weather` як `failed`, звіт про маршрут (81,4 км) виходить без погодного шару з явним поясненням |
| D2 | `uv run rail-agent --scenario D2` | погода для Katowice підмінена на неіснуюче місто `Kyivvvv` → сервер повертає «тихі нулі» (`isError=False`, порожня назва, всі значення 0), агент розпізнає це як збій і пропускає цю точку, погода для Ostrava надходить нормально - звіт частковий, а не порожній |
Джерело числа маршруту в будь-якому звіті можна простежити до сирого запису ERA:
```bash
uv run python scripts/trace_section.py c0fcd67d79
```
## Інструменти
| Інструмент | Що робить | Тип | Контракт |
|---|---|---|---|
| `resolve_operational_point` | знаходить операційні пункти PL/CZ за назвою (діакритика ігнорується) | пошук з обмеженим контрактом | [docs/TOOL_CONTRACTS.md](docs/TOOL_CONTRACTS.md) |
| `find_route` | обчислює маршрут по інфраструктурному графу ERA RINF (Dijkstra) | обчислення, первинне джерело | [docs/TOOL_CONTRACTS.md](docs/TOOL_CONTRACTS.md) |
| `check_train_compatibility` | перевіряє поїзд проти маршруту (габарит, осьове навантаження, довжина, просвіт) | валідація | [docs/TOOL_CONTRACTS.md](docs/TOOL_CONTRACTS.md) |
| `assess_weather_risk` | перетворює прогноз на маршруті на структурований ризик і рекомендацію | оцінка ризику | [docs/TOOL_CONTRACTS.md](docs/TOOL_CONTRACTS.md) |
| `plan_traction` | призначає локомотиви з парку (`data/locomotives.yaml`) на прогони маршруту, мінімізуючи кількість змін | планування | [docs/TOOL_CONTRACTS.md](docs/TOOL_CONTRACTS.md) |
| `weather` (наявний сервер, [mschneider82/mcp-openweather](https://github.com/mschneider82/mcp-openweather)) | поточна погода і прогноз на 5 діб для міста | наявний зовнішній сервіс | [docs/EXISTING_SERVER.md](docs/EXISTING_SERVER.md) |
Усі 5 назв вище дослівно збігаються з `tools/list` живого сервера (перевірено через MCP Inspector і тестом `tests/test_server_contract.py::test_tool_contracts_doc_names_match_tools_list`).
## Бонус: вебінтерфейс
Локальна вебапка поверх того самого агента, без жодних змін у `rail_agent/` чи `rail_mcp/`: форма з фільтрами (країна -> станція з пошуком по всіх 7883 точках -> пресет поїзда) або запит вільним текстом, живий прогрес по вузлах графа через SSE (умовні кроки adjust/replan видно як вставні), карта Leaflet з маршрутом (розфарбовка за енергосистемою або за сумісністю), лінійна схема, картки сумісності/тяги/погоди і журнал викликів інструментів.
```bash
# Термінал 1: власний сервер (як у Quick start)
uv run rail-interop-mcp --transport streamable-http
# Термінал 2: вебсервер бонусного інтерфейсу
uv run rail-web
# Браузер: http://127.0.0.1:8080
```
Це необов'язкове доповнення: основний спосіб роботи з проєктом - CLI вище. Фронтенд - vanilla JS + vendored [Leaflet 1.9.4](rail_web/static/vendor/leaflet/LICENSE.leaflet) (BSD-2), без node і кроку збірки; єдина зовнішня залежність у браузері - тайли OpenStreetMap (потрібен інтернет; перемикач «схема» працює і без них).
## Структура репозиторію
```
rail_mcp/ власний MCP-сервер: схеми, доменна логіка, main.py
domain/ чисті функції без залежності від MCP (graph, search, rules, weather_rules, traction, errors)
build/ збірка датасету з ERA SPARQL у data/rinf.sqlite
rail_agent/ агент LangGraph: граф, вузли, конфіг обох MCP-з'єднань, CLI
rail_web/ бонус: вебінтерфейс (FastAPI + SSE, static/ з vendored Leaflet 1.9.4)
data/ датасет (rinf.sqlite), довідники з джерелами (включно з locomotives.yaml), README.md
scripts/ doctor.py, smoke_weather.py, gen_tool_contracts.py, trace_section.py
tests/ 235 тестів: домен, контракт сервера (реальний stdio-виклик), агент, веб, дані
docs/ TOOL_CONTRACTS, contracts_extra.yaml, EXISTING_SERVER, DESIGN_RATIONALE
out/ звіти агента (ігнорується git)
```
## Design rationale
Кожен із 5 власних інструментів на межі MCP тому, що дані й правила сумісності (пороги, класифікація габариту, погодні ризики) мають жити біля датасету, а не в промпті: модель отримує вже структурований вердикт, не сирі рядки RINF. Компроміси, обмеження і причина, чому OpenWeather - єдиний потрібний наявний сервер для цього потоку, розписані в `docs/DESIGN_RATIONALE.md` (разом з розділом про співвідношення з офіційним ERA Route Compatibility Check).
## Дані й ліцензії
Реєстр інфраструктури ЄС (RINF), Європейське залізничне агентство, знімок Польщі й Чехії від 2026-08-18, ліцензія CC BY 4.0. Атрибуція («Data: European Union Agency for Railways, Register of Infrastructure (RINF), CC BY 4.0») додається до кожної відповіді власних інструментів і до звітів агента. Погодні дані - OpenWeather, атрибуція «Weather data provided by OpenWeather». Джерело, дата знімка, точні числа й команда відтворення (`uv run build-dataset --from-raw`) - [data/README.md](data/README.md).
## Обмеження
- RINF - реєстр інфраструктури, не розклад руху: `find_route` дає фізичний профіль маршруту, не час у дорозі й не наявність вільних вікон.
- ETCS майже відсутній у польській частині знімка (4726 з 4946 секцій без запису) - `etcs_level` там здебільшого `unknown`, це властивість джерела, не помилка сервера.
- Довідники `load_categories.yaml`, `train_length_limits.yaml`, `weather_thresholds.yaml` - вторинні джерела (EN 15528, Network Rail, RAIB), не офіційний реєстр ERA; кожен файл має посилання на джерело.
- `plan_traction` - жадібна евристика (тримає поточний локомотив, поки можливо), не доведений глобальний оптимум і не пошук по графу станів; фіксований демо-парк із двох локомотивів (`data/locomotives.yaml`); допуски локомотивів по країнах (габарит кузова, маса на вісь по типу локомотива) не моделюються.
- Погода: вітер відомий лише на день відправлення (наступні дні мають `wind_ms=null`); опорний пункт зводиться до найближчого великого міста зі словника (`rail_agent/city_names.py`), не координат - задокументоване обмеження контракту наявного сервера.
- Об'їзд закритої довільної ділянки і багатоітераційний цикл перепланування свідомо не реалізовані (один прохід кожного); зроблено лише перепланування, тригероване погодним ризиком (сценарій B2).
- Проєкт не намагається замінити офіційний ERA Route Compatibility Check - різниця й межі застосування розписані в `docs/DESIGN_RATIONALE.md` (розділ про prior art).
## Захист
Сценарій демонстрації на 12 хвилин буде в `docs/DEMO_SCRIPT.md`; коміт для захисту буде позначений тегом `v1.0.0`.
## Ліцензія
Код - MIT, [LICENSE](LICENSE). Дані ERA RINF - CC BY 4.0 (Європейське залізничне агентство). Погодні дані - OpenWeather. Дякую курсу KSE Agentic AI Summer School за завдання.
TDQS
Scored across 5 tools
Each tool targets a distinctly different aspect of the rail planning workflow: place resolution, route computation, infrastructure compatibility, weather risk, and traction planning. Their inputs and outputs are clearly separated, with no overlapping responsibilities or ambiguous boundaries.
All tool names follow a strict verb_noun pattern in snake_case: resolve_operational_point, find_route, check_train_compatibility, assess_weather_risk, plan_traction. The verbs are specific and the nouns clarify the object, making the naming entirely predictable.
With 5 tools, the server covers the core end-to-end rail interoperability workflow without redundancy. Each tool is necessary and contributes to a different step, so the count is well-calibrated for the stated purpose.
The tools form a complete lifecycle: resolve places to op_ids, find a route, validate train compatibility, assess weather risk, and plan traction. No obvious missing operations are apparent for the server's domain, and each tool's output feeds naturally into the next step.