kaax-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +274 -0
- package/dist/client.d.ts +118 -0
- package/dist/client.js +308 -0
- package/dist/client.js.map +1 -0
- package/dist/convert.d.ts +23 -0
- package/dist/convert.js +192 -0
- package/dist/convert.js.map +1 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.js +98 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +106 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +37 -0
- package/dist/platform.js +72 -0
- package/dist/platform.js.map +1 -0
- package/dist/prompts.d.ts +12 -0
- package/dist/prompts.js +185 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +17 -0
- package/dist/resources.js +305 -0
- package/dist/resources.js.map +1 -0
- package/dist/spatial.d.ts +51 -0
- package/dist/spatial.js +152 -0
- package/dist/spatial.js.map +1 -0
- package/dist/tools.d.ts +19 -0
- package/dist/tools.js +926 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +203 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static documentation resources exposed to the agent.
|
|
3
|
+
*
|
|
4
|
+
* MCP resources are read-only blobs the agent can fetch by URI. We keep them
|
|
5
|
+
* embedded in the binary instead of fetching the live web pages so the MCP
|
|
6
|
+
* server stays usable offline and snappy. The URIs map 1:1 to the references
|
|
7
|
+
* that the tools above emit (kaax://docs/quickstart, kaax://docs/tags, …) —
|
|
8
|
+
* if an agent says "see kaax://docs/X" the agent can then call
|
|
9
|
+
* `resources/read` on that URI to actually consume the doc.
|
|
10
|
+
*/
|
|
11
|
+
export function buildResources() {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
resource: {
|
|
15
|
+
uri: "kaax://docs/quickstart",
|
|
16
|
+
name: "Kaax — Quick start (30 min)",
|
|
17
|
+
description: "End-to-end walkthrough: account → field → model → first analysis. Use when the user is new.",
|
|
18
|
+
mimeType: "text/markdown",
|
|
19
|
+
},
|
|
20
|
+
text: `# Kaax — Quick start
|
|
21
|
+
|
|
22
|
+
You can take an account from zero to a working analysis in about 30 minutes. The
|
|
23
|
+
critical-path is short:
|
|
24
|
+
|
|
25
|
+
1. **Activate your account** — verify your email, complete the profile, and
|
|
26
|
+
confirm your subscription tier on \`/dashboard/payment\`. Replanting,
|
|
27
|
+
counting and path analyses are all included in the Pro tier.
|
|
28
|
+
2. **Upload a field** — go to \`/dashboard/manage\` → *Tus Campos* → *Nuevo
|
|
29
|
+
campo*. Drop a KML with the parcel boundary (or convert from shapefile via
|
|
30
|
+
\`kaax_convert_shapefile_to_kml\`). Name the field after the parcel; you can
|
|
31
|
+
tag it later.
|
|
32
|
+
3. **Upload imagery** — drone orthomosaic or satellite tile. The dashboard
|
|
33
|
+
accepts a single GeoTIFF or a folder of tiles. Multipart upload is on by
|
|
34
|
+
default for files > 100 MB.
|
|
35
|
+
4. **(For counting/replanting) Upload a model** — \`/dashboard/models\` →
|
|
36
|
+
*Subir modelo*. Without a detection model these two analysis types cannot
|
|
37
|
+
run; the path analysis works out of the box.
|
|
38
|
+
5. **Configure** — pick the analysis type (replanting / counting / path), set
|
|
39
|
+
the advanced parameters (call \`kaax_suggest_configuration\` for a sensible
|
|
40
|
+
default) and run.
|
|
41
|
+
6. **Inspect the report** — the analysis surfaces survival/depopulation rates,
|
|
42
|
+
total counts, total area in ha, and per-zone breakdowns.
|
|
43
|
+
|
|
44
|
+
Useful follow-ups:
|
|
45
|
+
- \`kaax_check_setup_status\` — what's still missing on the account.
|
|
46
|
+
- \`kaax_suggest_tags_for_field\` — organise across season / region / crop.
|
|
47
|
+
- \`kaax_explain_advanced_config\` — what every parameter does.
|
|
48
|
+
`,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
resource: {
|
|
52
|
+
uri: "kaax://docs/tags",
|
|
53
|
+
name: "Kaax — Tagging best practices",
|
|
54
|
+
description: "How to organise fields and analyses with tags.",
|
|
55
|
+
mimeType: "text/markdown",
|
|
56
|
+
},
|
|
57
|
+
text: `# Tagging in Kaax
|
|
58
|
+
|
|
59
|
+
Tags are the only structured way to slice across fields and analyses. Use them
|
|
60
|
+
as a lightweight CRM: a single field can carry several tags from different
|
|
61
|
+
axes, and the dashboard lets you filter analyses by any combination.
|
|
62
|
+
|
|
63
|
+
## Recommended taxonomy (4 axes)
|
|
64
|
+
|
|
65
|
+
| Axis | Example tag | Why |
|
|
66
|
+
| ------- | ----------------- | ---------------------------- |
|
|
67
|
+
| Season | \`zafra-2024\` | Compare year-over-year |
|
|
68
|
+
| Region | \`region-escuintla\` | Cluster by agronomy & climate |
|
|
69
|
+
| Crop | \`cultivo-cana\` | Filter analyses by species |
|
|
70
|
+
| Status | \`estado-activo\` | Hide archived parcels |
|
|
71
|
+
|
|
72
|
+
You can layer in extras — \`riego-secano\`, \`semilla-cp741714\`,
|
|
73
|
+
\`zonificacion-este\` — but resist the urge to invent a tag per field; use the
|
|
74
|
+
parcel name field for that.
|
|
75
|
+
|
|
76
|
+
## Normalisation rules
|
|
77
|
+
|
|
78
|
+
The server normalises every tag on the way in:
|
|
79
|
+
|
|
80
|
+
- lowercase
|
|
81
|
+
- accents stripped (NFD + diacritic removal)
|
|
82
|
+
- non-alphanumeric runs collapsed to \`-\`
|
|
83
|
+
- leading/trailing \`-\` trimmed
|
|
84
|
+
- 50-character hard cap
|
|
85
|
+
- 200-tag soft cap per user (forces curation)
|
|
86
|
+
|
|
87
|
+
Sending the same tag twice is **idempotent**, so it's safe for an agent to
|
|
88
|
+
retry.
|
|
89
|
+
|
|
90
|
+
## Useful tools
|
|
91
|
+
|
|
92
|
+
- \`kaax_list_tags\` — current taxonomy with usage counts.
|
|
93
|
+
- \`kaax_suggest_tags_for_field\` — heuristic tags from a field name.
|
|
94
|
+
- \`kaax_attach_field_tags\` — persist tags to a field.
|
|
95
|
+
`,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
resource: {
|
|
99
|
+
uri: "kaax://docs/configurations",
|
|
100
|
+
name: "Kaax — Advanced configuration",
|
|
101
|
+
description: "Parameter glossary and tuning recipes.",
|
|
102
|
+
mimeType: "text/markdown",
|
|
103
|
+
},
|
|
104
|
+
text: `# Advanced configuration
|
|
105
|
+
|
|
106
|
+
Every analysis has a JSON configuration. The defaults are reasonable, but
|
|
107
|
+
tuning is what separates a good report from a great one. Below is the cheat
|
|
108
|
+
sheet — call \`kaax_explain_advanced_config\` for the per-parameter detail.
|
|
109
|
+
|
|
110
|
+
## Counting
|
|
111
|
+
|
|
112
|
+
\`\`\`json
|
|
113
|
+
{
|
|
114
|
+
"blockSize": 2048,
|
|
115
|
+
"accuracy": 0.25,
|
|
116
|
+
"postProcessing": { "refinement": true, "radiusFactor": 0.8, "iou": 0.2 },
|
|
117
|
+
"sizeClassification": { "smallMax": 0.05, "mediumMax": 0.15, "largeMax": 0.3 }
|
|
118
|
+
}
|
|
119
|
+
\`\`\`
|
|
120
|
+
|
|
121
|
+
If you see too many duplicates in dense areas, lower \`iou\` to 0.15. If you
|
|
122
|
+
see misses on overlapping plants, raise \`radiusFactor\` to 1.0.
|
|
123
|
+
|
|
124
|
+
## Replanting
|
|
125
|
+
|
|
126
|
+
\`\`\`json
|
|
127
|
+
{
|
|
128
|
+
"blockSize": 2048,
|
|
129
|
+
"accuracy": 0.25,
|
|
130
|
+
"lineThickness": 1.5,
|
|
131
|
+
"seedsPerMeter": 3.75,
|
|
132
|
+
"seedsPerBox": 30,
|
|
133
|
+
"postProcessing": { "refinement": true, "iou": 0.2 }
|
|
134
|
+
}
|
|
135
|
+
\`\`\`
|
|
136
|
+
|
|
137
|
+
\`seedsPerMeter\` and \`seedsPerBox\` are agronomy-driven — set them from the
|
|
138
|
+
crop's planting plan, not from the model.
|
|
139
|
+
|
|
140
|
+
## Path
|
|
141
|
+
|
|
142
|
+
\`\`\`json
|
|
143
|
+
{ "blockSize": 2048, "accuracy": 0.3, "overlap": 0.1 }
|
|
144
|
+
\`\`\`
|
|
145
|
+
|
|
146
|
+
For very narrow paths, raise \`overlap\` to 0.2 to reduce seam artefacts.
|
|
147
|
+
|
|
148
|
+
## Sourcing defaults
|
|
149
|
+
|
|
150
|
+
- \`kaax_suggest_configuration\` — the curated preset above.
|
|
151
|
+
- \`kaax_peer_config_suggestions\` — anonymised crowd average for the same
|
|
152
|
+
analysis type. Use it as a sanity check, not a replacement.
|
|
153
|
+
`,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
resource: {
|
|
157
|
+
uri: "kaax://docs/shapefile-import",
|
|
158
|
+
name: "Kaax — Importing shapefiles",
|
|
159
|
+
description: "Convert .shp → .kml for upload. No GDAL or ogr2ogr needed.",
|
|
160
|
+
mimeType: "text/markdown",
|
|
161
|
+
},
|
|
162
|
+
text: `# Importing shapefiles into Kaax
|
|
163
|
+
|
|
164
|
+
Kaax accepts KML for parcel boundaries. If you only have ESRI shapefiles you
|
|
165
|
+
can convert them in one tool call:
|
|
166
|
+
|
|
167
|
+
\`\`\`
|
|
168
|
+
kaax_convert_shapefile_to_kml({
|
|
169
|
+
shpPath: "C:/finca/parcela_a.shp",
|
|
170
|
+
outputPath: "C:/finca/parcela_a.kml" // optional
|
|
171
|
+
})
|
|
172
|
+
\`\`\`
|
|
173
|
+
|
|
174
|
+
What the tool does:
|
|
175
|
+
|
|
176
|
+
1. Opens the \`.shp\` and the sibling \`.dbf\` (must be in the same folder).
|
|
177
|
+
2. Re-emits every geometry as KML 2.2 — Polygon, MultiPolygon, Point,
|
|
178
|
+
MultiPoint, LineString, MultiLineString. \`Z\` coordinates are dropped to
|
|
179
|
+
2D (Kaax does not consume elevation).
|
|
180
|
+
3. Preserves DBF attributes as \`<ExtendedData><Data name="…">\` pairs so
|
|
181
|
+
you don't lose plot ids / variety names.
|
|
182
|
+
4. Writes the file next to the .shp by default.
|
|
183
|
+
|
|
184
|
+
What it doesn't do:
|
|
185
|
+
|
|
186
|
+
- Reproject. Your .shp **must already be in WGS84 (EPSG:4326)**. If it isn't,
|
|
187
|
+
reproject first with QGIS or \`ogr2ogr -t_srs EPSG:4326\`.
|
|
188
|
+
- Merge multiple .shp files. Run one tool call per shapefile.
|
|
189
|
+
|
|
190
|
+
After conversion, upload the .kml at \`/dashboard/manage → Tus Campos →
|
|
191
|
+
Nuevo campo\`.
|
|
192
|
+
`,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
resource: {
|
|
196
|
+
uri: "kaax://docs/models",
|
|
197
|
+
name: "Kaax — Detection models",
|
|
198
|
+
description: "Why counting & replanting need a model, and how to upload one.",
|
|
199
|
+
mimeType: "text/markdown",
|
|
200
|
+
},
|
|
201
|
+
text: `# Detection models
|
|
202
|
+
|
|
203
|
+
Counting and replanting analyses depend on a detection model trained on your
|
|
204
|
+
crop. Kaax does **not** ship a one-size-fits-all model — accuracy on
|
|
205
|
+
sugarcane, pineapple, banana and oil palm is too dependent on resolution,
|
|
206
|
+
flight height and growth stage to use a single weights file.
|
|
207
|
+
|
|
208
|
+
## Uploading
|
|
209
|
+
|
|
210
|
+
- Page: \`/dashboard/models\`
|
|
211
|
+
- Format: PyTorch \`.pt\` checkpoint
|
|
212
|
+
- Naming: include crop + version (e.g. \`cana-v3.pt\`) — the agent uses the
|
|
213
|
+
name to match models against analyses.
|
|
214
|
+
|
|
215
|
+
## When you don't have a model
|
|
216
|
+
|
|
217
|
+
- **Path analysis** doesn't need one — point the user there first.
|
|
218
|
+
- **Pilot programmes** — the Kaax team can train one for you. Send a sample
|
|
219
|
+
orthomosaic + ground truth annotations to support.
|
|
220
|
+
|
|
221
|
+
## After upload
|
|
222
|
+
|
|
223
|
+
Run \`kaax_list_models\` to confirm the model is visible to the API. If it
|
|
224
|
+
shows up there, it'll show up in the analysis configuration drop-down too.
|
|
225
|
+
`,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
resource: {
|
|
229
|
+
uri: "kaax://docs/density-stats",
|
|
230
|
+
name: "Kaax — Density and rate aggregates",
|
|
231
|
+
description: "How density / survival / depopulation metrics are computed.",
|
|
232
|
+
mimeType: "text/markdown",
|
|
233
|
+
},
|
|
234
|
+
text: `# Density and rate metrics
|
|
235
|
+
|
|
236
|
+
Every analysis returns three families of metrics. The MCP server aggregates
|
|
237
|
+
them across fields when you call \`kaax_get_density_stats\`.
|
|
238
|
+
|
|
239
|
+
## Core counts
|
|
240
|
+
|
|
241
|
+
- \`totalAreaHa\` — analysed area in hectares.
|
|
242
|
+
- \`totalCounts\` — number of detected plants.
|
|
243
|
+
- \`averageTotalCounts\` — counts per analysis tile.
|
|
244
|
+
|
|
245
|
+
\`detectionsPerHa = totalCounts / totalAreaHa\` is the canonical density.
|
|
246
|
+
|
|
247
|
+
## Rates (0–1)
|
|
248
|
+
|
|
249
|
+
- \`survivalRate\` — fraction of expected plants that are alive.
|
|
250
|
+
- \`depopulationRate\` — fraction of expected positions that are empty.
|
|
251
|
+
- \`replantingRate\` — fraction of expected positions that need re-seeding.
|
|
252
|
+
- \`pathRate\` — fraction of the parcel covered by path-like geometry.
|
|
253
|
+
|
|
254
|
+
When you ask "which fields are underperforming", filter analyses where
|
|
255
|
+
\`survivalRate < 0.85\` or \`depopulationRate > 0.15\`. Use
|
|
256
|
+
\`kaax_find_similar_fields\` to find historic analyses with comparable
|
|
257
|
+
profiles.
|
|
258
|
+
`,
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
resource: {
|
|
262
|
+
uri: "kaax://docs/security",
|
|
263
|
+
name: "Kaax — MCP security model",
|
|
264
|
+
description: "How the MCP server protects the user's API key and respects privacy.",
|
|
265
|
+
mimeType: "text/markdown",
|
|
266
|
+
},
|
|
267
|
+
text: `# Security model
|
|
268
|
+
|
|
269
|
+
The MCP server is a local process. Every guarantee below holds **on the
|
|
270
|
+
user's machine** — there is no Kaax-hosted intermediary.
|
|
271
|
+
|
|
272
|
+
## API key
|
|
273
|
+
|
|
274
|
+
- Read once from the \`KAAX_API_KEY\` environment variable.
|
|
275
|
+
- Never logged, never echoed back to the agent.
|
|
276
|
+
- Sent only to the configured \`KAAX_BASE_URL\` (defaults to
|
|
277
|
+
https://www.kaax-agritech.com). Override only if you have a private
|
|
278
|
+
deployment.
|
|
279
|
+
- The server uses HTTPS and a 30 s timeout per request.
|
|
280
|
+
|
|
281
|
+
## Privacy
|
|
282
|
+
|
|
283
|
+
- Every Kaax API call is scoped to the api-key owner. The server cannot
|
|
284
|
+
return another user's fields, models or analyses.
|
|
285
|
+
- \`kaax_peer_config_suggestions\` is the **only** tool that touches other
|
|
286
|
+
users' data, and it does so through the \`/api/v2/peer-config-suggestions\`
|
|
287
|
+
endpoint, which:
|
|
288
|
+
- excludes the caller from the aggregate,
|
|
289
|
+
- requires a minimum number of distinct peers,
|
|
290
|
+
- returns only numeric averages rounded to natural quanta — no names, no
|
|
291
|
+
geometries, no user identifiers.
|
|
292
|
+
- Local conversions (\`kaax_convert_shapefile_to_kml\`) never leave the
|
|
293
|
+
user's machine; they are pure file I/O.
|
|
294
|
+
|
|
295
|
+
## What the agent sees
|
|
296
|
+
|
|
297
|
+
The agent only sees the tool outputs above — never the raw API key, never
|
|
298
|
+
HTTP details, never the user's session cookies. If you need to debug HTTP
|
|
299
|
+
errors, set \`DEBUG=kaax-mcp:*\` to print to stderr; stderr is not piped to
|
|
300
|
+
the LLM.
|
|
301
|
+
`,
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
//# sourceMappingURL=resources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AASH,MAAM,UAAU,cAAc;IAC5B,OAAO;QACL;YACE,QAAQ,EAAE;gBACR,GAAG,EAAE,wBAAwB;gBAC7B,IAAI,EAAE,6BAA6B;gBACnC,WAAW,EACT,6FAA6F;gBAC/F,QAAQ,EAAE,eAAe;aAC1B;YACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BX;SACI;QAED;YACE,QAAQ,EAAE;gBACR,GAAG,EAAE,kBAAkB;gBACvB,IAAI,EAAE,+BAA+B;gBACrC,WAAW,EAAE,gDAAgD;gBAC7D,QAAQ,EAAE,eAAe;aAC1B;YACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCX;SACI;QAED;YACE,QAAQ,EAAE;gBACR,GAAG,EAAE,4BAA4B;gBACjC,IAAI,EAAE,+BAA+B;gBACrC,WAAW,EAAE,wCAAwC;gBACrD,QAAQ,EAAE,eAAe;aAC1B;YACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiDX;SACI;QAED;YACE,QAAQ,EAAE;gBACR,GAAG,EAAE,8BAA8B;gBACnC,IAAI,EAAE,6BAA6B;gBACnC,WAAW,EACT,4DAA4D;gBAC9D,QAAQ,EAAE,eAAe;aAC1B;YACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BX;SACI;QAED;YACE,QAAQ,EAAE;gBACR,GAAG,EAAE,oBAAoB;gBACzB,IAAI,EAAE,yBAAyB;gBAC/B,WAAW,EACT,gEAAgE;gBAClE,QAAQ,EAAE,eAAe;aAC1B;YACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;CAwBX;SACI;QAED;YACE,QAAQ,EAAE;gBACR,GAAG,EAAE,2BAA2B;gBAChC,IAAI,EAAE,oCAAoC;gBAC1C,WAAW,EAAE,6DAA6D;gBAC1E,QAAQ,EAAE,eAAe;aAC1B;YACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;CAwBX;SACI;QAED;YACE,QAAQ,EAAE;gBACR,GAAG,EAAE,sBAAsB;gBAC3B,IAAI,EAAE,2BAA2B;gBACjC,WAAW,EACT,sEAAsE;gBACxE,QAAQ,EAAE,eAAe;aAC1B;YACD,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCX;SACI;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight spatial / similarity helpers — no external geospatial deps.
|
|
3
|
+
*
|
|
4
|
+
* Two flavours of "similar field" used by the MCP tools:
|
|
5
|
+
*
|
|
6
|
+
* 1. Metric similarity — cosine distance over the analysis numeric vector
|
|
7
|
+
* (survival, depopulation, density, etc.). Works even when the API
|
|
8
|
+
* does not return geometry, which is the common case today.
|
|
9
|
+
*
|
|
10
|
+
* 2. Geographic similarity — Haversine distance between centroids parsed
|
|
11
|
+
* from the KML string in the analysis record (when present).
|
|
12
|
+
*/
|
|
13
|
+
import type { AnalysisRecord, Coord } from "./types.js";
|
|
14
|
+
/**
|
|
15
|
+
* Great-circle distance between two coordinates in kilometres.
|
|
16
|
+
* Returns `Infinity` if either point is missing.
|
|
17
|
+
*/
|
|
18
|
+
export declare function haversineKm(a: Coord | undefined, b: Coord | undefined): number;
|
|
19
|
+
/**
|
|
20
|
+
* Parses a KML `<coordinates>` block (loose match) and returns the centroid
|
|
21
|
+
* of the *first* polygon found, in lon/lat decimal degrees.
|
|
22
|
+
* Returns `undefined` if no coordinates are found.
|
|
23
|
+
*/
|
|
24
|
+
export declare function centroidFromKml(kml: string | undefined): Coord | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Builds a numeric feature vector from an analysis record.
|
|
27
|
+
* Missing values become 0. The order is stable so cosine works.
|
|
28
|
+
*/
|
|
29
|
+
export declare function vectorize(rec: AnalysisRecord): number[];
|
|
30
|
+
/** Cosine similarity between two equal-length vectors. */
|
|
31
|
+
export declare function cosine(a: number[], b: number[]): number;
|
|
32
|
+
/**
|
|
33
|
+
* Given a reference analysis, find the most similar records from a pool.
|
|
34
|
+
* Hybrid score: 0.7 · cosine metric similarity + 0.3 · normalised proximity
|
|
35
|
+
* (closer = higher) when geometry is available, else metric only.
|
|
36
|
+
*/
|
|
37
|
+
export declare function findSimilar(ref: AnalysisRecord, pool: AnalysisRecord[], topK?: number): {
|
|
38
|
+
record: AnalysisRecord;
|
|
39
|
+
score: number;
|
|
40
|
+
distanceKm?: number;
|
|
41
|
+
}[];
|
|
42
|
+
/** Aggregates density stats across a pool of analyses. */
|
|
43
|
+
export declare function aggregateDensity(pool: AnalysisRecord[]): {
|
|
44
|
+
count: number;
|
|
45
|
+
totalAreaHa: number;
|
|
46
|
+
totalDetections: number;
|
|
47
|
+
avgDetectionsPerHa: number | null;
|
|
48
|
+
avgSurvivalRate: number | null;
|
|
49
|
+
avgDepopulationRate: number | null;
|
|
50
|
+
avgReplantingRate: number | null;
|
|
51
|
+
};
|
package/dist/spatial.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight spatial / similarity helpers — no external geospatial deps.
|
|
3
|
+
*
|
|
4
|
+
* Two flavours of "similar field" used by the MCP tools:
|
|
5
|
+
*
|
|
6
|
+
* 1. Metric similarity — cosine distance over the analysis numeric vector
|
|
7
|
+
* (survival, depopulation, density, etc.). Works even when the API
|
|
8
|
+
* does not return geometry, which is the common case today.
|
|
9
|
+
*
|
|
10
|
+
* 2. Geographic similarity — Haversine distance between centroids parsed
|
|
11
|
+
* from the KML string in the analysis record (when present).
|
|
12
|
+
*/
|
|
13
|
+
/** Earth radius in km. */
|
|
14
|
+
const EARTH_R = 6371.0088;
|
|
15
|
+
/** Convert degrees to radians. */
|
|
16
|
+
function toRad(deg) {
|
|
17
|
+
return (deg * Math.PI) / 180;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Great-circle distance between two coordinates in kilometres.
|
|
21
|
+
* Returns `Infinity` if either point is missing.
|
|
22
|
+
*/
|
|
23
|
+
export function haversineKm(a, b) {
|
|
24
|
+
if (!a || !b)
|
|
25
|
+
return Infinity;
|
|
26
|
+
const dLat = toRad(b.lat - a.lat);
|
|
27
|
+
const dLon = toRad(b.lon - a.lon);
|
|
28
|
+
const lat1 = toRad(a.lat);
|
|
29
|
+
const lat2 = toRad(b.lat);
|
|
30
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
|
|
31
|
+
return 2 * EARTH_R * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parses a KML `<coordinates>` block (loose match) and returns the centroid
|
|
35
|
+
* of the *first* polygon found, in lon/lat decimal degrees.
|
|
36
|
+
* Returns `undefined` if no coordinates are found.
|
|
37
|
+
*/
|
|
38
|
+
export function centroidFromKml(kml) {
|
|
39
|
+
if (!kml)
|
|
40
|
+
return undefined;
|
|
41
|
+
const m = /<coordinates>([\s\S]+?)<\/coordinates>/.exec(kml);
|
|
42
|
+
if (!m)
|
|
43
|
+
return undefined;
|
|
44
|
+
const raw = m[1].trim();
|
|
45
|
+
// KML coordinates are whitespace-separated triplets: lon,lat[,alt]
|
|
46
|
+
const points = raw
|
|
47
|
+
.split(/\s+/)
|
|
48
|
+
.map((triplet) => {
|
|
49
|
+
const parts = triplet.split(",").map(Number);
|
|
50
|
+
if (parts.length < 2 || parts.some((n) => !Number.isFinite(n)))
|
|
51
|
+
return null;
|
|
52
|
+
return { lon: parts[0], lat: parts[1] };
|
|
53
|
+
})
|
|
54
|
+
.filter((p) => p !== null);
|
|
55
|
+
if (points.length === 0)
|
|
56
|
+
return undefined;
|
|
57
|
+
const sum = points.reduce((acc, p) => ({ lon: acc.lon + p.lon, lat: acc.lat + p.lat }), { lon: 0, lat: 0 });
|
|
58
|
+
return { lon: sum.lon / points.length, lat: sum.lat / points.length };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Builds a numeric feature vector from an analysis record.
|
|
62
|
+
* Missing values become 0. The order is stable so cosine works.
|
|
63
|
+
*/
|
|
64
|
+
export function vectorize(rec) {
|
|
65
|
+
const a = rec.analysis ?? {};
|
|
66
|
+
return [
|
|
67
|
+
a.totalAreaHa ?? 0,
|
|
68
|
+
a.totalCounts ?? 0,
|
|
69
|
+
a.survivalRate ?? 0,
|
|
70
|
+
a.depopulationRate ?? 0,
|
|
71
|
+
a.replantingRate ?? 0,
|
|
72
|
+
a.pathRate ?? 0,
|
|
73
|
+
a.totalPathAreaHa ?? 0,
|
|
74
|
+
a.averageTotalCounts ?? 0,
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
/** Cosine similarity between two equal-length vectors. */
|
|
78
|
+
export function cosine(a, b) {
|
|
79
|
+
if (a.length !== b.length)
|
|
80
|
+
return 0;
|
|
81
|
+
let dot = 0;
|
|
82
|
+
let na = 0;
|
|
83
|
+
let nb = 0;
|
|
84
|
+
for (let i = 0; i < a.length; i++) {
|
|
85
|
+
dot += a[i] * b[i];
|
|
86
|
+
na += a[i] * a[i];
|
|
87
|
+
nb += b[i] * b[i];
|
|
88
|
+
}
|
|
89
|
+
if (na === 0 || nb === 0)
|
|
90
|
+
return 0;
|
|
91
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Given a reference analysis, find the most similar records from a pool.
|
|
95
|
+
* Hybrid score: 0.7 · cosine metric similarity + 0.3 · normalised proximity
|
|
96
|
+
* (closer = higher) when geometry is available, else metric only.
|
|
97
|
+
*/
|
|
98
|
+
export function findSimilar(ref, pool, topK = 5) {
|
|
99
|
+
const refVec = vectorize(ref);
|
|
100
|
+
const refCenter = centroidFromKml(ref.kml);
|
|
101
|
+
const scored = pool
|
|
102
|
+
.filter((r) => r._id !== ref._id)
|
|
103
|
+
.map((r) => {
|
|
104
|
+
const sim = cosine(refVec, vectorize(r));
|
|
105
|
+
const center = centroidFromKml(r.kml);
|
|
106
|
+
let geoScore = 0;
|
|
107
|
+
let distanceKm;
|
|
108
|
+
if (refCenter && center) {
|
|
109
|
+
distanceKm = haversineKm(refCenter, center);
|
|
110
|
+
// Decay over 50 km — beyond that, geographic relatedness is weak.
|
|
111
|
+
geoScore = Math.max(0, 1 - distanceKm / 50);
|
|
112
|
+
}
|
|
113
|
+
const score = refCenter ? sim * 0.7 + geoScore * 0.3 : sim;
|
|
114
|
+
return { record: r, score, distanceKm };
|
|
115
|
+
})
|
|
116
|
+
.sort((a, b) => b.score - a.score);
|
|
117
|
+
return scored.slice(0, topK);
|
|
118
|
+
}
|
|
119
|
+
/** Aggregates density stats across a pool of analyses. */
|
|
120
|
+
export function aggregateDensity(pool) {
|
|
121
|
+
if (pool.length === 0) {
|
|
122
|
+
return {
|
|
123
|
+
count: 0,
|
|
124
|
+
totalAreaHa: 0,
|
|
125
|
+
totalDetections: 0,
|
|
126
|
+
avgDetectionsPerHa: null,
|
|
127
|
+
avgSurvivalRate: null,
|
|
128
|
+
avgDepopulationRate: null,
|
|
129
|
+
avgReplantingRate: null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const totalAreaHa = pool.reduce((s, r) => s + (r.analysis?.totalAreaHa ?? 0), 0);
|
|
133
|
+
const totalDetections = pool.reduce((s, r) => s + (r.analysis?.totalCounts ?? 0), 0);
|
|
134
|
+
const mean = (key) => {
|
|
135
|
+
const values = pool
|
|
136
|
+
.map((r) => r.analysis?.[key])
|
|
137
|
+
.filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
138
|
+
if (values.length === 0)
|
|
139
|
+
return null;
|
|
140
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
count: pool.length,
|
|
144
|
+
totalAreaHa,
|
|
145
|
+
totalDetections,
|
|
146
|
+
avgDetectionsPerHa: totalAreaHa > 0 ? totalDetections / totalAreaHa : null,
|
|
147
|
+
avgSurvivalRate: mean("survivalRate"),
|
|
148
|
+
avgDepopulationRate: mean("depopulationRate"),
|
|
149
|
+
avgReplantingRate: mean("replantingRate"),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=spatial.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spatial.js","sourceRoot":"","sources":["../src/spatial.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry — every callable function exposed to the LLM agent.
|
|
3
|
+
*
|
|
4
|
+
* Each entry pairs a JSON-Schema input definition (what the agent must send)
|
|
5
|
+
* with an async handler that returns text-form results suitable for the
|
|
6
|
+
* agent to reason over. Outputs are kept compact and **explicit about next
|
|
7
|
+
* steps** so the agent never has to guess what to do next.
|
|
8
|
+
*/
|
|
9
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import type { KaaxClient } from "./client.js";
|
|
11
|
+
export interface ToolDef {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
inputSchema: object;
|
|
15
|
+
handler: (args: unknown, ctx: {
|
|
16
|
+
client: KaaxClient;
|
|
17
|
+
}) => Promise<CallToolResult>;
|
|
18
|
+
}
|
|
19
|
+
export declare function buildTools(): ToolDef[];
|