first commit

This commit is contained in:
Murat ÖZDEMİR 2026-03-24 16:24:16 +03:00
commit 28c24d8cb1
20 changed files with 4271 additions and 0 deletions

13
.env.example Normal file
View File

@ -0,0 +1,13 @@
# Ortam seçimi: prod | test | local
# IKLIM_BASE_URL tanımlıysa bu değer görmezden gelinir
IKLIM_ENV=prod
# Opsiyonel. Tanımlıysa IKLIM_ENV'i override eder
# IKLIM_BASE_URL=https://api.iklim.co
# HMAC-SHA256 istek imzalama anahtarı (zorunlu)
IKLIM_HMAC_SECRET=
# API kullanıcı bilgileri (zorunlu)
IKLIM_USERNAME=
IKLIM_PASSWORD=

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
dist/
.env

701
README.md Normal file
View File

@ -0,0 +1,701 @@
# 🌩️ iklim.co MCP Server
[Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for the iklim.co Weather API. AI asistanların (Claude, OpenClaw vb.) iklim.co'nun hava durumu, yıldırım, fırtına, yağış ve alarm API'lerine doğal dil ile erişmesini sağlar.
## 📋 İçindekiler
- [🌐 Genel Bakış](#-genel-bakış)
- [⚙️ Gereksinimler](#-gereksinimler)
- [🚀 Kurulum](#-kurulum)
- [🔧 Ortam Değişkenleri](#-ortam-değişkenleri)
- [▶️ Build ve Çalıştırma](#-build-ve-çalıştırma)
- [🔌 MCP Client Konfigürasyonu](#-mcp-client-konfigürasyonu)
- [Claude CLI (.mcp.json)](#claude-cli-mcpjson)
- [OpenClaw](#openclaw)
- [Diğer MCP İstemcileri](#diğer-mcp-i̇stemcileri)
- [🛠️ Araç Kataloğu](#-araç-kataloğu)
- [⚡ Yıldırım / Lightning](#-yıldırım--lightning)
- [🌪️ Fırtına / Thunderstorm](#-fırtına--thunderstorm)
- [🌧️ Yağış / Precipitation](#-yağış--precipitation)
- [🌤️ Hava Tahmini / Forecast](#-hava-tahmini--forecast)
- [👤 Auth & Kullanıcı](#-auth--kullanıcı)
- [🏢 Hesap / Account](#-hesap--account)
- [📍 Nokta Alarmları / Point Alarms](#-nokta-alarmları--point-alarms)
- [🗺️ Coğrafi Alarmlar / Geo Alarms](#-coğrafi-alarmlar--geo-alarms)
- [📅 Tahmin Alarmları / Forecast Alarms](#-tahmin-alarmları--forecast-alarms)
- [🏗️ Mimari](#-mimari)
- [🔐 Kimlik Doğrulama ve Güvenlik](#-kimlik-doğrulama-ve-güvenlik)
- [Dahili Auth Akışı](#dahili-auth-akışı)
- [JWT Token Yaşam Döngüsü](#jwt-token-yaşam-döngüsü)
- [HTTP İstek Header'ları](#http-i̇stek-headerları)
- [HMAC-SHA256 İmza Hesabı](#hmac-sha256-i̇mza-hesabı)
- [Auth ile Normal İstekler Arasındaki Fark](#auth-ile-normal-i̇stekler-arasındaki-fark)
- [Güvenlik Önerileri](#güvenlik-önerileri)
- [💻 Geliştirici Notları](#-geliştirici-notları)
---
## 🌐 Genel Bakış
Bu MCP server, iklim.co REST API'sinin tüm yeteneklerini **57 araç** (tool) olarak sunar. Araçlar 9 kategoriye ayrılmıştır:
| Kategori | Araç Sayısı | Kapsam |
|----------|:-----------:|--------|
| ⚡ Lightning | 2 | Yıldırım çarpma verileri |
| 🌪️ Thunderstorm | 3 | Fırtına hücresi takibi |
| 🌧️ Precipitation | 2 | Radar yağış verileri |
| 🌤️ Forecast | 3 | Saatlik / günlük / anlık tahmin |
| 👤 Auth & User | 11 | Kimlik doğrulama, kullanıcı yönetimi |
| 🏢 Account | 8 | Hesap ve abonelik yönetimi |
| 📍 Point Alarms | 6 | Nokta tabanlı uyarı abonelikleri |
| 🗺️ Geo Alarms | 12 | Coğrafi sınır bazlı uyarılar + il/ilçe/mahalle kataloğu |
| 📅 Forecast Alarms | 10 | Eşik bazlı tahmin uyarıları + il/ilçe kataloğu |
| **Toplam** | **57** | |
---
## ⚙️ Gereksinimler
- **Node.js** >= 18 (ES2022 desteği gerekli)
- **npm** >= 9
- iklim.co API erişim bilgileri (HMAC secret, kullanıcı adı ve şifre)
---
## 🚀 Kurulum
```bash
cd mcp-server
npm install
npm run build
```
---
## 🔧 Ortam Değişkenleri
Server başlamadan önce aşağıdaki değişkenlerin tanımlı olması gerekir. Geliştirme ortamında `.env` dosyası oluşturabilirsiniz (projenin `.gitignore` dosyasına ekli):
```bash
# .env
IKLIM_ENV=test # prod | test | local (IKLIM_BASE_URL yoksa kullanılır)
IKLIM_BASE_URL= # Opsiyonel. Tanımlıysa IKLIM_ENV'i override eder
IKLIM_HMAC_SECRET=<secret> # Zorunlu. İstek imzalama için HMAC-SHA256 anahtarı
IKLIM_USERNAME=<email> # Zorunlu. API kullanıcı e-postası
IKLIM_PASSWORD=<password> # Zorunlu. API kullanıcı şifresi
```
**🌍 Ortama göre base URL:**
| `IKLIM_ENV` | URL |
|-------------|-----|
| `prod` | `https://api.iklim.co` |
| `test` | `https://api-test.iklim.co` |
| `local` | `http://localhost:8080` |
---
## ▶️ Build ve Çalıştırma
```bash
# TypeScript'i derle (dist/ klasörünü oluşturur)
npm run build
# Derlenmiş server'ı başlat
npm start
# Geliştirme modunda çalıştır (derleme gerekmez, ts-node kullanır)
npm run dev
```
Başarılı başlatmada çıktı:
```
iklim.co MCP server running
```
> Server **stdio** transportu üzerinden iletişim kurar — doğrudan terminal ile çalıştırmak yerine bir MCP istemcisi tarafından yönetilmesi beklenir.
---
## 🔌 MCP Client Konfigürasyonu
### Claude CLI (.mcp.json)
Projenin kök dizinindeki `.mcp.json` dosyası Claude CLI tarafından otomatik olarak yüklenir:
```json
{
"mcpServers": {
"iklim": {
"command": "node",
"args": ["/tam/yol/mcp-server/dist/index.js"],
"env": {
"IKLIM_ENV": "test",
"IKLIM_HMAC_SECRET": "<secret>",
"IKLIM_USERNAME": "<email>",
"IKLIM_PASSWORD": "<password>"
}
}
}
}
```
Global olarak tanımlamak için `~/.claude/settings.json` içine aynı `mcpServers` bloğunu ekleyin.
### OpenClaw
`openclaw mcp set` komutu `env` parametresini desteklemez. Tüm alanlar tek satır JSON olarak geçirilmeli, `env` de dahil:
```bash
openclaw mcp set iklim '{"type":"stdio","command":"node","args":["/tam/yol/mcp-server/dist/index.js"],"env":{"IKLIM_ENV":"test","IKLIM_HMAC_SECRET":"<secret>","IKLIM_USERNAME":"<email>","IKLIM_PASSWORD":"<password>"}}'
```
Veya `~/.openclaw/openclaw.json` içinde `mcp` bölümüne doğrudan ekleyin (okunabilir format):
```json
{
"mcp": {
"iklim": {
"type": "stdio",
"command": "node",
"args": ["/tam/yol/mcp-server/dist/index.js"],
"env": {
"IKLIM_ENV": "test",
"IKLIM_HMAC_SECRET": "<secret>",
"IKLIM_USERNAME": "<email>",
"IKLIM_PASSWORD": "<password>"
}
}
}
}
```
### Diğer MCP İstemcileri
MCP stdio standardını destekleyen her istemci kullanılabilir. Gerekli bilgiler:
- **transport**: `stdio`
- **command**: `node`
- **args**: `["<dist/index.js tam yolu>"]`
- **env**: Yukarıdaki dört değişken
---
## 🛠️ Araç Kataloğu
### ⚡ Yıldırım / Lightning
#### `get_lightnings_within`
Belirli bir merkez noktası ve yarıçap içindeki yıldırım çarpmalarını sorgular.
| Parametre | Tip | Açıklama |
|-----------|-----|----------|
| `latitude` | number | Merkez enlem (-90 / 90) |
| `longitude` | number | Merkez boylam (-180 / 180) |
| `radius` | number | Yarıçap, metre (0 50.000) |
| `backwardInterval` | number | Geriye dönük süre, saniye (60 2.592.000 / 30 gün) |
| `endTimeEpoch` | number | Sorgu bitiş zamanı, epoch ms |
| `pageNumber` | number? | Sayfa numarası (default: 0) |
| `pageSize` | number? | Sayfa boyutu, max 100 (default: 10) |
#### `get_lightnings_page`
Zaman aralığına göre yıldırım verilerini sayfalı olarak getirir.
| Parametre | Tip | Açıklama |
|-----------|-----|----------|
| `backwardInterval` | number | Geriye dönük süre, saniye (60 432.000 / 5 gün) |
| `endTimeEpoch` | number | Sorgu bitiş zamanı, epoch ms |
| `pageNumber` | number? | Sayfa numarası |
| `pageSize` | number? | Sayfa boyutu, max 100 |
---
### 🌪️ Fırtına / Thunderstorm
#### `get_thunderstorms_within`
Yarıçap içindeki fırtına hücrelerini sorgular.
| Parametre | Tip | Açıklama |
|-----------|-----|----------|
| `latitude` | number | Merkez enlem |
| `longitude` | number | Merkez boylam |
| `radius` | number | Yarıçap, metre (0 50.000) |
| `backwardInterval` | number | Geriye dönük süre, saniye (60 2.592.000) |
| `endTimeEpoch` | number | Bitiş zamanı, epoch ms |
| `intersectsWith` | string? | `THREAT_POLYGON` veya `CELL_POLYGON` |
| `pageNumber` | number? | |
| `pageSize` | number? | |
#### `get_thunderstorms_page`
Zaman aralığına göre fırtına verilerini sayfalı getirir.
| Parametre | Tip | Açıklama |
|-----------|-----|----------|
| `backwardInterval` | number | Geriye dönük süre, saniye (60 432.000) |
| `endTimeEpoch` | number | Bitiş zamanı, epoch ms |
| `pageNumber` | number? | |
| `pageSize` | number? | |
#### `get_thunderstorm_details`
Belirli bir fırtına olayının geçmiş detaylarını getirir.
| Parametre | Tip | Açıklama |
|-----------|-----|----------|
| `eventId` | string | Örn: `EVT20240413001` |
| `pageNumber` | number? | |
| `pageSize` | number? | |
---
### 🌧️ Yağış / Precipitation
#### `get_precipitations_within`
Dairesel alan içindeki radar yağış verilerini sorgular.
| Parametre | Tip | Açıklama |
|-----------|-----|----------|
| `latitude` | number | Merkez enlem |
| `longitude` | number | Merkez boylam |
| `radius` | number | Yarıçap, metre (0 50.000) |
| `backwardInterval` | number | Geriye dönük süre, saniye (60 2.592.000) |
| `endTimeEpoch` | number | Bitiş zamanı, epoch ms |
| `intensityThreshold` | string? | Min. yoğunluk: `DRIZZLE` < `LIGHT` < `MODERATE` < `HEAVY` < `VERY_HEAVY` < `EXTREME` |
| `pageNumber` | number? | |
| `pageSize` | number? | |
#### `get_precipitations_page`
Zaman aralığına göre yağış verilerini sayfalı getirir. `intensityThreshold` **zorunludur**.
---
### 🌤️ Hava Tahmini / Forecast
#### `get_hourly_forecast`
Saatlik hava durumu tahminlerini getirir (114 gün).
| Parametre | Tip | Açıklama |
|-----------|-----|----------|
| `latitude` | number | Enlem |
| `longitude` | number | Boylam |
| `forecastDays` | number? | 114, default 7 |
| `metrics` | string[]? | İstenilen metrik listesi (bkz. aşağısı) |
| `startTime` | string? | ISO 8601 başlangıç zamanı |
| `solarPanelTiltForRadiation` | number? | Panel eğim açısı (radyasyon metriği için) |
| `solarPanelAzimuthForRadiation` | number? | Panel azimut açısı |
<details>
<summary>📊 Desteklenen metrikler (53 adet)</summary>
`WEATHER_ICON`, `TEMPERATURE`, `APPARENT_TEMPERATURE`, `DEW_POINT_TEMPERATURE`, `HUMIDITY`, `CLOUD_COVER`, `CLOUD_COVER_LOW`, `CLOUD_COVER_MID`, `CLOUD_COVER_HIGH`, `WIND_SPEED`, `WIND_GUST`, `WIND_DIRECTION`, `WIND_SPEED_AT_100M`, `WIND_DIRECTION_AT_100M`, `PRECIPITATION`, `RAIN`, `SHOWERS`, `SNOWFALL`, `SNOW_DEPTH`, `PRECIPITATION_PROBABILITY`, `WEATHER_CODE`, `PRESSURE_MSL`, `SURFACE_PRESSURE`, `VISIBILITY`, `EVAPOTRANSPIRATION`, `ET0_FAO_EVAPOTRANSPIRATION`, `VAPOUR_PRESSURE_DEFICIT`, `CAPE`, `LIFTED_INDEX`, `CONVECTIVE_INHIBITION`, `SUNSHINE_DURATION`, `SHORTWAVE_RADIATION`, `DIRECT_RADIATION`, `DIFFUSE_RADIATION`, `DIRECT_NORMAL_IRRADIANCE`, `GLOBAL_TILTED_IRRADIANCE`, `TERRESTRIAL_RADIATION`, `SHORTWAVE_RADIATION_INSTANT`, `DIRECT_RADIATION_INSTANT`, `DIFFUSE_RADIATION_INSTANT`, `DIRECT_NORMAL_IRRADIANCE_INSTANT`, `GLOBAL_TILTED_IRRADIANCE_INSTANT`, `TERRESTRIAL_RADIATION_INSTANT`, `SOIL_TEMPERATURE_0CM`, `SOIL_TEMPERATURE_6CM`, `SOIL_TEMPERATURE_18CM`, `SOIL_TEMPERATURE_54CM`, `SOIL_MOISTURE_0_TO_1CM`, `SOIL_MOISTURE_1_TO_3CM`, `SOIL_MOISTURE_3_TO_9CM`, `SOIL_MOISTURE_9_TO_27CM`, `SOIL_MOISTURE_27_TO_81CM`, `IS_DAY`
</details>
#### `get_daily_forecast`
Günlük agregat tahminleri getirir. Parametreler `get_hourly_forecast` ile aynıdır (`solarPanel*` parametreleri hariç).
#### `get_current_weather`
Konum için anlık (en güncel) hava verilerini getirir.
| Parametre | Tip | Açıklama |
|-----------|-----|----------|
| `latitude` | number | Enlem |
| `longitude` | number | Boylam |
| `metrics` | string[]? | Yukarıdaki metrik listesinden seçim |
---
### 👤 Auth & Kullanıcı
#### `auth_register`
Yeni kullanıcı kaydı oluşturur.
| Parametre | Tip |
|-----------|-----|
| `username` | string (e-posta) |
| `password` | string |
| `firstName` | string |
| `lastName` | string |
| `midName` | string? |
| `locale` | string? |
| `timezone` | string? |
#### `auth_logout`
Geçerli JWT token'ı geçersiz kılar.
#### `user_get_me`
Oturum açmış kullanıcının profilini getirir.
#### `user_get`
`userId` ile kullanıcı detayını getirir.
#### `user_create`
_(Admin)_ Yeni kullanıcı oluşturur. `roles` ve `status` zorunludur.
#### `user_update`
_(Admin)_ `userId` ile kullanıcı alanlarını günceller.
#### `user_list`
Kullanıcı listesini sayfalı getirir. `roles`, `status`, `pageNumber`, `pageSize` ile filtrelenir.
#### `user_unblock`
Bloke edilmiş kullanıcıyı açar.
#### `user_change_password`
`oldPassword` ve `newPassword` ile şifre değiştirir.
#### `user_password_reset_request`
Şifre sıfırlama e-postası gönderir. `userName` ve `passwordResetPageLink` gerekir.
#### `user_password_reset`
Sıfırlama token'ı ile şifre günceller. `userId`, `token`, `newPassword`, `loginPageLink` gerekir.
---
### 🏢 Hesap / Account
#### `account_get`
`userId` ile hesap detaylarını getirir.
#### `account_create`
Yeni hesap oluşturur.
| Parametre | Tip | Değerler |
|-----------|-----|----------|
| `type` | string | `INDIVIDUAL` \| `ORGANIZATION` |
| `subscriptionPlan` | string | `NONE` \| `TRIAL` \| `BASIC_MONTHLY` \| `BASIC_YEARLY` \| `PREMIUM_MONTHLY` \| `PREMIUM_YEARLY` \| `CUSTOM` |
| `mobilePhoneNumber` | string? | |
| `location` | string? | |
| `company` | string? | |
| `industry` | string? | |
| `profilePictureUrl` | string? | |
#### `account_update`
`accountId` ile hesap alanlarını günceller.
#### `account_activation_request`
Aktivasyon e-postası gönderir.
#### `account_activate`
E-posta doğrulama token'ı ile hesabı aktive eder.
#### `account_phone_activation_request`
SMS doğrulama kodu gönderir.
#### `account_activate_phone`
SMS token'ı ile telefonu doğrular.
#### `account_update_subscription`
Abonelik planını değiştirir.
---
### 📍 Nokta Alarmları / Point Alarms
Belirli bir GPS koordinatı ve yarıçap etrafındaki olaylar için uyarı abonelikleri.
#### `point_alarm_register`
Yeni nokta alarmı oluşturur.
| Parametre | Tip | Açıklama |
|-----------|-----|----------|
| `recipientId` | string | Uyarı alıcısı ID |
| `latitude` | number | Merkez enlem |
| `longitude` | number | Merkez boylam |
| `radius` | number | Yarıçap, metre (0 50.000) |
| `lightningFilter` | object? | Yıldırım filtresi |
| `thunderstormFilter` | object? | Fırtına filtresi |
| `precipitationFilter` | object? | Yağış filtresi |
| `webhook` | object? | Webhook konfigürasyonu |
#### `point_alarm_update`
Mevcut kaydı günceller.
#### `point_alarm_delete`
Alarm kaydını siler.
#### `point_alarm_get_by_id`
Tekil alarm detayını getirir.
#### `point_alarm_get_by_recipient`
Alıcıya ait tüm alarmları listeler.
#### `point_alarm_list`
Sayfalı alarm listesi. `recipientIds` ile filtreleme yapılabilir.
---
### 🗺️ Coğrafi Alarmlar / Geo Alarms
İdari sınır, poligon veya H3 adresi bazlı uyarı abonelikleri.
#### `geo_alarm_register`
Yeni coğrafi alarm oluşturur. Üç sınır tipi desteklenir:
```json
// İdari sınır
{ "type": "ADMINISTRATIVE", "cityId": 6, "districtId": 60 }
// Poligon
{ "type": "POLYGON", "polygon": { "exterior": [{"lat": 39.9, "lng": 32.8}, ...] } }
// H3 hücre indeksi
{ "type": "H3INDEX", "h3Address": "8f2830828052d25" }
```
#### `geo_alarm_update` / `geo_alarm_delete` / `geo_alarm_get_by_id` / `geo_alarm_get_by_recipient` / `geo_alarm_list`
Nokta alarmlarıyla aynı imza.
#### 🏙️ Konum Kataloğu
| Araç | Açıklama |
|------|----------|
| `geo_alarm_list_cities` | Tüm illeri listeler |
| `geo_alarm_get_city` | `cityId` ile il detayı |
| `geo_alarm_list_districts` | `cityId` ile ilçeleri listeler |
| `geo_alarm_get_district` | `districtId` ile ilçe detayı |
| `geo_alarm_list_neighbourhoods` | `districtId` ile mahalleleri listeler |
| `geo_alarm_get_neighbourhood` | `neighbourhoodId` ile mahalle detayı |
---
### 📅 Tahmin Alarmları / Forecast Alarms
Eşik aşıldığında sabah (04:00 UTC) veya akşam (16:00 UTC) uyarı gönderir.
#### `forecast_alarm_register`
Yeni tahmin alarmı oluşturur.
**Sınır tipleri:**
```json
{ "type": "ADMINISTRATIVE", "cityId": 6, "districtId": 60 }
{ "type": "POINT", "latitude": 39.92, "longitude": 32.85 }
```
**⚠️ Eşik parametreleri:**
| Parametre | Değerler |
|-----------|----------|
| `precipitationThreshold` | mm cinsinden sayısal değer |
| `snowFallThreshold` | `LIGHT` \| `MODERATE` \| `HEAVY` |
| `windGustThreshold` | `STRONG_WIND` \| `STORM` \| `SEVERE_STORM` \| `HURRICANE` |
| `hotTemperatureThreshold` | `HOT_SNAP` \| `HEAVY_HOT_SNAP` \| `EXTREME_HOT_SNAP` |
| `coldTemperatureThreshold` | `COLD_SNAP` \| `HEAVY_COLD_SNAP` \| `EXTREME_COLD_SNAP` |
**📬 Teslimat:**
| Parametre | Açıklama |
|-----------|----------|
| `forecastDays` | 17, kaç günlük tahmin |
| `forecastAlarmDelivery` | `MORNING` (04:00 UTC) \| `EVENING` (16:00 UTC) |
#### `forecast_alarm_update` / `forecast_alarm_delete` / `forecast_alarm_get_by_id` / `forecast_alarm_get_by_recipient` / `forecast_alarm_list`
Nokta alarmlarıyla aynı imza.
#### 🏙️ Konum Kataloğu
| Araç | Açıklama |
|------|----------|
| `forecast_alarm_list_cities` | Tüm illeri listeler |
| `forecast_alarm_get_city` | `cityId` ile il detayı |
| `forecast_alarm_list_districts` | `cityId` ile ilçeleri listeler |
| `forecast_alarm_get_district` | `districtId` ile ilçe detayı |
---
## 🏗️ Mimari
```
src/
├── index.ts # MCP server başlatma, tool routing
├── config.ts # Ortam değişkenleri
├── auth.ts # JWT token yönetimi (otomatik yenileme)
├── client.ts # HTTP API istemcisi (HMAC imzalama)
├── security.ts # HMAC-SHA256, nonce, idempotency key
└── tools/
├── lightnings.ts
├── thunderstorms.ts
├── precipitations.ts
├── forecasts.ts
├── auth.ts
├── accounts.ts
├── point-alarms.ts
├── geo-alarms.ts
└── forecast-alarms.ts
```
**İstek akışı:**
```
MCP İstemci
index.ts (CallToolRequestSchema)
tools/<kategori>.ts ← Zod validasyonu
client.ts (apiGet / apiPost / apiPatch / apiDelete)
│ ├── auth.ts → geçerli JWT token al (gerekirse otomatik yenile)
│ └── security.ts → HMAC-SHA256 imzası üret
iklim.co REST API
```
---
## 🔐 Kimlik Doğrulama ve Güvenlik
API ile iletişim iki katmanlı güvenlik mekanizması üzerine kuruludur: **JWT tabanlı kimlik doğrulama** ve **HMAC-SHA256 istek imzalama**. Her ikisi de her istekte birlikte kullanılır.
### Dahili Auth Akışı
Server ilk araç çağrısında otomatik olarak login olur; bu işlem dışarıdan tetiklenmez, tamamen içseldir.
```
İlk araç çağrısı
getValidAccessToken() ← auth.ts
├─ tokenState yok → login()
│ POST /v1/auth/login
│ { username, password }
│ ← { accessToken, refreshToken }
│ JWT payload decode → expiry hesapla
│ tokenState'e kaydet
├─ accessToken süresi dolmak üzere (< 30 sn kaldı) refresh()
│ POST /v1/auth/refresh
│ { refreshToken }
│ ← { accessToken, refreshToken }
│ tokenState güncelle
└─ accessToken geçerli → doğrudan döndür
```
> ⚠️ **Önemli:** `login` ve `refresh` endpoint'leri `Authorization: Bearer` header'ı **içermez** — bu istekler yalnızca HMAC imzasıyla doğrulanır (bkz. aşağısı).
### JWT Token Yaşam Döngüsü
Token state bellekte (`tokenState`) tutulur ve her araç çağrısından önce kontrol edilir:
```
tokenState = {
accessToken: string // API isteklerinde kullanılan JWT
refreshToken: string // accessToken yenileme için
accessTokenExpiresAt: number // epoch ms (JWT payload'dan decode edilir)
refreshTokenExpiresAt: number // epoch ms
}
```
Karar ağacı (`EXPIRY_BUFFER_MS = 30.000 ms`):
```
now < accessTokenExpiresAt - 30s mevcut token'ı kullan
now < refreshTokenExpiresAt - 30s refresh token ile yenile
aksi hâlde → yeniden login ol
```
30 saniyelik tampon, istek transit süresinde token'ın geçersiz kalması riskini ortadan kaldırır.
### HTTP İstek Header'ları
Her API isteğine (`login` ve `refresh` dahil) aşağıdaki header'lar eklenir:
| Header | Değer | Açıklama |
|--------|-------|----------|
| `Content-Type` | `application/json` | Sabit |
| `Authorization` | `Bearer <accessToken>` | Yalnızca normal API isteklerinde; login/refresh'te **yoktur** |
| `X-Signature` | hex string | HMAC-SHA256 imzası (bkz. aşağısı) |
| `X-Timestamp` | `Date.now()` string | Unix epoch, milisaniye |
| `X-Nonce` | UUID v4 | Her istekte tekil, replay saldırısı önleme |
| `X-Idempotency-Key` | UUID v4 | Yalnızca `POST` ve `PATCH` isteklerinde |
### HMAC-SHA256 İmza Hesabı
`X-Signature` değeri şu dört bileşenin `|` ile birleştirilmesinden elde edilen string'in HMAC-SHA256'sıdır:
```
imzalanacak_veri = "METHOD|PATH_WITH_QUERY|TIMESTAMP|BODY"
X-Signature = HMAC-SHA256(imzalanacak_veri, IKLIM_HMAC_SECRET) → hex
```
**Bileşenler:**
| Bileşen | Açıklama | Örnek |
|---------|----------|-------|
| `METHOD` | HTTP metodu, büyük harf | `POST` |
| `PATH_WITH_QUERY` | Sorgu parametreleri dahil path | `/v1/lightnings/within` veya `/v1/users?page=0` |
| `TIMESTAMP` | `X-Timestamp` ile aynı değer | `1774349677000` |
| `BODY` | JSON body string; body yoksa boş string `""` | `{"username":"..."}` |
**Örnek hesaplama (GET isteği):**
```
METHOD = "GET"
PATH = "/v1/users?pageNumber=0&pageSize=10"
TIMESTAMP = "1774349677000"
BODY = "" ← GET isteğinde body yok
imzalanacak = "GET|/v1/users?pageNumber=0&pageSize=10|1774349677000|"
X-Signature = HMAC-SHA256(imzalanacak, secret) → "a3f9c2..."
```
**Örnek hesaplama (POST isteği):**
```
METHOD = "POST"
PATH = "/v1/lightnings/within"
TIMESTAMP = "1774349677000"
BODY = '{"center":{"lat":39.87,"lng":32.74},"radius":50000,...}'
imzalanacak = "POST|/v1/lightnings/within|1774349677000|{\"center\":...}"
X-Signature = HMAC-SHA256(imzalanacak, secret) → "7be41d..."
```
> Kaynak: [`src/security.ts`](src/security.ts) — `buildSignature()` fonksiyonu
### Auth ile Normal İstekler Arasındaki Fark
| | `POST /v1/auth/login` | `POST /v1/auth/refresh` | Normal API İstekleri |
|---|---|---|---|
| `Authorization` | ❌ | ❌ | ✅ `Bearer <token>` |
| `X-Signature` | ✅ | ✅ | ✅ |
| `X-Timestamp` | ✅ | ✅ | ✅ |
| `X-Nonce` | ✅ | ✅ | ✅ |
| `X-Idempotency-Key` | ✅ | ✅ | ✅ (POST/PATCH) |
### Güvenlik Önerileri
- 🔒 `IKLIM_HMAC_SECRET` ve `IKLIM_PASSWORD` değerlerini kaynak koda veya git geçmişine eklemeyin
- 🏭 Üretim ortamında `.env` dosyası yerine sistem ortam değişkenlerini veya secret manager kullanın
- 🌍 Her ortam (prod/test/local) için ayrı kimlik bilgileri kullanın
- 🔄 HMAC secret'ı düzenli olarak rotate edin
---
## 💻 Geliştirici Notları
** Yeni araç eklemek**
1. `src/tools/` altında ilgili dosyaya yeni tool tanımı ve handler ekle
2. `src/index.ts` içinde `toolHandlerMap`'e ve `allTools` dizisine kaydet
3. `npm run build` ile derle
**✅ Zod şemaları**
Tüm araç girdileri Zod ile çalışma zamanında doğrulanır. Her araç `schema.parse(args)` çağrısından geçer; geçersiz girdi anlamlı bir hata mesajıyla geri döner.
**🔧 TypeScript derleme hedefi**
`tsconfig.json``target: ES2022`, `module: Node16`
**📦 Bağımlılıklar**
| Paket | Versiyon | Kullanım |
|-------|----------|----------|
| `@modelcontextprotocol/sdk` | ^1.0.0 | MCP altyapısı |
| `zod` | ^3.23.8 | Girdi validasyonu |
| `typescript` | ^5.5.0 | Derleme |
| `ts-node` | ^10.9.2 | Geliştirme modu |

1362
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "iklim-co-mcp-server",
"version": "1.0.0",
"description": "MCP Server for iklim.co Weather API",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.5.0"
}
}

108
src/auth.ts Normal file
View File

@ -0,0 +1,108 @@
import { config } from "./config.js";
import {
buildSignature,
generateIdempotencyKey,
generateNonce,
generateTimestamp,
} from "./security.js";
interface TokenState {
accessToken: string;
refreshToken: string;
accessTokenExpiresAt: number;
refreshTokenExpiresAt: number;
}
let tokenState: TokenState | null = null;
function parseJwtExpiry(token: string): number {
const payload = token.split(".")[1];
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
// JWT exp is in seconds, convert to milliseconds
return decoded.exp * 1000;
}
async function callAuthEndpoint(
path: string,
body: Record<string, string>
): Promise<{ accessToken: string; refreshToken: string }> {
const method = "POST";
const bodyStr = JSON.stringify(body);
const timestamp = generateTimestamp();
const nonce = generateNonce();
const idempotencyKey = generateIdempotencyKey();
const signature = buildSignature(method, path, timestamp, bodyStr, config.hmacSecret);
const response = await fetch(`${config.baseUrl}${path}`, {
method,
headers: {
"Content-Type": "application/json",
"X-Signature": signature,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Idempotency-Key": idempotencyKey,
},
body: bodyStr,
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(
`Auth failed [${response.status}]: ${(error as { message?: string }).message ?? response.statusText}`
);
}
const data = (await response.json()) as {
accessToken: string;
refreshToken: string;
};
return { accessToken: data.accessToken, refreshToken: data.refreshToken };
}
async function login(): Promise<void> {
const { accessToken, refreshToken } = await callAuthEndpoint("/v1/auth/login", {
username: config.username,
password: config.password,
});
tokenState = {
accessToken,
refreshToken,
accessTokenExpiresAt: parseJwtExpiry(accessToken),
refreshTokenExpiresAt: parseJwtExpiry(refreshToken),
};
}
async function refresh(): Promise<void> {
if (!tokenState) throw new Error("No token state to refresh");
const { accessToken, refreshToken } = await callAuthEndpoint("/v1/auth/refresh", {
refreshToken: tokenState.refreshToken,
});
tokenState = {
accessToken,
refreshToken,
accessTokenExpiresAt: parseJwtExpiry(accessToken),
refreshTokenExpiresAt: parseJwtExpiry(refreshToken),
};
}
// 30 second buffer to avoid using a token that expires mid-request
const EXPIRY_BUFFER_MS = 30_000;
export async function getValidAccessToken(): Promise<string> {
const now = Date.now();
if (tokenState && tokenState.accessTokenExpiresAt - EXPIRY_BUFFER_MS > now) {
return tokenState.accessToken;
}
if (tokenState && tokenState.refreshTokenExpiresAt - EXPIRY_BUFFER_MS > now) {
await refresh();
return tokenState!.accessToken;
}
await login();
return tokenState!.accessToken;
}

109
src/client.ts Normal file
View File

@ -0,0 +1,109 @@
import { config } from "./config.js";
import { getValidAccessToken } from "./auth.js";
import {
buildSignature,
generateIdempotencyKey,
generateNonce,
generateTimestamp,
} from "./security.js";
function buildPathWithQuery(path: string, params?: Record<string, string>): string {
if (!params || Object.keys(params).length === 0) return path;
const qs = new URLSearchParams(params).toString();
return `${path}?${qs}`;
}
async function buildHeaders(
method: string,
pathWithQuery: string,
body: string,
withIdempotency: boolean
): Promise<Record<string, string>> {
const accessToken = await getValidAccessToken();
const timestamp = generateTimestamp();
const nonce = generateNonce();
const signature = buildSignature(method, pathWithQuery, timestamp, body, config.hmacSecret);
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
"X-Signature": signature,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
};
if (withIdempotency) {
headers["X-Idempotency-Key"] = generateIdempotencyKey();
}
return headers;
}
async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const error = await response.json().catch(() => ({}));
const msg = (error as { message?: string }).message ?? response.statusText;
throw new Error(`API error [${response.status}]: ${msg}`);
}
// 204 No Content
if (response.status === 204) return {} as T;
return response.json() as Promise<T>;
}
export async function apiGet<T>(
path: string,
params?: Record<string, string>
): Promise<T> {
const pathWithQuery = buildPathWithQuery(path, params);
const headers = await buildHeaders("GET", pathWithQuery, "", false);
const response = await fetch(`${config.baseUrl}${pathWithQuery}`, {
method: "GET",
headers,
});
return handleResponse<T>(response);
}
export async function apiPost<T>(
path: string,
body: unknown
): Promise<T> {
const bodyStr = JSON.stringify(body);
const headers = await buildHeaders("POST", path, bodyStr, true);
const response = await fetch(`${config.baseUrl}${path}`, {
method: "POST",
headers,
body: bodyStr,
});
return handleResponse<T>(response);
}
export async function apiPatch<T>(
path: string,
body: unknown
): Promise<T> {
const bodyStr = JSON.stringify(body);
const headers = await buildHeaders("PATCH", path, bodyStr, true);
const response = await fetch(`${config.baseUrl}${path}`, {
method: "PATCH",
headers,
body: bodyStr,
});
return handleResponse<T>(response);
}
export async function apiDelete<T>(path: string): Promise<T> {
const headers = await buildHeaders("DELETE", path, "", false);
const response = await fetch(`${config.baseUrl}${path}`, {
method: "DELETE",
headers,
});
return handleResponse<T>(response);
}

34
src/config.ts Normal file
View File

@ -0,0 +1,34 @@
type Environment = "prod" | "test" | "local";
const BASE_URLS: Record<Environment, string> = {
prod: "https://api.iklim.co",
test: "https://api-test.iklim.co",
local: "http://localhost:8080",
};
function resolveBaseUrl(): string {
// Explicit URL override takes precedence
if (process.env.IKLIM_BASE_URL) return process.env.IKLIM_BASE_URL;
const env = (process.env.IKLIM_ENV ?? "prod") as Environment;
const url = BASE_URLS[env];
if (!url) {
throw new Error(
`Invalid IKLIM_ENV value: "${env}". Must be one of: ${Object.keys(BASE_URLS).join(", ")}`
);
}
return url;
}
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing required environment variable: ${name}`);
return value;
}
export const config = {
baseUrl: resolveBaseUrl(),
hmacSecret: requireEnv("IKLIM_HMAC_SECRET"),
username: requireEnv("IKLIM_USERNAME"),
password: requireEnv("IKLIM_PASSWORD"),
};

83
src/index.ts Normal file
View File

@ -0,0 +1,83 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { lightningTools, handleLightningTool } from "./tools/lightnings.js";
import { thunderstormTools, handleThunderstormTool } from "./tools/thunderstorms.js";
import { precipitationTools, handlePrecipitationTool } from "./tools/precipitations.js";
import { forecastTools, handleForecastTool } from "./tools/forecasts.js";
import { authTools, handleAuthTool } from "./tools/auth.js";
import { accountTools, handleAccountTool } from "./tools/accounts.js";
import { pointAlarmTools, handlePointAlarmTool } from "./tools/point-alarms.js";
import { geoAlarmTools, handleGeoAlarmTool } from "./tools/geo-alarms.js";
import { forecastAlarmTools, handleForecastAlarmTool } from "./tools/forecast-alarms.js";
const toolGroups = [
{ tools: lightningTools, handler: handleLightningTool },
{ tools: thunderstormTools, handler: handleThunderstormTool },
{ tools: precipitationTools, handler: handlePrecipitationTool },
{ tools: forecastTools, handler: handleForecastTool },
{ tools: authTools, handler: handleAuthTool },
{ tools: accountTools, handler: handleAccountTool },
{ tools: pointAlarmTools, handler: handlePointAlarmTool },
{ tools: geoAlarmTools, handler: handleGeoAlarmTool },
{ tools: forecastAlarmTools, handler: handleForecastAlarmTool },
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const allTools = toolGroups.flatMap((g) => g.tools as unknown as any[]);
const toolHandlerMap = new Map<string, (name: string, args: Record<string, unknown>) => Promise<unknown>>();
for (const { tools, handler } of toolGroups) {
for (const tool of tools) {
toolHandlerMap.set(tool.name, handler);
}
}
const server = new Server(
{ name: "iklim-co", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: allTools,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const handler = toolHandlerMap.get(name);
if (!handler) {
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true,
};
}
try {
const result = await handler(name, args as Record<string, unknown>);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
};
}
});
async function main(): Promise<void> {
const transport = new StdioServerTransport();
await server.connect(transport);
process.stderr.write("iklim.co MCP server running\n");
}
main().catch((error) => {
process.stderr.write(`Fatal error: ${error}\n`);
process.exit(1);
});

24
src/security.ts Normal file
View File

@ -0,0 +1,24 @@
import crypto from "crypto";
export function generateTimestamp(): string {
return Date.now().toString();
}
export function generateNonce(): string {
return crypto.randomUUID();
}
export function generateIdempotencyKey(): string {
return crypto.randomUUID();
}
export function buildSignature(
method: string,
pathWithQuery: string,
timestamp: string,
body: string,
secret: string
): string {
const dataToSign = `${method.toUpperCase()}|${pathWithQuery}|${timestamp}|${body}`;
return crypto.createHmac("sha256", secret).update(dataToSign).digest("hex");
}

233
src/tools/accounts.ts Normal file
View File

@ -0,0 +1,233 @@
import { z } from "zod";
import { apiGet, apiPatch, apiPost } from "../client.js";
const SUBSCRIPTION_PLANS = [
"NONE", "TRIAL", "BASIC_MONTHLY", "BASIC_YEARLY",
"PREMIUM_MONTHLY", "PREMIUM_YEARLY", "CUSTOM",
] as const;
export const accountTools = [
{
name: "account_get",
description: "Get account details for a given user ID.",
inputSchema: {
type: "object" as const,
properties: {
userId: { type: "string", description: "User UUID" },
},
required: ["userId"],
},
},
{
name: "account_create",
description: "Create a new account for an existing user.",
inputSchema: {
type: "object" as const,
properties: {
userId: { type: "string", description: "User UUID" },
type: {
type: "string",
enum: ["INDIVIDUAL", "ORGANIZATION"],
description: "Account type",
},
subscriptionPlan: {
type: "string",
enum: SUBSCRIPTION_PLANS,
description: "Initial subscription plan",
},
mobilePhoneNumber: { type: "string", description: "Mobile phone (e.g. 905321112233)" },
location: { type: "string", description: "Address" },
company: { type: "string", description: "Company name (optional)" },
industry: { type: "string", description: "Industry (optional)" },
profilePictureUrl: { type: "string", description: "Profile picture URL (optional)" },
},
required: ["userId", "type", "subscriptionPlan"],
},
},
{
name: "account_update",
description: "Update an existing account's fields.",
inputSchema: {
type: "object" as const,
properties: {
accountId: { type: "string", description: "Account UUID" },
type: { type: "string", enum: ["INDIVIDUAL", "ORGANIZATION"], description: "Account type (optional)" },
mobilePhoneNumber: { type: "string", description: "Mobile phone (optional)" },
location: { type: "string", description: "Address (optional)" },
company: { type: "string", description: "Company name (optional)" },
industry: { type: "string", description: "Industry (optional)" },
profilePictureUrl: { type: "string", description: "Profile picture URL (optional)" },
},
required: ["accountId"],
},
},
{
name: "account_activation_request",
description: "Initiate account activation by sending a verification email to the user.",
inputSchema: {
type: "object" as const,
properties: {
userId: { type: "string", description: "User UUID" },
type: { type: "string", enum: ["INDIVIDUAL", "ORGANIZATION"], description: "Account type" },
accountActivationPageLink: { type: "string", description: "URL of the activation page" },
mobilePhoneNumber: { type: "string", description: "Mobile phone (optional)" },
location: { type: "string", description: "Address (optional)" },
company: { type: "string", description: "Company name (optional)" },
industry: { type: "string", description: "Industry (optional)" },
profilePictureUrl: { type: "string", description: "Profile picture URL (optional)" },
},
required: ["userId", "type", "accountActivationPageLink"],
},
},
{
name: "account_activate",
description: "Activate an account using the email verification token.",
inputSchema: {
type: "object" as const,
properties: {
userId: { type: "string", description: "User UUID" },
accountId: { type: "string", description: "Account UUID" },
emailVerificationToken: { type: "string", description: "Token received by email" },
},
required: ["userId", "accountId", "emailVerificationToken"],
},
},
{
name: "account_phone_activation_request",
description: "Initiate phone number activation by sending a verification SMS.",
inputSchema: {
type: "object" as const,
properties: {
userId: { type: "string", description: "User UUID" },
accountId: { type: "string", description: "Account UUID" },
phoneNumberActivationPageLink: { type: "string", description: "URL of the phone activation page" },
},
required: ["userId", "accountId", "phoneNumberActivationPageLink"],
},
},
{
name: "account_activate_phone",
description: "Activate a phone number using the SMS verification token.",
inputSchema: {
type: "object" as const,
properties: {
userId: { type: "string", description: "User UUID" },
accountId: { type: "string", description: "Account UUID" },
phoneNumberVerificationToken: { type: "string", description: "Token received by SMS" },
},
required: ["userId", "accountId", "phoneNumberVerificationToken"],
},
},
{
name: "account_update_subscription",
description: "Update the subscription plan for an account.",
inputSchema: {
type: "object" as const,
properties: {
userId: { type: "string", description: "User UUID" },
accountId: { type: "string", description: "Account UUID" },
subscriptionPlan: {
type: "string",
enum: SUBSCRIPTION_PLANS,
description: "New subscription plan",
},
},
required: ["userId", "accountId", "subscriptionPlan"],
},
},
] as const;
const AccountType = z.enum(["INDIVIDUAL", "ORGANIZATION"]);
const SubscriptionPlan = z.enum(SUBSCRIPTION_PLANS);
const CreateAccountSchema = z.object({
userId: z.string(),
type: AccountType,
subscriptionPlan: SubscriptionPlan,
mobilePhoneNumber: z.string().optional(),
location: z.string().optional(),
company: z.string().optional(),
industry: z.string().optional(),
profilePictureUrl: z.string().optional(),
});
const UpdateAccountSchema = z.object({
accountId: z.string(),
type: AccountType.optional(),
mobilePhoneNumber: z.string().optional(),
location: z.string().optional(),
company: z.string().optional(),
industry: z.string().optional(),
profilePictureUrl: z.string().optional(),
});
const ActivationRequestSchema = z.object({
userId: z.string(),
type: AccountType,
accountActivationPageLink: z.string(),
mobilePhoneNumber: z.string().optional(),
location: z.string().optional(),
company: z.string().optional(),
industry: z.string().optional(),
profilePictureUrl: z.string().optional(),
});
const PhoneActivationRequestSchema = z.object({
userId: z.string(),
accountId: z.string(),
phoneNumberActivationPageLink: z.string(),
});
const UpdateSubscriptionSchema = z.object({
userId: z.string(),
accountId: z.string(),
subscriptionPlan: SubscriptionPlan,
});
export async function handleAccountTool(
name: string,
args: Record<string, unknown>
): Promise<unknown> {
switch (name) {
case "account_get": {
const userId = z.string().parse(args.userId);
return apiGet(`/v1/accounts/${userId}`);
}
case "account_create":
return apiPost("/v1/accounts/create", CreateAccountSchema.parse(args));
case "account_update":
return apiPatch("/v1/accounts/update", UpdateAccountSchema.parse(args));
case "account_activation_request":
return apiPost("/v1/accounts/account-activation-request", ActivationRequestSchema.parse(args));
case "account_activate": {
const { userId, accountId, emailVerificationToken } = z.object({
userId: z.string(),
accountId: z.string(),
emailVerificationToken: z.string(),
}).parse(args);
return apiGet("/v1/accounts/activate-account", { userId, accountId, emailVerificationToken });
}
case "account_phone_activation_request":
return apiPost("/v1/accounts/phone-number-activation-request", PhoneActivationRequestSchema.parse(args));
case "account_activate_phone": {
const { userId, accountId, phoneNumberVerificationToken } = z.object({
userId: z.string(),
accountId: z.string(),
phoneNumberVerificationToken: z.string(),
}).parse(args);
return apiGet("/v1/accounts/activate-phone-number", { userId, accountId, phoneNumberVerificationToken });
}
case "account_update_subscription":
return apiPost("/v1/accounts/update-subscription", UpdateSubscriptionSchema.parse(args));
default:
throw new Error(`Unknown account tool: ${name}`);
}
}

304
src/tools/auth.ts Normal file
View File

@ -0,0 +1,304 @@
import { z } from "zod";
import { apiGet, apiPatch, apiPost } from "../client.js";
export const authTools = [
{
name: "auth_register",
description: "Register a new user account. No authentication required.",
inputSchema: {
type: "object" as const,
properties: {
username: { type: "string", description: "User email address" },
password: { type: "string", description: "Password" },
firstName: { type: "string", description: "First name" },
midName: { type: "string", description: "Middle name (optional)" },
lastName: { type: "string", description: "Last name" },
locale: { type: "string", description: "Locale (e.g. tr_TR, en_US)" },
timezone: { type: "string", description: "Timezone (e.g. Europe/Istanbul)" },
},
required: ["username", "password", "firstName", "lastName", "locale", "timezone"],
},
},
{
name: "auth_logout",
description: "Logout by invalidating the given JWT access token.",
inputSchema: {
type: "object" as const,
properties: {
jwtToken: { type: "string", description: "The access token to invalidate" },
},
required: ["jwtToken"],
},
},
{
name: "user_get_me",
description: "Get the profile of the currently authenticated user.",
inputSchema: {
type: "object" as const,
properties: {},
required: [],
},
},
{
name: "user_get",
description: "Get a user's profile by their user ID.",
inputSchema: {
type: "object" as const,
properties: {
userId: { type: "string", description: "User UUID" },
},
required: ["userId"],
},
},
{
name: "user_create",
description: "Create a new user (admin operation).",
inputSchema: {
type: "object" as const,
properties: {
username: { type: "string", description: "User email address" },
password: { type: "string", description: "Initial password" },
firstName: { type: "string", description: "First name" },
midName: { type: "string", description: "Middle name (optional)" },
lastName: { type: "string", description: "Last name" },
locale: { type: "string", description: "Locale (e.g. tr_TR, en_US)" },
timezone: { type: "string", description: "Timezone (e.g. Europe/Istanbul)" },
roles: {
type: "array",
items: {
type: "string",
enum: ["GUEST", "API_USER", "STANDARD_USER", "POWER_USER", "EXTENDED_USER", "ADMIN"],
},
description: "Roles to assign",
},
status: {
type: "string",
enum: ["INACTIVE", "ACTIVE", "EXPIRED", "BLOCKED", "DELETED"],
description: "Initial account status",
},
},
required: ["username", "firstName", "lastName", "locale", "timezone", "roles", "status"],
},
},
{
name: "user_update",
description: "Update an existing user's profile fields.",
inputSchema: {
type: "object" as const,
properties: {
userId: { type: "string", description: "User UUID to update" },
username: { type: "string", description: "New email address (optional)" },
firstName: { type: "string", description: "New first name (optional)" },
midName: { type: "string", description: "New middle name (optional)" },
lastName: { type: "string", description: "New last name (optional)" },
locale: { type: "string", description: "New locale (optional)" },
timezone: { type: "string", description: "New timezone (optional)" },
roles: {
type: "array",
items: {
type: "string",
enum: ["GUEST", "API_USER", "STANDARD_USER", "POWER_USER", "EXTENDED_USER", "ADMIN"],
},
description: "Updated roles (optional)",
},
status: {
type: "string",
enum: ["INACTIVE", "ACTIVE", "EXPIRED", "BLOCKED", "DELETED"],
description: "Updated status (optional)",
},
},
required: ["userId"],
},
},
{
name: "user_list",
description: "List users with optional filters and pagination.",
inputSchema: {
type: "object" as const,
properties: {
roles: {
type: "array",
items: {
type: "string",
enum: ["GUEST", "API_USER", "STANDARD_USER", "POWER_USER", "EXTENDED_USER", "ADMIN"],
},
description: "Filter by roles (optional)",
},
status: {
type: "array",
items: {
type: "string",
enum: ["INACTIVE", "ACTIVE", "EXPIRED", "BLOCKED", "DELETED"],
},
description: "Filter by statuses (optional)",
},
pageNumber: { type: "number", description: "Zero-based page number (default: 0)" },
pageSize: { type: "number", description: "Results per page, max 100 (default: 10)" },
includeAccount: { type: "boolean", description: "Include account details in response" },
},
required: [],
},
},
{
name: "user_unblock",
description: "Unblock a blocked user by their user ID.",
inputSchema: {
type: "object" as const,
properties: {
userId: { type: "string", description: "User UUID to unblock" },
},
required: ["userId"],
},
},
{
name: "user_change_password",
description: "Change the password for a user by providing old and new passwords.",
inputSchema: {
type: "object" as const,
properties: {
userId: { type: "string", description: "User UUID" },
oldPassword: { type: "string", description: "Current password" },
newPassword: { type: "string", description: "New password" },
},
required: ["userId", "oldPassword", "newPassword"],
},
},
{
name: "user_password_reset_request",
description: "Initiate a password reset flow by sending a reset link to the user's email.",
inputSchema: {
type: "object" as const,
properties: {
userName: { type: "string", description: "User email address" },
passwordResetPageLink: { type: "string", description: "URL of the password reset page" },
},
required: ["userName", "passwordResetPageLink"],
},
},
{
name: "user_password_reset",
description: "Complete a password reset using the token received by email.",
inputSchema: {
type: "object" as const,
properties: {
userId: { type: "string", description: "User UUID" },
token: { type: "string", description: "Password reset token from email" },
newPassword: { type: "string", description: "New password to set" },
loginPageLink: { type: "string", description: "Login page URL (optional)" },
},
required: ["userId", "token", "newPassword"],
},
},
] as const;
const RegisterSchema = z.object({
username: z.string(),
password: z.string(),
firstName: z.string(),
midName: z.string().optional(),
lastName: z.string(),
locale: z.string(),
timezone: z.string(),
});
const UserRoles = z.enum(["GUEST", "API_USER", "STANDARD_USER", "POWER_USER", "EXTENDED_USER", "ADMIN"]);
const UserStatus = z.enum(["INACTIVE", "ACTIVE", "EXPIRED", "BLOCKED", "DELETED"]);
const CreateUserSchema = z.object({
username: z.string(),
password: z.string().optional(),
firstName: z.string(),
midName: z.string().optional(),
lastName: z.string(),
locale: z.string(),
timezone: z.string(),
roles: z.array(UserRoles),
status: UserStatus,
});
const UpdateUserSchema = z.object({
userId: z.string(),
username: z.string().optional(),
firstName: z.string().optional(),
midName: z.string().optional(),
lastName: z.string().optional(),
locale: z.string().optional(),
timezone: z.string().optional(),
roles: z.array(UserRoles).optional(),
status: UserStatus.optional(),
});
const ListUsersSchema = z.object({
roles: z.array(UserRoles).optional(),
status: z.array(UserStatus).optional(),
pageNumber: z.number().int().min(0).default(0),
pageSize: z.number().int().min(1).max(100).default(10),
includeAccount: z.boolean().optional(),
});
const ChangePasswordSchema = z.object({
userId: z.string(),
oldPassword: z.string(),
newPassword: z.string(),
});
const PasswordResetRequestSchema = z.object({
userName: z.string(),
passwordResetPageLink: z.string(),
});
const PasswordResetSchema = z.object({
userId: z.string(),
token: z.string(),
newPassword: z.string(),
loginPageLink: z.string().optional(),
});
export async function handleAuthTool(
name: string,
args: Record<string, unknown>
): Promise<unknown> {
switch (name) {
case "auth_register":
return apiPost("/v1/auth/register", RegisterSchema.parse(args));
case "auth_logout":
return apiPost("/v1/auth/logout", { jwtToken: z.string().parse(args.jwtToken) });
case "user_get_me":
return apiGet("/v1/users/me");
case "user_get": {
const userId = z.string().parse(args.userId);
return apiGet(`/v1/users/${userId}`);
}
case "user_create":
return apiPost("/v1/users/create", CreateUserSchema.parse(args));
case "user_update": {
const { userId, ...rest } = UpdateUserSchema.parse(args);
return apiPatch("/v1/users/update", { userId, ...rest });
}
case "user_list":
return apiPost("/v1/users/page", ListUsersSchema.parse(args));
case "user_unblock": {
const userId = z.string().parse(args.userId);
return apiGet(`/v1/users/unblock/${userId}`);
}
case "user_change_password":
return apiPost("/v1/users/change-password", ChangePasswordSchema.parse(args));
case "user_password_reset_request":
return apiPost("/v1/users/password-reset-request", PasswordResetRequestSchema.parse(args));
case "user_password_reset":
return apiPost("/v1/users/password-reset", PasswordResetSchema.parse(args));
default:
throw new Error(`Unknown auth tool: ${name}`);
}
}

View File

@ -0,0 +1,261 @@
import { z } from "zod";
import { apiDelete, apiGet, apiPatch, apiPost } from "../client.js";
const webhookDescription =
"Webhook callback config. Example: { url: 'https://...', httpMethod: 'POST', contentType: 'application/json', authentication: { type: 'BASIC', username: 'u', password: 'p' }, deliveryPolicy: { timeout: 60, maxRetries: 3, retryDelay: 10 } }";
const boundaryDescription =
"Boundary definition. One of:\n" +
"- Administrative: { type: 'ADMINISTRATIVE', cityId: 6, districtId: 557 }\n" +
"- Point: { type: 'POINT', point: { lat: 39.93, lng: 32.85 } }";
export const forecastAlarmTools = [
{
name: "forecast_alarm_register",
description:
"Create a new forecast alarm registration. Triggers alerts when forecast thresholds (precipitation, snow, wind, temperature) are met.",
inputSchema: {
type: "object" as const,
properties: {
recipientId: { type: "string", description: "Recipient identifier (US-ASCII)" },
boundary: { type: "object", description: boundaryDescription },
forecastDays: {
type: "number",
description: "Number of forecast days ahead to evaluate (1-7)",
},
forecastAlarmDelivery: {
type: "array",
items: { type: "string", enum: ["MORNING", "EVENING"] },
description: "Delivery times: MORNING (04:00 UTC) and/or EVENING (16:00 UTC)",
},
webhook: { type: "object", description: webhookDescription },
precipitationThreshold: {
type: "string",
enum: ["DRIZZLE", "LIGHT", "MODERATE", "HEAVY", "VERY_HEAVY", "EXTREME"],
description: "Minimum precipitation intensity to trigger an alarm (optional)",
},
snowFallThreshold: {
type: "string",
enum: ["LIGHT", "MODERATE", "HEAVY"],
description: "Minimum snowfall depth category to trigger an alarm (optional)",
},
windGustThreshold: {
type: "string",
enum: ["STRONG_WIND", "STORM", "SEVERE_STORM", "HURRICANE"],
description: "Minimum wind gust category to trigger an alarm (optional)",
},
hotTemperatureThreshold: {
type: "string",
enum: ["HOT_SNAP", "HEAVY_HOT_SNAP", "EXTREME_HOT_SNAP"],
description: "Minimum heat-wave category to trigger an alarm (optional)",
},
coldTemperatureThreshold: {
type: "string",
enum: ["EXTREME_COLD_SNAP", "HEAVY_COLD_SNAP", "COLD_SNAP"],
description: "Minimum cold-snap category to trigger an alarm (optional)",
},
},
required: ["recipientId", "boundary"],
},
},
{
name: "forecast_alarm_update",
description: "Update an existing forecast alarm registration.",
inputSchema: {
type: "object" as const,
properties: {
registrationId: { type: "string", description: "Registration UUID to update" },
recipientId: { type: "string", description: "Recipient identifier (optional)" },
boundary: { type: "object", description: boundaryDescription + " (optional)" },
forecastDays: { type: "number", description: "Forecast days ahead (1-7, optional)" },
forecastAlarmDelivery: {
type: "array",
items: { type: "string", enum: ["MORNING", "EVENING"] },
description: "Delivery windows (optional)",
},
webhook: { type: "object", description: webhookDescription + " (optional)" },
precipitationThreshold: { type: "string", description: "Updated precipitation threshold (optional)" },
snowFallThreshold: { type: "string", description: "Updated snowfall threshold (optional)" },
windGustThreshold: { type: "string", description: "Updated wind gust threshold (optional)" },
hotTemperatureThreshold: { type: "string", description: "Updated heat-wave threshold (optional)" },
coldTemperatureThreshold: { type: "string", description: "Updated cold-snap threshold (optional)" },
},
required: ["registrationId"],
},
},
{
name: "forecast_alarm_delete",
description: "Delete (unregister) a forecast alarm registration by its UUID.",
inputSchema: {
type: "object" as const,
properties: {
registrationId: { type: "string", description: "Registration UUID to delete" },
},
required: ["registrationId"],
},
},
{
name: "forecast_alarm_get_by_id",
description: "Get a single forecast alarm registration by its UUID.",
inputSchema: {
type: "object" as const,
properties: {
registrationId: { type: "string", description: "Registration UUID" },
},
required: ["registrationId"],
},
},
{
name: "forecast_alarm_get_by_recipient",
description: "List all forecast alarm registrations for a given recipient ID.",
inputSchema: {
type: "object" as const,
properties: {
recipientId: { type: "string", description: "Recipient identifier" },
},
required: ["recipientId"],
},
},
{
name: "forecast_alarm_list",
description: "List forecast alarm registrations with pagination and optional filters.",
inputSchema: {
type: "object" as const,
properties: {
pageNumber: { type: "number", description: "Zero-based page number (default: 0)" },
pageSize: { type: "number", description: "Results per page, max 100 (default: 20)" },
filterByRecipientIds: {
type: "array",
items: { type: "string" },
description: "Filter by specific recipient IDs (optional)",
},
accountId: { type: "string", description: "Query on behalf of another account (admin only, optional)" },
},
required: [],
},
},
{
name: "forecast_alarm_list_cities",
description: "List all cities in Türkiye available for forecast alarm boundaries.",
inputSchema: {
type: "object" as const,
properties: {},
required: [],
},
},
{
name: "forecast_alarm_get_city",
description: "Get boundary details for a specific city by its ID.",
inputSchema: {
type: "object" as const,
properties: {
cityId: { type: "number", description: "City ID (e.g. 6 for Ankara)" },
},
required: ["cityId"],
},
},
{
name: "forecast_alarm_list_districts",
description: "List all districts for a given city.",
inputSchema: {
type: "object" as const,
properties: {
cityId: { type: "string", description: "City ID" },
},
required: ["cityId"],
},
},
{
name: "forecast_alarm_get_district",
description: "Get boundary details for a specific district by its ID.",
inputSchema: {
type: "object" as const,
properties: {
districtId: { type: "number", description: "District ID (e.g. 557 for Çankaya)" },
},
required: ["districtId"],
},
},
] as const;
function buildForecastAlarmPayload(args: Record<string, unknown>): Record<string, unknown> {
const {
precipitationThreshold,
snowFallThreshold,
windGustThreshold,
hotTemperatureThreshold,
coldTemperatureThreshold,
...rest
} = args;
const payload: Record<string, unknown> = { ...rest };
const filter: Record<string, unknown> = {};
if (precipitationThreshold) filter.precipitationThreshold = precipitationThreshold;
if (snowFallThreshold) filter.snowFallThreshold = snowFallThreshold;
if (windGustThreshold) filter.windGustThreshold = windGustThreshold;
if (hotTemperatureThreshold) filter.hotTemperatureThreshold = hotTemperatureThreshold;
if (coldTemperatureThreshold) filter.coldTemperatureThreshold = coldTemperatureThreshold;
if (Object.keys(filter).length > 0) payload.filter = filter;
return payload;
}
const ListSchema = z.object({
pageNumber: z.number().int().min(0).default(0),
pageSize: z.number().int().min(1).max(100).default(20),
filterByRecipientIds: z.array(z.string()).optional(),
accountId: z.string().optional(),
});
export async function handleForecastAlarmTool(
name: string,
args: Record<string, unknown>
): Promise<unknown> {
switch (name) {
case "forecast_alarm_register":
return apiPost("/v1/alarms/forecasts/register", buildForecastAlarmPayload(args));
case "forecast_alarm_update":
return apiPatch("/v1/alarms/forecasts/update", buildForecastAlarmPayload(args));
case "forecast_alarm_delete": {
const registrationId = z.string().parse(args.registrationId);
return apiDelete(`/v1/alarms/forecasts/unregister/${registrationId}`);
}
case "forecast_alarm_get_by_id": {
const registrationId = z.string().parse(args.registrationId);
return apiGet(`/v1/alarms/forecasts/get-by-id/${registrationId}`);
}
case "forecast_alarm_get_by_recipient": {
const recipientId = z.string().parse(args.recipientId);
return apiGet(`/v1/alarms/forecasts/get-by-recipient-id/${recipientId}`);
}
case "forecast_alarm_list":
return apiPost("/v1/alarms/forecasts/page", ListSchema.parse(args));
case "forecast_alarm_list_cities":
return apiGet("/v1/alarms/forecasts/cities");
case "forecast_alarm_get_city": {
const cityId = z.number().parse(args.cityId);
return apiGet(`/v1/alarms/forecasts/city/${cityId}`);
}
case "forecast_alarm_list_districts": {
const cityId = z.string().parse(args.cityId);
return apiGet(`/v1/alarms/forecasts/districts/${cityId}`);
}
case "forecast_alarm_get_district": {
const districtId = z.number().parse(args.districtId);
return apiGet(`/v1/alarms/forecasts/district/${districtId}`);
}
default:
throw new Error(`Unknown forecast alarm tool: ${name}`);
}
}

201
src/tools/forecasts.ts Normal file
View File

@ -0,0 +1,201 @@
import { z } from "zod";
import { apiPost } from "../client.js";
const ALL_METRICS = [
"WEATHER_ICON",
"TEMPERATURE",
"TEMPERATURE_AVG",
"TEMPERATURE_MIN",
"TEMPERATURE_MAX",
"APPARENT_TEMPERATURE",
"APPARENT_TEMPERATURE_AVG",
"APPARENT_TEMPERATURE_MIN",
"APPARENT_TEMPERATURE_MAX",
"HUMIDITY",
"HUMIDITY_AVG",
"HUMIDITY_MIN",
"HUMIDITY_MAX",
"CLOUD_COVER",
"CLOUD_COVER_AVG",
"CLOUD_COVER_MIN",
"CLOUD_COVER_MAX",
"WIND_SPEED",
"WIND_SPEED_AVG",
"WIND_SPEED_MIN",
"WIND_SPEED_MAX",
"WIND_GUSTS",
"WIND_GUSTS_AVG",
"WIND_GUSTS_MIN",
"WIND_GUSTS_MAX",
"WIND_DIRECTION",
"SNOWFALL",
"PRECIPITATION",
"PRECIPITATION_PROBABILITY",
"PRECIPITATION_PROBABILITY_AVG",
"PRECIPITATION_PROBABILITY_MIN",
"PRECIPITATION_PROBABILITY_MAX",
"REF_EVAPORATION_TRANSPIRATION_ET0",
"REF_EVAPORATION_TRANSPIRATION_ET0_SUM",
"DEW_POINT",
"DEW_POINT_AVG",
"DEW_POINT_MIN",
"DEW_POINT_MAX",
"SOIL_TEMPERATURE",
"SOIL_MOISTURE",
"GLOBAL_TILTED_RADIATION_GTI",
"DIRECT_NORMAL_IRRADIANCE_DNI",
"SHORTWAVE_SOLAR_RADIATION_GHI",
"SHORTWAVE_RADIATION_SUM",
"VISIBILITY",
"VISIBILITY_AVG",
"VISIBILITY_MIN",
"VISIBILITY_MAX",
] as const;
const metricsDescription =
"List of weather metrics to return. Available values: " + ALL_METRICS.join(", ");
const locationProperties = {
latitude: {
type: "number" as const,
description: "Latitude of the location (-90 to 90)",
},
longitude: {
type: "number" as const,
description: "Longitude of the location (-180 to 180)",
},
};
export const forecastTools = [
{
name: "get_hourly_forecast",
description:
"Returns hour-by-hour weather metrics for the requested location. Supports up to 14 forecast days.",
inputSchema: {
type: "object" as const,
properties: {
...locationProperties,
forecastDays: {
type: "number",
description: "Number of forecast days to return (1-14, default: 7)",
},
startTime: {
type: "number",
description: "Optional start time in epoch milliseconds for the forecast horizon",
},
metrics: {
type: "array",
items: { type: "string" as const, enum: ALL_METRICS },
description: metricsDescription,
},
solarPanelTiltForRadiation: {
type: "number",
description: "Solar panel tilt angle in degrees for radiation metrics",
},
solarPanelAzimuthForRadiation: {
type: "number",
description: "Solar panel azimuth angle in degrees for radiation metrics",
},
},
required: ["latitude", "longitude", "metrics"],
},
},
{
name: "get_daily_forecast",
description:
"Returns aggregated daily weather metrics for the requested location. Supports up to 14 forecast days.",
inputSchema: {
type: "object" as const,
properties: {
...locationProperties,
forecastDays: {
type: "number",
description: "Number of forecast days to return (1-14, default: 7)",
},
startTime: {
type: "number",
description: "Optional start time in epoch milliseconds for the forecast horizon",
},
metrics: {
type: "array",
items: { type: "string" as const, enum: ALL_METRICS },
description: metricsDescription,
},
},
required: ["latitude", "longitude", "metrics"],
},
},
{
name: "get_current_weather",
description:
"Returns the latest available weather metrics for the requested location.",
inputSchema: {
type: "object" as const,
properties: {
...locationProperties,
metrics: {
type: "array",
items: { type: "string" as const, enum: ALL_METRICS },
description:
"Metrics for current weather. Recommended: WEATHER_ICON, TEMPERATURE, APPARENT_TEMPERATURE, HUMIDITY, CLOUD_COVER, WIND_SPEED, WIND_GUSTS, WIND_DIRECTION, SNOWFALL, PRECIPITATION",
},
},
required: ["latitude", "longitude", "metrics"],
},
},
] as const;
const MetricSchema = z.enum(ALL_METRICS);
const HourlySchema = z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180),
forecastDays: z.number().int().min(1).max(14).default(7),
startTime: z.number().int().optional(),
metrics: z.array(MetricSchema).min(1),
solarPanelTiltForRadiation: z.number().optional(),
solarPanelAzimuthForRadiation: z.number().optional(),
});
const DailySchema = z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180),
forecastDays: z.number().int().min(1).max(14).default(7),
startTime: z.number().int().optional(),
metrics: z.array(MetricSchema).min(1),
});
const CurrentSchema = z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180),
metrics: z.array(MetricSchema).min(1),
});
function toApiPayload(
parsed: { latitude: number; longitude: number } & Record<string, unknown>
): Record<string, unknown> {
const { latitude, longitude, ...rest } = parsed;
return { location: { lat: latitude, lng: longitude }, ...rest };
}
export async function handleForecastTool(
name: string,
args: Record<string, unknown>
): Promise<unknown> {
switch (name) {
case "get_hourly_forecast": {
const parsed = HourlySchema.parse(args);
return apiPost("/v1/forecasts/hourly", toApiPayload(parsed));
}
case "get_daily_forecast": {
const parsed = DailySchema.parse(args);
return apiPost("/v1/forecasts/daily", toApiPayload(parsed));
}
case "get_current_weather": {
const parsed = CurrentSchema.parse(args);
return apiPost("/v1/forecasts/current", toApiPayload(parsed));
}
default:
throw new Error(`Unknown forecast tool: ${name}`);
}
}

255
src/tools/geo-alarms.ts Normal file
View File

@ -0,0 +1,255 @@
import { z } from "zod";
import { apiDelete, apiGet, apiPatch, apiPost } from "../client.js";
const webhookDescription =
"Webhook callback config. Example: { url: 'https://...', httpMethod: 'POST', contentType: 'application/json', authentication: { type: 'BASIC', username: 'u', password: 'p' }, deliveryPolicy: { timeout: 60, maxRetries: 3, retryDelay: 10 } }";
const boundaryDescription =
"Boundary definition. One of:\n" +
"- Administrative: { type: 'ADMINISTRATIVE', cityId: 6, districtId: 557, neighborhoodId: 55766 }\n" +
"- Polygon: { type: 'POLYGON', polygon: { exterior: [{lat, lng}, ...] } }\n" +
"- H3 Index: { type: 'H3INDEX', h3Address: '8928308280fffff' }";
export const geoAlarmTools = [
{
name: "geo_alarm_register",
description:
"Create a new geo alarm registration using an administrative boundary, polygon, or H3 index.",
inputSchema: {
type: "object" as const,
properties: {
recipientId: { type: "string", description: "Recipient identifier (US-ASCII)" },
boundary: { type: "object", description: boundaryDescription },
webhook: { type: "object", description: webhookDescription },
lightningFilter: {
type: "object",
description: "Lightning filter: { type: 'FLASH_CLOUD_TO_GROUND'|'PULSE_IN_CLOUD', peakCurrent: number, inCloudHeight: number }",
},
thunderstormFilter: {
type: "object",
description: "Thunderstorm filter: { intersectsAffectedPolygon: boolean, intersectsCellPolygon: boolean, severityThreshold: 'LOW'|'MEDIUM'|'HIGH', speedThreshold: number }",
},
precipitationFilter: {
type: "object",
description: "Precipitation filter: { intensities: ['DRIZZLE'|'LIGHT'|'MODERATE'|'HEAVY'|'VERY_HEAVY'|'EXTREME'] }",
},
},
required: ["recipientId", "boundary"],
},
},
{
name: "geo_alarm_update",
description: "Update an existing geo alarm registration.",
inputSchema: {
type: "object" as const,
properties: {
registrationId: { type: "string", description: "Registration UUID to update" },
recipientId: { type: "string", description: "Recipient identifier (optional)" },
boundary: { type: "object", description: boundaryDescription + " (optional)" },
webhook: { type: "object", description: webhookDescription + " (optional)" },
lightningFilter: { type: "object", description: "Updated lightning filter (optional)" },
thunderstormFilter: { type: "object", description: "Updated thunderstorm filter (optional)" },
precipitationFilter: { type: "object", description: "Updated precipitation filter (optional)" },
},
required: ["registrationId"],
},
},
{
name: "geo_alarm_delete",
description: "Delete (unregister) a geo alarm registration by its UUID.",
inputSchema: {
type: "object" as const,
properties: {
registrationId: { type: "string", description: "Registration UUID to delete" },
},
required: ["registrationId"],
},
},
{
name: "geo_alarm_get_by_id",
description: "Get a single geo alarm registration by its UUID.",
inputSchema: {
type: "object" as const,
properties: {
registrationId: { type: "string", description: "Registration UUID" },
},
required: ["registrationId"],
},
},
{
name: "geo_alarm_get_by_recipient",
description: "List all geo alarm registrations for a given recipient ID.",
inputSchema: {
type: "object" as const,
properties: {
recipientId: { type: "string", description: "Recipient identifier" },
},
required: ["recipientId"],
},
},
{
name: "geo_alarm_list",
description: "List geo alarm registrations with pagination and optional filters.",
inputSchema: {
type: "object" as const,
properties: {
pageNumber: { type: "number", description: "Zero-based page number (default: 0)" },
pageSize: { type: "number", description: "Results per page, max 100 (default: 10)" },
filterByRecipientIds: {
type: "array",
items: { type: "string" },
description: "Filter by specific recipient IDs (optional)",
},
accountId: { type: "string", description: "Query on behalf of another account (admin only, optional)" },
},
required: [],
},
},
{
name: "geo_alarm_list_cities",
description: "List all cities in Türkiye available for geo alarm boundaries.",
inputSchema: {
type: "object" as const,
properties: {},
required: [],
},
},
{
name: "geo_alarm_get_city",
description: "Get boundary details for a specific city by its ID.",
inputSchema: {
type: "object" as const,
properties: {
cityId: { type: "number", description: "City ID (e.g. 6 for Ankara)" },
},
required: ["cityId"],
},
},
{
name: "geo_alarm_list_districts",
description: "List all districts for a given city.",
inputSchema: {
type: "object" as const,
properties: {
cityId: { type: "string", description: "City ID" },
},
required: ["cityId"],
},
},
{
name: "geo_alarm_get_district",
description: "Get boundary details for a specific district by its ID.",
inputSchema: {
type: "object" as const,
properties: {
districtId: { type: "number", description: "District ID (e.g. 557 for Çankaya)" },
},
required: ["districtId"],
},
},
{
name: "geo_alarm_list_neighbourhoods",
description: "List all neighbourhoods for a given district.",
inputSchema: {
type: "object" as const,
properties: {
districtId: { type: "string", description: "District ID" },
},
required: ["districtId"],
},
},
{
name: "geo_alarm_get_neighbourhood",
description: "Get boundary details for a specific neighbourhood by its ID.",
inputSchema: {
type: "object" as const,
properties: {
neighbourhoodId: { type: "number", description: "Neighbourhood ID" },
},
required: ["neighbourhoodId"],
},
},
] as const;
function buildGeoRegistrationPayload(args: Record<string, unknown>): Record<string, unknown> {
const { lightningFilter, thunderstormFilter, precipitationFilter, ...rest } = args;
const payload: Record<string, unknown> = { ...rest };
const filter: Record<string, unknown> = {};
if (lightningFilter) filter.lightning = lightningFilter;
if (thunderstormFilter) filter.thunderstorm = thunderstormFilter;
if (precipitationFilter) filter.precipitation = precipitationFilter;
if (Object.keys(filter).length > 0) payload.filter = filter;
return payload;
}
const ListSchema = z.object({
pageNumber: z.number().int().min(0).default(0),
pageSize: z.number().int().min(1).max(100).default(10),
filterByRecipientIds: z.array(z.string()).optional(),
accountId: z.string().optional(),
});
export async function handleGeoAlarmTool(
name: string,
args: Record<string, unknown>
): Promise<unknown> {
switch (name) {
case "geo_alarm_register":
return apiPost("/v1/alarms/geometries/register", buildGeoRegistrationPayload(args));
case "geo_alarm_update":
return apiPatch("/v1/alarms/geometries/update", buildGeoRegistrationPayload(args));
case "geo_alarm_delete": {
const registrationId = z.string().parse(args.registrationId);
return apiDelete(`/v1/alarms/geometries/unregister/${registrationId}`);
}
case "geo_alarm_get_by_id": {
const registrationId = z.string().parse(args.registrationId);
return apiGet(`/v1/alarms/geometries/get-by-id/${registrationId}`);
}
case "geo_alarm_get_by_recipient": {
const recipientId = z.string().parse(args.recipientId);
return apiGet(`/v1/alarms/geometries/get-by-recipient-id/${recipientId}`);
}
case "geo_alarm_list":
return apiPost("/v1/alarms/geometries/page", ListSchema.parse(args));
case "geo_alarm_list_cities":
return apiGet("/v1/alarms/geometries/cities");
case "geo_alarm_get_city": {
const cityId = z.number().parse(args.cityId);
return apiGet(`/v1/alarms/geometries/city/${cityId}`);
}
case "geo_alarm_list_districts": {
const cityId = z.string().parse(args.cityId);
return apiGet(`/v1/alarms/geometries/districts/${cityId}`);
}
case "geo_alarm_get_district": {
const districtId = z.number().parse(args.districtId);
return apiGet(`/v1/alarms/geometries/district/${districtId}`);
}
case "geo_alarm_list_neighbourhoods": {
const districtId = z.string().parse(args.districtId);
return apiGet(`/v1/alarms/geometries/neighbourhoods/${districtId}`);
}
case "geo_alarm_get_neighbourhood": {
const neighbourhoodId = z.number().parse(args.neighbourhoodId);
return apiGet(`/v1/alarms/geometries/neighbourhood/${neighbourhoodId}`);
}
default:
throw new Error(`Unknown geo alarm tool: ${name}`);
}
}

106
src/tools/lightnings.ts Normal file
View File

@ -0,0 +1,106 @@
import { z } from "zod";
import { apiPost } from "../client.js";
export const lightningTools = [
{
name: "get_lightnings_within",
description:
"Query lightning strikes within a given center point and radius. Maximum radius is 50 km, maximum backward interval is 30 days.",
inputSchema: {
type: "object" as const,
properties: {
latitude: {
type: "number",
description: "Latitude of center point in decimal degrees (-90 to 90)",
},
longitude: {
type: "number",
description: "Longitude of center point in decimal degrees (-180 to 180)",
},
radius: {
type: "number",
description: "Search radius in meters (0 to 50000)",
},
backwardInterval: {
type: "number",
description: "Time range in seconds counting back from endTimeEpoch (60 to 2592000)",
},
endTimeEpoch: {
type: "number",
description: "Query end time in epoch milliseconds",
},
pageNumber: {
type: "number",
description: "Zero-based page number (default: 0)",
},
pageSize: {
type: "number",
description: "Number of results per page, max 100 (default: 10)",
},
},
required: ["latitude", "longitude", "radius", "backwardInterval", "endTimeEpoch"],
},
},
{
name: "get_lightnings_page",
description:
"Query lightning strikes by time range with pagination. Maximum backward interval is 5 days.",
inputSchema: {
type: "object" as const,
properties: {
backwardInterval: {
type: "number",
description: "Time range in seconds counting back from endTimeEpoch (60 to 432000)",
},
endTimeEpoch: {
type: "number",
description: "Query end time in epoch milliseconds",
},
pageNumber: {
type: "number",
description: "Zero-based page number (default: 0)",
},
pageSize: {
type: "number",
description: "Number of results per page, max 100 (default: 10)",
},
},
required: ["backwardInterval", "endTimeEpoch"],
},
},
] as const;
const WithinSchema = z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180),
radius: z.number().min(0).max(50000),
backwardInterval: z.number().int().min(60).max(2592000),
endTimeEpoch: z.number().int().min(0),
pageNumber: z.number().int().min(0).default(0),
pageSize: z.number().int().min(1).max(100).default(10),
});
const PageSchema = z.object({
backwardInterval: z.number().int().min(60).max(432000),
endTimeEpoch: z.number().int().min(0),
pageNumber: z.number().int().min(0).default(0),
pageSize: z.number().int().min(1).max(100).default(10),
});
export async function handleLightningTool(
name: string,
args: Record<string, unknown>
): Promise<unknown> {
switch (name) {
case "get_lightnings_within": {
const params = WithinSchema.parse(args);
return apiPost("/v1/lightnings/within", params);
}
case "get_lightnings_page": {
const params = PageSchema.parse(args);
return apiPost("/v1/lightnings/page", params);
}
default:
throw new Error(`Unknown lightning tool: ${name}`);
}
}

168
src/tools/point-alarms.ts Normal file
View File

@ -0,0 +1,168 @@
import { z } from "zod";
import { apiDelete, apiGet, apiPatch, apiPost } from "../client.js";
const webhookDescription =
"Webhook callback config. Example: { url: 'https://...', httpMethod: 'POST', contentType: 'application/json', authentication: { type: 'BASIC', username: 'u', password: 'p' }, deliveryPolicy: { timeout: 60, maxRetries: 3, retryDelay: 10 } }";
export const pointAlarmTools = [
{
name: "point_alarm_register",
description:
"Create a new point alarm registration. Triggers alerts when lightning, thunderstorm, or precipitation events occur within the specified radius of a center point.",
inputSchema: {
type: "object" as const,
properties: {
recipientId: { type: "string", description: "Recipient identifier (US-ASCII)" },
latitude: { type: "number", description: "Center point latitude (-90 to 90)" },
longitude: { type: "number", description: "Center point longitude (-180 to 180)" },
radius: { type: "number", description: "Alert radius in meters (0 to 50000)" },
webhook: { type: "object", description: webhookDescription },
lightningFilter: {
type: "object",
description: "Lightning filter: { type: 'FLASH_CLOUD_TO_GROUND'|'PULSE_IN_CLOUD', peakCurrent: number, inCloudHeight: number }",
},
thunderstormFilter: {
type: "object",
description: "Thunderstorm filter: { intersectsAffectedPolygon: boolean, intersectsCellPolygon: boolean, severityThreshold: 'LOW'|'MEDIUM'|'HIGH', speedThreshold: number }",
},
precipitationFilter: {
type: "object",
description: "Precipitation filter: { intensities: ['DRIZZLE'|'LIGHT'|'MODERATE'|'HEAVY'|'VERY_HEAVY'|'EXTREME'] }",
},
},
required: ["recipientId", "latitude", "longitude", "radius"],
},
},
{
name: "point_alarm_update",
description: "Update an existing point alarm registration.",
inputSchema: {
type: "object" as const,
properties: {
registrationId: { type: "string", description: "Registration UUID to update" },
recipientId: { type: "string", description: "Recipient identifier (optional)" },
latitude: { type: "number", description: "New center latitude (optional)" },
longitude: { type: "number", description: "New center longitude (optional)" },
radius: { type: "number", description: "New radius in meters (optional)" },
webhook: { type: "object", description: webhookDescription + " (optional)" },
lightningFilter: { type: "object", description: "Updated lightning filter (optional)" },
thunderstormFilter: { type: "object", description: "Updated thunderstorm filter (optional)" },
precipitationFilter: { type: "object", description: "Updated precipitation filter (optional)" },
},
required: ["registrationId"],
},
},
{
name: "point_alarm_delete",
description: "Delete (unregister) a point alarm registration by its ID.",
inputSchema: {
type: "object" as const,
properties: {
registrationId: { type: "string", description: "Registration UUID to delete" },
},
required: ["registrationId"],
},
},
{
name: "point_alarm_get_by_id",
description: "Get a single point alarm registration by its UUID.",
inputSchema: {
type: "object" as const,
properties: {
registrationId: { type: "string", description: "Registration UUID" },
},
required: ["registrationId"],
},
},
{
name: "point_alarm_get_by_recipient",
description: "List all point alarm registrations for a given recipient ID.",
inputSchema: {
type: "object" as const,
properties: {
recipientId: { type: "string", description: "Recipient identifier" },
},
required: ["recipientId"],
},
},
{
name: "point_alarm_list",
description: "List point alarm registrations with pagination and optional filters.",
inputSchema: {
type: "object" as const,
properties: {
pageNumber: { type: "number", description: "Zero-based page number (default: 0)" },
pageSize: { type: "number", description: "Results per page, max 100 (default: 10)" },
filterByRecipientIds: {
type: "array",
items: { type: "string" },
description: "Filter by specific recipient IDs (optional)",
},
accountId: { type: "string", description: "Query on behalf of another account (admin only, optional)" },
},
required: [],
},
},
] as const;
function buildPointRegistrationPayload(args: Record<string, unknown>): Record<string, unknown> {
const { latitude, longitude, radius, lightningFilter, thunderstormFilter, precipitationFilter, ...rest } = args;
const payload: Record<string, unknown> = { ...rest };
if (latitude !== undefined && longitude !== undefined) {
payload.boundary = {
point: { lat: latitude, lng: longitude },
radius: radius ?? 0,
};
}
const filter: Record<string, unknown> = {};
if (lightningFilter) filter.lightning = lightningFilter;
if (thunderstormFilter) filter.thunderstorm = thunderstormFilter;
if (precipitationFilter) filter.precipitation = precipitationFilter;
if (Object.keys(filter).length > 0) payload.filter = filter;
return payload;
}
const ListSchema = z.object({
pageNumber: z.number().int().min(0).default(0),
pageSize: z.number().int().min(1).max(100).default(10),
filterByRecipientIds: z.array(z.string()).optional(),
accountId: z.string().optional(),
});
export async function handlePointAlarmTool(
name: string,
args: Record<string, unknown>
): Promise<unknown> {
switch (name) {
case "point_alarm_register":
return apiPost("/v1/alarms/points/register", buildPointRegistrationPayload(args));
case "point_alarm_update":
return apiPatch("/v1/alarms/points/update", buildPointRegistrationPayload(args));
case "point_alarm_delete": {
const registrationId = z.string().parse(args.registrationId);
return apiDelete(`/v1/alarms/points/unregister/${registrationId}`);
}
case "point_alarm_get_by_id": {
const registrationId = z.string().parse(args.registrationId);
return apiGet(`/v1/alarms/points/get-by-id/${registrationId}`);
}
case "point_alarm_get_by_recipient": {
const recipientId = z.string().parse(args.recipientId);
return apiGet(`/v1/alarms/points/get-by-recipient-id/${recipientId}`);
}
case "point_alarm_list":
return apiPost("/v1/alarms/points/page", ListSchema.parse(args));
default:
throw new Error(`Unknown point alarm tool: ${name}`);
}
}

125
src/tools/precipitations.ts Normal file
View File

@ -0,0 +1,125 @@
import { z } from "zod";
import { apiPost } from "../client.js";
const INTENSITY_VALUES = ["DRIZZLE", "LIGHT", "MODERATE", "HEAVY", "VERY_HEAVY", "EXTREME"] as const;
export const precipitationTools = [
{
name: "get_precipitations_within",
description:
"Query precipitation data within a circular area and time range. Maximum radius is 50 km, maximum backward interval is 30 days.",
inputSchema: {
type: "object" as const,
properties: {
latitude: {
type: "number",
description: "Latitude of center point in decimal degrees (-90 to 90)",
},
longitude: {
type: "number",
description: "Longitude of center point in decimal degrees (-180 to 180)",
},
radius: {
type: "number",
description: "Search radius in meters (0 to 50000)",
},
backwardInterval: {
type: "number",
description: "Time range in seconds counting back from endTimeEpoch (60 to 2592000)",
},
endTimeEpoch: {
type: "number",
description: "Query end time in epoch milliseconds (can be up to 2 hours in the future)",
},
intensityThreshold: {
type: "string",
enum: INTENSITY_VALUES,
description:
"Minimum precipitation intensity to include: DRIZZLE < LIGHT < MODERATE < HEAVY < VERY_HEAVY < EXTREME",
},
pageNumber: {
type: "number",
description: "Zero-based page number (default: 0)",
},
pageSize: {
type: "number",
description: "Number of results per page, max 100 (default: 10)",
},
},
required: ["latitude", "longitude", "radius", "backwardInterval", "endTimeEpoch"],
},
},
{
name: "get_precipitations_page",
description:
"Query precipitation data by time range with pagination within subscription geographic boundaries. Maximum backward interval is 5 days.",
inputSchema: {
type: "object" as const,
properties: {
backwardInterval: {
type: "number",
description: "Time range in seconds counting back from endTimeEpoch (60 to 432000)",
},
endTimeEpoch: {
type: "number",
description: "Query end time in epoch milliseconds",
},
intensityThreshold: {
type: "string",
enum: INTENSITY_VALUES,
description:
"Minimum precipitation intensity to include: DRIZZLE < LIGHT < MODERATE < HEAVY < VERY_HEAVY < EXTREME",
},
pageNumber: {
type: "number",
description: "Zero-based page number (default: 0)",
},
pageSize: {
type: "number",
description: "Number of results per page, max 100 (default: 10)",
},
},
required: ["backwardInterval", "endTimeEpoch", "intensityThreshold"],
},
},
] as const;
const WithinSchema = z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180),
radius: z.number().min(0).max(50000),
backwardInterval: z.number().int().min(60).max(2592000),
endTimeEpoch: z.number().int().min(0),
intensityThreshold: z.enum(INTENSITY_VALUES).optional(),
pageNumber: z.number().int().min(0).default(0),
pageSize: z.number().int().min(1).max(100).default(10),
});
const PageSchema = z.object({
backwardInterval: z.number().int().min(60).max(432000),
endTimeEpoch: z.number().int().min(0),
intensityThreshold: z.enum(INTENSITY_VALUES),
pageNumber: z.number().int().min(0).default(0),
pageSize: z.number().int().min(1).max(100).default(10),
});
export async function handlePrecipitationTool(
name: string,
args: Record<string, unknown>
): Promise<unknown> {
switch (name) {
case "get_precipitations_within": {
const { latitude, longitude, ...rest } = WithinSchema.parse(args);
return apiPost("/v1/precipitations/within", {
center: { lat: latitude, lng: longitude },
...rest,
});
}
case "get_precipitations_page": {
const params = PageSchema.parse(args);
return apiPost("/v1/precipitations/page", params);
}
default:
throw new Error(`Unknown precipitation tool: ${name}`);
}
}

145
src/tools/thunderstorms.ts Normal file
View File

@ -0,0 +1,145 @@
import { z } from "zod";
import { apiPost } from "../client.js";
export const thunderstormTools = [
{
name: "get_thunderstorms_within",
description:
"Query thunderstorm events within a given center point and radius. Maximum radius is 50 km, maximum backward interval is 30 days.",
inputSchema: {
type: "object" as const,
properties: {
latitude: {
type: "number",
description: "Latitude of center point in decimal degrees (-90 to 90)",
},
longitude: {
type: "number",
description: "Longitude of center point in decimal degrees (-180 to 180)",
},
radius: {
type: "number",
description: "Search radius in meters (0 to 50000)",
},
backwardInterval: {
type: "number",
description: "Time range in seconds counting back from endTimeEpoch (60 to 2592000)",
},
endTimeEpoch: {
type: "number",
description: "Query end time in epoch milliseconds",
},
pageNumber: {
type: "number",
description: "Zero-based page number (default: 0)",
},
pageSize: {
type: "number",
description: "Number of results per page, max 100 (default: 10)",
},
intersectsWith: {
type: "string",
enum: ["THREAT_POLYGON", "CELL_POLYGON"],
description:
"Which polygon to use for intersection check: THREAT_POLYGON (expected affected area) or CELL_POLYGON (storm cell area)",
},
},
required: ["latitude", "longitude", "radius", "backwardInterval", "endTimeEpoch"],
},
},
{
name: "get_thunderstorms_page",
description:
"Query thunderstorm events by time range with pagination. Maximum backward interval is 5 days.",
inputSchema: {
type: "object" as const,
properties: {
backwardInterval: {
type: "number",
description: "Time range in seconds counting back from endTimeEpoch (60 to 432000)",
},
endTimeEpoch: {
type: "number",
description: "Query end time in epoch milliseconds",
},
pageNumber: {
type: "number",
description: "Zero-based page number (default: 0)",
},
pageSize: {
type: "number",
description: "Number of results per page, max 100 (default: 10)",
},
},
required: ["backwardInterval", "endTimeEpoch"],
},
},
{
name: "get_thunderstorm_details",
description: "Get detailed historic information about a specific thunderstorm event by its event ID.",
inputSchema: {
type: "object" as const,
properties: {
eventId: {
type: "string",
description: "The event ID of the thunderstorm (e.g. EVT20240413001)",
},
pageNumber: {
type: "number",
description: "Zero-based page number (default: 0)",
},
pageSize: {
type: "number",
description: "Number of results per page, max 100 (default: 10)",
},
},
required: ["eventId"],
},
},
] as const;
const WithinSchema = z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180),
radius: z.number().min(0).max(50000),
backwardInterval: z.number().int().min(60).max(2592000),
endTimeEpoch: z.number().int().min(0),
pageNumber: z.number().int().min(0).default(0),
pageSize: z.number().int().min(1).max(100).default(10),
intersectsWith: z.enum(["THREAT_POLYGON", "CELL_POLYGON"]).optional(),
});
const PageSchema = z.object({
backwardInterval: z.number().int().min(60).max(432000),
endTimeEpoch: z.number().int().min(0),
pageNumber: z.number().int().min(0).default(0),
pageSize: z.number().int().min(1).max(100).default(10),
});
const EventSchema = z.object({
eventId: z.string().min(1),
pageNumber: z.number().int().min(0).default(0),
pageSize: z.number().int().min(1).max(100).default(10),
});
export async function handleThunderstormTool(
name: string,
args: Record<string, unknown>
): Promise<unknown> {
switch (name) {
case "get_thunderstorms_within": {
const params = WithinSchema.parse(args);
return apiPost("/v1/thunderstorms/within", params);
}
case "get_thunderstorms_page": {
const params = PageSchema.parse(args);
return apiPost("/v1/thunderstorms/page", params);
}
case "get_thunderstorm_details": {
const params = EventSchema.parse(args);
return apiPost("/v1/thunderstorms/get", params);
}
default:
throw new Error(`Unknown thunderstorm tool: ${name}`);
}
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}