@ansvar/ch-farm-grants-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 +18 -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 +28 -0
  7. package/.github/workflows/publish.yml +24 -0
  8. package/CHANGELOG.md +22 -0
  9. package/CODEOWNERS +1 -0
  10. package/COVERAGE.md +45 -0
  11. package/DISCLAIMER.md +39 -0
  12. package/Dockerfile +26 -0
  13. package/LICENSE +17 -0
  14. package/PRIVACY.md +36 -0
  15. package/README.md +80 -0
  16. package/SECURITY.md +31 -0
  17. package/TOOLS.md +154 -0
  18. package/data/coverage.json +19 -0
  19. package/data/database.db +0 -0
  20. package/data/sources.yml +29 -0
  21. package/dist/db.d.ts +25 -0
  22. package/dist/db.d.ts.map +1 -0
  23. package/dist/db.js +167 -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 +261 -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 +22 -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 +207 -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 +28 -0
  44. package/dist/tools/about.js.map +1 -0
  45. package/dist/tools/check-eligibility.d.ts +31 -0
  46. package/dist/tools/check-eligibility.d.ts.map +1 -0
  47. package/dist/tools/check-eligibility.js +57 -0
  48. package/dist/tools/check-eligibility.js.map +1 -0
  49. package/dist/tools/check-freshness.d.ts +15 -0
  50. package/dist/tools/check-freshness.d.ts.map +1 -0
  51. package/dist/tools/check-freshness.js +26 -0
  52. package/dist/tools/check-freshness.js.map +1 -0
  53. package/dist/tools/get-application-deadlines.d.ts +27 -0
  54. package/dist/tools/get-application-deadlines.d.ts.map +1 -0
  55. package/dist/tools/get-application-deadlines.js +40 -0
  56. package/dist/tools/get-application-deadlines.js.map +1 -0
  57. package/dist/tools/get-grant-details.d.ts +42 -0
  58. package/dist/tools/get-grant-details.d.ts.map +1 -0
  59. package/dist/tools/get-grant-details.js +24 -0
  60. package/dist/tools/get-grant-details.js.map +1 -0
  61. package/dist/tools/get-payment-rates.d.ts +49 -0
  62. package/dist/tools/get-payment-rates.d.ts.map +1 -0
  63. package/dist/tools/get-payment-rates.js +37 -0
  64. package/dist/tools/get-payment-rates.js.map +1 -0
  65. package/dist/tools/list-grant-options.d.ts +56 -0
  66. package/dist/tools/list-grant-options.d.ts.map +1 -0
  67. package/dist/tools/list-grant-options.js +39 -0
  68. package/dist/tools/list-grant-options.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-application-guidance.d.ts +36 -0
  74. package/dist/tools/search-application-guidance.d.ts.map +1 -0
  75. package/dist/tools/search-application-guidance.js +52 -0
  76. package/dist/tools/search-application-guidance.js.map +1 -0
  77. package/dist/tools/search-grants.d.ts +25 -0
  78. package/dist/tools/search-grants.d.ts.map +1 -0
  79. package/dist/tools/search-grants.js +26 -0
  80. package/dist/tools/search-grants.js.map +1 -0
  81. package/docker-compose.yml +12 -0
  82. package/eslint.config.js +26 -0
  83. package/package.json +54 -0
  84. package/scripts/ingest.ts +742 -0
  85. package/server.json +41 -0
  86. package/src/db.ts +208 -0
  87. package/src/http-server.ts +293 -0
  88. package/src/jurisdiction.ts +30 -0
  89. package/src/metadata.ts +32 -0
  90. package/src/server.ts +230 -0
  91. package/src/tools/about.ts +29 -0
  92. package/src/tools/check-eligibility.ts +81 -0
  93. package/src/tools/check-freshness.ts +42 -0
  94. package/src/tools/get-application-deadlines.ts +55 -0
  95. package/src/tools/get-grant-details.ts +51 -0
  96. package/src/tools/get-payment-rates.ts +60 -0
  97. package/src/tools/list-grant-options.ts +63 -0
  98. package/src/tools/list-sources.ts +65 -0
  99. package/src/tools/search-application-guidance.ts +59 -0
  100. package/src/tools/search-grants.ts +35 -0
  101. package/tests/db.test.ts +69 -0
  102. package/tests/helpers/seed-db.ts +188 -0
  103. package/tests/jurisdiction.test.ts +35 -0
  104. package/tests/tools/about.test.ts +23 -0
  105. package/tests/tools/check-freshness.test.ts +53 -0
  106. package/tests/tools/list-sources.test.ts +47 -0
  107. package/tests/tools/search-grants.test.ts +57 -0
  108. package/tsconfig.json +19 -0
@@ -0,0 +1,59 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import { validateJurisdiction } from '../jurisdiction.js';
3
+ import { ftsSearch, type Database } from '../db.js';
4
+
5
+ interface GuidanceArgs {
6
+ query: string;
7
+ jurisdiction?: string;
8
+ }
9
+
10
+ export function handleSearchApplicationGuidance(db: Database, args: GuidanceArgs) {
11
+ const jv = validateJurisdiction(args.jurisdiction);
12
+ if (!jv.valid) return jv.error;
13
+
14
+ // Search across grants for application-related content
15
+ const results = ftsSearch(db, args.query, 20);
16
+
17
+ // Also return general application guidance
18
+ const guidance = {
19
+ general_process: [
20
+ 'Gesuch beim kantonalen Landwirtschaftsamt einreichen (vor Baubeginn)',
21
+ 'Vorabklaerung mit kantonaler Fachstelle empfohlen',
22
+ 'Kostenvoranschlag und Plaene beilegen',
23
+ 'Baubewilligung der Gemeinde erforderlich (separat)',
24
+ 'Bauausfuehrung erst nach Bewilligung beginnen — sonst kein Beitrag',
25
+ 'Abrechnung nach Fertigstellung mit Rechnungsbelegen',
26
+ 'AGRIDEA-Beratung fuer Projektentwicklung verfuegbar',
27
+ ],
28
+ required_documents: [
29
+ 'Gesuchsformular (kantonal)',
30
+ 'Betriebskonzept / Businessplan',
31
+ 'Bau- und Situationsplaene',
32
+ 'Kostenvoranschlag (3 Offerten bei grossen Projekten)',
33
+ 'Finanzierungsplan (Eigenkapital, Kredit, Beitraege)',
34
+ 'Bodennutzungsplan / SAK-Berechnung',
35
+ 'Baubewilligung oder Vorentscheid',
36
+ 'Bei Gemeinschaftsprojekten: Statuten / Vereinbarung',
37
+ ],
38
+ contact_points: [
39
+ { role: 'Kantonales Landwirtschaftsamt', description: 'Erstanlaufstelle, Gesuchseinreichung, kantonale Beitraege' },
40
+ { role: 'AGRIDEA', description: 'Projektberatung, Machbarkeitsstudien, Wirtschaftlichkeitsberechnungen', url: 'https://www.agridea.ch' },
41
+ { role: 'Schweizer Berghilfe', description: 'Ergaenzende private Foerderung fuer Bergbetriebe', url: 'https://www.berghilfe.ch' },
42
+ { role: 'BLW Strukturverbesserungen', description: 'Bundesebene, Investitionskredite', url: 'https://www.blw.admin.ch/blw/de/home/instrumente/strukturverbesserungen.html' },
43
+ ],
44
+ };
45
+
46
+ return {
47
+ query: args.query,
48
+ jurisdiction: jv.jurisdiction,
49
+ search_results_count: results.length,
50
+ search_results: results.map(r => ({
51
+ title: r.title,
52
+ body: r.body,
53
+ grant_type: r.grant_type,
54
+ relevance_rank: r.rank,
55
+ })),
56
+ application_guidance: guidance,
57
+ _meta: buildMeta(),
58
+ };
59
+ }
@@ -0,0 +1,35 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import { validateJurisdiction } from '../jurisdiction.js';
3
+ import { ftsSearch, type Database } from '../db.js';
4
+
5
+ interface SearchArgs {
6
+ query: string;
7
+ grant_type?: string;
8
+ jurisdiction?: string;
9
+ limit?: number;
10
+ }
11
+
12
+ export function handleSearchGrants(db: Database, args: SearchArgs) {
13
+ const jv = validateJurisdiction(args.jurisdiction);
14
+ if (!jv.valid) return jv.error;
15
+
16
+ const limit = Math.min(args.limit ?? 20, 50);
17
+ let results = ftsSearch(db, args.query, limit);
18
+
19
+ if (args.grant_type) {
20
+ results = results.filter(r => r.grant_type.toLowerCase() === args.grant_type!.toLowerCase());
21
+ }
22
+
23
+ return {
24
+ query: args.query,
25
+ jurisdiction: jv.jurisdiction,
26
+ results_count: results.length,
27
+ results: results.map(r => ({
28
+ title: r.title,
29
+ body: r.body,
30
+ grant_type: r.grant_type,
31
+ relevance_rank: r.rank,
32
+ })),
33
+ _meta: buildMeta(),
34
+ };
35
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from 'vitest';
2
+ import { createDatabase, type Database } from '../src/db.js';
3
+ import { existsSync, unlinkSync } from 'fs';
4
+
5
+ const TEST_DB = 'tests/test-database.db';
6
+
7
+ describe('database layer', () => {
8
+ let db: Database;
9
+
10
+ beforeAll(() => {
11
+ db = createDatabase(TEST_DB);
12
+ });
13
+
14
+ afterAll(() => {
15
+ db.close();
16
+ if (existsSync(TEST_DB)) unlinkSync(TEST_DB);
17
+ });
18
+
19
+ test('creates database with db_metadata table', () => {
20
+ const row = db.get<{ value: string }>(
21
+ 'SELECT value FROM db_metadata WHERE key = ?',
22
+ ['schema_version']
23
+ );
24
+ expect(row?.value).toBe('1.0');
25
+ });
26
+
27
+ test('FTS5 search_index exists', () => {
28
+ const result = db.all<{ name: string }>(
29
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='search_index'"
30
+ );
31
+ expect(result).toHaveLength(1);
32
+ });
33
+
34
+ test('journal mode is DELETE', () => {
35
+ const row = db.get<{ journal_mode: string }>('PRAGMA journal_mode');
36
+ expect(row?.journal_mode).toBe('delete');
37
+ });
38
+
39
+ test('foreign keys are enabled', () => {
40
+ const row = db.get<{ foreign_keys: number }>('PRAGMA foreign_keys');
41
+ expect(row?.foreign_keys).toBe(1);
42
+ });
43
+
44
+ test('all domain tables exist', () => {
45
+ const tables = ['grants', 'grant_options', 'eligibility_rules', 'application_deadlines'];
46
+ for (const table of tables) {
47
+ const result = db.all<{ name: string }>(
48
+ `SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`
49
+ );
50
+ expect(result, `table ${table} should exist`).toHaveLength(1);
51
+ }
52
+ });
53
+
54
+ test('mcp_name metadata is set', () => {
55
+ const row = db.get<{ value: string }>(
56
+ 'SELECT value FROM db_metadata WHERE key = ?',
57
+ ['mcp_name']
58
+ );
59
+ expect(row?.value).toBe('Switzerland Farm Grants MCP');
60
+ });
61
+
62
+ test('jurisdiction metadata is CH', () => {
63
+ const row = db.get<{ value: string }>(
64
+ 'SELECT value FROM db_metadata WHERE key = ?',
65
+ ['jurisdiction']
66
+ );
67
+ expect(row?.value).toBe('CH');
68
+ });
69
+ });
@@ -0,0 +1,188 @@
1
+ import { createDatabase, type Database } from '../../src/db.js';
2
+
3
+ export function createSeededDatabase(dbPath: string): Database {
4
+ const db = createDatabase(dbPath);
5
+
6
+ // --- Grants ---
7
+ const grants: [string, string, string, string, number | null, number | null, number | null, string | null, string, string][] = [
8
+ [
9
+ 'ik-oekonomiegebaeude',
10
+ 'Investitionskredit Oekonomiegebaeude',
11
+ 'investitionskredit',
12
+ 'Zinsloses Darlehen des Bundes fuer den Bau oder Umbau von Oekonomiegebaeuden (Staelle, Scheunen, Remisen).',
13
+ null,
14
+ null,
15
+ 20,
16
+ JSON.stringify({ bergzone_i: '+10% Beitragssatz', bergzone_ii: '+15% Beitragssatz', soemmerungsgebiet: '+20% Beitragssatz' }),
17
+ 'SVV Art. 44 ff.; BLW Weisungen Investitionskredite',
18
+ 'DE',
19
+ ],
20
+ [
21
+ 'beitrag-stallbau',
22
+ 'Beitrag Stallbau',
23
+ 'beitrag',
24
+ 'Bundesbeitrag fuer den Neubau oder Umbau von Staellen. Beitragssatz abhaengig von Zone und Tierkategorie.',
25
+ 30,
26
+ 200000,
27
+ null,
28
+ JSON.stringify({ huegelzone: '+5% Beitragssatz', bergzone_i: '+10% Beitragssatz' }),
29
+ 'SVV Art. 18 ff.',
30
+ 'DE',
31
+ ],
32
+ [
33
+ 'meliorationsbeitrag-wegebau',
34
+ 'Meliorationsbeitrag Wegebau',
35
+ 'meliorationsbeitrag',
36
+ 'Beitrag fuer den Bau und Unterhalt von landwirtschaftlichen Gueterwegen und Erschliessungsstrassen.',
37
+ 40,
38
+ 500000,
39
+ null,
40
+ null,
41
+ 'SVV Art. 14 ff.',
42
+ 'DE',
43
+ ],
44
+ [
45
+ 'pre-regionale-entwicklung',
46
+ 'Projekt zur regionalen Entwicklung (PRE)',
47
+ 'pre',
48
+ 'Foerderung von Projekten zur regionalen Entwicklung in laendlichen Gebieten.',
49
+ 50,
50
+ 1000000,
51
+ 8,
52
+ null,
53
+ 'SVV Art. 93 ff.; LwG Art. 93',
54
+ 'DE',
55
+ ],
56
+ [
57
+ 'starthilfe-junglandwirte',
58
+ 'Starthilfe Junglandwirte',
59
+ 'starthilfe',
60
+ 'Zinsloses Darlehen oder Beitrag fuer Junglandwirte bei Betriebsuebernahme.',
61
+ null,
62
+ 250000,
63
+ 15,
64
+ null,
65
+ 'SVV Art. 86 ff.',
66
+ 'DE',
67
+ ],
68
+ [
69
+ 'ressourcenprogramm-ammoniak',
70
+ 'Ressourcenprogramm Ammoniakreduktion',
71
+ 'ressourcenprogramm',
72
+ 'Beitraege fuer Massnahmen zur Reduktion von Ammoniakemissionen in der Landwirtschaft.',
73
+ 80,
74
+ null,
75
+ 6,
76
+ null,
77
+ 'LwG Art. 77a/77b',
78
+ 'DE',
79
+ ],
80
+ [
81
+ 'gewaesserschutz-nitrat',
82
+ 'Gewaesserschutzprojekt Nitratreduktion',
83
+ 'gewaesserschutz',
84
+ 'Beitraege fuer Massnahmen zur Reduktion von Nitratbelastung in Gewaessern.',
85
+ 80,
86
+ null,
87
+ 6,
88
+ null,
89
+ 'GSchG Art. 62a',
90
+ 'DE',
91
+ ],
92
+ ];
93
+
94
+ for (const [id, name, grantType, desc, rate, maxAmt, duration, zoneBonus, legalBasis, lang] of grants) {
95
+ db.run(
96
+ `INSERT INTO grants (id, name, grant_type, description, federal_rate_pct, max_amount, duration_years, zone_bonus, legal_basis, language, jurisdiction)
97
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'CH')`,
98
+ [id, name, grantType, desc, rate, maxAmt, duration, zoneBonus, legalBasis, lang]
99
+ );
100
+ }
101
+
102
+ // --- Grant Options ---
103
+ const grantOptions: [string, string, string, string, string][] = [
104
+ ['ik-oekonomiegebaeude', 'Neubau Milchviehstall', 'Neubau eines BTS-konformen Milchviehstalls', 'Darlehen bis max. anrechenbare Kosten', 'BTS-Konformitaet erforderlich'],
105
+ ['ik-oekonomiegebaeude', 'Umbau Scheune', 'Umbau bestehender Oekonomiegebaeude', 'Darlehen bis max. anrechenbare Kosten', 'Baubewilligung erforderlich'],
106
+ ['beitrag-stallbau', 'Laufstall Milchvieh', 'Beitrag fuer Laufstallsysteme mit BTS', '30% der anrechenbaren Kosten', 'Min. 20 GVE'],
107
+ ['beitrag-stallbau', 'Anbindestall Umbau', 'Beitrag fuer Umbau von Anbinde- zu Laufstallhaltung', '35% der anrechenbaren Kosten', 'Verbesserung Tierwohl nachweisen'],
108
+ ['meliorationsbeitrag-wegebau', 'Gueterweg Neubau', 'Neubau landwirtschaftlicher Gueterwege', '40% der anrechenbaren Kosten', 'Erschliessung von min. 3 Betrieben'],
109
+ ['pre-regionale-entwicklung', 'Verarbeitung und Vermarktung', 'Regionale Verarbeitungs- und Vermarktungsprojekte', 'Bis 50% Bundesbeitrag', 'Min. 5 Betriebe beteiligt'],
110
+ ];
111
+
112
+ for (const [grantId, optionName, desc, rate, conditions] of grantOptions) {
113
+ db.run(
114
+ `INSERT INTO grant_options (grant_id, option_name, description, rate, conditions)
115
+ VALUES (?, ?, ?, ?, ?)`,
116
+ [grantId, optionName, desc, rate, conditions]
117
+ );
118
+ }
119
+
120
+ // --- Eligibility Rules ---
121
+ const eligibilityRules: [string, string | null, string | null, string | null, string][] = [
122
+ ['ik-oekonomiegebaeude', 'milchwirtschaft', 'stallbau', null, 'Betrieb muss SAK-Mindestgroesse erfuellen (mind. 1.0 SAK)'],
123
+ ['ik-oekonomiegebaeude', 'bergbetrieb', 'stallbau', 'bergzone_i', 'Betrieb im Berggebiet, SAK 0.75 genuegend'],
124
+ ['ik-oekonomiegebaeude', 'alle', 'hofduengerlager', null, 'Gewaesserschutzkonformitaet nachweisen'],
125
+ ['beitrag-stallbau', 'milchwirtschaft', 'stallbau', null, 'BTS-Konformitaet, min. 20 GVE'],
126
+ ['beitrag-stallbau', 'mutterkuhhaltung', 'stallbau', null, 'BTS-Konformitaet, min. 15 GVE'],
127
+ ['beitrag-stallbau', 'bergbetrieb', 'stallbau', 'bergzone_i', 'BTS-Konformitaet, min. 10 GVE im Berggebiet'],
128
+ ['meliorationsbeitrag-wegebau', 'alle', 'wegebau', null, 'Erschliessung von min. 3 Betrieben, Traegerschaft erforderlich'],
129
+ ['starthilfe-junglandwirte', 'junglandwirt', null, null, 'Max. 35 Jahre alt bei Betriebsuebernahme, landwirtschaftliche Ausbildung'],
130
+ ['pre-regionale-entwicklung', 'gemeinschaft', null, null, 'Min. 5 Betriebe, Traegerschaft, regionale Wertschoepfung'],
131
+ ['ressourcenprogramm-ammoniak', 'alle', null, null, 'Teilnahme an kantonal genehmigtem Ressourcenprogramm'],
132
+ ['gewaesserschutz-nitrat', 'ackerbau', null, null, 'Betrieb in Projektperimeter eines Gewaesserschutzprojekts'],
133
+ ];
134
+
135
+ for (const [grantId, farmType, investType, zone, req] of eligibilityRules) {
136
+ db.run(
137
+ `INSERT INTO eligibility_rules (grant_id, farm_type, investment_type, zone, requirement)
138
+ VALUES (?, ?, ?, ?, ?)`,
139
+ [grantId, farmType, investType, zone, req]
140
+ );
141
+ }
142
+
143
+ // --- Application Deadlines ---
144
+ const deadlines: [string, string | null, string | null, string | null][] = [
145
+ ['ik-oekonomiegebaeude', null, null, 'Gesuch jederzeit beim kantonalen Landwirtschaftsamt einreichen'],
146
+ ['ik-oekonomiegebaeude', 'BE', '2026-03-31', 'Kanton Bern: Frist 31. Maerz fuer Fruehjahrsrunde'],
147
+ ['ik-oekonomiegebaeude', 'ZH', '2026-06-30', 'Kanton Zuerich: Frist 30. Juni'],
148
+ ['beitrag-stallbau', null, null, 'Gesuch vor Baubeginn einreichen'],
149
+ ['beitrag-stallbau', 'GR', '2026-04-30', 'Kanton Graubuenden: Frist 30. April'],
150
+ ['starthilfe-junglandwirte', null, null, 'Innerhalb 2 Jahren nach Betriebsuebernahme'],
151
+ ['pre-regionale-entwicklung', null, null, 'Projektskizze beim BLW einreichen, keine feste Frist'],
152
+ ];
153
+
154
+ for (const [grantId, canton, deadline, notes] of deadlines) {
155
+ db.run(
156
+ `INSERT INTO application_deadlines (grant_id, canton, deadline_date, notes)
157
+ VALUES (?, ?, ?, ?)`,
158
+ [grantId, canton, deadline, notes]
159
+ );
160
+ }
161
+
162
+ // --- FTS5 Search Index ---
163
+ const ftsData: [string, string, string, string][] = [
164
+ ['Investitionskredit Oekonomiegebaeude', 'Zinsloses Darlehen fuer Stallbau, Scheunenbau, Remisen. SVV Art. 44. Laufzeit 20 Jahre. BTS-konforme Staelle bevorzugt.', 'investitionskredit', 'CH'],
165
+ ['Beitrag Stallbau', 'Bundesbeitrag fuer Neubau oder Umbau von Staellen. Beitragssatz 30% in der Talzone, hoeher im Berggebiet. BTS Tierwohl.', 'beitrag', 'CH'],
166
+ ['Meliorationsbeitrag Wegebau', 'Beitrag fuer landwirtschaftliche Gueterwege und Erschliessungsstrassen. Beitragssatz 40%.', 'meliorationsbeitrag', 'CH'],
167
+ ['PRE Regionale Entwicklung', 'Projekte zur regionalen Entwicklung in laendlichen Gebieten. Verarbeitung, Vermarktung, Agrotourismus.', 'pre', 'CH'],
168
+ ['Starthilfe Junglandwirte', 'Zinsloses Darlehen fuer Junglandwirte bei Betriebsuebernahme. Max. 35 Jahre. Landwirtschaftliche Ausbildung.', 'starthilfe', 'CH'],
169
+ ['Ressourcenprogramm Ammoniak', 'Beitraege zur Reduktion von Ammoniakemissionen. LwG Art. 77a. Schleppschlauch, Abdeckung Guellelager.', 'ressourcenprogramm', 'CH'],
170
+ ['Gewaesserschutz Nitrat', 'Beitraege zur Reduktion von Nitratbelastung. GSchG Art. 62a. Extensivierung in Projektperimetern.', 'gewaesserschutz', 'CH'],
171
+ ['Hofduengerlager Investitionskredit', 'Darlehen fuer Bau und Erweiterung von Guellegruben, Mistplatten. Gewaesserschutzkonformitaet.', 'investitionskredit', 'CH'],
172
+ ['Alpgebaeude Sanierung Berggebiet', 'Investitionskredit und Beitrag fuer Sanierung von Alpgebaeuden. Erhoehte Saetze im Soemmerungsgebiet.', 'beitrag', 'CH'],
173
+ ['Kaeserei Gemeinschaftsprojekt', 'Investitionskredit fuer Bau oder Umbau von Alpkaesereien. Gemeinschaftliche Traegerschaft.', 'investitionskredit', 'CH'],
174
+ ];
175
+
176
+ for (const [title, body, grantType, jurisdiction] of ftsData) {
177
+ db.run(
178
+ `INSERT INTO search_index (title, body, grant_type, jurisdiction) VALUES (?, ?, ?, ?)`,
179
+ [title, body, grantType, jurisdiction]
180
+ );
181
+ }
182
+
183
+ // --- Metadata ---
184
+ db.run("INSERT OR REPLACE INTO db_metadata (key, value) VALUES ('last_ingest', '2026-04-05')", []);
185
+ db.run("INSERT OR REPLACE INTO db_metadata (key, value) VALUES ('build_date', '2026-04-05')", []);
186
+
187
+ return db;
188
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { validateJurisdiction, SUPPORTED_JURISDICTIONS } from '../src/jurisdiction.js';
3
+
4
+ describe('jurisdiction validation', () => {
5
+ test('accepts CH', () => {
6
+ const result = validateJurisdiction('CH');
7
+ expect(result).toEqual({ valid: true, jurisdiction: 'CH' });
8
+ });
9
+
10
+ test('defaults to CH when undefined', () => {
11
+ const result = validateJurisdiction(undefined);
12
+ expect(result).toEqual({ valid: true, jurisdiction: 'CH' });
13
+ });
14
+
15
+ test('rejects unsupported jurisdiction', () => {
16
+ const result = validateJurisdiction('DE');
17
+ expect(result).toEqual({
18
+ valid: false,
19
+ error: {
20
+ error: 'jurisdiction_not_supported',
21
+ supported: ['CH'],
22
+ message: 'This server currently covers Switzerland. More jurisdictions are planned.',
23
+ },
24
+ });
25
+ });
26
+
27
+ test('normalises lowercase input', () => {
28
+ const result = validateJurisdiction('ch');
29
+ expect(result).toEqual({ valid: true, jurisdiction: 'CH' });
30
+ });
31
+
32
+ test('SUPPORTED_JURISDICTIONS contains CH', () => {
33
+ expect(SUPPORTED_JURISDICTIONS).toContain('CH');
34
+ });
35
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { handleAbout } from '../../src/tools/about.js';
3
+
4
+ describe('about tool', () => {
5
+ test('returns server metadata', () => {
6
+ const result = handleAbout();
7
+ expect(result.name).toBe('Switzerland Farm Grants MCP');
8
+ expect(result.description).toContain('SVV');
9
+ expect(result.description).toContain('Investitionskredite');
10
+ expect(result.jurisdiction).toEqual(['CH']);
11
+ expect(result.tools_count).toBe(10);
12
+ expect(result.links).toHaveProperty('homepage');
13
+ expect(result.links).toHaveProperty('repository');
14
+ expect(result._meta).toHaveProperty('disclaimer');
15
+ });
16
+
17
+ test('data_sources lists all sources', () => {
18
+ const result = handleAbout();
19
+ expect(result.data_sources).toContain('Strukturverbesserungsverordnung (SVV, SR 913.1)');
20
+ expect(result.data_sources).toContain('BLW Weisungen Investitionskredite und Beitraege');
21
+ expect(result.data_sources.length).toBe(4);
22
+ });
23
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from 'vitest';
2
+ import { handleCheckFreshness } from '../../src/tools/check-freshness.js';
3
+ import { createDatabase, type Database } from '../../src/db.js';
4
+ import { existsSync, unlinkSync } from 'fs';
5
+
6
+ const TEST_DB = 'tests/test-check-freshness.db';
7
+
8
+ describe('check_data_freshness tool', () => {
9
+ let db: Database;
10
+
11
+ beforeAll(() => {
12
+ db = createDatabase(TEST_DB);
13
+ });
14
+
15
+ afterAll(() => {
16
+ db.close();
17
+ if (existsSync(TEST_DB)) unlinkSync(TEST_DB);
18
+ });
19
+
20
+ test('returns unknown when no ingest date', () => {
21
+ const result = handleCheckFreshness(db);
22
+ expect(result.status).toBe('unknown');
23
+ expect(result.days_since_ingest).toBeNull();
24
+ });
25
+
26
+ test('returns fresh when recently ingested', () => {
27
+ const today = new Date().toISOString().split('T')[0];
28
+ db.run("INSERT OR REPLACE INTO db_metadata (key, value) VALUES ('last_ingest', ?)", [today]);
29
+
30
+ const result = handleCheckFreshness(db);
31
+ expect(result.status).toBe('fresh');
32
+ expect(result.days_since_ingest).toBeLessThanOrEqual(1);
33
+ });
34
+
35
+ test('returns stale when old ingest', () => {
36
+ db.run("INSERT OR REPLACE INTO db_metadata (key, value) VALUES ('last_ingest', '2025-01-01')", []);
37
+
38
+ const result = handleCheckFreshness(db);
39
+ expect(result.status).toBe('stale');
40
+ expect(result.days_since_ingest).toBeGreaterThan(90);
41
+ });
42
+
43
+ test('includes refresh command', () => {
44
+ const result = handleCheckFreshness(db);
45
+ expect(result.refresh_command).toContain('gh workflow run');
46
+ expect(result.refresh_command).toContain('ch-farm-grants-mcp');
47
+ });
48
+
49
+ test('includes schema_version', () => {
50
+ const result = handleCheckFreshness(db);
51
+ expect(result.schema_version).toBe('1.0');
52
+ });
53
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from 'vitest';
2
+ import { handleListSources } from '../../src/tools/list-sources.js';
3
+ import { createDatabase, type Database } from '../../src/db.js';
4
+ import { existsSync, unlinkSync } from 'fs';
5
+
6
+ const TEST_DB = 'tests/test-list-sources.db';
7
+
8
+ describe('list_sources tool', () => {
9
+ let db: Database;
10
+
11
+ beforeAll(() => {
12
+ db = createDatabase(TEST_DB);
13
+ });
14
+
15
+ afterAll(() => {
16
+ db.close();
17
+ if (existsSync(TEST_DB)) unlinkSync(TEST_DB);
18
+ });
19
+
20
+ test('returns 4 data sources', () => {
21
+ const result = handleListSources(db);
22
+ expect(result.sources).toHaveLength(4);
23
+ });
24
+
25
+ test('first source is SVV', () => {
26
+ const result = handleListSources(db);
27
+ expect(result.sources[0].name).toContain('SVV');
28
+ expect(result.sources[0].authority).toContain('BLW');
29
+ });
30
+
31
+ test('all sources have required fields', () => {
32
+ const result = handleListSources(db);
33
+ for (const source of result.sources) {
34
+ expect(source).toHaveProperty('name');
35
+ expect(source).toHaveProperty('authority');
36
+ expect(source).toHaveProperty('official_url');
37
+ expect(source).toHaveProperty('license');
38
+ expect(source).toHaveProperty('update_frequency');
39
+ }
40
+ });
41
+
42
+ test('includes _meta with disclaimer', () => {
43
+ const result = handleListSources(db);
44
+ expect(result._meta).toHaveProperty('disclaimer');
45
+ expect(result._meta.server).toBe('ch-farm-grants-mcp');
46
+ });
47
+ });
@@ -0,0 +1,57 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from 'vitest';
2
+ import { handleSearchGrants } from '../../src/tools/search-grants.js';
3
+ import { createSeededDatabase } from '../helpers/seed-db.js';
4
+ import type { Database } from '../../src/db.js';
5
+ import { existsSync, unlinkSync } from 'fs';
6
+
7
+ const TEST_DB = 'tests/test-search-grants.db';
8
+
9
+ describe('search_grants tool', () => {
10
+ let db: Database;
11
+
12
+ beforeAll(() => {
13
+ db = createSeededDatabase(TEST_DB);
14
+ });
15
+
16
+ afterAll(() => {
17
+ db.close();
18
+ if (existsSync(TEST_DB)) unlinkSync(TEST_DB);
19
+ });
20
+
21
+ test('returns results for Investitionskredit query', () => {
22
+ const result = handleSearchGrants(db, { query: 'Investitionskredit' });
23
+ expect(result).toHaveProperty('results_count');
24
+ expect((result as { results_count: number }).results_count).toBeGreaterThan(0);
25
+ });
26
+
27
+ test('returns results for Stallbau query', () => {
28
+ const result = handleSearchGrants(db, { query: 'Stallbau' });
29
+ expect(result).toHaveProperty('results_count');
30
+ expect((result as { results_count: number }).results_count).toBeGreaterThan(0);
31
+ });
32
+
33
+ test('respects grant_type filter', () => {
34
+ const result = handleSearchGrants(db, { query: 'Darlehen Beitrag Stallbau Investitionskredit', grant_type: 'beitrag' });
35
+ const typed = result as { results: { grant_type: string }[] };
36
+ for (const r of typed.results) {
37
+ expect(r.grant_type).toBe('beitrag');
38
+ }
39
+ });
40
+
41
+ test('rejects unsupported jurisdiction', () => {
42
+ const result = handleSearchGrants(db, { query: 'Stallbau', jurisdiction: 'DE' });
43
+ expect(result).toHaveProperty('error', 'jurisdiction_not_supported');
44
+ });
45
+
46
+ test('limits results', () => {
47
+ const result = handleSearchGrants(db, { query: 'Investitionskredit Beitrag Stallbau Wegebau PRE Starthilfe Ressourcenprogramm Gewaesserschutz', limit: 3 });
48
+ const typed = result as { results: unknown[] };
49
+ expect(typed.results.length).toBeLessThanOrEqual(3);
50
+ });
51
+
52
+ test('returns _meta with disclaimer', () => {
53
+ const result = handleSearchGrants(db, { query: 'Stallbau' });
54
+ expect(result).toHaveProperty('_meta');
55
+ expect((result as { _meta: { disclaimer: string } })._meta.disclaimer).toBeTruthy();
56
+ });
57
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist", "tests", "scripts"]
19
+ }