@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/src/server.ts ADDED
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import {
6
+ ListToolsRequestSchema,
7
+ CallToolRequestSchema,
8
+ } from '@modelcontextprotocol/sdk/types.js';
9
+ import { z } from 'zod';
10
+ import { createDatabase } from './db.js';
11
+ import { handleAbout } from './tools/about.js';
12
+ import { handleListSources } from './tools/list-sources.js';
13
+ import { handleCheckFreshness } from './tools/check-freshness.js';
14
+ import { handleSearchPests } from './tools/search-pests.js';
15
+ import { handleGetPestDetails } from './tools/get-pest-details.js';
16
+ import { handleGetTreatments } from './tools/get-treatments.js';
17
+ import { handleGetIpmGuidance } from './tools/get-ipm-guidance.js';
18
+ import { handleSearchCropThreats } from './tools/search-crop-threats.js';
19
+ import { handleIdentifyFromSymptoms } from './tools/identify-from-symptoms.js';
20
+ import { handleGetApprovedProducts } from './tools/get-approved-products.js';
21
+
22
+ const SERVER_NAME = 'ch-pest-management-mcp';
23
+ const SERVER_VERSION = '0.1.0';
24
+
25
+ const TOOLS = [
26
+ {
27
+ name: 'about',
28
+ description: 'Get server metadata: name, version, coverage, data sources, and links.',
29
+ inputSchema: { type: 'object' as const, properties: {} },
30
+ },
31
+ {
32
+ name: 'list_sources',
33
+ description: 'List all data sources with authority, URL, license, and freshness info.',
34
+ inputSchema: { type: 'object' as const, properties: {} },
35
+ },
36
+ {
37
+ name: 'check_data_freshness',
38
+ description: 'Check when data was last ingested, staleness status, and how to trigger a refresh.',
39
+ inputSchema: { type: 'object' as const, properties: {} },
40
+ },
41
+ {
42
+ name: 'search_pests',
43
+ description: 'Search pests, diseases, and weeds affecting Swiss crops. Use for broad queries about Schaedlinge, Krankheiten, Unkraeuter.',
44
+ inputSchema: {
45
+ type: 'object' as const,
46
+ properties: {
47
+ query: { type: 'string', description: 'Free-text search query (German or English, e.g. "Blattlaeuse Weizen", "Septoria")' },
48
+ pest_type: { type: 'string', description: 'Filter by type: insect, disease, or weed' },
49
+ crop: { type: 'string', description: 'Filter by affected crop (e.g. Winterweizen, Kartoffeln, Reben)' },
50
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
51
+ limit: { type: 'number', description: 'Max results (default: 20, max: 50)' },
52
+ },
53
+ required: ['query'],
54
+ },
55
+ },
56
+ {
57
+ name: 'get_pest_details',
58
+ description: 'Get full profile for a pest: lifecycle, identification, crops affected, and treatments. Use pest ID from search_pests.',
59
+ inputSchema: {
60
+ type: 'object' as const,
61
+ properties: {
62
+ pest_id: { type: 'string', description: 'Pest ID (e.g. blattlaeuse, septoria, ackerfuchsschwanz)' },
63
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
64
+ },
65
+ required: ['pest_id'],
66
+ },
67
+ },
68
+ {
69
+ name: 'get_treatments',
70
+ description: 'Get approved chemical and non-chemical controls for a specific pest. Includes products, active substances, and OELN alternatives.',
71
+ inputSchema: {
72
+ type: 'object' as const,
73
+ properties: {
74
+ pest_id: { type: 'string', description: 'Pest ID (e.g. rapsglanzkaefer, krautfaeule)' },
75
+ approach: { type: 'string', description: 'Filter by approach: chemical, biological, cultural, or mechanical' },
76
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
77
+ },
78
+ required: ['pest_id'],
79
+ },
80
+ },
81
+ {
82
+ name: 'get_ipm_guidance',
83
+ description: 'Get IPM (Integrierter Pflanzenschutz) guidance for a crop: OELN Schadschwellen, monitoring methods, cultural controls, prognosis systems.',
84
+ inputSchema: {
85
+ type: 'object' as const,
86
+ properties: {
87
+ crop: { type: 'string', description: 'Crop name (e.g. Winterweizen, Kartoffeln, Reben, Apfel)' },
88
+ pest_id: { type: 'string', description: 'Optional: filter to specific pest' },
89
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
90
+ },
91
+ required: ['crop'],
92
+ },
93
+ },
94
+ {
95
+ name: 'search_crop_threats',
96
+ description: 'List all pests, diseases, and weeds that threaten a specific crop, with damage thresholds and monitoring advice.',
97
+ inputSchema: {
98
+ type: 'object' as const,
99
+ properties: {
100
+ crop: { type: 'string', description: 'Crop name (e.g. Winterweizen, Winterraps, Kartoffeln)' },
101
+ growth_stage: { type: 'string', description: 'Optional growth stage filter (e.g. Bestockung, Bluete, Knollenbildung)' },
102
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
103
+ },
104
+ required: ['crop'],
105
+ },
106
+ },
107
+ {
108
+ name: 'identify_from_symptoms',
109
+ description: 'Identify a pest, disease, or weed from symptom descriptions. Returns ranked differential diagnosis.',
110
+ inputSchema: {
111
+ type: 'object' as const,
112
+ properties: {
113
+ symptoms: { type: 'string', description: 'Symptom description (e.g. "gelbe Flecken auf Weizenblaettern", "Frass an Rapsblueten")' },
114
+ crop: { type: 'string', description: 'Affected crop for narrowing results' },
115
+ season: { type: 'string', description: 'Time of year (e.g. Fruehling, Sommer, Herbst)' },
116
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
117
+ },
118
+ required: ['symptoms'],
119
+ },
120
+ },
121
+ {
122
+ name: 'get_approved_products',
123
+ description: 'Search approved plant protection products (Pflanzenschutzmittel) from the BLW Verzeichnis. Filter by active substance, target pest, or crop.',
124
+ inputSchema: {
125
+ type: 'object' as const,
126
+ properties: {
127
+ active_substance: { type: 'string', description: 'Active substance name (e.g. Glyphosat, Mancozeb, Spinosad)' },
128
+ target_pest: { type: 'string', description: 'Target organism (e.g. Blattlaeuse, Unkraeuter, Pilzkrankheiten)' },
129
+ crop: { type: 'string', description: 'Target crop (e.g. Winterweizen, Kartoffeln, Reben)' },
130
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
131
+ },
132
+ },
133
+ },
134
+ ];
135
+
136
+ const SearchPestsArgsSchema = z.object({
137
+ query: z.string(),
138
+ pest_type: z.string().optional(),
139
+ crop: z.string().optional(),
140
+ jurisdiction: z.string().optional(),
141
+ limit: z.number().optional(),
142
+ });
143
+
144
+ const PestDetailsArgsSchema = z.object({
145
+ pest_id: z.string(),
146
+ jurisdiction: z.string().optional(),
147
+ });
148
+
149
+ const TreatmentsArgsSchema = z.object({
150
+ pest_id: z.string(),
151
+ approach: z.string().optional(),
152
+ jurisdiction: z.string().optional(),
153
+ });
154
+
155
+ const IpmGuidanceArgsSchema = z.object({
156
+ crop: z.string(),
157
+ pest_id: z.string().optional(),
158
+ jurisdiction: z.string().optional(),
159
+ });
160
+
161
+ const CropThreatsArgsSchema = z.object({
162
+ crop: z.string(),
163
+ growth_stage: z.string().optional(),
164
+ jurisdiction: z.string().optional(),
165
+ });
166
+
167
+ const IdentifyArgsSchema = z.object({
168
+ symptoms: z.string(),
169
+ crop: z.string().optional(),
170
+ season: z.string().optional(),
171
+ jurisdiction: z.string().optional(),
172
+ });
173
+
174
+ const ApprovedProductsArgsSchema = z.object({
175
+ active_substance: z.string().optional(),
176
+ target_pest: z.string().optional(),
177
+ crop: z.string().optional(),
178
+ jurisdiction: z.string().optional(),
179
+ });
180
+
181
+ function textResult(data: unknown) {
182
+ return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };
183
+ }
184
+
185
+ function errorResult(message: string) {
186
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ error: message }) }], isError: true };
187
+ }
188
+
189
+ const db = createDatabase();
190
+
191
+ const server = new Server(
192
+ { name: SERVER_NAME, version: SERVER_VERSION },
193
+ { capabilities: { tools: {} } }
194
+ );
195
+
196
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
197
+
198
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
199
+ const { name, arguments: args = {} } = request.params;
200
+
201
+ try {
202
+ switch (name) {
203
+ case 'about':
204
+ return textResult(handleAbout());
205
+ case 'list_sources':
206
+ return textResult(handleListSources(db));
207
+ case 'check_data_freshness':
208
+ return textResult(handleCheckFreshness(db));
209
+ case 'search_pests':
210
+ return textResult(handleSearchPests(db, SearchPestsArgsSchema.parse(args)));
211
+ case 'get_pest_details':
212
+ return textResult(handleGetPestDetails(db, PestDetailsArgsSchema.parse(args)));
213
+ case 'get_treatments':
214
+ return textResult(handleGetTreatments(db, TreatmentsArgsSchema.parse(args)));
215
+ case 'get_ipm_guidance':
216
+ return textResult(handleGetIpmGuidance(db, IpmGuidanceArgsSchema.parse(args)));
217
+ case 'search_crop_threats':
218
+ return textResult(handleSearchCropThreats(db, CropThreatsArgsSchema.parse(args)));
219
+ case 'identify_from_symptoms':
220
+ return textResult(handleIdentifyFromSymptoms(db, IdentifyArgsSchema.parse(args)));
221
+ case 'get_approved_products':
222
+ return textResult(handleGetApprovedProducts(db, ApprovedProductsArgsSchema.parse(args)));
223
+ default:
224
+ return errorResult(`Unknown tool: ${name}`);
225
+ }
226
+ } catch (err) {
227
+ return errorResult(err instanceof Error ? err.message : String(err));
228
+ }
229
+ });
230
+
231
+ async function main(): Promise<void> {
232
+ const transport = new StdioServerTransport();
233
+ await server.connect(transport);
234
+ }
235
+
236
+ main().catch((err) => {
237
+ process.stderr.write(`Fatal error: ${err.message}\n`);
238
+ process.exit(1);
239
+ });
@@ -0,0 +1,28 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import { SUPPORTED_JURISDICTIONS } from '../jurisdiction.js';
3
+
4
+ export function handleAbout() {
5
+ return {
6
+ name: 'Switzerland Pest Management MCP',
7
+ description:
8
+ 'Swiss crop protection and pest management data based on the BLW Pflanzenschutzmittelverzeichnis, ' +
9
+ 'Agroscope Pflanzenschutzempfehlungen, OELN-Schadschwellenprinzip, and the Aktionsplan Pflanzenschutzmittel. ' +
10
+ 'Covers pests (insects, diseases, weeds), approved products, IPM guidance, damage thresholds, ' +
11
+ 'and prognosis systems (PhytoPRE, SOPRA, FusaProg) for Swiss agriculture.',
12
+ version: '0.1.0',
13
+ jurisdiction: [...SUPPORTED_JURISDICTIONS],
14
+ data_sources: [
15
+ 'BLW Pflanzenschutzmittelverzeichnis (psm.admin.ch)',
16
+ 'Agroscope Pflanzenschutzempfehlungen',
17
+ 'AGRIDEA OELN-Checklisten (Feldbau, Rebbau, Obstbau)',
18
+ 'Aktionsplan Pflanzenschutzmittel',
19
+ ],
20
+ tools_count: 10,
21
+ links: {
22
+ homepage: 'https://ansvar.eu/open-agriculture',
23
+ repository: 'https://github.com/ansvar-systems/ch-pest-management-mcp',
24
+ mcp_network: 'https://ansvar.ai/mcp',
25
+ },
26
+ _meta: buildMeta(),
27
+ };
28
+ }
@@ -0,0 +1,42 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import type { Database } from '../db.js';
3
+
4
+ interface FreshnessResult {
5
+ status: 'fresh' | 'stale' | 'unknown';
6
+ last_ingest: string | null;
7
+ build_date: string | null;
8
+ schema_version: string | null;
9
+ days_since_ingest: number | null;
10
+ staleness_threshold_days: number;
11
+ refresh_command: string;
12
+ _meta: ReturnType<typeof buildMeta>;
13
+ }
14
+
15
+ const STALENESS_THRESHOLD_DAYS = 90;
16
+
17
+ export function handleCheckFreshness(db: Database): FreshnessResult {
18
+ const lastIngest = db.get<{ value: string }>('SELECT value FROM db_metadata WHERE key = ?', ['last_ingest']);
19
+ const buildDate = db.get<{ value: string }>('SELECT value FROM db_metadata WHERE key = ?', ['build_date']);
20
+ const schemaVersion = db.get<{ value: string }>('SELECT value FROM db_metadata WHERE key = ?', ['schema_version']);
21
+
22
+ let status: 'fresh' | 'stale' | 'unknown' = 'unknown';
23
+ let daysSinceIngest: number | null = null;
24
+
25
+ if (lastIngest?.value) {
26
+ const ingestDate = new Date(lastIngest.value);
27
+ const now = new Date();
28
+ daysSinceIngest = Math.floor((now.getTime() - ingestDate.getTime()) / (1000 * 60 * 60 * 24));
29
+ status = daysSinceIngest <= STALENESS_THRESHOLD_DAYS ? 'fresh' : 'stale';
30
+ }
31
+
32
+ return {
33
+ status,
34
+ last_ingest: lastIngest?.value ?? null,
35
+ build_date: buildDate?.value ?? null,
36
+ schema_version: schemaVersion?.value ?? null,
37
+ days_since_ingest: daysSinceIngest,
38
+ staleness_threshold_days: STALENESS_THRESHOLD_DAYS,
39
+ refresh_command: 'gh workflow run ingest.yml -R ansvar-systems/ch-pest-management-mcp',
40
+ _meta: buildMeta(),
41
+ };
42
+ }
@@ -0,0 +1,68 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import { validateJurisdiction } from '../jurisdiction.js';
3
+ import type { Database } from '../db.js';
4
+
5
+ interface GetApprovedProductsArgs {
6
+ active_substance?: string;
7
+ target_pest?: string;
8
+ crop?: string;
9
+ jurisdiction?: string;
10
+ }
11
+
12
+ export function handleGetApprovedProducts(db: Database, args: GetApprovedProductsArgs) {
13
+ const jv = validateJurisdiction(args.jurisdiction);
14
+ if (!jv.valid) return jv.error;
15
+
16
+ let sql = 'SELECT * FROM approved_products WHERE jurisdiction = ?';
17
+ const params: unknown[] = [jv.jurisdiction];
18
+
19
+ if (args.active_substance) {
20
+ sql += ' AND LOWER(active_substance) LIKE LOWER(?)';
21
+ params.push(`%${args.active_substance}%`);
22
+ }
23
+
24
+ if (args.target_pest) {
25
+ sql += ' AND LOWER(target_organisms) LIKE LOWER(?)';
26
+ params.push(`%${args.target_pest}%`);
27
+ }
28
+
29
+ if (args.crop) {
30
+ sql += ' AND LOWER(crops) LIKE LOWER(?)';
31
+ params.push(`%${args.crop}%`);
32
+ }
33
+
34
+ sql += ' ORDER BY product_name LIMIT 50';
35
+
36
+ const products = db.all<{
37
+ id: number; w_number: string; product_name: string; active_substance: string;
38
+ product_type: string; crops: string; target_organisms: string;
39
+ auflagen: string; wartefrist: string; dosage: string;
40
+ application_method: string; spe3_buffer: string; aktionsplan_status: string;
41
+ }>(sql, params);
42
+
43
+ return {
44
+ filters: {
45
+ active_substance: args.active_substance ?? null,
46
+ target_pest: args.target_pest ?? null,
47
+ crop: args.crop ?? null,
48
+ },
49
+ jurisdiction: jv.jurisdiction,
50
+ products_count: products.length,
51
+ products: products.map(p => ({
52
+ w_number: p.w_number,
53
+ product_name: p.product_name,
54
+ active_substance: p.active_substance,
55
+ product_type: p.product_type,
56
+ crops: p.crops,
57
+ target_organisms: p.target_organisms,
58
+ auflagen: p.auflagen,
59
+ wartefrist: p.wartefrist,
60
+ dosage: p.dosage,
61
+ application_method: p.application_method,
62
+ spe3_buffer: p.spe3_buffer,
63
+ aktionsplan_status: p.aktionsplan_status,
64
+ })),
65
+ warning: 'Immer aktuelle Zulassung auf psm.admin.ch pruefen. Fachbewilligung PSM erforderlich.',
66
+ _meta: buildMeta(),
67
+ };
68
+ }
@@ -0,0 +1,56 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import { validateJurisdiction } from '../jurisdiction.js';
3
+ import type { Database } from '../db.js';
4
+
5
+ interface GetIpmGuidanceArgs {
6
+ crop: string;
7
+ pest_id?: string;
8
+ jurisdiction?: string;
9
+ }
10
+
11
+ export function handleGetIpmGuidance(db: Database, args: GetIpmGuidanceArgs) {
12
+ const jv = validateJurisdiction(args.jurisdiction);
13
+ if (!jv.valid) return jv.error;
14
+
15
+ let sql = 'SELECT g.*, p.name as pest_name, p.pest_type FROM ipm_guidance g LEFT JOIN pests p ON g.pest_id = p.id WHERE LOWER(g.crop) = LOWER(?) AND g.jurisdiction = ?';
16
+ const params: unknown[] = [args.crop, jv.jurisdiction];
17
+
18
+ if (args.pest_id) {
19
+ sql += ' AND g.pest_id = ?';
20
+ params.push(args.pest_id);
21
+ }
22
+
23
+ const guidance = db.all<{
24
+ id: number; crop: string; crop_category: string; pest_id: string;
25
+ pest_name: string; pest_type: string; threshold: string;
26
+ monitoring_method: string; cultural_controls: string;
27
+ prognose_system: string; oeln_requirements: string; notes: string;
28
+ }>(sql, params);
29
+
30
+ if (guidance.length === 0) {
31
+ return {
32
+ error: 'not_found',
33
+ message: `No IPM guidance found for crop '${args.crop}'${args.pest_id ? ` and pest '${args.pest_id}'` : ''}. Try search_pests or search_crop_threats to find valid identifiers.`,
34
+ };
35
+ }
36
+
37
+ return {
38
+ crop: args.crop,
39
+ pest_filter: args.pest_id ?? null,
40
+ jurisdiction: jv.jurisdiction,
41
+ guidance_count: guidance.length,
42
+ guidance: guidance.map(g => ({
43
+ pest_id: g.pest_id,
44
+ pest_name: g.pest_name,
45
+ pest_type: g.pest_type,
46
+ crop_category: g.crop_category,
47
+ threshold: g.threshold,
48
+ monitoring_method: g.monitoring_method,
49
+ cultural_controls: g.cultural_controls,
50
+ prognose_system: g.prognose_system,
51
+ oeln_requirements: g.oeln_requirements,
52
+ notes: g.notes,
53
+ })),
54
+ _meta: buildMeta(),
55
+ };
56
+ }
@@ -0,0 +1,44 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import { validateJurisdiction } from '../jurisdiction.js';
3
+ import type { Database } from '../db.js';
4
+
5
+ interface PestDetailsArgs {
6
+ pest_id: string;
7
+ jurisdiction?: string;
8
+ }
9
+
10
+ export function handleGetPestDetails(db: Database, args: PestDetailsArgs) {
11
+ const jv = validateJurisdiction(args.jurisdiction);
12
+ if (!jv.valid) return jv.error;
13
+
14
+ const pest = db.get<{
15
+ id: string; name: string; pest_type: string; scientific_name: string;
16
+ crops_affected: string; crop_category: string; lifecycle: string;
17
+ identification: string; damage_description: string; language: string;
18
+ jurisdiction: string;
19
+ }>(
20
+ 'SELECT * FROM pests WHERE (id = ? OR LOWER(name) = LOWER(?)) AND jurisdiction = ?',
21
+ [args.pest_id, args.pest_id, jv.jurisdiction]
22
+ );
23
+
24
+ if (!pest) {
25
+ return { error: 'not_found', message: `Pest '${args.pest_id}' not found. Use search_pests to find pest IDs.` };
26
+ }
27
+
28
+ const treatments = db.all<{
29
+ approach: string; product_name: string; active_substance: string;
30
+ w_number: string; dosage: string; waiting_period: string;
31
+ timing: string; restrictions: string; pufferstreifen: string; notes: string;
32
+ }>(
33
+ 'SELECT approach, product_name, active_substance, w_number, dosage, waiting_period, timing, restrictions, pufferstreifen, notes FROM treatments WHERE pest_id = ? AND jurisdiction = ?',
34
+ [pest.id, jv.jurisdiction]
35
+ );
36
+
37
+ return {
38
+ ...pest,
39
+ crops_affected: pest.crops_affected ? JSON.parse(pest.crops_affected) : [],
40
+ treatments_count: treatments.length,
41
+ treatments,
42
+ _meta: buildMeta(),
43
+ };
44
+ }
@@ -0,0 +1,61 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import { validateJurisdiction } from '../jurisdiction.js';
3
+ import type { Database } from '../db.js';
4
+
5
+ interface GetTreatmentsArgs {
6
+ pest_id: string;
7
+ approach?: string;
8
+ jurisdiction?: string;
9
+ }
10
+
11
+ export function handleGetTreatments(db: Database, args: GetTreatmentsArgs) {
12
+ const jv = validateJurisdiction(args.jurisdiction);
13
+ if (!jv.valid) return jv.error;
14
+
15
+ // Verify pest exists
16
+ const pest = db.get<{ id: string; name: string; pest_type: string }>(
17
+ 'SELECT id, name, pest_type FROM pests WHERE (id = ? OR LOWER(name) = LOWER(?)) AND jurisdiction = ?',
18
+ [args.pest_id, args.pest_id, jv.jurisdiction]
19
+ );
20
+
21
+ if (!pest) {
22
+ return { error: 'not_found', message: `Pest '${args.pest_id}' not found. Use search_pests to find pest IDs.` };
23
+ }
24
+
25
+ let sql = 'SELECT * FROM treatments WHERE pest_id = ? AND jurisdiction = ?';
26
+ const params: unknown[] = [pest.id, jv.jurisdiction];
27
+
28
+ if (args.approach) {
29
+ sql += ' AND approach = ?';
30
+ params.push(args.approach.toLowerCase());
31
+ }
32
+
33
+ const treatments = db.all<{
34
+ id: number; pest_id: string; approach: string; product_name: string;
35
+ active_substance: string; w_number: string; dosage: string;
36
+ waiting_period: string; timing: string; restrictions: string;
37
+ pufferstreifen: string; notes: string;
38
+ }>(sql, params);
39
+
40
+ return {
41
+ pest_id: pest.id,
42
+ pest_name: pest.name,
43
+ pest_type: pest.pest_type,
44
+ approach_filter: args.approach ?? 'all',
45
+ jurisdiction: jv.jurisdiction,
46
+ treatments_count: treatments.length,
47
+ treatments: treatments.map(t => ({
48
+ approach: t.approach,
49
+ product_name: t.product_name,
50
+ active_substance: t.active_substance,
51
+ w_number: t.w_number,
52
+ dosage: t.dosage,
53
+ waiting_period: t.waiting_period,
54
+ timing: t.timing,
55
+ restrictions: t.restrictions,
56
+ pufferstreifen: t.pufferstreifen,
57
+ notes: t.notes,
58
+ })),
59
+ _meta: buildMeta(),
60
+ };
61
+ }
@@ -0,0 +1,64 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import { validateJurisdiction } from '../jurisdiction.js';
3
+ import { ftsSearch, type Database } from '../db.js';
4
+
5
+ interface IdentifyArgs {
6
+ symptoms: string;
7
+ crop?: string;
8
+ season?: string;
9
+ jurisdiction?: string;
10
+ }
11
+
12
+ export function handleIdentifyFromSymptoms(db: Database, args: IdentifyArgs) {
13
+ const jv = validateJurisdiction(args.jurisdiction);
14
+ if (!jv.valid) return jv.error;
15
+
16
+ // Use FTS to search symptoms + identification descriptions
17
+ let results = ftsSearch(db, args.symptoms, 30);
18
+
19
+ // Filter by crop if provided
20
+ if (args.crop) {
21
+ const cropLower = args.crop.toLowerCase();
22
+ results = results.filter(r =>
23
+ r.body.toLowerCase().includes(cropLower) ||
24
+ r.crop_category?.toLowerCase().includes(cropLower)
25
+ );
26
+ }
27
+
28
+ // Get full pest details for top matches
29
+ const pestIds = results.slice(0, 10).map(r => {
30
+ // Extract pest ID from the title (format: "PestName — Type")
31
+ const pest = db.get<{ id: string; name: string; pest_type: string; scientific_name: string; identification: string; damage_description: string; crops_affected: string }>(
32
+ `SELECT id, name, pest_type, scientific_name, identification, damage_description, crops_affected FROM pests WHERE LOWER(name) = LOWER(?) OR LOWER(identification) LIKE LOWER(?) LIMIT 1`,
33
+ [r.title.split(' — ')[0], `%${r.title.split(' — ')[0]}%`]
34
+ );
35
+ return pest;
36
+ }).filter(Boolean);
37
+
38
+ // De-duplicate
39
+ const seen = new Set<string>();
40
+ const matches = pestIds.filter(p => {
41
+ if (!p || seen.has(p.id)) return false;
42
+ seen.add(p.id);
43
+ return true;
44
+ });
45
+
46
+ return {
47
+ symptoms: args.symptoms,
48
+ crop_filter: args.crop ?? null,
49
+ season: args.season ?? null,
50
+ jurisdiction: jv.jurisdiction,
51
+ matches_count: matches.length,
52
+ possible_matches: matches.map(p => ({
53
+ pest_id: p!.id,
54
+ name: p!.name,
55
+ pest_type: p!.pest_type,
56
+ scientific_name: p!.scientific_name,
57
+ identification: p!.identification,
58
+ damage_description: p!.damage_description,
59
+ crops_affected: p!.crops_affected ? JSON.parse(p!.crops_affected) : [],
60
+ })),
61
+ recommendation: 'Bei Unsicherheit kantonale Pflanzenschutzfachstelle oder AGRIDEA kontaktieren. Bestimmung durch Fachperson empfohlen.',
62
+ _meta: buildMeta(),
63
+ };
64
+ }
@@ -0,0 +1,65 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import type { Database } from '../db.js';
3
+
4
+ interface Source {
5
+ name: string;
6
+ authority: string;
7
+ official_url: string;
8
+ retrieval_method: string;
9
+ update_frequency: string;
10
+ license: string;
11
+ coverage: string;
12
+ last_retrieved?: string;
13
+ }
14
+
15
+ export function handleListSources(db: Database): { sources: Source[]; _meta: ReturnType<typeof buildMeta> } {
16
+ const lastIngest = db.get<{ value: string }>('SELECT value FROM db_metadata WHERE key = ?', ['last_ingest']);
17
+
18
+ const sources: Source[] = [
19
+ {
20
+ name: 'BLW Pflanzenschutzmittelverzeichnis',
21
+ authority: 'Bundesamt fuer Landwirtschaft (BLW)',
22
+ official_url: 'https://www.psm.admin.ch/de/produkte',
23
+ retrieval_method: 'STRUCTURED_EXTRACT',
24
+ update_frequency: 'continuous (products added/removed as authorisations change)',
25
+ license: 'Swiss Federal Administration — free reuse',
26
+ coverage: 'Approved plant protection products: W-number, active substance, crops, auflagen, wartefrist',
27
+ last_retrieved: lastIngest?.value,
28
+ },
29
+ {
30
+ name: 'Agroscope Pflanzenschutzempfehlungen / Schadschwellen',
31
+ authority: 'Agroscope',
32
+ official_url: 'https://www.agroscope.admin.ch/agroscope/de/home/themen/pflanzenbau/pflanzenschutz.html',
33
+ retrieval_method: 'PDF_EXTRACT',
34
+ update_frequency: 'annual (updated per growing season)',
35
+ license: 'Swiss Federal Administration — free reuse',
36
+ coverage: 'Damage thresholds, monitoring methods, prognosis systems (PhytoPRE, SOPRA, FusaProg)',
37
+ last_retrieved: lastIngest?.value,
38
+ },
39
+ {
40
+ name: 'AGRIDEA OELN-Pflanzenschutz-Checklisten',
41
+ authority: 'AGRIDEA',
42
+ official_url: 'https://www.agridea.ch/de/themen/pflanzenbau/pflanzenschutz/',
43
+ retrieval_method: 'PDF_EXTRACT',
44
+ update_frequency: 'annual',
45
+ license: 'Public advisory material',
46
+ coverage: 'OELN crop protection requirements, approved products lists (Feldbau, Rebbau, Obstbau)',
47
+ last_retrieved: lastIngest?.value,
48
+ },
49
+ {
50
+ name: 'Aktionsplan Pflanzenschutzmittel',
51
+ authority: 'Bundesrat / BLW',
52
+ official_url: 'https://www.blw.admin.ch/blw/de/home/nachhaltige-produktion/pflanzenschutz/aktionsplan.html',
53
+ retrieval_method: 'PDF_EXTRACT',
54
+ update_frequency: 'periodic (adopted 2017, targets to 2027)',
55
+ license: 'Swiss Federal Administration — free reuse',
56
+ coverage: '50% risk reduction targets, indicators, IPM strategy, buffer zone requirements',
57
+ last_retrieved: lastIngest?.value,
58
+ },
59
+ ];
60
+
61
+ return {
62
+ sources,
63
+ _meta: buildMeta(),
64
+ };
65
+ }