GAIA-431: add enroute tools and a precipitation geometry tool that returns GeoJSON
Adds enroute_register_route and enroute_get_route_weather for the BE-Enroute endpoints /v1/enroute/register and /v1/enroute/weather, with zod validation that matches the REST request (geometry needs an encodedPolyline or coordinates, routeId must be a UUID, timestamps are epoch milliseconds). Adds get_precipitation_geometry for POST /v1/precipitations/geometry. That endpoint answers with a protobuf PrecipitationFeatureCollection, which an AI client cannot read, so the response is decoded with protobufjs and returned as a GeoJSON FeatureCollection of MultiPolygons with intensity, radarTimestamp and forecast properties. The needed subset of geojson.proto is embedded as text in src/geojson-proto.ts because only dist is shipped in the release package; it must be kept in sync with BE-GeoCommons. client.ts gets apiPostBinary, which reads the body as bytes and logs only its length so binary responses are neither corrupted nor dumped into the HTTP log. README and package files are updated accordingly (60 tools in 10 categories).
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
- [📍 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)
|
||||
- [🛣️ Güzergah / Enroute](#️-güzergah--enroute)
|
||||
- [🏗️ Mimari](#️-mimari)
|
||||
- [🔐 Kimlik Doğrulama ve Güvenlik](#-kimlik-doğrulama-ve-güvenlik)
|
||||
- [Dahili Auth Akışı](#dahili-auth-akışı)
|
||||
@@ -37,20 +38,21 @@
|
||||
|
||||
## 🌐 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:
|
||||
Bu MCP server, iklim.co REST API'sinin tüm yeteneklerini **60 araç** (tool) olarak sunar. Araçlar 10 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 |
|
||||
| 🌧️ Precipitation | 3 | 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** | |
|
||||
| 🛣️ Enroute | 2 | Hava durumu farkındalıklı rota izleme |
|
||||
| **Toplam** | **60** | |
|
||||
|
||||
---
|
||||
|
||||
@@ -284,6 +286,18 @@ Dairesel alan içindeki radar yağış verilerini sorgular.
|
||||
#### `get_precipitations_page`
|
||||
Zaman aralığına göre yağış verilerini sayfalı getirir. `intensityThreshold` **zorunludur**.
|
||||
|
||||
#### `get_precipitation_geometry`
|
||||
Verilen yağış olaylarının (`get_precipitations_*` sonuçlarındaki `eventId`) poligon geometrisini **GeoJSON FeatureCollection** olarak döndürür. API protobuf yanıt verir; MCP bunu GeoJSON'a çevirir. Her feature bir `MultiPolygon`dur ve `intensity`, `radarTimestamp` (epoch ms), `forecast` özelliklerini taşır.
|
||||
|
||||
| Parametre | Tip | Açıklama |
|
||||
|-----------|-----|----------|
|
||||
| `eventIds` | string[] | Yağış olay ID'leri (1 – 10) |
|
||||
| `latitude` | number? | Kırpma dairesi merkez enlemi |
|
||||
| `longitude` | number? | Kırpma dairesi merkez boylamı |
|
||||
| `radius` | number? | Kırpma dairesi yarıçapı, metre |
|
||||
|
||||
`latitude`/`longitude` birlikte verilmelidir. Merkez ve yarıçap yalnızca power-user rolüne sahip hesaplarda atlanabilir.
|
||||
|
||||
---
|
||||
|
||||
### 🌤️ Hava Tahmini / Forecast
|
||||
@@ -450,7 +464,7 @@ Yeni coğrafi alarm oluşturur. Üç sınır tipi desteklenir:
|
||||
|
||||
```json
|
||||
// İdari sınır
|
||||
{ "type": "ADMINISTRATIVE", "cityId": 6, "districtId": 60 }
|
||||
{ "type": "ADMINISTRATIVE", "countryCode": "TUR", "cityId": 6, "districtId": 60 }
|
||||
|
||||
// Poligon
|
||||
{ "type": "POLYGON", "polygon": { "exterior": [{"lat": 39.9, "lng": 32.8}, ...] } }
|
||||
@@ -462,11 +476,13 @@ Yeni coğrafi alarm oluşturur. Üç sınır tipi desteklenir:
|
||||
#### `geo_alarm_update` / `geo_alarm_delete` / `geo_alarm_get_by_id` / `geo_alarm_get_by_recipient` / `geo_alarm_list`
|
||||
Nokta alarmlarıyla aynı imza.
|
||||
|
||||
İdari sınırda `countryCode` zorunludur: `TUR` (Türkiye) veya `CTR` (KKTC). İl, ilçe ve mahalle ID'leri ülkeye göre tanımlıdır, bu yüzden ilgili ülkenin katalog araçlarından alınmalıdır. `CTR` için mahalle düzeyi bucaktır.
|
||||
|
||||
#### 🏙️ Konum Kataloğu
|
||||
|
||||
| Araç | Açıklama |
|
||||
|------|----------|
|
||||
| `geo_alarm_list_cities` | Tüm illeri listeler |
|
||||
| `geo_alarm_list_cities` | Bir ülkenin illerini listeler (`countryCode`: `TUR` veya `CTR`) |
|
||||
| `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ı |
|
||||
@@ -484,7 +500,7 @@ Yeni tahmin alarmı oluşturur.
|
||||
|
||||
**Sınır tipleri:**
|
||||
```json
|
||||
{ "type": "ADMINISTRATIVE", "cityId": 6, "districtId": 60 }
|
||||
{ "type": "ADMINISTRATIVE", "countryCode": "TUR", "cityId": 6, "districtId": 60 }
|
||||
{ "type": "POINT", "latitude": 39.92, "longitude": 32.85 }
|
||||
```
|
||||
|
||||
@@ -508,15 +524,40 @@ Yeni tahmin alarmı oluşturur.
|
||||
#### `forecast_alarm_update` / `forecast_alarm_delete` / `forecast_alarm_get_by_id` / `forecast_alarm_get_by_recipient` / `forecast_alarm_list`
|
||||
Nokta alarmlarıyla aynı imza.
|
||||
|
||||
İdari sınırda `countryCode` zorunludur: `TUR` (Türkiye) veya `CTR` (KKTC). İl ve ilçe ID'leri ülkeye göre tanımlıdır, bu yüzden ilgili ülkenin katalog araçlarından alınmalıdır.
|
||||
|
||||
#### 🏙️ Konum Kataloğu
|
||||
|
||||
| Araç | Açıklama |
|
||||
|------|----------|
|
||||
| `forecast_alarm_list_cities` | Tüm illeri listeler |
|
||||
| `forecast_alarm_list_cities` | Bir ülkenin illerini listeler (`countryCode`: `TUR` veya `CTR`) |
|
||||
| `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ı |
|
||||
|
||||
### 🛣️ Güzergah / Enroute
|
||||
|
||||
#### `enroute_register_route`
|
||||
Rotayı kaydeder; rota H3 segmentlerine bölünür ve her segment için ETA hesaplanır. `routeId`, mevcut segment (nowcast + tahmin) ve gelecek segmentler (tahmin) döner. Tüm zamanlar epoch ms'dir.
|
||||
|
||||
| Parametre | Tip | Açıklama |
|
||||
|-----------|-----|----------|
|
||||
| `origin` | {lat, lng} | Başlangıç noktası |
|
||||
| `destination` | {lat, lng} | Varış noktası |
|
||||
| `departureTime` | number | Kalkış zamanı, epoch ms |
|
||||
| `distanceMeters` | number | Toplam mesafe, metre |
|
||||
| `durationSeconds` | number | Toplam süre, saniye |
|
||||
| `geometry` | object | `encodedPolyline` veya `coordinates` (en az biri); `polylinePrecision` 5 (varsayılan) veya 6 |
|
||||
| `waypoints` | {lat, lng}[]? | Ara duraklar |
|
||||
|
||||
#### `enroute_get_route_weather`
|
||||
Kayıtlı rota için güncel hava durumunu döndürür. `currentLocation` verilirse mevcut segment yeniden değerlendirilir ve sürücü programın dışındaysa ETA'lar yeniden hesaplanır.
|
||||
|
||||
| Parametre | Tip | Açıklama |
|
||||
|-----------|-----|----------|
|
||||
| `routeId` | string (UUID) | `enroute_register_route` yanıtındaki rota ID'si |
|
||||
| `currentLocation` | {lat, lng}? | Sürücünün mevcut konumu |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Mimari
|
||||
@@ -528,6 +569,7 @@ src/
|
||||
├── auth.ts # JWT token yönetimi (otomatik yenileme)
|
||||
├── client.ts # HTTP API istemcisi (HMAC imzalama)
|
||||
├── security.ts # HMAC-SHA256, nonce, idempotency key
|
||||
├── geojson-proto.ts # Yağış geometrisi protobuf → GeoJSON çözücü
|
||||
└── tools/
|
||||
├── lightnings.ts
|
||||
├── thunderstorms.ts
|
||||
@@ -537,7 +579,8 @@ src/
|
||||
├── accounts.ts
|
||||
├── point-alarms.ts
|
||||
├── geo-alarms.ts
|
||||
└── forecast-alarms.ts
|
||||
├── forecast-alarms.ts
|
||||
└── enroute.ts
|
||||
```
|
||||
|
||||
**İstek akışı:**
|
||||
|
||||
Generated
+19
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"protobufjs": "^8.8.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -804,6 +805,12 @@
|
||||
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
@@ -960,6 +967,18 @@
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz",
|
||||
"integrity": "sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"long": "^5.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"protobufjs": "^8.8.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -264,3 +264,64 @@ export async function apiDelete<T>(path: string): Promise<T> {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// POST for endpoints that answer with a binary body (e.g. protobuf). The body is never read as text,
|
||||
// so only response metadata and the byte length are logged.
|
||||
export async function apiPostBinary(path: string, body: unknown): Promise<Uint8Array> {
|
||||
const bodyStr = JSON.stringify(body);
|
||||
const headers = await buildHeaders("POST", path, bodyStr, true);
|
||||
const startedAt = Date.now();
|
||||
let response: Response | undefined;
|
||||
let responseHeaders: Record<string, string> | undefined;
|
||||
|
||||
try {
|
||||
response = await fetch(`${config.baseUrl}${path}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: bodyStr,
|
||||
});
|
||||
responseHeaders = sanitizeHeaders(headersToRecord(response.headers));
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
let msg = response.statusText;
|
||||
if (errorBody) {
|
||||
try {
|
||||
msg = (JSON.parse(errorBody) as { message?: string }).message ?? errorBody;
|
||||
} catch {
|
||||
msg = errorBody;
|
||||
}
|
||||
}
|
||||
throw new Error(`API error [${response.status}]: ${msg}`);
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(await response.arrayBuffer());
|
||||
logHttpRequest({
|
||||
at: new Date().toISOString(),
|
||||
method: "POST",
|
||||
path,
|
||||
status: response.status,
|
||||
ok: true,
|
||||
durationMs: Date.now() - startedAt,
|
||||
requestHeaders: sanitizeHeaders(headers),
|
||||
requestBody: sanitizeRequestBody(bodyStr, path),
|
||||
responseHeaders,
|
||||
responseBody: `<binary ${bytes.length} bytes>`,
|
||||
});
|
||||
return bytes;
|
||||
} catch (error) {
|
||||
logHttpRequest({
|
||||
at: new Date().toISOString(),
|
||||
method: "POST",
|
||||
path,
|
||||
status: response?.status ?? null,
|
||||
ok: false,
|
||||
durationMs: Date.now() - startedAt,
|
||||
requestHeaders: sanitizeHeaders(headers),
|
||||
requestBody: sanitizeRequestBody(bodyStr, path),
|
||||
responseHeaders,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import protobuf from "protobufjs";
|
||||
|
||||
// Subset of BE-GeoCommons/src/main/resources/proto/geojson.proto needed to decode PrecipitationFeatureCollection.
|
||||
// Embedded as text (not a .proto file) because only dist/ is shipped in the release package. Keep in sync with the source.
|
||||
const PROTO_SOURCE = `
|
||||
syntax = "proto3";
|
||||
package geo;
|
||||
|
||||
message Coordinate { double lng = 1; double lat = 2; }
|
||||
message Ring { repeated Coordinate coordinates = 1; }
|
||||
message LineStringShape { repeated Coordinate coordinates = 1; }
|
||||
message PolygonShape { repeated Ring rings = 1; }
|
||||
message MultiPointShape { repeated Coordinate points = 1; }
|
||||
message MultiLineStringShape { repeated LineStringShape line_strings = 1; }
|
||||
message MultiPolygonShape { repeated PolygonShape polygons = 1; }
|
||||
message GeometryCollectionShape { repeated Geometry geometries = 1; }
|
||||
|
||||
message Geometry {
|
||||
enum GeometryType {
|
||||
POINT = 0;
|
||||
LINE_STRING = 1;
|
||||
POLYGON = 2;
|
||||
MULTI_POINT = 3;
|
||||
MULTI_LINE_STRING = 4;
|
||||
MULTI_POLYGON = 5;
|
||||
GEOMETRY_COLLECTION = 6;
|
||||
}
|
||||
GeometryType type = 1;
|
||||
oneof shape {
|
||||
Coordinate point = 2;
|
||||
LineStringShape line_string = 3;
|
||||
PolygonShape polygon = 4;
|
||||
MultiPointShape multi_point = 5;
|
||||
MultiLineStringShape multi_line_string = 6;
|
||||
MultiPolygonShape multi_polygon = 7;
|
||||
GeometryCollectionShape geometry_collection = 8;
|
||||
}
|
||||
}
|
||||
|
||||
message PrecipitationProperties { string intensity = 1; int64 radar_timestamp = 2; bool forecast = 3; }
|
||||
message PrecipitationFeature { string id = 1; Geometry geometry = 2; PrecipitationProperties properties = 3; }
|
||||
message PrecipitationFeatureCollection { repeated PrecipitationFeature features = 1; }
|
||||
`;
|
||||
|
||||
type Coord = { lng?: number; lat?: number };
|
||||
type Pos = [number, number];
|
||||
|
||||
const pos = (c: Coord): Pos => [c.lng ?? 0, c.lat ?? 0];
|
||||
const ring = (r: { coordinates?: Coord[] }): Pos[] => (r.coordinates ?? []).map(pos);
|
||||
const polygon = (p: { rings?: { coordinates?: Coord[] }[] }): Pos[][] => (p.rings ?? []).map(ring);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function geometryToGeoJson(g: any): unknown {
|
||||
switch (g.type) {
|
||||
case "POINT":
|
||||
return { type: "Point", coordinates: pos(g.point ?? {}) };
|
||||
case "LINE_STRING":
|
||||
return { type: "LineString", coordinates: ring(g.lineString ?? {}) };
|
||||
case "POLYGON":
|
||||
return { type: "Polygon", coordinates: polygon(g.polygon ?? {}) };
|
||||
case "MULTI_POINT":
|
||||
return { type: "MultiPoint", coordinates: (g.multiPoint?.points ?? []).map(pos) };
|
||||
case "MULTI_LINE_STRING":
|
||||
return {
|
||||
type: "MultiLineString",
|
||||
coordinates: (g.multiLineString?.lineStrings ?? []).map(ring),
|
||||
};
|
||||
case "MULTI_POLYGON":
|
||||
return { type: "MultiPolygon", coordinates: (g.multiPolygon?.polygons ?? []).map(polygon) };
|
||||
case "GEOMETRY_COLLECTION":
|
||||
return {
|
||||
type: "GeometryCollection",
|
||||
geometries: (g.geometryCollection?.geometries ?? []).map(geometryToGeoJson),
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unsupported geometry type in response: ${String(g.type)}`);
|
||||
}
|
||||
}
|
||||
|
||||
let collectionType: protobuf.Type | undefined;
|
||||
|
||||
export function decodePrecipitationFeatureCollection(bytes: Uint8Array): unknown {
|
||||
collectionType ??= protobuf.parse(PROTO_SOURCE).root.lookupType("geo.PrecipitationFeatureCollection");
|
||||
const message = collectionType.decode(bytes);
|
||||
const obj = collectionType.toObject(message, {
|
||||
enums: String,
|
||||
longs: Number,
|
||||
defaults: true,
|
||||
arrays: true,
|
||||
}) as { features: Array<{ id: string; geometry: unknown; properties: { intensity: string; radarTimestamp: number; forecast: boolean } }> };
|
||||
|
||||
return {
|
||||
type: "FeatureCollection",
|
||||
features: obj.features.map((f) => ({
|
||||
type: "Feature",
|
||||
id: f.id,
|
||||
geometry: geometryToGeoJson(f.geometry),
|
||||
properties: {
|
||||
intensity: f.properties.intensity,
|
||||
radarTimestamp: f.properties.radarTimestamp,
|
||||
forecast: f.properties.forecast,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -14,6 +14,7 @@ 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";
|
||||
import { enrouteTools, handleEnrouteTool } from "./tools/enroute.js";
|
||||
import { initializeHttpLogger, logToolInvocation } from "./http-logger.js";
|
||||
|
||||
const toolGroups = [
|
||||
@@ -26,6 +27,7 @@ const toolGroups = [
|
||||
{ tools: pointAlarmTools, handler: handlePointAlarmTool },
|
||||
{ tools: geoAlarmTools, handler: handleGeoAlarmTool },
|
||||
{ tools: forecastAlarmTools, handler: handleForecastAlarmTool },
|
||||
{ tools: enrouteTools, handler: handleEnrouteTool },
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { z } from "zod";
|
||||
import { apiPost } from "../client.js";
|
||||
|
||||
const geoPointSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
lat: { type: "number", description: "Latitude in decimal degrees (-90 to 90)" },
|
||||
lng: { type: "number", description: "Longitude in decimal degrees (-180 to 180)" },
|
||||
},
|
||||
required: ["lat", "lng"],
|
||||
} as const;
|
||||
|
||||
export const enrouteTools = [
|
||||
{
|
||||
name: "enroute_register_route",
|
||||
description:
|
||||
"Register a planned route for weather monitoring. The route is split into H3 segments with an ETA each; returns a routeId plus the current segment (nowcast + forecast weather) and upcoming segments (forecast weather). Use the routeId with enroute_get_route_weather. All timestamps are epoch milliseconds.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
origin: { ...geoPointSchema, description: "Route start point" },
|
||||
destination: { ...geoPointSchema, description: "Route end point" },
|
||||
departureTime: {
|
||||
type: "number",
|
||||
description: "Planned departure time in epoch milliseconds",
|
||||
},
|
||||
distanceMeters: { type: "number", description: "Total route distance in meters" },
|
||||
durationSeconds: { type: "number", description: "Total route duration in seconds" },
|
||||
geometry: {
|
||||
type: "object",
|
||||
description:
|
||||
"Route geometry. Provide encodedPolyline or coordinates (at least one).",
|
||||
properties: {
|
||||
encodedPolyline: {
|
||||
type: "string",
|
||||
description: "Encoded polyline string of the route",
|
||||
},
|
||||
polylinePrecision: {
|
||||
type: "number",
|
||||
description: "Polyline precision: 5 (Google, default) or 6 (Mapbox)",
|
||||
},
|
||||
coordinates: {
|
||||
type: "array",
|
||||
items: geoPointSchema,
|
||||
description: "Ordered route coordinates, alternative to encodedPolyline",
|
||||
},
|
||||
},
|
||||
},
|
||||
waypoints: {
|
||||
type: "array",
|
||||
items: geoPointSchema,
|
||||
description: "Optional intermediate stops",
|
||||
},
|
||||
},
|
||||
required: [
|
||||
"origin",
|
||||
"destination",
|
||||
"departureTime",
|
||||
"distanceMeters",
|
||||
"durationSeconds",
|
||||
"geometry",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "enroute_get_route_weather",
|
||||
description:
|
||||
"Get updated weather for a registered route. Pass the driver's current location so the current segment is re-evaluated and ETAs are recalculated when the driver is off schedule. Without currentLocation the schedule-based position is used.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
routeId: {
|
||||
type: "string",
|
||||
description: "Route ID returned by enroute_register_route",
|
||||
},
|
||||
currentLocation: { ...geoPointSchema, description: "Driver's current position" },
|
||||
},
|
||||
required: ["routeId"],
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
const GeoPointSchema = z.object({
|
||||
lat: z.number().min(-90).max(90),
|
||||
lng: z.number().min(-180).max(180),
|
||||
});
|
||||
|
||||
const RegisterSchema = z.object({
|
||||
origin: GeoPointSchema,
|
||||
destination: GeoPointSchema,
|
||||
departureTime: z.number().int().min(0),
|
||||
distanceMeters: z.number().int().min(0),
|
||||
durationSeconds: z.number().int().min(0),
|
||||
geometry: z
|
||||
.object({
|
||||
encodedPolyline: z.string().optional(),
|
||||
polylinePrecision: z.number().int().min(5).max(6).optional(),
|
||||
coordinates: z.array(GeoPointSchema).optional(),
|
||||
})
|
||||
.refine((g) => !!g.encodedPolyline?.trim() || (g.coordinates?.length ?? 0) > 0, {
|
||||
message: "encodedPolyline or coordinates must be provided",
|
||||
}),
|
||||
waypoints: z.array(GeoPointSchema).optional(),
|
||||
});
|
||||
|
||||
const WeatherSchema = z.object({
|
||||
routeId: z.string().uuid(),
|
||||
currentLocation: GeoPointSchema.optional(),
|
||||
});
|
||||
|
||||
export async function handleEnrouteTool(
|
||||
name: string,
|
||||
args: Record<string, unknown>
|
||||
): Promise<unknown> {
|
||||
switch (name) {
|
||||
case "enroute_register_route":
|
||||
return apiPost("/v1/enroute/register", RegisterSchema.parse(args));
|
||||
case "enroute_get_route_weather":
|
||||
return apiPost("/v1/enroute/weather", WeatherSchema.parse(args));
|
||||
default:
|
||||
throw new Error(`Unknown enroute tool: ${name}`);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { apiPost } from "../client.js";
|
||||
import { apiPost, apiPostBinary } from "../client.js";
|
||||
import { decodePrecipitationFeatureCollection } from "../geojson-proto.js";
|
||||
|
||||
const INTENSITY_VALUES = ["DRIZZLE", "LIGHT", "MODERATE", "HEAVY", "VERY_HEAVY", "EXTREME"] as const;
|
||||
|
||||
@@ -82,6 +83,34 @@ export const precipitationTools = [
|
||||
required: ["backwardInterval", "endTimeEpoch", "intensityThreshold"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_precipitation_geometry",
|
||||
description:
|
||||
"Get precipitation polygon geometries for given precipitation event IDs (taken from get_precipitations_within / get_precipitations_page results) as a GeoJSON FeatureCollection. Each feature is a MultiPolygon with intensity, radarTimestamp (epoch ms) and forecast properties. center and radius clip the result to a circular area; they may be omitted only for accounts with the power-user role.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
eventIds: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Precipitation event IDs (1 to 10)",
|
||||
},
|
||||
latitude: {
|
||||
type: "number",
|
||||
description: "Latitude of the clipping circle center in decimal degrees (-90 to 90)",
|
||||
},
|
||||
longitude: {
|
||||
type: "number",
|
||||
description: "Longitude of the clipping circle center in decimal degrees (-180 to 180)",
|
||||
},
|
||||
radius: {
|
||||
type: "number",
|
||||
description: "Clipping circle radius in meters",
|
||||
},
|
||||
},
|
||||
required: ["eventIds"],
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
const WithinSchema = z.object({
|
||||
@@ -103,6 +132,18 @@ const PageSchema = z.object({
|
||||
pageSize: z.number().int().min(1).max(100).default(10),
|
||||
});
|
||||
|
||||
const GeometrySchema = z
|
||||
.object({
|
||||
eventIds: z.array(z.string().min(1)).min(1).max(10),
|
||||
latitude: z.number().min(-90).max(90).optional(),
|
||||
longitude: z.number().min(-180).max(180).optional(),
|
||||
radius: z.number().positive().optional(),
|
||||
})
|
||||
.refine((a) => (a.latitude === undefined) === (a.longitude === undefined), {
|
||||
message: "latitude and longitude must be provided together",
|
||||
path: ["latitude"],
|
||||
});
|
||||
|
||||
export async function handlePrecipitationTool(
|
||||
name: string,
|
||||
args: Record<string, unknown>
|
||||
@@ -119,6 +160,17 @@ export async function handlePrecipitationTool(
|
||||
const params = PageSchema.parse(args);
|
||||
return apiPost("/v1/precipitations/page", params);
|
||||
}
|
||||
case "get_precipitation_geometry": {
|
||||
const { eventIds, latitude, longitude, radius } = GeometrySchema.parse(args);
|
||||
const bytes = await apiPostBinary("/v1/precipitations/geometry", {
|
||||
eventIds,
|
||||
...(latitude !== undefined && longitude !== undefined
|
||||
? { center: { lat: latitude, lng: longitude } }
|
||||
: {}),
|
||||
...(radius !== undefined ? { radius } : {}),
|
||||
});
|
||||
return decodePrecipitationFeatureCollection(bytes);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown precipitation tool: ${name}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user