@ansvar/ch-farm-safety-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 (109) hide show
  1. package/.github/workflows/check-freshness.yml +53 -0
  2. package/.github/workflows/ci.yml +21 -0
  3. package/.github/workflows/codeql.yml +30 -0
  4. package/.github/workflows/ghcr-build.yml +45 -0
  5. package/.github/workflows/gitleaks.yml +24 -0
  6. package/.github/workflows/ingest.yml +67 -0
  7. package/.github/workflows/publish.yml +24 -0
  8. package/CHANGELOG.md +23 -0
  9. package/CODEOWNERS +1 -0
  10. package/COVERAGE.md +54 -0
  11. package/DISCLAIMER.md +51 -0
  12. package/Dockerfile +26 -0
  13. package/LICENSE +17 -0
  14. package/PRIVACY.md +25 -0
  15. package/README.md +129 -0
  16. package/SECURITY.md +25 -0
  17. package/TOOLS.md +140 -0
  18. package/data/coverage.json +22 -0
  19. package/data/database.db +0 -0
  20. package/data/sources.yml +36 -0
  21. package/dist/db.d.ts +25 -0
  22. package/dist/db.d.ts.map +1 -0
  23. package/dist/db.js +200 -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 +250 -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 +20 -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 +196 -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-accident-reporting.d.ts +49 -0
  50. package/dist/tools/get-accident-reporting.d.ts.map +1 -0
  51. package/dist/tools/get-accident-reporting.js +31 -0
  52. package/dist/tools/get-accident-reporting.js.map +1 -0
  53. package/dist/tools/get-chemical-safety.d.ts +47 -0
  54. package/dist/tools/get-chemical-safety.d.ts.map +1 -0
  55. package/dist/tools/get-chemical-safety.js +31 -0
  56. package/dist/tools/get-chemical-safety.js.map +1 -0
  57. package/dist/tools/get-machinery-safety.d.ts +46 -0
  58. package/dist/tools/get-machinery-safety.d.ts.map +1 -0
  59. package/dist/tools/get-machinery-safety.js +31 -0
  60. package/dist/tools/get-machinery-safety.js.map +1 -0
  61. package/dist/tools/get-risk-assessment-requirements.d.ts +47 -0
  62. package/dist/tools/get-risk-assessment-requirements.d.ts.map +1 -0
  63. package/dist/tools/get-risk-assessment-requirements.js +31 -0
  64. package/dist/tools/get-risk-assessment-requirements.js.map +1 -0
  65. package/dist/tools/get-young-worker-rules.d.ts +47 -0
  66. package/dist/tools/get-young-worker-rules.d.ts.map +1 -0
  67. package/dist/tools/get-young-worker-rules.js +31 -0
  68. package/dist/tools/get-young-worker-rules.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-bul-guidance.d.ts +23 -0
  74. package/dist/tools/search-bul-guidance.d.ts.map +1 -0
  75. package/dist/tools/search-bul-guidance.js +35 -0
  76. package/dist/tools/search-bul-guidance.js.map +1 -0
  77. package/dist/tools/search-safety-rules.d.ts +25 -0
  78. package/dist/tools/search-safety-rules.d.ts.map +1 -0
  79. package/dist/tools/search-safety-rules.js +26 -0
  80. package/dist/tools/search-safety-rules.js.map +1 -0
  81. package/docker-compose.yml +12 -0
  82. package/eslint.config.js +27 -0
  83. package/package.json +54 -0
  84. package/scripts/ingest.ts +879 -0
  85. package/server.json +31 -0
  86. package/src/db.ts +241 -0
  87. package/src/http-server.ts +282 -0
  88. package/src/jurisdiction.ts +30 -0
  89. package/src/metadata.ts +30 -0
  90. package/src/server.ts +219 -0
  91. package/src/tools/about.ts +28 -0
  92. package/src/tools/check-freshness.ts +42 -0
  93. package/src/tools/get-accident-reporting.ts +52 -0
  94. package/src/tools/get-chemical-safety.ts +52 -0
  95. package/src/tools/get-machinery-safety.ts +52 -0
  96. package/src/tools/get-risk-assessment-requirements.ts +52 -0
  97. package/src/tools/get-young-worker-rules.ts +52 -0
  98. package/src/tools/list-sources.ts +65 -0
  99. package/src/tools/search-bul-guidance.ts +51 -0
  100. package/src/tools/search-safety-rules.ts +35 -0
  101. package/tests/db.test.ts +121 -0
  102. package/tests/helpers/seed-db.ts +134 -0
  103. package/tests/jurisdiction.test.ts +54 -0
  104. package/tests/tools/about.test.ts +44 -0
  105. package/tests/tools/check-freshness.test.ts +68 -0
  106. package/tests/tools/list-sources.test.ts +75 -0
  107. package/tests/tools/search-safety-rules.test.ts +75 -0
  108. package/tsconfig.json +19 -0
  109. package/vitest.config.ts +9 -0
