@ansvar/ch-pest-management-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.
Files changed (108) hide show
  1. package/.github/workflows/check-freshness.yml +49 -0
  2. package/.github/workflows/ci.yml +21 -0
  3. package/.github/workflows/codeql.yml +25 -0
  4. package/.github/workflows/ghcr-build.yml +45 -0
  5. package/.github/workflows/gitleaks.yml +18 -0
  6. package/.github/workflows/ingest.yml +59 -0
  7. package/.github/workflows/publish.yml +24 -0
  8. package/CHANGELOG.md +16 -0
  9. package/CODEOWNERS +1 -0
  10. package/COVERAGE.md +66 -0
  11. package/DISCLAIMER.md +39 -0
  12. package/Dockerfile +26 -0
  13. package/LICENSE +17 -0
  14. package/PRIVACY.md +23 -0
  15. package/README.md +98 -0
  16. package/SECURITY.md +26 -0
  17. package/TOOLS.md +202 -0
  18. package/data/coverage.json +34 -0
  19. package/data/database.db +0 -0
  20. package/data/sources.yml +57 -0
  21. package/dist/db.d.ts +26 -0
  22. package/dist/db.d.ts.map +1 -0
  23. package/dist/db.js +189 -0
  24. package/dist/db.js.map +1 -0
  25. package/dist/http-server.d.ts +2 -0
  26. package/dist/http-server.d.ts.map +1 -0
  27. package/dist/http-server.js +270 -0
  28. package/dist/http-server.js.map +1 -0
  29. package/dist/jurisdiction.d.ts +18 -0
  30. package/dist/jurisdiction.d.ts.map +1 -0
  31. package/dist/jurisdiction.js +16 -0
  32. package/dist/jurisdiction.js.map +1 -0
  33. package/dist/metadata.d.ts +10 -0
  34. package/dist/metadata.d.ts.map +1 -0
  35. package/dist/metadata.js +21 -0
  36. package/dist/metadata.js.map +1 -0
  37. package/dist/server.d.ts +3 -0
  38. package/dist/server.d.ts.map +1 -0
  39. package/dist/server.js +216 -0
  40. package/dist/server.js.map +1 -0
  41. package/dist/tools/about.d.ts +15 -0
  42. package/dist/tools/about.d.ts.map +1 -0
  43. package/dist/tools/about.js +27 -0
  44. package/dist/tools/about.js.map +1 -0
  45. package/dist/tools/check-freshness.d.ts +15 -0
  46. package/dist/tools/check-freshness.d.ts.map +1 -0
  47. package/dist/tools/check-freshness.js +26 -0
  48. package/dist/tools/check-freshness.js.map +1 -0
  49. package/dist/tools/get-approved-products.d.ts +38 -0
  50. package/dist/tools/get-approved-products.d.ts.map +1 -0
  51. package/dist/tools/get-approved-products.js +49 -0
  52. package/dist/tools/get-approved-products.js.map +1 -0
  53. package/dist/tools/get-ipm-guidance.d.ts +42 -0
  54. package/dist/tools/get-ipm-guidance.d.ts.map +1 -0
  55. package/dist/tools/get-ipm-guidance.js +40 -0
  56. package/dist/tools/get-ipm-guidance.js.map +1 -0
  57. package/dist/tools/get-pest-details.d.ts +43 -0
  58. package/dist/tools/get-pest-details.d.ts.map +1 -0
  59. package/dist/tools/get-pest-details.js +20 -0
  60. package/dist/tools/get-pest-details.js.map +1 -0
  61. package/dist/tools/get-treatments.d.ts +46 -0
  62. package/dist/tools/get-treatments.d.ts.map +1 -0
  63. package/dist/tools/get-treatments.js +41 -0
  64. package/dist/tools/get-treatments.js.map +1 -0
  65. package/dist/tools/identify-from-symptoms.d.ts +31 -0
  66. package/dist/tools/identify-from-symptoms.d.ts.map +1 -0
  67. package/dist/tools/identify-from-symptoms.js +49 -0
  68. package/dist/tools/identify-from-symptoms.js.map +1 -0
  69. package/dist/tools/list-sources.d.ts +18 -0
  70. package/dist/tools/list-sources.d.ts.map +1 -0
  71. package/dist/tools/list-sources.js +51 -0
  72. package/dist/tools/list-sources.js.map +1 -0
  73. package/dist/tools/search-crop-threats.d.ts +36 -0
  74. package/dist/tools/search-crop-threats.d.ts.map +1 -0
  75. package/dist/tools/search-crop-threats.js +48 -0
  76. package/dist/tools/search-crop-threats.js.map +1 -0
  77. package/dist/tools/search-pests.d.ts +31 -0
  78. package/dist/tools/search-pests.d.ts.map +1 -0
  79. package/dist/tools/search-pests.js +36 -0
  80. package/dist/tools/search-pests.js.map +1 -0
  81. package/docker-compose.yml +12 -0
  82. package/eslint.config.js +23 -0
  83. package/package.json +54 -0
  84. package/scripts/ingest.ts +1037 -0
  85. package/server.json +71 -0
  86. package/src/db.ts +230 -0
  87. package/src/http-server.ts +302 -0
  88. package/src/jurisdiction.ts +30 -0
  89. package/src/metadata.ts +31 -0
  90. package/src/server.ts +239 -0
  91. package/src/tools/about.ts +28 -0
  92. package/src/tools/check-freshness.ts +42 -0
  93. package/src/tools/get-approved-products.ts +68 -0
  94. package/src/tools/get-ipm-guidance.ts +56 -0
  95. package/src/tools/get-pest-details.ts +44 -0
  96. package/src/tools/get-treatments.ts +61 -0
  97. package/src/tools/identify-from-symptoms.ts +64 -0
  98. package/src/tools/list-sources.ts +65 -0
  99. package/src/tools/search-crop-threats.ts +74 -0
  100. package/src/tools/search-pests.ts +49 -0
  101. package/tests/db.test.ts +82 -0
  102. package/tests/helpers/seed-db.ts +187 -0
  103. package/tests/jurisdiction.test.ts +35 -0
  104. package/tests/tools/about.test.ts +27 -0
  105. package/tests/tools/check-freshness.test.ts +58 -0
  106. package/tests/tools/list-sources.test.ts +57 -0
  107. package/tests/tools/search-pests.test.ts +64 -0
  108. package/tsconfig.json +19 -0
