@ansvar/ch-environmental-compliance-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/.github/workflows/check-freshness.yml +49 -0
- package/.github/workflows/ci.yml +21 -0
- package/.github/workflows/codeql.yml +25 -0
- package/.github/workflows/ghcr-build.yml +45 -0
- package/.github/workflows/gitleaks.yml +18 -0
- package/.github/workflows/ingest.yml +59 -0
- package/.github/workflows/publish.yml +24 -0
- package/CHANGELOG.md +15 -0
- package/CODEOWNERS +1 -0
- package/COVERAGE.md +50 -0
- package/DISCLAIMER.md +48 -0
- package/Dockerfile +26 -0
- package/LICENSE +17 -0
- package/PRIVACY.md +23 -0
- package/README.md +116 -0
- package/SECURITY.md +25 -0
- package/TOOLS.md +142 -0
- package/data/coverage.json +24 -0
- package/data/database.db +0 -0
- package/data/sources.yml +36 -0
- package/dist/db.d.ts +25 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +197 -0
- package/dist/db.js.map +1 -0
- package/dist/http-server.d.ts +2 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +274 -0
- package/dist/http-server.js.map +1 -0
- package/dist/jurisdiction.d.ts +18 -0
- package/dist/jurisdiction.d.ts.map +1 -0
- package/dist/jurisdiction.js +16 -0
- package/dist/jurisdiction.js.map +1 -0
- package/dist/metadata.d.ts +10 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +22 -0
- package/dist/metadata.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +220 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/about.d.ts +15 -0
- package/dist/tools/about.d.ts.map +1 -0
- package/dist/tools/about.js +27 -0
- package/dist/tools/about.js.map +1 -0
- package/dist/tools/check-environmental-compliance.d.ts +30 -0
- package/dist/tools/check-environmental-compliance.d.ts.map +1 -0
- package/dist/tools/check-environmental-compliance.js +103 -0
- package/dist/tools/check-environmental-compliance.js.map +1 -0
- package/dist/tools/check-freshness.d.ts +15 -0
- package/dist/tools/check-freshness.d.ts.map +1 -0
- package/dist/tools/check-freshness.js +26 -0
- package/dist/tools/check-freshness.js.map +1 -0
- package/dist/tools/get-ammonia-rules.d.ts +24 -0
- package/dist/tools/get-ammonia-rules.d.ts.map +1 -0
- package/dist/tools/get-ammonia-rules.js +31 -0
- package/dist/tools/get-ammonia-rules.js.map +1 -0
- package/dist/tools/get-bff-requirements.d.ts +26 -0
- package/dist/tools/get-bff-requirements.d.ts.map +1 -0
- package/dist/tools/get-bff-requirements.js +36 -0
- package/dist/tools/get-bff-requirements.js.map +1 -0
- package/dist/tools/get-buffer-zone-rules.d.ts +23 -0
- package/dist/tools/get-buffer-zone-rules.d.ts.map +1 -0
- package/dist/tools/get-buffer-zone-rules.js +30 -0
- package/dist/tools/get-buffer-zone-rules.js.map +1 -0
- package/dist/tools/get-eip-requirements.d.ts +31 -0
- package/dist/tools/get-eip-requirements.d.ts.map +1 -0
- package/dist/tools/get-eip-requirements.js +40 -0
- package/dist/tools/get-eip-requirements.js.map +1 -0
- package/dist/tools/get-nutrient-loss-limits.d.ts +24 -0
- package/dist/tools/get-nutrient-loss-limits.d.ts.map +1 -0
- package/dist/tools/get-nutrient-loss-limits.js +31 -0
- package/dist/tools/get-nutrient-loss-limits.js.map +1 -0
- package/dist/tools/get-water-protection-zones.d.ts +32 -0
- package/dist/tools/get-water-protection-zones.d.ts.map +1 -0
- package/dist/tools/get-water-protection-zones.js +36 -0
- package/dist/tools/get-water-protection-zones.js.map +1 -0
- package/dist/tools/list-sources.d.ts +18 -0
- package/dist/tools/list-sources.d.ts.map +1 -0
- package/dist/tools/list-sources.js +61 -0
- package/dist/tools/list-sources.js.map +1 -0
- package/dist/tools/search-environmental-rules.d.ts +25 -0
- package/dist/tools/search-environmental-rules.d.ts.map +1 -0
- package/dist/tools/search-environmental-rules.js +26 -0
- package/dist/tools/search-environmental-rules.js.map +1 -0
- package/docker-compose.yml +12 -0
- package/eslint.config.js +26 -0
- package/package.json +54 -0
- package/scripts/ingest.ts +911 -0
- package/server.json +16 -0
- package/src/db.ts +238 -0
- package/src/http-server.ts +307 -0
- package/src/jurisdiction.ts +30 -0
- package/src/metadata.ts +32 -0
- package/src/server.ts +244 -0
- package/src/tools/about.ts +28 -0
- package/src/tools/check-environmental-compliance.ts +143 -0
- package/src/tools/check-freshness.ts +42 -0
- package/src/tools/get-ammonia-rules.ts +44 -0
- package/src/tools/get-bff-requirements.ts +52 -0
- package/src/tools/get-buffer-zone-rules.ts +43 -0
- package/src/tools/get-eip-requirements.ts +57 -0
- package/src/tools/get-nutrient-loss-limits.ts +44 -0
- package/src/tools/get-water-protection-zones.ts +50 -0
- package/src/tools/list-sources.ts +75 -0
- package/src/tools/search-environmental-rules.ts +35 -0
- package/tests/db.test.ts +80 -0
- package/tests/helpers/seed-db.ts +173 -0
- package/tests/jurisdiction.test.ts +35 -0
- package/tests/tools/about.test.ts +26 -0
- package/tests/tools/check-freshness.test.ts +50 -0
- package/tests/tools/list-sources.test.ts +61 -0
- package/tests/tools/search-environmental-rules.test.ts +47 -0
- package/tsconfig.json +19 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
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 { handleSearchEnvironmentalRules } from './tools/search-environmental-rules.js';
|
|
15
|
+
import { handleGetWaterProtectionZones } from './tools/get-water-protection-zones.js';
|
|
16
|
+
import { handleGetBufferZoneRules } from './tools/get-buffer-zone-rules.js';
|
|
17
|
+
import { handleGetAmmoniaRules } from './tools/get-ammonia-rules.js';
|
|
18
|
+
import { handleGetBffRequirements } from './tools/get-bff-requirements.js';
|
|
19
|
+
import { handleGetNutrientLossLimits } from './tools/get-nutrient-loss-limits.js';
|
|
20
|
+
import { handleGetEipRequirements } from './tools/get-eip-requirements.js';
|
|
21
|
+
import { handleCheckEnvironmentalCompliance } from './tools/check-environmental-compliance.js';
|
|
22
|
+
|
|
23
|
+
const SERVER_NAME = 'ch-environmental-compliance-mcp';
|
|
24
|
+
const SERVER_VERSION = '0.1.0';
|
|
25
|
+
|
|
26
|
+
const TOOLS = [
|
|
27
|
+
{
|
|
28
|
+
name: 'about',
|
|
29
|
+
description: 'Get server metadata: name, version, coverage, data sources, and links.',
|
|
30
|
+
inputSchema: { type: 'object' as const, properties: {} },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'list_sources',
|
|
34
|
+
description: 'List all data sources with authority, URL, license, and freshness info.',
|
|
35
|
+
inputSchema: { type: 'object' as const, properties: {} },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'check_data_freshness',
|
|
39
|
+
description: 'Check when data was last ingested, staleness status, and how to trigger a refresh.',
|
|
40
|
+
inputSchema: { type: 'object' as const, properties: {} },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'search_environmental_rules',
|
|
44
|
+
description: 'Search Swiss environmental compliance rules across all topics: Gewaesserschutz, Ammoniak, BFF, Pufferstreifen, VBBo, UVP. Use for broad queries.',
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: 'object' as const,
|
|
47
|
+
properties: {
|
|
48
|
+
query: { type: 'string', description: 'Free-text search query (German or English)' },
|
|
49
|
+
topic: { type: 'string', description: 'Filter by topic (e.g. Gewaesserschutz, Ammoniak, BFF, UVP, VBBo)' },
|
|
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_water_protection_zones',
|
|
58
|
+
description: 'Get Grundwasserschutzzonen (S1, S2, S3, Sm, Zu) with restrictions and legal basis. Based on GSchG/GSchV.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object' as const,
|
|
61
|
+
properties: {
|
|
62
|
+
zone_type: { type: 'string', description: 'Zone type: S1, S2, S3, Sm, Zu (omit for all zones)' },
|
|
63
|
+
jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'get_buffer_zone_rules',
|
|
69
|
+
description: 'Get Pufferstreifen (buffer zone) distances and requirements along water bodies, hedges, and field boundaries. Based on OELN/ChemRRV.',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object' as const,
|
|
72
|
+
properties: {
|
|
73
|
+
zone_type: { type: 'string', description: 'Buffer type filter (e.g. Gewaesser, Hecke, Nachbar, SPe)' },
|
|
74
|
+
jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'get_ammonia_rules',
|
|
80
|
+
description: 'Get Ammoniakemissionen rules: emission factors by technique, Schleppschlauch-Pflicht, Agrammon model parameters. Based on LRV.',
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: 'object' as const,
|
|
83
|
+
properties: {
|
|
84
|
+
technique: { type: 'string', description: 'Application technique filter (e.g. Schleppschlauch, Prallteller, Injektion)' },
|
|
85
|
+
jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'get_bff_requirements',
|
|
91
|
+
description: 'Get Biodiversitaetsfoerderflaechen (BFF) types, QI/QII payment rates, minimum area requirements, and botanical criteria. Based on DZV.',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object' as const,
|
|
94
|
+
properties: {
|
|
95
|
+
bff_type: { type: 'string', description: 'BFF type filter (e.g. extensiv-wiese, buntbrache, hecke)' },
|
|
96
|
+
quality_level: { type: 'string', description: 'Quality level: QI or QII' },
|
|
97
|
+
jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'get_nutrient_loss_limits',
|
|
103
|
+
description: 'Get Pa.Iv. 19.475 nutrient loss reduction targets (Absenkpfad) for N and P through 2030. Yearly milestones and Suisse-Bilanz tolerance changes.',
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: 'object' as const,
|
|
106
|
+
properties: {
|
|
107
|
+
nutrient: { type: 'string', description: 'Nutrient filter: N (Stickstoff) or P (Phosphor)' },
|
|
108
|
+
jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'get_eip_requirements',
|
|
114
|
+
description: 'Get UVP (Umweltvertraeglichkeitspruefung) thresholds for agricultural buildings and VBBo Richtwerte for soil contamination.',
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: 'object' as const,
|
|
117
|
+
properties: {
|
|
118
|
+
project_type: { type: 'string', description: 'Project type filter (e.g. Stallbau, Biogasanlage, Schweinehaltung)' },
|
|
119
|
+
jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'check_environmental_compliance',
|
|
125
|
+
description: 'Check which environmental rules apply to a given agricultural operation: water protection, buffer zones, ammonia, BFF, UVP, nutrient loss limits.',
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: 'object' as const,
|
|
128
|
+
properties: {
|
|
129
|
+
facility_type: { type: 'string', description: 'Type of operation (e.g. Milchwirtschaft, Schweinehaltung, Ackerbau, Gemuese)' },
|
|
130
|
+
animal_count: { type: 'number', description: 'Number of animals (GVE) — used for ammonia and UVP thresholds' },
|
|
131
|
+
area_ha: { type: 'number', description: 'Farm area in hectares — used for BFF minimum calculations' },
|
|
132
|
+
jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
|
|
133
|
+
},
|
|
134
|
+
required: ['facility_type'],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const SearchArgsSchema = z.object({
|
|
140
|
+
query: z.string(),
|
|
141
|
+
topic: z.string().optional(),
|
|
142
|
+
jurisdiction: z.string().optional(),
|
|
143
|
+
limit: z.number().optional(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const WaterProtectionArgsSchema = z.object({
|
|
147
|
+
zone_type: z.string().optional(),
|
|
148
|
+
jurisdiction: z.string().optional(),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const BufferZoneArgsSchema = z.object({
|
|
152
|
+
zone_type: z.string().optional(),
|
|
153
|
+
jurisdiction: z.string().optional(),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const AmmoniaArgsSchema = z.object({
|
|
157
|
+
technique: z.string().optional(),
|
|
158
|
+
jurisdiction: z.string().optional(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const BffArgsSchema = z.object({
|
|
162
|
+
bff_type: z.string().optional(),
|
|
163
|
+
quality_level: z.string().optional(),
|
|
164
|
+
jurisdiction: z.string().optional(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const NutrientLossArgsSchema = z.object({
|
|
168
|
+
nutrient: z.string().optional(),
|
|
169
|
+
jurisdiction: z.string().optional(),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const EipArgsSchema = z.object({
|
|
173
|
+
project_type: z.string().optional(),
|
|
174
|
+
jurisdiction: z.string().optional(),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const ComplianceArgsSchema = z.object({
|
|
178
|
+
facility_type: z.string(),
|
|
179
|
+
animal_count: z.number().optional(),
|
|
180
|
+
area_ha: z.number().optional(),
|
|
181
|
+
jurisdiction: z.string().optional(),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
function textResult(data: unknown) {
|
|
185
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function errorResult(message: string) {
|
|
189
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: message }) }], isError: true };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const db = createDatabase();
|
|
193
|
+
|
|
194
|
+
const server = new Server(
|
|
195
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
196
|
+
{ capabilities: { tools: {} } }
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
200
|
+
|
|
201
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
202
|
+
const { name, arguments: args = {} } = request.params;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
switch (name) {
|
|
206
|
+
case 'about':
|
|
207
|
+
return textResult(handleAbout());
|
|
208
|
+
case 'list_sources':
|
|
209
|
+
return textResult(handleListSources(db));
|
|
210
|
+
case 'check_data_freshness':
|
|
211
|
+
return textResult(handleCheckFreshness(db));
|
|
212
|
+
case 'search_environmental_rules':
|
|
213
|
+
return textResult(handleSearchEnvironmentalRules(db, SearchArgsSchema.parse(args)));
|
|
214
|
+
case 'get_water_protection_zones':
|
|
215
|
+
return textResult(handleGetWaterProtectionZones(db, WaterProtectionArgsSchema.parse(args)));
|
|
216
|
+
case 'get_buffer_zone_rules':
|
|
217
|
+
return textResult(handleGetBufferZoneRules(db, BufferZoneArgsSchema.parse(args)));
|
|
218
|
+
case 'get_ammonia_rules':
|
|
219
|
+
return textResult(handleGetAmmoniaRules(db, AmmoniaArgsSchema.parse(args)));
|
|
220
|
+
case 'get_bff_requirements':
|
|
221
|
+
return textResult(handleGetBffRequirements(db, BffArgsSchema.parse(args)));
|
|
222
|
+
case 'get_nutrient_loss_limits':
|
|
223
|
+
return textResult(handleGetNutrientLossLimits(db, NutrientLossArgsSchema.parse(args)));
|
|
224
|
+
case 'get_eip_requirements':
|
|
225
|
+
return textResult(handleGetEipRequirements(db, EipArgsSchema.parse(args)));
|
|
226
|
+
case 'check_environmental_compliance':
|
|
227
|
+
return textResult(handleCheckEnvironmentalCompliance(db, ComplianceArgsSchema.parse(args)));
|
|
228
|
+
default:
|
|
229
|
+
return errorResult(`Unknown tool: ${name}`);
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
async function main(): Promise<void> {
|
|
237
|
+
const transport = new StdioServerTransport();
|
|
238
|
+
await server.connect(transport);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
main().catch((err) => {
|
|
242
|
+
process.stderr.write(`Fatal error: ${err.message}\n`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
});
|
|
@@ -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 Environmental Compliance MCP',
|
|
7
|
+
description:
|
|
8
|
+
'Swiss environmental compliance data for agriculture: Gewaesserschutz (GSchG), Grundwasserschutzzonen, ' +
|
|
9
|
+
'Pufferstreifen, Ammoniakemissionen (LRV, Agrammon), Biodiversitaetsfoerderflaechen (BFF), ' +
|
|
10
|
+
'Naehrstoffverlust-Absenkpfad (Pa.Iv. 19.475), UVP-Schwellenwerte, and Bodenbelastung (VBBo). ' +
|
|
11
|
+
'Covers federal regulations and cantonal implementation for Swiss agricultural operations.',
|
|
12
|
+
version: '0.1.0',
|
|
13
|
+
jurisdiction: [...SUPPORTED_JURISDICTIONS],
|
|
14
|
+
data_sources: [
|
|
15
|
+
'BAFU — Gewaesserschutz, Luftreinhaltung, Biodiversitaet',
|
|
16
|
+
'BLW — OELN-Auflagen, BFF-Typen, DZV',
|
|
17
|
+
'Agroscope — Agrammon Ammoniakemissionsfaktoren',
|
|
18
|
+
'Kantonale Umweltaemter — Grundwasserschutzzonen',
|
|
19
|
+
],
|
|
20
|
+
tools_count: 11,
|
|
21
|
+
links: {
|
|
22
|
+
homepage: 'https://ansvar.eu/open-agriculture',
|
|
23
|
+
repository: 'https://github.com/ansvar-systems/ch-environmental-compliance-mcp',
|
|
24
|
+
mcp_network: 'https://ansvar.ai/mcp',
|
|
25
|
+
},
|
|
26
|
+
_meta: buildMeta(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { buildMeta } from '../metadata.js';
|
|
2
|
+
import { validateJurisdiction } from '../jurisdiction.js';
|
|
3
|
+
import type { Database } from '../db.js';
|
|
4
|
+
|
|
5
|
+
interface ComplianceArgs {
|
|
6
|
+
facility_type: string;
|
|
7
|
+
animal_count?: number;
|
|
8
|
+
area_ha?: number;
|
|
9
|
+
jurisdiction?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function handleCheckEnvironmentalCompliance(db: Database, args: ComplianceArgs) {
|
|
13
|
+
const jv = validateJurisdiction(args.jurisdiction);
|
|
14
|
+
if (!jv.valid) return jv.error;
|
|
15
|
+
|
|
16
|
+
const applicable: {
|
|
17
|
+
category: string;
|
|
18
|
+
rule: string;
|
|
19
|
+
requirement: string;
|
|
20
|
+
legal_basis: string;
|
|
21
|
+
applies: boolean;
|
|
22
|
+
reason: string;
|
|
23
|
+
}[] = [];
|
|
24
|
+
|
|
25
|
+
// Check water protection requirements
|
|
26
|
+
const waterZones = db.all<{ zone_type: string; restrictions: string; legal_basis: string }>(
|
|
27
|
+
'SELECT zone_type, restrictions, legal_basis FROM water_protection_zones WHERE jurisdiction = ?',
|
|
28
|
+
[jv.jurisdiction]
|
|
29
|
+
);
|
|
30
|
+
for (const z of waterZones) {
|
|
31
|
+
applicable.push({
|
|
32
|
+
category: 'Gewaesserschutz',
|
|
33
|
+
rule: `Grundwasserschutzzone ${z.zone_type}`,
|
|
34
|
+
requirement: z.restrictions,
|
|
35
|
+
legal_basis: z.legal_basis ?? 'GSchG Art. 20',
|
|
36
|
+
applies: true,
|
|
37
|
+
reason: 'Pruefen Sie, ob Ihr Standort in einer Schutzzone liegt (kantonaler Zonenplan).',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check buffer zone requirements
|
|
42
|
+
const buffers = db.all<{ type: string; distance_m: number; requirement: string; source_law: string }>(
|
|
43
|
+
'SELECT type, distance_m, requirement, source_law FROM buffer_zones WHERE jurisdiction = ?',
|
|
44
|
+
[jv.jurisdiction]
|
|
45
|
+
);
|
|
46
|
+
for (const b of buffers) {
|
|
47
|
+
applicable.push({
|
|
48
|
+
category: 'Pufferstreifen',
|
|
49
|
+
rule: `${b.type} — ${b.distance_m}m`,
|
|
50
|
+
requirement: b.requirement,
|
|
51
|
+
legal_basis: b.source_law,
|
|
52
|
+
applies: true,
|
|
53
|
+
reason: 'OELN-Pflicht fuer alle Direktzahlungsempfaenger.',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check ammonia rules
|
|
58
|
+
const ammoniaRules = db.all<{ technique: string; requirement: string; legal_basis: string }>(
|
|
59
|
+
'SELECT technique, requirement, legal_basis FROM ammonia_rules WHERE jurisdiction = ?',
|
|
60
|
+
[jv.jurisdiction]
|
|
61
|
+
);
|
|
62
|
+
const hasLivestock = args.animal_count !== undefined && args.animal_count > 0;
|
|
63
|
+
for (const a of ammoniaRules) {
|
|
64
|
+
applicable.push({
|
|
65
|
+
category: 'Ammoniakemissionen',
|
|
66
|
+
rule: a.technique,
|
|
67
|
+
requirement: a.requirement,
|
|
68
|
+
legal_basis: a.legal_basis ?? 'LRV Anhang 2',
|
|
69
|
+
applies: hasLivestock,
|
|
70
|
+
reason: hasLivestock
|
|
71
|
+
? `Relevant bei ${args.animal_count} Tieren.`
|
|
72
|
+
: 'Nur relevant bei Tierhaltung.',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check BFF minimum
|
|
77
|
+
const bffMin = db.get<{ min_area_pct: number }>(
|
|
78
|
+
`SELECT min_area_pct FROM bff_types WHERE min_area_pct > 0 AND jurisdiction = ? ORDER BY min_area_pct DESC LIMIT 1`,
|
|
79
|
+
[jv.jurisdiction]
|
|
80
|
+
);
|
|
81
|
+
if (bffMin && args.area_ha) {
|
|
82
|
+
applicable.push({
|
|
83
|
+
category: 'Biodiversitaetsfoerderflaechen',
|
|
84
|
+
rule: 'Mindestanteil BFF',
|
|
85
|
+
requirement: `Mindestens ${bffMin.min_area_pct}% der LN als BFF (${(args.area_ha * bffMin.min_area_pct / 100).toFixed(2)} ha bei ${args.area_ha} ha LN)`,
|
|
86
|
+
legal_basis: 'DZV Art. 14',
|
|
87
|
+
applies: true,
|
|
88
|
+
reason: 'OELN-Pflicht fuer alle Direktzahlungsempfaenger.',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check UVP thresholds
|
|
93
|
+
const uvpRules = db.all<{ rule: string; threshold: string; legal_basis: string }>(
|
|
94
|
+
`SELECT rule, threshold, legal_basis FROM environmental_rules WHERE topic = 'UVP' AND jurisdiction = ?`,
|
|
95
|
+
[jv.jurisdiction]
|
|
96
|
+
);
|
|
97
|
+
for (const u of uvpRules) {
|
|
98
|
+
const uvpApplies = args.animal_count !== undefined && args.animal_count >= 150;
|
|
99
|
+
applicable.push({
|
|
100
|
+
category: 'UVP (Umweltvertraeglichkeitspruefung)',
|
|
101
|
+
rule: u.rule,
|
|
102
|
+
requirement: u.threshold ?? '',
|
|
103
|
+
legal_basis: u.legal_basis,
|
|
104
|
+
applies: uvpApplies,
|
|
105
|
+
reason: uvpApplies
|
|
106
|
+
? `Bei ${args.animal_count} Tieren ist eine UVP-Pruefung wahrscheinlich erforderlich.`
|
|
107
|
+
: 'UVP-Schwellenwerte pruefen (kantonal, ab ca. 150 GVE).',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check Pa.Iv. 19.475 nutrient loss limits
|
|
112
|
+
const currentYear = new Date().getFullYear();
|
|
113
|
+
const limits = db.all<{ nutrient: string; limit_pct: number; target: string }>(
|
|
114
|
+
'SELECT nutrient, limit_pct, target FROM nutrient_loss_limits WHERE year <= ? AND jurisdiction = ? ORDER BY year DESC',
|
|
115
|
+
[currentYear, jv.jurisdiction]
|
|
116
|
+
);
|
|
117
|
+
const latestN = limits.find(l => l.nutrient === 'N');
|
|
118
|
+
const latestP = limits.find(l => l.nutrient === 'P');
|
|
119
|
+
if (latestN || latestP) {
|
|
120
|
+
applicable.push({
|
|
121
|
+
category: 'Naehrstoffverluste (Pa.Iv. 19.475)',
|
|
122
|
+
rule: 'Absenkpfad Stickstoff und Phosphor',
|
|
123
|
+
requirement: [
|
|
124
|
+
latestN ? `N: -${latestN.limit_pct}% (${latestN.target})` : '',
|
|
125
|
+
latestP ? `P: -${latestP.limit_pct}% (${latestP.target})` : '',
|
|
126
|
+
].filter(Boolean).join('; '),
|
|
127
|
+
legal_basis: 'LwG Art. 6a (Pa.Iv. 19.475)',
|
|
128
|
+
applies: true,
|
|
129
|
+
reason: 'Betrifft alle Betriebe mit Direktzahlungen.',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
facility_type: args.facility_type,
|
|
135
|
+
animal_count: args.animal_count ?? null,
|
|
136
|
+
area_ha: args.area_ha ?? null,
|
|
137
|
+
jurisdiction: jv.jurisdiction,
|
|
138
|
+
applicable_rules: applicable.length,
|
|
139
|
+
rules: applicable,
|
|
140
|
+
note: 'Diese Zusammenstellung ist indikativ. Kantonale Auflagen und standortspezifische Anforderungen sind separat zu pruefen.',
|
|
141
|
+
_meta: buildMeta(),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -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-environmental-compliance-mcp',
|
|
40
|
+
_meta: buildMeta(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -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 AmmoniaArgs {
|
|
6
|
+
technique?: string;
|
|
7
|
+
jurisdiction?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function handleGetAmmoniaRules(db: Database, args: AmmoniaArgs) {
|
|
11
|
+
const jv = validateJurisdiction(args.jurisdiction);
|
|
12
|
+
if (!jv.valid) return jv.error;
|
|
13
|
+
|
|
14
|
+
let sql = 'SELECT * FROM ammonia_rules WHERE jurisdiction = ?';
|
|
15
|
+
const params: unknown[] = [jv.jurisdiction];
|
|
16
|
+
|
|
17
|
+
if (args.technique) {
|
|
18
|
+
sql += ' AND LOWER(technique) LIKE LOWER(?)';
|
|
19
|
+
params.push(`%${args.technique}%`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
sql += ' ORDER BY technique';
|
|
23
|
+
|
|
24
|
+
const rules = db.all<{
|
|
25
|
+
technique: string; emission_factor: number; requirement: string;
|
|
26
|
+
legal_basis: string; effective_date: string; notes: string;
|
|
27
|
+
}>(sql, params);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
jurisdiction: jv.jurisdiction,
|
|
31
|
+
rules_count: rules.length,
|
|
32
|
+
rules: rules.map(r => ({
|
|
33
|
+
technique: r.technique,
|
|
34
|
+
emission_factor_pct: r.emission_factor,
|
|
35
|
+
requirement: r.requirement,
|
|
36
|
+
legal_basis: r.legal_basis,
|
|
37
|
+
effective_date: r.effective_date,
|
|
38
|
+
notes: r.notes,
|
|
39
|
+
})),
|
|
40
|
+
_meta: buildMeta({
|
|
41
|
+
source_url: 'https://www.bafu.admin.ch/bafu/de/home/themen/luft/fachinformationen/luftschadstoffquellen/emissionen-der-landwirtschaft.html',
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { buildMeta } from '../metadata.js';
|
|
2
|
+
import { validateJurisdiction } from '../jurisdiction.js';
|
|
3
|
+
import type { Database } from '../db.js';
|
|
4
|
+
|
|
5
|
+
interface BffArgs {
|
|
6
|
+
bff_type?: string;
|
|
7
|
+
quality_level?: string;
|
|
8
|
+
jurisdiction?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function handleGetBffRequirements(db: Database, args: BffArgs) {
|
|
12
|
+
const jv = validateJurisdiction(args.jurisdiction);
|
|
13
|
+
if (!jv.valid) return jv.error;
|
|
14
|
+
|
|
15
|
+
let sql = 'SELECT * FROM bff_types WHERE jurisdiction = ?';
|
|
16
|
+
const params: unknown[] = [jv.jurisdiction];
|
|
17
|
+
|
|
18
|
+
if (args.bff_type) {
|
|
19
|
+
sql += ' AND (LOWER(id) LIKE LOWER(?) OR LOWER(name) LIKE LOWER(?))';
|
|
20
|
+
params.push(`%${args.bff_type}%`, `%${args.bff_type}%`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (args.quality_level) {
|
|
24
|
+
sql += ' AND UPPER(quality_level) = UPPER(?)';
|
|
25
|
+
params.push(args.quality_level);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
sql += ' ORDER BY name, quality_level';
|
|
29
|
+
|
|
30
|
+
const types = db.all<{
|
|
31
|
+
id: string; name: string; quality_level: string;
|
|
32
|
+
payment_chf_ha: number; min_area_pct: number;
|
|
33
|
+
botanical_criteria: string; notes: string;
|
|
34
|
+
}>(sql, params);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
jurisdiction: jv.jurisdiction,
|
|
38
|
+
types_count: types.length,
|
|
39
|
+
types: types.map(t => ({
|
|
40
|
+
id: t.id,
|
|
41
|
+
name: t.name,
|
|
42
|
+
quality_level: t.quality_level,
|
|
43
|
+
payment_chf_ha: t.payment_chf_ha,
|
|
44
|
+
min_area_pct: t.min_area_pct,
|
|
45
|
+
botanical_criteria: t.botanical_criteria,
|
|
46
|
+
notes: t.notes,
|
|
47
|
+
})),
|
|
48
|
+
_meta: buildMeta({
|
|
49
|
+
source_url: 'https://www.blw.admin.ch/blw/de/home/instrumente/direktzahlungen/biodiversitaetsbeitraege.html',
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { buildMeta } from '../metadata.js';
|
|
2
|
+
import { validateJurisdiction } from '../jurisdiction.js';
|
|
3
|
+
import type { Database } from '../db.js';
|
|
4
|
+
|
|
5
|
+
interface BufferZoneArgs {
|
|
6
|
+
zone_type?: string;
|
|
7
|
+
jurisdiction?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function handleGetBufferZoneRules(db: Database, args: BufferZoneArgs) {
|
|
11
|
+
const jv = validateJurisdiction(args.jurisdiction);
|
|
12
|
+
if (!jv.valid) return jv.error;
|
|
13
|
+
|
|
14
|
+
let sql = 'SELECT * FROM buffer_zones WHERE jurisdiction = ?';
|
|
15
|
+
const params: unknown[] = [jv.jurisdiction];
|
|
16
|
+
|
|
17
|
+
if (args.zone_type) {
|
|
18
|
+
sql += ' AND LOWER(type) LIKE LOWER(?)';
|
|
19
|
+
params.push(`%${args.zone_type}%`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
sql += ' ORDER BY distance_m DESC';
|
|
23
|
+
|
|
24
|
+
const zones = db.all<{
|
|
25
|
+
type: string; distance_m: number; requirement: string;
|
|
26
|
+
source_law: string; notes: string;
|
|
27
|
+
}>(sql, params);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
jurisdiction: jv.jurisdiction,
|
|
31
|
+
rules_count: zones.length,
|
|
32
|
+
rules: zones.map(z => ({
|
|
33
|
+
type: z.type,
|
|
34
|
+
distance_m: z.distance_m,
|
|
35
|
+
requirement: z.requirement,
|
|
36
|
+
source_law: z.source_law,
|
|
37
|
+
notes: z.notes,
|
|
38
|
+
})),
|
|
39
|
+
_meta: buildMeta({
|
|
40
|
+
source_url: 'https://www.blw.admin.ch/blw/de/home/instrumente/direktzahlungen/oekologischer-leistungsnachweis.html',
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { buildMeta } from '../metadata.js';
|
|
2
|
+
import { validateJurisdiction } from '../jurisdiction.js';
|
|
3
|
+
import type { Database } from '../db.js';
|
|
4
|
+
|
|
5
|
+
interface EipArgs {
|
|
6
|
+
project_type?: string;
|
|
7
|
+
jurisdiction?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function handleGetEipRequirements(db: Database, args: EipArgs) {
|
|
11
|
+
const jv = validateJurisdiction(args.jurisdiction);
|
|
12
|
+
if (!jv.valid) return jv.error;
|
|
13
|
+
|
|
14
|
+
let sql = `SELECT * FROM environmental_rules WHERE topic = 'UVP' AND jurisdiction = ?`;
|
|
15
|
+
const params: unknown[] = [jv.jurisdiction];
|
|
16
|
+
|
|
17
|
+
if (args.project_type) {
|
|
18
|
+
sql += ' AND LOWER(rule) LIKE LOWER(?)';
|
|
19
|
+
params.push(`%${args.project_type}%`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
sql += ' ORDER BY id';
|
|
23
|
+
|
|
24
|
+
const rules = db.all<{
|
|
25
|
+
topic: string; rule: string; authority: string;
|
|
26
|
+
legal_basis: string; threshold: string; notes: string;
|
|
27
|
+
}>(sql, params);
|
|
28
|
+
|
|
29
|
+
// Also get VBBo (soil contamination) rules
|
|
30
|
+
const vbbo = db.all<{
|
|
31
|
+
topic: string; rule: string; authority: string;
|
|
32
|
+
legal_basis: string; threshold: string; notes: string;
|
|
33
|
+
}>(`SELECT * FROM environmental_rules WHERE topic = 'VBBo' AND jurisdiction = ? ORDER BY id`, [jv.jurisdiction]);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
jurisdiction: jv.jurisdiction,
|
|
37
|
+
uvp_rules_count: rules.length,
|
|
38
|
+
uvp_rules: rules.map(r => ({
|
|
39
|
+
rule: r.rule,
|
|
40
|
+
authority: r.authority,
|
|
41
|
+
legal_basis: r.legal_basis,
|
|
42
|
+
threshold: r.threshold,
|
|
43
|
+
notes: r.notes,
|
|
44
|
+
})),
|
|
45
|
+
vbbo_rules_count: vbbo.length,
|
|
46
|
+
vbbo_rules: vbbo.map(r => ({
|
|
47
|
+
rule: r.rule,
|
|
48
|
+
authority: r.authority,
|
|
49
|
+
legal_basis: r.legal_basis,
|
|
50
|
+
threshold: r.threshold,
|
|
51
|
+
notes: r.notes,
|
|
52
|
+
})),
|
|
53
|
+
_meta: buildMeta({
|
|
54
|
+
source_url: 'https://www.bafu.admin.ch/bafu/de/home/themen/uvp.html',
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
}
|