package/src/server.ts ADDED
@@ -0,0 +1,219 @@
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 { handleSearchSafetyRules } from './tools/search-safety-rules.js';
15
+ import { handleGetRiskAssessmentRequirements } from './tools/get-risk-assessment-requirements.js';
16
+ import { handleGetMachinerySafety } from './tools/get-machinery-safety.js';
17
+ import { handleGetChemicalSafety } from './tools/get-chemical-safety.js';
18
+ import { handleGetYoungWorkerRules } from './tools/get-young-worker-rules.js';
19
+ import { handleGetAccidentReporting } from './tools/get-accident-reporting.js';
20
+ import { handleSearchBulGuidance } from './tools/search-bul-guidance.js';
21
+
22
+ const SERVER_NAME = 'ch-farm-safety-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_safety_rules',
43
+ description: 'Search Swiss farm workplace safety rules (BUL lebenswichtige Regeln, Suva, EKAS). Use for broad queries about farm safety in Switzerland.',
44
+ inputSchema: {
45
+ type: 'object' as const,
46
+ properties: {
47
+ query: { type: 'string', description: 'Free-text search query (German or English)' },
48
+ topic: { type: 'string', description: 'Filter by safety area: maschinen, tiere, wald, hoehe, chemie, silogas, allgemein' },
49
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
50
+ limit: { type: 'number', description: 'Max results (default: 20, max: 50)' },
51
+ },
52
+ required: ['query'],
53
+ },
54
+ },
55
+ {
56
+ name: 'get_risk_assessment_requirements',
57
+ description: 'Get risk assessment (Risikobeurteilung) obligations for farm activities. Based on EKAS Branchenloesung Landwirtschaft and VUV.',
58
+ inputSchema: {
59
+ type: 'object' as const,
60
+ properties: {
61
+ activity_type: { type: 'string', description: 'Activity type (e.g. Alleinarbeit, Siloarbeiten, Holzerei, Tierhaltung)' },
62
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
63
+ },
64
+ },
65
+ },
66
+ {
67
+ name: 'get_machinery_safety',
68
+ description: 'Get safety requirements for agricultural machinery. Covers ROPS, Gurtpflicht, MFK, speed limits, PTO guards.',
69
+ inputSchema: {
70
+ type: 'object' as const,
71
+ properties: {
72
+ machine_type: { type: 'string', description: 'Machine type (e.g. Traktor, Maehdrescher, Motorsaege, Kreiselmaeher)' },
73
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
74
+ },
75
+ },
76
+ },
77
+ {
78
+ name: 'get_chemical_safety',
79
+ description: 'Get chemical safety data: exposure limits (MAK), required PPE, and storage rules. Covers PSM (Pflanzenschutzmittel), Silogas, Duenger.',
80
+ inputSchema: {
81
+ type: 'object' as const,
82
+ properties: {
83
+ substance_type: { type: 'string', description: 'Substance type (e.g. Pflanzenschutzmittel, Silogas, Ammoniak, Dieselkraftstoff)' },
84
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
85
+ },
86
+ },
87
+ },
88
+ {
89
+ name: 'get_young_worker_rules',
90
+ description: 'Get restrictions for young workers (Jugendarbeitsschutz) on Swiss farms. Covers age thresholds, allowed/forbidden tasks, exceptions for family farms.',
91
+ inputSchema: {
92
+ type: 'object' as const,
93
+ properties: {
94
+ age_group: { type: 'string', description: 'Age group (e.g. unter-13, 13-15, 15-18)' },
95
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
96
+ },
97
+ },
98
+ },
99
+ {
100
+ name: 'get_accident_reporting',
101
+ description: 'Get accident reporting obligations: deadlines, forms, and responsible insurer (Suva for employees, private for self-employed).',
102
+ inputSchema: {
103
+ type: 'object' as const,
104
+ properties: {
105
+ severity: { type: 'string', description: 'Accident severity (e.g. toedlich, schwer, leicht, Berufskrankheit)' },
106
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
107
+ },
108
+ },
109
+ },
110
+ {
111
+ name: 'search_bul_guidance',
112
+ description: 'Search BUL/SPAA safety guidance documents, fact sheets, and campaigns. Returns topic, title, description, and URL.',
113
+ inputSchema: {
114
+ type: 'object' as const,
115
+ properties: {
116
+ query: { type: 'string', description: 'Free-text search query (German or English)' },
117
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
118
+ },
119
+ required: ['query'],
120
+ },
121
+ },
122
+ ];
123
+
124
+ const SearchArgsSchema = z.object({
125
+ query: z.string(),
126
+ topic: z.string().optional(),
127
+ jurisdiction: z.string().optional(),
128
+ limit: z.number().optional(),
129
+ });
130
+
131
+ const RiskAssessmentArgsSchema = z.object({
132
+ activity_type: z.string().optional(),
133
+ jurisdiction: z.string().optional(),
134
+ });
135
+
136
+ const MachinerySafetyArgsSchema = z.object({
137
+ machine_type: z.string().optional(),
138
+ jurisdiction: z.string().optional(),
139
+ });
140
+
141
+ const ChemicalSafetyArgsSchema = z.object({
142
+ substance_type: z.string().optional(),
143
+ jurisdiction: z.string().optional(),
144
+ });
145
+
146
+ const YoungWorkerArgsSchema = z.object({
147
+ age_group: z.string().optional(),
148
+ jurisdiction: z.string().optional(),
149
+ });
150
+
151
+ const AccidentReportingArgsSchema = z.object({
152
+ severity: z.string().optional(),
153
+ jurisdiction: z.string().optional(),
154
+ });
155
+
156
+ const BulGuidanceArgsSchema = z.object({
157
+ query: z.string(),
158
+ jurisdiction: z.string().optional(),
159
+ });
160
+
161
+ function textResult(data: unknown) {
162
+ return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };
163
+ }
164
+
165
+ function errorResult(message: string) {
166
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ error: message }) }], isError: true };
167
+ }
168
+
169
+ const db = createDatabase();
170
+
171
+ const server = new Server(
172
+ { name: SERVER_NAME, version: SERVER_VERSION },
173
+ { capabilities: { tools: {} } }
174
+ );
175
+
176
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
177
+
178
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
179
+ const { name, arguments: args = {} } = request.params;
180
+
181
+ try {
182
+ switch (name) {
183
+ case 'about':
184
+ return textResult(handleAbout());
185
+ case 'list_sources':
186
+ return textResult(handleListSources(db));
187
+ case 'check_data_freshness':
188
+ return textResult(handleCheckFreshness(db));
189
+ case 'search_safety_rules':
190
+ return textResult(handleSearchSafetyRules(db, SearchArgsSchema.parse(args)));
191
+ case 'get_risk_assessment_requirements':
192
+ return textResult(handleGetRiskAssessmentRequirements(db, RiskAssessmentArgsSchema.parse(args)));
193
+ case 'get_machinery_safety':
194
+ return textResult(handleGetMachinerySafety(db, MachinerySafetyArgsSchema.parse(args)));
195
+ case 'get_chemical_safety':
196
+ return textResult(handleGetChemicalSafety(db, ChemicalSafetyArgsSchema.parse(args)));
197
+ case 'get_young_worker_rules':
198
+ return textResult(handleGetYoungWorkerRules(db, YoungWorkerArgsSchema.parse(args)));
199
+ case 'get_accident_reporting':
200
+ return textResult(handleGetAccidentReporting(db, AccidentReportingArgsSchema.parse(args)));
201
+ case 'search_bul_guidance':
202
+ return textResult(handleSearchBulGuidance(db, BulGuidanceArgsSchema.parse(args)));
203
+ default:
204
+ return errorResult(`Unknown tool: ${name}`);
205
+ }
206
+ } catch (err) {
207
+ return errorResult(err instanceof Error ? err.message : String(err));
208
+ }
209
+ });
210
+
211
+ async function main(): Promise<void> {
212
+ const transport = new StdioServerTransport();
213
+ await server.connect(transport);
214
+ }
215
+
216
+ main().catch((err) => {
217
+ process.stderr.write(`Fatal error: ${err.message}\n`);
218
+ process.exit(1);
219
+ });
@@ -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 Farm Safety MCP',
7
+ description:
8
+ 'Swiss farm workplace safety data based on BUL/SPAA safety rules, Suva requirements, ' +
9
+ 'EKAS Branchenloesung Landwirtschaft, and Agriss accident statistics. Covers machinery safety (ROPS, MFK), ' +
10
+ 'chemical exposure (PSM-Anwenderschutz), young worker restrictions, accident reporting obligations, ' +
11
+ 'risk assessment requirements, and first aid / emergency numbers for Swiss agriculture.',
12
+ version: '0.1.0',
13
+ jurisdiction: [...SUPPORTED_JURISDICTIONS],
14
+ data_sources: [
15
+ 'BUL/SPAA — Sicherheitsregeln, Merkblaetter',
16
+ 'Suva — Arbeitssicherheit Landwirtschaft, MAK-Werte',
17
+ 'EKAS — Branchenloesung Landwirtschaft',
18
+ 'Agriss — Unfallstatistik Schweizer Landwirtschaft',
19
+ ],
20
+ tools_count: 10,
21
+ links: {
22
+ homepage: 'https://ansvar.eu/open-agriculture',
23
+ repository: 'https://github.com/ansvar-systems/ch-farm-safety-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-farm-safety-mcp',
40
+ _meta: buildMeta(),
41
+ };
42
+ }
@@ -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 AccidentReportingArgs {
6
+ severity?: string;
7
+ jurisdiction?: string;
8
+ }
9
+
10
+ export function handleGetAccidentReporting(db: Database, args: AccidentReportingArgs) {
11
+ const jv = validateJurisdiction(args.jurisdiction);
12
+ if (!jv.valid) return jv.error;
13
+
14
+ if (args.severity) {
15
+ const results = db.all<{
16
+ id: number; severity: string; obligation: string;
17
+ deadline: string; form: string; insurer: string;
18
+ }>(
19
+ 'SELECT * FROM accident_reporting WHERE LOWER(severity) LIKE LOWER(?) AND jurisdiction = ?',
20
+ [`%${args.severity}%`, jv.jurisdiction]
21
+ );
22
+
23
+ if (results.length === 0) {
24
+ return {
25
+ error: 'not_found',
26
+ message: `No reporting obligations found for severity '${args.severity}'.`,
27
+ };
28
+ }
29
+
30
+ return {
31
+ severity: args.severity,
32
+ jurisdiction: jv.jurisdiction,
33
+ results_count: results.length,
34
+ results,
35
+ _meta: buildMeta({ source_url: 'https://www.suva.ch/de-ch/unfall-melden' }),
36
+ };
37
+ }
38
+
39
+ const all = db.all<{
40
+ severity: string; obligation: string; deadline: string; form: string; insurer: string;
41
+ }>(
42
+ 'SELECT severity, obligation, deadline, form, insurer FROM accident_reporting WHERE jurisdiction = ? ORDER BY severity',
43
+ [jv.jurisdiction]
44
+ );
45
+
46
+ return {
47
+ jurisdiction: jv.jurisdiction,
48
+ results_count: all.length,
49
+ results: all,
50
+ _meta: buildMeta({ source_url: 'https://www.suva.ch/de-ch/unfall-melden' }),
51
+ };
52
+ }
@@ -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 ChemicalSafetyArgs {
6
+ substance_type?: string;
7
+ jurisdiction?: string;
8
+ }
9
+
10
+ export function handleGetChemicalSafety(db: Database, args: ChemicalSafetyArgs) {
11
+ const jv = validateJurisdiction(args.jurisdiction);
12
+ if (!jv.valid) return jv.error;
13
+
14
+ if (args.substance_type) {
15
+ const results = db.all<{
16
+ id: number; substance_type: string; exposure_limit: string;
17
+ ppe_required: string; storage_rule: string;
18
+ }>(
19
+ 'SELECT * FROM chemical_safety WHERE LOWER(substance_type) LIKE LOWER(?) AND jurisdiction = ?',
20
+ [`%${args.substance_type}%`, jv.jurisdiction]
21
+ );
22
+
23
+ if (results.length === 0) {
24
+ return {
25
+ error: 'not_found',
26
+ message: `No chemical safety data found for substance type '${args.substance_type}'.`,
27
+ };
28
+ }
29
+
30
+ return {
31
+ substance_type: args.substance_type,
32
+ jurisdiction: jv.jurisdiction,
33
+ results_count: results.length,
34
+ results,
35
+ _meta: buildMeta({ source_url: 'https://www.suva.ch/de-ch/praevention/sachthemen/chemische-produkte' }),
36
+ };
37
+ }
38
+
39
+ const all = db.all<{
40
+ substance_type: string; exposure_limit: string; ppe_required: string; storage_rule: string;
41
+ }>(
42
+ 'SELECT substance_type, exposure_limit, ppe_required, storage_rule FROM chemical_safety WHERE jurisdiction = ? ORDER BY substance_type',
43
+ [jv.jurisdiction]
44
+ );
45
+
46
+ return {
47
+ jurisdiction: jv.jurisdiction,
48
+ results_count: all.length,
49
+ results: all,
50
+ _meta: buildMeta({ source_url: 'https://www.suva.ch/de-ch/praevention/sachthemen/chemische-produkte' }),
51
+ };
52
+ }
@@ -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 MachinerySafetyArgs {
6
+ machine_type?: string;
7
+ jurisdiction?: string;
8
+ }
9
+
10
+ export function handleGetMachinerySafety(db: Database, args: MachinerySafetyArgs) {
11
+ const jv = validateJurisdiction(args.jurisdiction);
12
+ if (!jv.valid) return jv.error;
13
+
14
+ if (args.machine_type) {
15
+ const results = db.all<{
16
+ id: number; machine_type: string; requirement: string;
17
+ certification: string; notes: string;
18
+ }>(
19
+ 'SELECT * FROM machinery_safety WHERE LOWER(machine_type) LIKE LOWER(?) AND jurisdiction = ?',
20
+ [`%${args.machine_type}%`, jv.jurisdiction]
21
+ );
22
+
23
+ if (results.length === 0) {
24
+ return {
25
+ error: 'not_found',
26
+ message: `No machinery safety requirements found for '${args.machine_type}'.`,
27
+ };
28
+ }
29
+
30
+ return {
31
+ machine_type: args.machine_type,
32
+ jurisdiction: jv.jurisdiction,
33
+ results_count: results.length,
34
+ results,
35
+ _meta: buildMeta({ source_url: 'https://www.bul.ch/de/themen/maschinen' }),
36
+ };
37
+ }
38
+
39
+ const all = db.all<{
40
+ machine_type: string; requirement: string; certification: string;
41
+ }>(
42
+ 'SELECT machine_type, requirement, certification FROM machinery_safety WHERE jurisdiction = ? ORDER BY machine_type',
43
+ [jv.jurisdiction]
44
+ );
45
+
46
+ return {
47
+ jurisdiction: jv.jurisdiction,
48
+ results_count: all.length,
49
+ results: all,
50
+ _meta: buildMeta({ source_url: 'https://www.bul.ch/de/themen/maschinen' }),
51
+ };
52
+ }
@@ -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 RiskAssessmentArgs {
6
+ activity_type?: string;
7
+ jurisdiction?: string;
8
+ }
9
+
10
+ export function handleGetRiskAssessmentRequirements(db: Database, args: RiskAssessmentArgs) {
11
+ const jv = validateJurisdiction(args.jurisdiction);
12
+ if (!jv.valid) return jv.error;
13
+
14
+ if (args.activity_type) {
15
+ const results = db.all<{
16
+ id: number; activity_type: string; requirement: string;
17
+ content: string; update_trigger: string; legal_basis: string;
18
+ }>(
19
+ 'SELECT * FROM risk_assessments WHERE LOWER(activity_type) LIKE LOWER(?) AND jurisdiction = ?',
20
+ [`%${args.activity_type}%`, jv.jurisdiction]
21
+ );
22
+
23
+ if (results.length === 0) {
24
+ return {
25
+ error: 'not_found',
26
+ message: `No risk assessment requirements found for activity type '${args.activity_type}'.`,
27
+ };
28
+ }
29
+
30
+ return {
31
+ activity_type: args.activity_type,
32
+ jurisdiction: jv.jurisdiction,
33
+ results_count: results.length,
34
+ results,
35
+ _meta: buildMeta({ source_url: 'https://www.bul.ch/de/themen/risikobeurteilung' }),
36
+ };
37
+ }
38
+
39
+ const all = db.all<{
40
+ activity_type: string; requirement: string; legal_basis: string;
41
+ }>(
42
+ 'SELECT activity_type, requirement, legal_basis FROM risk_assessments WHERE jurisdiction = ? ORDER BY activity_type',
43
+ [jv.jurisdiction]
44
+ );
45
+
46
+ return {
47
+ jurisdiction: jv.jurisdiction,
48
+ results_count: all.length,
49
+ results: all,
50
+ _meta: buildMeta({ source_url: 'https://www.bul.ch/de/themen/risikobeurteilung' }),
51
+ };
52
+ }
@@ -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 YoungWorkerArgs {
6
+ age_group?: string;
7
+ jurisdiction?: string;
8
+ }
9
+
10
+ export function handleGetYoungWorkerRules(db: Database, args: YoungWorkerArgs) {
11
+ const jv = validateJurisdiction(args.jurisdiction);
12
+ if (!jv.valid) return jv.error;
13
+
14
+ if (args.age_group) {
15
+ const results = db.all<{
16
+ id: number; age_group: string; restriction: string;
17
+ exception: string; legal_basis: string;
18
+ }>(
19
+ 'SELECT * FROM young_worker_rules WHERE LOWER(age_group) LIKE LOWER(?) AND jurisdiction = ?',
20
+ [`%${args.age_group}%`, jv.jurisdiction]
21
+ );
22
+
23
+ if (results.length === 0) {
24
+ return {
25
+ error: 'not_found',
26
+ message: `No rules found for age group '${args.age_group}'.`,
27
+ };
28
+ }
29
+
30
+ return {
31
+ age_group: args.age_group,
32
+ jurisdiction: jv.jurisdiction,
33
+ results_count: results.length,
34
+ results,
35
+ _meta: buildMeta({ source_url: 'https://www.seco.admin.ch/seco/de/home/Arbeit/Arbeitsbedingungen/jugendarbeitsschutz.html' }),
36
+ };
37
+ }
38
+
39
+ const all = db.all<{
40
+ age_group: string; restriction: string; exception: string; legal_basis: string;
41
+ }>(
42
+ 'SELECT age_group, restriction, exception, legal_basis FROM young_worker_rules WHERE jurisdiction = ? ORDER BY age_group',
43
+ [jv.jurisdiction]
44
+ );
45
+
46
+ return {
47
+ jurisdiction: jv.jurisdiction,
48
+ results_count: all.length,
49
+ results: all,
50
+ _meta: buildMeta({ source_url: 'https://www.seco.admin.ch/seco/de/home/Arbeit/Arbeitsbedingungen/jugendarbeitsschutz.html' }),
51
+ };
52
+ }
@@ -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: 'BUL/SPAA — Sicherheitsregeln und Merkblaetter',
21
+ authority: 'Beratungsstelle fuer Unfallverhuetung in der Landwirtschaft (BUL)',
22
+ official_url: 'https://www.bul.ch',
23
+ retrieval_method: 'PDF_EXTRACT + HTML_SCRAPE',
24
+ update_frequency: 'periodic (rules updated as needed)',
25
+ license: 'Swiss public-sector information — free reuse',
26
+ coverage: 'Lebenswichtige Regeln per Arbeitsbereich, Merkblaetter, Safe@Work-Kampagne',
27
+ last_retrieved: lastIngest?.value,
28
+ },
29
+ {
30
+ name: 'Suva — Arbeitssicherheit Landwirtschaft',
31
+ authority: 'Schweizerische Unfallversicherungsanstalt (Suva)',
32
+ official_url: 'https://www.suva.ch/de-ch/praevention/nach-branche/landwirtschaft',
33
+ retrieval_method: 'PDF_EXTRACT + HTML_SCRAPE',
34
+ update_frequency: 'periodic',
35
+ license: 'Swiss public-sector information — free reuse',
36
+ coverage: 'MAK-Werte, Grenzwerte, Unfallmeldung, PSA-Anforderungen',
37
+ last_retrieved: lastIngest?.value,
38
+ },
39
+ {
40
+ name: 'EKAS — Branchenloesung Landwirtschaft',
41
+ authority: 'Eidgenoessische Koordinationskommission fuer Arbeitssicherheit (EKAS)',
42
+ official_url: 'https://www.ekas.admin.ch',
43
+ retrieval_method: 'PDF_EXTRACT',
44
+ update_frequency: 'periodic',
45
+ license: 'Swiss Federal Administration — free reuse',
46
+ coverage: 'ASA-Konzept, systematische Sicherheitsarbeit, Branchenspezifisches',
47
+ last_retrieved: lastIngest?.value,
48
+ },
49
+ {
50
+ name: 'Agriss — Unfallstatistik Schweizer Landwirtschaft',
51
+ authority: 'BUL / Agroscope',
52
+ official_url: 'https://www.bul.ch/de/themen/unfallstatistik',
53
+ retrieval_method: 'PDF_EXTRACT',
54
+ update_frequency: 'annual',
55
+ license: 'Swiss public-sector information — free reuse',
56
+ coverage: 'Toedliche und schwere Unfaelle, Unfallursachen, saisonale Verteilung',
57
+ last_retrieved: lastIngest?.value,
58
+ },
59
+ ];
60
+
61
+ return {
62
+ sources,
63
+ _meta: buildMeta(),
64
+ };
65
+ }
@@ -0,0 +1,51 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import { validateJurisdiction } from '../jurisdiction.js';
3
+ import type { Database } from '../db.js';
4
+
5
+ interface BulGuidanceArgs {
6
+ query: string;
7
+ jurisdiction?: string;
8
+ }
9
+
10
+ export function handleSearchBulGuidance(db: Database, args: BulGuidanceArgs) {
11
+ const jv = validateJurisdiction(args.jurisdiction);
12
+ if (!jv.valid) return jv.error;
13
+
14
+ // Search bul_guidance table using LIKE since it is not indexed in FTS
15
+ const words = args.query.split(/\s+/).filter(w => w.length > 1);
16
+ if (words.length === 0) {
17
+ return {
18
+ query: args.query,
19
+ jurisdiction: jv.jurisdiction,
20
+ results_count: 0,
21
+ results: [],
22
+ _meta: buildMeta({ source_url: 'https://www.bul.ch' }),
23
+ };
24
+ }
25
+
26
+ const conditions = words.map(() =>
27
+ '(LOWER(topic) LIKE LOWER(?) OR LOWER(title) LIKE LOWER(?) OR LOWER(description) LIKE LOWER(?))'
28
+ ).join(' AND ');
29
+ const params: unknown[] = words.flatMap(w => [`%${w}%`, `%${w}%`, `%${w}%`]);
30
+ params.push(jv.jurisdiction);
31
+
32
+ const results = db.all<{
33
+ id: number; topic: string; title: string; description: string; url: string;
34
+ }>(
35
+ `SELECT * FROM bul_guidance WHERE ${conditions} AND jurisdiction = ? ORDER BY topic LIMIT 20`,
36
+ params
37
+ );
38
+
39
+ return {
40
+ query: args.query,
41
+ jurisdiction: jv.jurisdiction,
42
+ results_count: results.length,
43
+ results: results.map(r => ({
44
+ topic: r.topic,
45
+ title: r.title,
46
+ description: r.description,
47
+ url: r.url,
48
+ })),
49
+ _meta: buildMeta({ source_url: 'https://www.bul.ch' }),
50
+ };
51
+ }