package/TOOLS.md ADDED
@@ -0,0 +1,202 @@
1
+ # Tools Reference
2
+
3
+ ## about
4
+
5
+ Get server metadata: name, version, coverage, data sources, and links.
6
+
7
+ **Parameters:** None
8
+
9
+ **Returns:** Server name, version, jurisdiction, data sources list, tool count, links, disclaimer metadata.
10
+
11
+ **Example:**
12
+ ```json
13
+ { }
14
+ ```
15
+
16
+ ---
17
+
18
+ ## list_sources
19
+
20
+ List all data sources with authority, URL, license, and freshness info.
21
+
22
+ **Parameters:** None
23
+
24
+ **Returns:** Array of sources, each with `name`, `authority`, `official_url`, `retrieval_method`, `update_frequency`, `license`, `coverage`, `last_retrieved`.
25
+
26
+ **Example:**
27
+ ```json
28
+ { }
29
+ ```
30
+
31
+ ---
32
+
33
+ ## check_data_freshness
34
+
35
+ Check when data was last ingested, staleness status, and how to trigger a refresh.
36
+
37
+ **Parameters:** None
38
+
39
+ **Returns:**
40
+
41
+ | Field | Type | Description |
42
+ |-------|------|-------------|
43
+ | `status` | string | `fresh`, `stale`, or `unknown` |
44
+ | `last_ingest` | string or null | ISO date of last ingestion |
45
+ | `days_since_ingest` | number or null | Days since last ingestion |
46
+ | `staleness_threshold_days` | number | Threshold (90 days) |
47
+ | `refresh_command` | string | CLI command to trigger re-ingestion |
48
+
49
+ **Example:**
50
+ ```json
51
+ { }
52
+ ```
53
+
54
+ ---
55
+
56
+ ## search_pests
57
+
58
+ Search pests, diseases, and weeds affecting Swiss crops. Supports German and English queries. Uses tiered FTS5 search (phrase > AND > prefix > stemmed > OR > LIKE fallback).
59
+
60
+ **Parameters:**
61
+
62
+ | Name | Type | Required | Description |
63
+ |------|------|----------|-------------|
64
+ | `query` | string | yes | Free-text search (e.g. "Blattlaeuse Weizen", "Septoria") |
65
+ | `pest_type` | string | no | Filter by type: `insect`, `disease`, or `weed` |
66
+ | `crop` | string | no | Filter by affected crop (e.g. "Winterweizen", "Kartoffeln") |
67
+ | `jurisdiction` | string | no | ISO 3166-1 alpha-2 code (default: CH) |
68
+ | `limit` | number | no | Max results (default: 20, max: 50) |
69
+
70
+ **Returns:** Array of matching pests with `title`, `body`, `pest_type`, `crop_category`, `relevance_rank`.
71
+
72
+ **Example:**
73
+ ```json
74
+ { "query": "Blattlaeuse", "crop": "Winterweizen" }
75
+ ```
76
+
77
+ ---
78
+
79
+ ## get_pest_details
80
+
81
+ Get full profile for a pest: lifecycle, identification, crops affected, and treatments.
82
+
83
+ **Parameters:**
84
+
85
+ | Name | Type | Required | Description |
86
+ |------|------|----------|-------------|
87
+ | `pest_id` | string | yes | Pest ID from search_pests (e.g. "blattlaeuse", "septoria") |
88
+ | `jurisdiction` | string | no | ISO 3166-1 alpha-2 code (default: CH) |
89
+
90
+ **Returns:** Pest profile with `id`, `name`, `pest_type`, `scientific_name`, `crops_affected` (array), `lifecycle`, `identification`, `damage_description`, plus associated `treatments` array.
91
+
92
+ **Example:**
93
+ ```json
94
+ { "pest_id": "blattlaeuse" }
95
+ ```
96
+
97
+ ---
98
+
99
+ ## get_treatments
100
+
101
+ Get approved chemical and non-chemical controls for a specific pest. Includes products, active substances, W-numbers, dosage, waiting periods, Pufferstreifen, and OELN alternatives.
102
+
103
+ **Parameters:**
104
+
105
+ | Name | Type | Required | Description |
106
+ |------|------|----------|-------------|
107
+ | `pest_id` | string | yes | Pest ID (e.g. "rapsglanzkaefer", "krautfaeule") |
108
+ | `approach` | string | no | Filter: `chemical`, `biological`, `cultural`, or `mechanical` |
109
+ | `jurisdiction` | string | no | ISO 3166-1 alpha-2 code (default: CH) |
110
+
111
+ **Returns:** Pest info plus array of treatments, each with `approach`, `product_name`, `active_substance`, `w_number`, `dosage`, `waiting_period`, `timing`, `restrictions`, `pufferstreifen`, `notes`.
112
+
113
+ **Example:**
114
+ ```json
115
+ { "pest_id": "krautfaeule", "approach": "chemical" }
116
+ ```
117
+
118
+ ---
119
+
120
+ ## get_ipm_guidance
121
+
122
+ Get IPM (Integrierter Pflanzenschutz) guidance for a crop: OELN Schadschwellen, monitoring methods, cultural controls, and prognosis systems.
123
+
124
+ **Parameters:**
125
+
126
+ | Name | Type | Required | Description |
127
+ |------|------|----------|-------------|
128
+ | `crop` | string | yes | Crop name (e.g. "Winterweizen", "Kartoffeln", "Reben", "Apfel") |
129
+ | `pest_id` | string | no | Filter to specific pest |
130
+ | `jurisdiction` | string | no | ISO 3166-1 alpha-2 code (default: CH) |
131
+
132
+ **Returns:** Array of guidance entries per pest, each with `threshold` (Schadschwelle), `monitoring_method`, `cultural_controls`, `prognose_system`, `oeln_requirements`.
133
+
134
+ **Example:**
135
+ ```json
136
+ { "crop": "Winterweizen" }
137
+ ```
138
+
139
+ ---
140
+
141
+ ## search_crop_threats
142
+
143
+ List all pests, diseases, and weeds that threaten a specific crop, with damage thresholds and monitoring advice. Groups results by type (insects, diseases, weeds).
144
+
145
+ **Parameters:**
146
+
147
+ | Name | Type | Required | Description |
148
+ |------|------|----------|-------------|
149
+ | `crop` | string | yes | Crop name (e.g. "Winterweizen", "Winterraps", "Kartoffeln") |
150
+ | `growth_stage` | string | no | Growth stage filter (e.g. "Bestockung", "Bluete") |
151
+ | `jurisdiction` | string | no | ISO 3166-1 alpha-2 code (default: CH) |
152
+
153
+ **Returns:** `total_threats`, breakdown `by_type` (insects/diseases/weeds counts), and `threats` array with `pest_id`, `name`, `pest_type`, `scientific_name`, `damage`, `identification`, `threshold`, `monitoring`, `prognose_system`.
154
+
155
+ **Example:**
156
+ ```json
157
+ { "crop": "Kartoffeln" }
158
+ ```
159
+
160
+ ---
161
+
162
+ ## identify_from_symptoms
163
+
164
+ Identify a pest, disease, or weed from symptom descriptions. Returns a ranked differential diagnosis.
165
+
166
+ **Parameters:**
167
+
168
+ | Name | Type | Required | Description |
169
+ |------|------|----------|-------------|
170
+ | `symptoms` | string | yes | Symptom description (e.g. "gelbe Flecken auf Weizenblaettern") |
171
+ | `crop` | string | no | Affected crop to narrow results |
172
+ | `season` | string | no | Time of year (e.g. "Fruehling", "Sommer") |
173
+ | `jurisdiction` | string | no | ISO 3166-1 alpha-2 code (default: CH) |
174
+
175
+ **Returns:** `possible_matches` array with `pest_id`, `name`, `pest_type`, `scientific_name`, `identification`, `damage_description`, `crops_affected`, plus a recommendation to consult the cantonal Pflanzenschutzfachstelle for uncertain cases.
176
+
177
+ **Example:**
178
+ ```json
179
+ { "symptoms": "Frass an Rapsblueten", "crop": "Winterraps" }
180
+ ```
181
+
182
+ ---
183
+
184
+ ## get_approved_products
185
+
186
+ Search approved plant protection products (Pflanzenschutzmittel) from the BLW Verzeichnis. At least one filter is recommended.
187
+
188
+ **Parameters:**
189
+
190
+ | Name | Type | Required | Description |
191
+ |------|------|----------|-------------|
192
+ | `active_substance` | string | no | Active substance name (e.g. "Glyphosat", "Mancozeb") |
193
+ | `target_pest` | string | no | Target organism (e.g. "Blattlaeuse", "Pilzkrankheiten") |
194
+ | `crop` | string | no | Target crop (e.g. "Winterweizen", "Reben") |
195
+ | `jurisdiction` | string | no | ISO 3166-1 alpha-2 code (default: CH) |
196
+
197
+ **Returns:** Array of products with `w_number`, `product_name`, `active_substance`, `product_type`, `crops`, `target_organisms`, `auflagen`, `wartefrist`, `dosage`, `application_method`, `spe3_buffer`, `aktionsplan_status`. Always includes a warning to verify current authorisation on psm.admin.ch.
198
+
199
+ **Example:**
200
+ ```json
201
+ { "active_substance": "Spinosad", "crop": "Kartoffeln" }
202
+ ```
@@ -0,0 +1,34 @@
1
+ {
2
+ "server": "ch-pest-management-mcp",
3
+ "jurisdiction": "CH",
4
+ "version": "0.1.0",
5
+ "last_ingest": "2026-04-05",
6
+ "data": {
7
+ "pests": 23,
8
+ "pests_insects": 9,
9
+ "pests_diseases": 8,
10
+ "pests_weeds": 6,
11
+ "treatments": 30,
12
+ "ipm_guidance": 12,
13
+ "approved_products": 14,
14
+ "prognosis_systems": [
15
+ "PhytoPRE",
16
+ "SOPRA",
17
+ "FusaProg",
18
+ "VitiMeteo"
19
+ ],
20
+ "crop_categories": [
21
+ "feldbau",
22
+ "rebbau",
23
+ "obstbau",
24
+ "gemuese"
25
+ ]
26
+ },
27
+ "tools": 10,
28
+ "sources": [
29
+ "BLW Pflanzenschutzmittelverzeichnis (psm.admin.ch)",
30
+ "Agroscope Pflanzenschutzempfehlungen",
31
+ "AGRIDEA OELN-Checklisten",
32
+ "Aktionsplan Pflanzenschutzmittel"
33
+ ]
34
+ }
Binary file
@@ -0,0 +1,57 @@
1
+ # Data sources for ch-pest-management-mcp
2
+ sources:
3
+ - name: BLW Pflanzenschutzmittelverzeichnis
4
+ authority: Bundesamt fuer Landwirtschaft (BLW)
5
+ url: https://www.psm.admin.ch/de/produkte
6
+ license: Swiss Federal Administration — free reuse
7
+ update_frequency: continuous (products added/removed)
8
+ last_retrieved: "2026-04-05"
9
+
10
+ - name: Agroscope Pflanzenschutzempfehlungen
11
+ authority: Agroscope
12
+ url: https://www.agroscope.admin.ch/agroscope/de/home/themen/pflanzenbau/pflanzenschutz.html
13
+ license: Swiss Federal Administration — free reuse
14
+ update_frequency: annual
15
+ last_retrieved: "2026-04-05"
16
+
17
+ - name: AGRIDEA OELN-Pflanzenschutz-Checklisten
18
+ authority: AGRIDEA
19
+ url: https://www.agridea.ch/de/themen/pflanzenbau/pflanzenschutz/
20
+ license: Public advisory material
21
+ update_frequency: annual
22
+ last_retrieved: "2026-04-05"
23
+
24
+ - name: Aktionsplan Pflanzenschutzmittel
25
+ authority: Bundesrat / BLW
26
+ url: https://www.blw.admin.ch/blw/de/home/nachhaltige-produktion/pflanzenschutz/aktionsplan.html
27
+ license: Swiss Federal Administration — free reuse
28
+ update_frequency: periodic (adopted 2017, targets to 2027)
29
+ last_retrieved: "2026-04-05"
30
+
31
+ - name: PhytoPRE Prognosemodell
32
+ authority: Agroscope
33
+ url: https://www.phytopre.ch
34
+ license: Public access
35
+ update_frequency: real-time (growing season)
36
+ last_retrieved: "2026-04-05"
37
+
38
+ - name: SOPRA Schaedlingsprognose
39
+ authority: Agroscope
40
+ url: https://www.sopra.info
41
+ license: Public access
42
+ update_frequency: real-time (growing season)
43
+ last_retrieved: "2026-04-05"
44
+
45
+ - name: FusaProg Fusariumprognose
46
+ authority: Agroscope / Meteotest
47
+ url: https://www.fusaprog.ch
48
+ license: Public access
49
+ update_frequency: real-time (growing season)
50
+ last_retrieved: "2026-04-05"
51
+
52
+ - name: VitiMeteo Rebenprognose
53
+ authority: Agroscope Changins
54
+ url: https://www.vitimeteo.info
55
+ license: Public access
56
+ update_frequency: real-time (growing season)
57
+ last_retrieved: "2026-04-05"
package/dist/db.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import BetterSqlite3 from 'better-sqlite3';
2
+ export interface Database {
3
+ get<T>(sql: string, params?: unknown[]): T | undefined;
4
+ all<T>(sql: string, params?: unknown[]): T[];
5
+ run(sql: string, params?: unknown[]): void;
6
+ close(): void;
7
+ readonly instance: BetterSqlite3.Database;
8
+ }
9
+ export declare function createDatabase(dbPath?: string): Database;
10
+ export declare function ftsSearch(db: Database, query: string, limit?: number): {
11
+ title: string;
12
+ body: string;
13
+ pest_type: string;
14
+ crop_category: string;
15
+ jurisdiction: string;
16
+ rank: number;
17
+ }[];
18
+ /**
19
+ * Tiered FTS5 search with automatic fallback.
20
+ * Tiers: exact phrase -> AND -> prefix -> stemmed prefix -> OR -> LIKE
21
+ */
22
+ export declare function tieredFtsSearch(db: Database, table: string, columns: string[], query: string, limit?: number): {
23
+ tier: string;
24
+ results: Record<string, unknown>[];
25
+ };
26
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,gBAAgB,CAAC;AAI3C,MAAM,WAAW,QAAQ;IACvB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;IACvD,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC;IAC7C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC3C,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,QAAQ,CAAC;CAC3C;AAED,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CA4BxD;AAmFD,wBAAgB,SAAS,CACvB,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAW,GACjB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAGjH;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAW,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;CAAE,CA4DtD"}
package/dist/db.js ADDED
@@ -0,0 +1,189 @@
1
+ import BetterSqlite3 from 'better-sqlite3';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ export function createDatabase(dbPath) {
5
+ const resolvedPath = dbPath ??
6
+ join(dirname(fileURLToPath(import.meta.url)), '..', 'data', 'database.db');
7
+ const db = new BetterSqlite3(resolvedPath);
8
+ db.pragma('journal_mode = DELETE');
9
+ db.pragma('foreign_keys = ON');
10
+ initSchema(db);
11
+ return {
12
+ get(sql, params = []) {
13
+ return db.prepare(sql).get(...params);
14
+ },
15
+ all(sql, params = []) {
16
+ return db.prepare(sql).all(...params);
17
+ },
18
+ run(sql, params = []) {
19
+ db.prepare(sql).run(...params);
20
+ },
21
+ close() {
22
+ db.close();
23
+ },
24
+ get instance() {
25
+ return db;
26
+ },
27
+ };
28
+ }
29
+ function initSchema(db) {
30
+ db.exec(`
31
+ CREATE TABLE IF NOT EXISTS pests (
32
+ id TEXT PRIMARY KEY,
33
+ name TEXT NOT NULL,
34
+ pest_type TEXT NOT NULL CHECK (pest_type IN ('insect', 'disease', 'weed')),
35
+ scientific_name TEXT,
36
+ crops_affected TEXT NOT NULL,
37
+ crop_category TEXT,
38
+ lifecycle TEXT,
39
+ identification TEXT,
40
+ damage_description TEXT,
41
+ language TEXT DEFAULT 'DE',
42
+ jurisdiction TEXT NOT NULL DEFAULT 'CH'
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS treatments (
46
+ id INTEGER PRIMARY KEY,
47
+ pest_id TEXT NOT NULL REFERENCES pests(id),
48
+ approach TEXT NOT NULL CHECK (approach IN ('chemical', 'biological', 'cultural', 'mechanical')),
49
+ product_name TEXT,
50
+ active_substance TEXT,
51
+ w_number TEXT,
52
+ dosage TEXT,
53
+ waiting_period TEXT,
54
+ timing TEXT,
55
+ restrictions TEXT,
56
+ pufferstreifen TEXT,
57
+ notes TEXT,
58
+ jurisdiction TEXT NOT NULL DEFAULT 'CH'
59
+ );
60
+
61
+ CREATE TABLE IF NOT EXISTS ipm_guidance (
62
+ id INTEGER PRIMARY KEY,
63
+ crop TEXT NOT NULL,
64
+ crop_category TEXT,
65
+ pest_id TEXT REFERENCES pests(id),
66
+ threshold TEXT,
67
+ monitoring_method TEXT,
68
+ cultural_controls TEXT,
69
+ prognose_system TEXT,
70
+ oeln_requirements TEXT,
71
+ notes TEXT,
72
+ jurisdiction TEXT NOT NULL DEFAULT 'CH'
73
+ );
74
+
75
+ CREATE TABLE IF NOT EXISTS approved_products (
76
+ id INTEGER PRIMARY KEY,
77
+ w_number TEXT NOT NULL,
78
+ product_name TEXT NOT NULL,
79
+ active_substance TEXT NOT NULL,
80
+ product_type TEXT,
81
+ crops TEXT,
82
+ target_organisms TEXT,
83
+ auflagen TEXT,
84
+ wartefrist TEXT,
85
+ dosage TEXT,
86
+ application_method TEXT,
87
+ spe3_buffer TEXT,
88
+ aktionsplan_status TEXT,
89
+ language TEXT DEFAULT 'DE',
90
+ jurisdiction TEXT NOT NULL DEFAULT 'CH'
91
+ );
92
+
93
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
94
+ title, body, pest_type, crop_category, jurisdiction
95
+ );
96
+
97
+ CREATE TABLE IF NOT EXISTS db_metadata (
98
+ key TEXT PRIMARY KEY,
99
+ value TEXT
100
+ );
101
+
102
+ INSERT OR IGNORE INTO db_metadata (key, value) VALUES ('schema_version', '1.0');
103
+ INSERT OR IGNORE INTO db_metadata (key, value) VALUES ('mcp_name', 'Switzerland Pest Management MCP');
104
+ INSERT OR IGNORE INTO db_metadata (key, value) VALUES ('jurisdiction', 'CH');
105
+ `);
106
+ }
107
+ const FTS_COLUMNS = ['title', 'body', 'pest_type', 'crop_category', 'jurisdiction'];
108
+ export function ftsSearch(db, query, limit = 20) {
109
+ const { results } = tieredFtsSearch(db, 'search_index', FTS_COLUMNS, query, limit);
110
+ return results;
111
+ }
112
+ /**
113
+ * Tiered FTS5 search with automatic fallback.
114
+ * Tiers: exact phrase -> AND -> prefix -> stemmed prefix -> OR -> LIKE
115
+ */
116
+ export function tieredFtsSearch(db, table, columns, query, limit = 20) {
117
+ const sanitized = sanitizeFtsInput(query);
118
+ if (!sanitized.trim())
119
+ return { tier: 'empty', results: [] };
120
+ const columnList = columns.join(', ');
121
+ const select = `SELECT ${columnList}, rank FROM ${table}`;
122
+ const order = `ORDER BY rank LIMIT ?`;
123
+ // Tier 1: Exact phrase
124
+ const phrase = `"${sanitized}"`;
125
+ let results = tryFts(db, select, table, order, phrase, limit);
126
+ if (results.length > 0)
127
+ return { tier: 'phrase', results };
128
+ // Tier 2: AND
129
+ const words = sanitized.split(/\s+/).filter(w => w.length > 1);
130
+ if (words.length > 1) {
131
+ const andQuery = words.join(' AND ');
132
+ results = tryFts(db, select, table, order, andQuery, limit);
133
+ if (results.length > 0)
134
+ return { tier: 'and', results };
135
+ }
136
+ // Tier 3: Prefix
137
+ const prefixQuery = words.map(w => `${w}*`).join(' AND ');
138
+ results = tryFts(db, select, table, order, prefixQuery, limit);
139
+ if (results.length > 0)
140
+ return { tier: 'prefix', results };
141
+ // Tier 4: Stemmed prefix
142
+ const stemmed = words.map(w => stemWord(w) + '*');
143
+ const stemmedQuery = stemmed.join(' AND ');
144
+ if (stemmedQuery !== prefixQuery) {
145
+ results = tryFts(db, select, table, order, stemmedQuery, limit);
146
+ if (results.length > 0)
147
+ return { tier: 'stemmed', results };
148
+ }
149
+ // Tier 5: OR
150
+ if (words.length > 1) {
151
+ const orQuery = words.join(' OR ');
152
+ results = tryFts(db, select, table, order, orQuery, limit);
153
+ if (results.length > 0)
154
+ return { tier: 'or', results };
155
+ }
156
+ // Tier 6: LIKE fallback
157
+ const baseCols = ['name', 'pest_type'];
158
+ const likeConditions = words.map(() => `(${baseCols.map(c => `${c} LIKE ?`).join(' OR ')})`).join(' AND ');
159
+ const likeParams = words.flatMap(w => baseCols.map(() => `%${w}%`));
160
+ try {
161
+ const likeResults = db.all(`SELECT name as title, COALESCE(identification, '') as body, pest_type, COALESCE(crop_category, '') as crop_category, jurisdiction FROM pests WHERE ${likeConditions} LIMIT ?`, [...likeParams, limit]);
162
+ if (likeResults.length > 0)
163
+ return { tier: 'like', results: likeResults };
164
+ }
165
+ catch {
166
+ // LIKE fallback failed
167
+ }
168
+ return { tier: 'none', results: [] };
169
+ }
170
+ function tryFts(db, select, table, order, matchExpr, limit) {
171
+ try {
172
+ return db.all(`${select} WHERE ${table} MATCH ? ${order}`, [matchExpr, limit]);
173
+ }
174
+ catch {
175
+ return [];
176
+ }
177
+ }
178
+ function sanitizeFtsInput(query) {
179
+ return query
180
+ .replace(/["\u201C\u201D\u2018\u2019\u201A\u201E\u2039\u203A]/g, '"')
181
+ .replace(/[^a-zA-Z0-9\s*"_\u00C0-\u024F-]/g, ' ')
182
+ .replace(/\s+/g, ' ')
183
+ .trim();
184
+ }
185
+ function stemWord(word) {
186
+ return word
187
+ .replace(/(ung|heit|keit|lich|isch|ieren|tion|ment|ness|able|ible|ous|ive|ing|ers|ed|es|er|en|ly|s)$/i, '');
188
+ }
189
+ //# sourceMappingURL=db.js.map
package/dist/db.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=http-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":""}