first commit
This commit is contained in:
commit
28c24d8cb1
13
.env.example
Normal file
13
.env.example
Normal 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
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
701
README.md
Normal file
701
README.md
Normal 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 (1–14 gün).
|
||||||
|
|
||||||
|
| Parametre | Tip | Açıklama |
|
||||||
|
|-----------|-----|----------|
|
||||||
|
| `latitude` | number | Enlem |
|
||||||
|
| `longitude` | number | Boylam |
|
||||||
|
| `forecastDays` | number? | 1–14, 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` | 1–7, 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
1362
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal 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
108
src/auth.ts
Normal 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
109
src/client.ts
Normal 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
34
src/config.ts
Normal 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
83
src/index.ts
Normal 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
24
src/security.ts
Normal 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
233
src/tools/accounts.ts
Normal 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
304
src/tools/auth.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
261
src/tools/forecast-alarms.ts
Normal file
261
src/tools/forecast-alarms.ts
Normal 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
201
src/tools/forecasts.ts
Normal 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
255
src/tools/geo-alarms.ts
Normal 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
106
src/tools/lightnings.ts
Normal 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
168
src/tools/point-alarms.ts
Normal 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
125
src/tools/precipitations.ts
Normal 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
145
src/tools/thunderstorms.ts
Normal 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
16
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user