solar_mcp
README.md
# solar-plan-mcp
Агент, який відповідає на одне побутове питання: **чи витягну я завтра цей набір
споживачів на власній генерації, і якщо ні — що перенести?**
Дах із панелями, батарея, інвертор, відомий графік відключень. Агент бере прогноз погоди
з готового MCP-сервера, рахує очікувану виробку по годинах на власному MCP-сервері,
перевіряє план проти фізичних правил, а якщо той не проходить — переносить гнучкі
навантаження і доводить числом, що стало краще.
Дві MCP-конекції:
| | Сервер | Роль |
|---|---|---|
| Готовий | [`mschneider82/mcp-openweather`](https://github.com/mschneider82/mcp-openweather), коміт `e032683` | прогноз: клас неба й температура на кожні 3 години |
| Власний | `solar_mcp` (цей репозиторій) | 4 змістовні інструменти домену + розбір тексту прогнозу |
Документація: [контракти інструментів](docs/TOOLS.md) ·
[раціо дизайну](docs/DESIGN.md) · [сценарій демонстрації](docs/DEMO.md)
## Що потрібно
| | Для чого | Примітка |
|---|---|---|
| Python 3.13 | агент і власний сервер | адмінських прав не треба |
| Go 1.24+ | **тільки** щоб зібрати сервер погоди | готових бінарників проєкт не публікує; `go.mod` вимагає 1.24, хоча README там пише 1.20 |
| Ключ OpenWeather | сервер погоди | безкоштовний, [openweathermap.org/api](https://openweathermap.org/api); **активується до кількох годин** |
| `claude` CLI + доступ до моделі | **тільки** агент; власний сервер і тести його не потребують | Claude Agent SDK спавнить цей CLI як дочірній процес — див. [Доступ до моделі](#доступ-до-моделі) |
| Node + npx | необов'язково — MCP Inspector | `npx @modelcontextprotocol/inspector` |
Датасет PVGIS уже лежить у репозиторії (`data/pvgis_kyiv_5kwp.csv`, 1.1 МБ), тому власний
сервер працює **без мережі**. Викачувати нічого не треба.
## Установка
Windows, і це не косметика: усі команди — у PowerShell, бо `&&` у PowerShell 5.1 взагалі
не оператор. Далі всюди `.venv\Scripts\python.exe`.
**Спершу дві змінні кодування, і вони різні.** `PYTHONUTF8=1` каже Python **писати** UTF-8;
`[Console]::OutputEncoding` каже PowerShell так само його **читати**. Без другої вивід
українською перетворюється на `╨▓╨╗╨░╤ü╨╜╨╕╨╣` — виміряно, і саме в трубі
(`| Tee-Object`, `| Select-String`), бо там PowerShell декодує байти кодовою сторінкою
консолі. Виставляти в **кожному** новому вікні, і до установки, а не після:
```powershell
[Console]::OutputEncoding = [Text.Encoding]::UTF8
$env:PYTHONUTF8 = "1"
```
```powershell
git clone https://github.com/prasolantoncp-bot/solar-plan-mcp.git
cd solar-plan-mcp
python -m venv .venv # або: uv venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt
```
Потрібен Python **3.13** — саме той, що відповідає на `python --version`, бо `venv`
успадковує версію інтерпретатора, яким його створили.
**Чому версії запінені.** Демонстрація має відтворюватись, а не «десь працювати».
`mcp==2.0.0` — це вже `MCPServer` замість `FastMCP`, і код під 1.x тут не піде;
`claude-agent-sdk==0.2.144` — на ній перевірені `tools` і формат init-повідомлення, з якого
агент друкує обидві конекції; `tzdata` — бо Windows не має системної бази часових поясів, а
без `Europe/Kyiv` не буде жодної місцевої години. Сам `requirements.txt` тримається в ASCII
навмисно: pip читає його **кодуванням локалі**, і одна кирилична літера в комментарі валить
`pip install -r` із `UnicodeDecodeError` на будь-якій машині, де локаль не UTF-8. Виміряно
на cp1252 із чистого клону — тому пояснення живе тут, а не в тому файлі.
### Зібрати сервер погоди
Go ставиться в профіль користувача, без адміністратора й без змін у реєстрі:
```powershell
# 1. портативний Go у профіль (один раз). curl.exe є у Windows 10 1803+
curl.exe -Lo go.zip https://go.dev/dl/go1.27.0.windows-amd64.zip
Expand-Archive go.zip -DestinationPath "$env:LOCALAPPDATA\Programs"
# 2. клон і збірка. GOROOT і PATH живуть лише в цьому вікні — так і треба
$env:GOROOT = "$env:LOCALAPPDATA\Programs\go"
$env:PATH = "$env:GOROOT\bin;$env:PATH"
New-Item -ItemType Directory -Force vendor | Out-Null
cd vendor
git clone https://github.com/mschneider82/mcp-openweather.git
cd mcp-openweather
git checkout e032683574a0723591445462ef7104d360ad0889
go build -o mcp-weather.exe .
cd ..\..
```
Готовий бінарник агент шукає за шляхом
`vendor\mcp-openweather\mcp-weather.exe`. Якщо він у тебе лежить інакше — не переміщуй, а
наведи змінну: `$env:WEATHER_MCP_BINARY = "…\mcp-weather.exe"` (див.
[`env.example`](env.example)). Агент перевіряє наявність файла **до** старту сесії й
відмовляється реченням, а не трасуванням із нутра SDK.
`vendor/` у `.gitignore`: чужа історія git і 13 МБ бінарника в цьому репозиторії ні до
чого. Коміт зафіксований — саме на ньому написана
[документація контракту](docs/TOOLS.md#готовий-сервер-weather-із-mcp-openweather).
> Якщо курс фіксує інший коміт `mcp-openweather` — беріть його й запишіть тут; опис
> контракту в `docs/TOOLS.md` писався з `main.go` на `e032683`.
### Ключ
Секрети в репозиторій не потрапляють: `.env` і `.env.*` у `.gitignore`, а зразок лежить у
[`env.example`](env.example) без жодного значення.
Демонстрація потребує трьох терміналів, а `$env:` живе лише в одному, тому ключ варто
виставити **на рівні користувача** — прав адміністратора для цього не треба:
```powershell
# так ключ не потрапляє ні в скролбек, ні в історію PSReadLine
$s = Read-Host "OWM_API_KEY" -AsSecureString
[Environment]::SetEnvironmentVariable("OWM_API_KEY",
[Runtime.InteropServices.Marshal]::PtrToStringBSTR(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($s)), "User")
```
Нове значення побачать **лише нові** термінали. Перевірка, що воно доїхало, не розкриваючи
ключа: `.venv/Scripts/python.exe -c "import os; print(len(os.environ.get('OWM_API_KEY','')))"`
— має бути `32`. На камері не запускати `dir env:`: воно друкує ключ.
Разова сесійна форма `$env:OWM_API_KEY = "…"` теж працює, але має тут одне правильне
застосування — **скинути** ключ в окремому вікні під сценарій відмови: `$env:OWM_API_KEY = ""`.
Ключ читається **тільки** з середовища — ні в коді, ні в `.mcp.json.example` його немає;
там стоїть підстановка `${OWM_API_KEY}`. Файл `.env` **ніхто не читає**: у коді лише
`os.environ.get`, тож копіювати `env.example` у `.env` — порожня дія.
### Доступ до моделі
Власний сервер і всі 57 тестів працюють **без** будь-яких креденшлів Anthropic — це різні
речі, і плутати їх не варто. Модель потрібна рівно одному файлу, `agent/run.py`.
Claude Agent SDK не звертається до API сам: він **спавнить `claude` CLI** як дочірній
процес, і саме той CLI шукає авторизацію. Тому потрібні дві речі:
1. **`claude` у `PATH`.** Перевірка: `(Get-Command claude).Source`. Установка — за
[офіційною інструкцією](https://docs.claude.com/en/docs/claude-code/overview); у цьому
проєкті він поставлений через WinGet і лежить у
`%LOCALAPPDATA%\Microsoft\WinGet\Links\claude.exe`.
2. **Авторизація — один із двох шляхів**, і CLI бере той, який знайде:
- `claude login` — інтерактивний вхід; токен CLI кладе в `~/.claude/.credentials.json`.
**Саме цей шлях використаний тут**: у середовищі процесу немає жодної змінної
`ANTHROPIC_*`, а файл креденшлів є. Записаний прогін 25 серпня 2026 пройшов так.
- `ANTHROPIC_API_KEY` у середовищі — ключ з [console.anthropic.com](https://console.anthropic.com).
Виставляється так само, як `OWM_API_KEY` вище, і так само не потрапляє в репозиторій.
Що запінено в коді: модель `claude-opus-5` ([`agent/run.py`](agent/run.py)) і
`claude-agent-sdk==0.2.144` ([`requirements.txt`](requirements.txt)). Якщо в тебе інший
доступ і цей id моделі не резолвиться — заміни його в `run.py` на доступний і запиши тут,
який саме; решта прогону від id не залежить.
Жодного креденшла цей код не читає й нікуди не передає: `agent/run.py` не звертається ні
до `ANTHROPIC_API_KEY`, ні до файлу креденшлів — цим займається CLI. У репозиторії
секретів немає, а `env.example` лежить порожній.
### Ліміти зовнішнього API
Безкоштовний план OpenWeather дає **60 викликів на хвилину**
([документація](https://docs.openweather.co.uk/appid)). Один прогін агента робить **один**
виклик інструмента `weather`; усередині сервер погоди перетворює його на два HTTP-запити
(поточна погода + прогноз на 5 діб). Тобто до стелі три порядки запасу навіть при
безперервних репетиціях.
У коді немає жодного циклу опитування, повтору при помилці чи фонового оновлення: погода
запитується рівно тоді, коли модель викликає інструмент. Власний сервер у мережу не
ходить взагалі — його датасет лежить у `data/`, тому скільки завгодно прогонів
`estimate_pv_generation`, `validate_energy_plan` і решти не створюють жодного зовнішнього
запиту.
## Запуск: два незалежні процеси
Власний сервер піднімається окремо від агента і про агента не знає нічого.
**Термінал 1 — власний MCP-сервер:**
```powershell
$env:PYTHONUTF8 = "1"
.venv\Scripts\python.exe -m solar_mcp --transport streamable-http --port 8931
```
**Термінал 2 — агент:**
```powershell
[Console]::OutputEncoding = [Text.Encoding]::UTF8
$env:PYTHONUTF8 = "1"
.venv\Scripts\python.exe agent\run.py
```
Без `--date` агент планує **завтрашню** добу: питання продукту саме про завтра, а прогноз
OpenWeather накриває лише `зараз … +5 діб`, тож сьогоднішня доба вже наполовину поза
горизонтом. Дата поза цим вікном дасть `NO_FORECAST_FOR_DATE`, а не мовчазні нулі.
Сервер погоди агент піднімає сам, по stdio — так налаштована конекція. Власний сервер
можна теж запускати по stdio (`python -m solar_mcp`, це дефолт) — так його чекають клієнти
типу Claude Code, і саме такий варіант описаний у [`.mcp.json.example`](.mcp.json.example).
Для демонстрації краще HTTP: тоді видно, що сервер справді окремий процес.
Корисні прапорці агента:
```text
--plan boiler:18:2 --plan aircon:18:3 # свій план замість дефолтного (можна кілька разів)
--date YYYY-MM-DD # інша доба; вт/чт/пт — без відключень, сб/нд — вечірнє вікно
--objective maximize_outage_reserve # інша цільова функція
--city Lviv # інше місто
--width 120 # скільки символів сліду друкувати
```
`--date` приймає лише добу **в межах горизонту прогнозу** — `завтра … сьогодні + 5`.
Дата поза ним дасть `NO_FORECAST_FOR_DATE`, і наявність вікна відключення в графіку цього
не рятує: графік лежить у репозиторії й знає будь-яку дату, а прогноз живе п'ять діб.
Перевірити перед запуском: `scripts/call_weather.py --city Kyiv --covers YYYY-MM-DD`.
Дати в графіку відключень мають тижневий візерунок і провенанс — див.
[`data/outage_windows.json`](data/outage_windows.json): вікна на 22–26 серпня внесені з
публічного розкладу, далі той самий візерунок повторений уперед, щоб демонстрація не
залежала від дати запису.
## Перевірка, що все живе
```powershell
# 4 інструменти домену + 1 допоміжний, зі схемами входу І виходу
.venv\Scripts\python.exe scripts\inspect_tools.py
.venv\Scripts\python.exe scripts\inspect_tools.py --url http://127.0.0.1:8931/mcp --schemas
# сервер погоди напряму: сирий текст і те, що з нього вийшло
.venv\Scripts\python.exe scripts\call_weather.py --city Kyiv
# 57 тестів: фізика, правила домену, планувальник, контракт через MCP-клієнта
$env:PYTHONUTF8 = "1"; $env:PYTHONPATH = "."
.venv\Scripts\python.exe -m pytest tests\ -q
```
Тести не потребують ні мережі, ні ключа OpenWeather, ні доступу до моделі: датасет лежить
у репозиторії, а відповіді чужого сервера — записані у
[`tests/fixtures/`](tests/fixtures/README.md).
## Що де лежить
```
solar_mcp/ власний MCP-сервер (окремий процес)
server.py інструменти й ресурс — увесь контракт
models.py схеми входу й виходу (Pydantic → справжні inputSchema/outputSchema)
errors.py закритий перелік кодів; помилка ≠ порожній результат
pv.py огинаюча ясного неба × прозорість × температурний дерейтинг
rules.py симуляція балансу, порушення, планувальник, порівняння
forecast.py розбір плоского тексту сервера погоди
dataset.py store.py читання датасету; реєстр виданих оцінок
agent/run.py Claude Agent SDK, дві MCP-конекції, слід викликів
scripts/ inspect_tools.py — контракт; call_weather.py — чужий сервер напряму
data/ датасет + fetch_pvgis.py (провенанс)
tests/ 57 тестів; у fixtures/ — три записані відповіді сервера погоди й одна синтетична
docs/ TOOLS.md · DESIGN.md · DEMO.md
```
Два з цих каталогів мають власний README, і саме їх шукають під «джерело даних» і
«фікстури»: [`data/README.md`](data/README.md) — звідки взято ряд PVGIS, тариф і графік
відключень; [`tests/fixtures/README.md`](tests/fixtures/README.md) — що саме записано з
чужого сервера, коли й чим.
## Одне спостереження, на якому стоїть половина дизайну
Сервер погоди **не відрізняє поломку від порожньої відповіді**. Без ключа він повертає
`is_error: false` і текст із нулями та порожньою назвою міста — записано дослівно у
[`tests/fixtures/owm_no_api_key.txt`](tests/fixtures/owm_no_api_key.txt), хоча його
README обіцяє «FATAL: OWM_API_KEY environment variable not set».
Тому власний сервер зроблено навпаки: закритий перелік кодів помилок, `field` із
вказівкою на винне поле, і окремо — `reason` там, де порожньо законно (ніч, відсутність
порушень). Подробиці: [DESIGN.md](docs/DESIGN.md), [TOOLS.md](docs/TOOLS.md).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues