@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.
- package/.github/workflows/check-freshness.yml +18 -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 +28 -0
- package/.github/workflows/publish.yml +24 -0
- package/CHANGELOG.md +22 -0
- package/CODEOWNERS +1 -0
- package/COVERAGE.md +45 -0
- package/DISCLAIMER.md +39 -0
- package/Dockerfile +26 -0
- package/LICENSE +17 -0
- package/PRIVACY.md +36 -0
- package/README.md +80 -0
- package/SECURITY.md +31 -0
- package/TOOLS.md +154 -0
- package/data/coverage.json +19 -0
- package/data/database.db +0 -0
- package/data/sources.yml +29 -0
- package/dist/db.d.ts +25 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +167 -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 +261 -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 +207 -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 +28 -0
- package/dist/tools/about.js.map +1 -0
- package/dist/tools/check-eligibility.d.ts +31 -0
- package/dist/tools/check-eligibility.d.ts.map +1 -0
- package/dist/tools/check-eligibility.js +57 -0
- package/dist/tools/check-eligibility.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-application-deadlines.d.ts +27 -0
- package/dist/tools/get-application-deadlines.d.ts.map +1 -0
- package/dist/tools/get-application-deadlines.js +40 -0
- package/dist/tools/get-application-deadlines.js.map +1 -0
- package/dist/tools/get-grant-details.d.ts +42 -0
- package/dist/tools/get-grant-details.d.ts.map +1 -0
- package/dist/tools/get-grant-details.js +24 -0
- package/dist/tools/get-grant-details.js.map +1 -0
- package/dist/tools/get-payment-rates.d.ts +49 -0
- package/dist/tools/get-payment-rates.d.ts.map +1 -0
- package/dist/tools/get-payment-rates.js +37 -0
- package/dist/tools/get-payment-rates.js.map +1 -0
- package/dist/tools/list-grant-options.d.ts +56 -0
- package/dist/tools/list-grant-options.d.ts.map +1 -0
- package/dist/tools/list-grant-options.js +39 -0
- package/dist/tools/list-grant-options.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 +51 -0
- package/dist/tools/list-sources.js.map +1 -0
- package/dist/tools/search-application-guidance.d.ts +36 -0
- package/dist/tools/search-application-guidance.d.ts.map +1 -0
- package/dist/tools/search-application-guidance.js +52 -0
- package/dist/tools/search-application-guidance.js.map +1 -0
- package/dist/tools/search-grants.d.ts +25 -0
- package/dist/tools/search-grants.d.ts.map +1 -0
- package/dist/tools/search-grants.js +26 -0
- package/dist/tools/search-grants.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 +742 -0
- package/server.json +41 -0
- package/src/db.ts +208 -0
- package/src/http-server.ts +293 -0
- package/src/jurisdiction.ts +30 -0
- package/src/metadata.ts +32 -0
- package/src/server.ts +230 -0
- package/src/tools/about.ts +29 -0
- package/src/tools/check-eligibility.ts +81 -0
- package/src/tools/check-freshness.ts +42 -0
- package/src/tools/get-application-deadlines.ts +55 -0
- package/src/tools/get-grant-details.ts +51 -0
- package/src/tools/get-payment-rates.ts +60 -0
- package/src/tools/list-grant-options.ts +63 -0
- package/src/tools/list-sources.ts +65 -0
- package/src/tools/search-application-guidance.ts +59 -0
- package/src/tools/search-grants.ts +35 -0
- package/tests/db.test.ts +69 -0
- package/tests/helpers/seed-db.ts +188 -0
- package/tests/jurisdiction.test.ts +35 -0
- package/tests/tools/about.test.ts +23 -0
- package/tests/tools/check-freshness.test.ts +53 -0
- package/tests/tools/list-sources.test.ts +47 -0
- package/tests/tools/search-grants.test.ts +57 -0
- package/tsconfig.json +19 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
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 { handleSearchGrants } from './tools/search-grants.js';
|
|
15
|
+
import { handleGetGrantDetails } from './tools/get-grant-details.js';
|
|
16
|
+
import { handleGetPaymentRates } from './tools/get-payment-rates.js';
|
|
17
|
+
import { handleCheckEligibility } from './tools/check-eligibility.js';
|
|
18
|
+
import { handleListGrantOptions } from './tools/list-grant-options.js';
|
|
19
|
+
import { handleGetApplicationDeadlines } from './tools/get-application-deadlines.js';
|
|
20
|
+
import { handleSearchApplicationGuidance } from './tools/search-application-guidance.js';
|
|
21
|
+
|
|
22
|
+
const SERVER_NAME = 'ch-farm-grants-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_grants',
|
|
43
|
+
description: 'Search Swiss agricultural structural improvement grants, investment credits, and funding programmes. Use for broad queries about Investitionskredite, Beitraege, Meliorationen, PRE.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object' as const,
|
|
46
|
+
properties: {
|
|
47
|
+
query: { type: 'string', description: 'Free-text search query (German or English)' },
|
|
48
|
+
grant_type: { type: 'string', description: 'Filter by type: investitionskredit, beitrag, meliorationsbeitrag, pre, ressourcenprogramm, gewaesserschutz, starthilfe' },
|
|
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_grant_details',
|
|
57
|
+
description: 'Get full details for a specific grant programme: objectives, contribution rates, conditions, eligibility rules, and sub-options.',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object' as const,
|
|
60
|
+
properties: {
|
|
61
|
+
grant_id: { type: 'string', description: 'Grant ID or name (e.g. investitionskredit-oekonomiegebaeude, starthilfe-junglandwirte)' },
|
|
62
|
+
jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
|
|
63
|
+
},
|
|
64
|
+
required: ['grant_id'],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'get_payment_rates',
|
|
69
|
+
description: 'Get federal and cantonal contribution rates for a grant programme, with zone-specific bonuses (Berggebiet, Huegelzone).',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object' as const,
|
|
72
|
+
properties: {
|
|
73
|
+
grant_id: { type: 'string', description: 'Grant ID or name' },
|
|
74
|
+
zone: { type: 'string', description: 'Altitude zone: talzone, huegelzone, bergzone_i, bergzone_ii, bergzone_iii, bergzone_iv, soemmerungsgebiet' },
|
|
75
|
+
jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
|
|
76
|
+
},
|
|
77
|
+
required: ['grant_id'],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'check_eligibility',
|
|
82
|
+
description: 'Check which grants a farm is eligible for based on farm type, planned investment, and altitude zone.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object' as const,
|
|
85
|
+
properties: {
|
|
86
|
+
farm_type: { type: 'string', description: 'Farm type: milchwirtschaft, ackerbau, mutterkuhhaltung, bergbetrieb, alpwirtschaft, gemischt, gemeinschaft, junglandwirt' },
|
|
87
|
+
investment_type: { type: 'string', description: 'Investment type: stallbau, hofduengerlager, wohnhaus, diversifikation, alpgebaeude, wegebau, wasserversorgung, biogasanlage, kaeserei' },
|
|
88
|
+
zone: { type: 'string', description: 'Altitude zone: talzone, huegelzone, bergzone_i-iv, soemmerungsgebiet' },
|
|
89
|
+
jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
|
|
90
|
+
},
|
|
91
|
+
required: ['farm_type'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'list_grant_options',
|
|
96
|
+
description: 'List all sub-options (Massnahmen) within a grant programme, or list all grants with their option counts.',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object' as const,
|
|
99
|
+
properties: {
|
|
100
|
+
grant_id: { type: 'string', description: 'Grant ID to list options for. If omitted, lists all grants.' },
|
|
101
|
+
jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'get_application_deadlines',
|
|
107
|
+
description: 'Get application deadlines for grants, optionally filtered by canton (2-letter code: ZH, BE, LU, etc.).',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object' as const,
|
|
110
|
+
properties: {
|
|
111
|
+
grant_id: { type: 'string', description: 'Grant ID to check deadlines for' },
|
|
112
|
+
canton: { type: 'string', description: 'Canton code (e.g. ZH, BE, LU, SG, GR, VS, TI)' },
|
|
113
|
+
jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'search_application_guidance',
|
|
119
|
+
description: 'How to apply for Swiss farm grants: required documents, cantonal contacts, general process, and AGRIDEA advisory support.',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object' as const,
|
|
122
|
+
properties: {
|
|
123
|
+
query: { type: 'string', description: 'Free-text search query about the application process' },
|
|
124
|
+
jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
|
|
125
|
+
},
|
|
126
|
+
required: ['query'],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const SearchGrantsArgsSchema = z.object({
|
|
132
|
+
query: z.string(),
|
|
133
|
+
grant_type: z.string().optional(),
|
|
134
|
+
jurisdiction: z.string().optional(),
|
|
135
|
+
limit: z.number().optional(),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const GrantDetailsArgsSchema = z.object({
|
|
139
|
+
grant_id: z.string(),
|
|
140
|
+
jurisdiction: z.string().optional(),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const PaymentRatesArgsSchema = z.object({
|
|
144
|
+
grant_id: z.string(),
|
|
145
|
+
zone: z.string().optional(),
|
|
146
|
+
jurisdiction: z.string().optional(),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const EligibilityArgsSchema = z.object({
|
|
150
|
+
farm_type: z.string(),
|
|
151
|
+
investment_type: z.string().optional(),
|
|
152
|
+
zone: z.string().optional(),
|
|
153
|
+
jurisdiction: z.string().optional(),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const ListGrantOptionsArgsSchema = z.object({
|
|
157
|
+
grant_id: z.string().optional(),
|
|
158
|
+
jurisdiction: z.string().optional(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const DeadlinesArgsSchema = z.object({
|
|
162
|
+
grant_id: z.string().optional(),
|
|
163
|
+
canton: z.string().optional(),
|
|
164
|
+
jurisdiction: z.string().optional(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const GuidanceArgsSchema = z.object({
|
|
168
|
+
query: z.string(),
|
|
169
|
+
jurisdiction: z.string().optional(),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
function textResult(data: unknown) {
|
|
173
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function errorResult(message: string) {
|
|
177
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: message }) }], isError: true };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const db = createDatabase();
|
|
181
|
+
|
|
182
|
+
const server = new Server(
|
|
183
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
184
|
+
{ capabilities: { tools: {} } }
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
188
|
+
|
|
189
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
190
|
+
const { name, arguments: args = {} } = request.params;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
switch (name) {
|
|
194
|
+
case 'about':
|
|
195
|
+
return textResult(handleAbout());
|
|
196
|
+
case 'list_sources':
|
|
197
|
+
return textResult(handleListSources(db));
|
|
198
|
+
case 'check_data_freshness':
|
|
199
|
+
return textResult(handleCheckFreshness(db));
|
|
200
|
+
case 'search_grants':
|
|
201
|
+
return textResult(handleSearchGrants(db, SearchGrantsArgsSchema.parse(args)));
|
|
202
|
+
case 'get_grant_details':
|
|
203
|
+
return textResult(handleGetGrantDetails(db, GrantDetailsArgsSchema.parse(args)));
|
|
204
|
+
case 'get_payment_rates':
|
|
205
|
+
return textResult(handleGetPaymentRates(db, PaymentRatesArgsSchema.parse(args)));
|
|
206
|
+
case 'check_eligibility':
|
|
207
|
+
return textResult(handleCheckEligibility(db, EligibilityArgsSchema.parse(args)));
|
|
208
|
+
case 'list_grant_options':
|
|
209
|
+
return textResult(handleListGrantOptions(db, ListGrantOptionsArgsSchema.parse(args)));
|
|
210
|
+
case 'get_application_deadlines':
|
|
211
|
+
return textResult(handleGetApplicationDeadlines(db, DeadlinesArgsSchema.parse(args)));
|
|
212
|
+
case 'search_application_guidance':
|
|
213
|
+
return textResult(handleSearchApplicationGuidance(db, GuidanceArgsSchema.parse(args)));
|
|
214
|
+
default:
|
|
215
|
+
return errorResult(`Unknown tool: ${name}`);
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
async function main(): Promise<void> {
|
|
223
|
+
const transport = new StdioServerTransport();
|
|
224
|
+
await server.connect(transport);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
main().catch((err) => {
|
|
228
|
+
process.stderr.write(`Fatal error: ${err.message}\n`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { buildMeta } from '../metadata.js';
|
|
2
|
+
import { SUPPORTED_JURISDICTIONS } from '../jurisdiction.js';
|
|
3
|
+
|
|
4
|
+
export function handleAbout() {
|
|
5
|
+
return {
|
|
6
|
+
name: 'Switzerland Farm Grants MCP',
|
|
7
|
+
description:
|
|
8
|
+
'Swiss agricultural structural improvement grants based on the Strukturverbesserungsverordnung (SVV), ' +
|
|
9
|
+
'BLW investment credit guidelines, and cantonal funding programmes. Covers Investitionskredite (interest-free ' +
|
|
10
|
+
'loans), Beitraege (capital grants), Meliorationen, Projekte zur regionalen Entwicklung (PRE), ' +
|
|
11
|
+
'Ressourcenprogramme (Art. 77a/77b LwG), Gewaesserschutzprojekte (Art. 62a GSchG), and Starthilfe ' +
|
|
12
|
+
'Junglandwirte. All amounts in CHF.',
|
|
13
|
+
version: '0.1.0',
|
|
14
|
+
jurisdiction: [...SUPPORTED_JURISDICTIONS],
|
|
15
|
+
data_sources: [
|
|
16
|
+
'Strukturverbesserungsverordnung (SVV, SR 913.1)',
|
|
17
|
+
'BLW Weisungen Investitionskredite und Beitraege',
|
|
18
|
+
'Kantonale Landwirtschaftsaemter',
|
|
19
|
+
'AGRIDEA Beratungsunterlagen',
|
|
20
|
+
],
|
|
21
|
+
tools_count: 10,
|
|
22
|
+
links: {
|
|
23
|
+
homepage: 'https://ansvar.eu/open-agriculture',
|
|
24
|
+
repository: 'https://github.com/ansvar-systems/ch-farm-grants-mcp',
|
|
25
|
+
mcp_network: 'https://ansvar.ai/mcp',
|
|
26
|
+
},
|
|
27
|
+
_meta: buildMeta(),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { buildMeta } from '../metadata.js';
|
|
2
|
+
import { validateJurisdiction } from '../jurisdiction.js';
|
|
3
|
+
import type { Database } from '../db.js';
|
|
4
|
+
|
|
5
|
+
interface EligibilityArgs {
|
|
6
|
+
farm_type: string;
|
|
7
|
+
investment_type?: string;
|
|
8
|
+
zone?: string;
|
|
9
|
+
jurisdiction?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function handleCheckEligibility(db: Database, args: EligibilityArgs) {
|
|
13
|
+
const jv = validateJurisdiction(args.jurisdiction);
|
|
14
|
+
if (!jv.valid) return jv.error;
|
|
15
|
+
|
|
16
|
+
let sql = `
|
|
17
|
+
SELECT DISTINCT g.id, g.name, g.grant_type, g.description, g.federal_rate_pct,
|
|
18
|
+
g.max_amount, g.duration_years, e.requirement, e.farm_type, e.investment_type, e.zone
|
|
19
|
+
FROM eligibility_rules e
|
|
20
|
+
JOIN grants g ON e.grant_id = g.id
|
|
21
|
+
WHERE g.jurisdiction = ?
|
|
22
|
+
AND (LOWER(e.farm_type) = LOWER(?) OR e.farm_type IS NULL OR e.farm_type = 'alle')
|
|
23
|
+
`;
|
|
24
|
+
const params: unknown[] = [jv.jurisdiction, args.farm_type];
|
|
25
|
+
|
|
26
|
+
if (args.investment_type) {
|
|
27
|
+
sql += ' AND (LOWER(e.investment_type) = LOWER(?) OR e.investment_type IS NULL OR e.investment_type = \'alle\')';
|
|
28
|
+
params.push(args.investment_type);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (args.zone) {
|
|
32
|
+
sql += ' AND (LOWER(e.zone) = LOWER(?) OR e.zone IS NULL OR e.zone = \'alle\')';
|
|
33
|
+
params.push(args.zone);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
sql += ' ORDER BY g.grant_type, g.name';
|
|
37
|
+
|
|
38
|
+
const rows = db.all<{
|
|
39
|
+
id: string; name: string; grant_type: string; description: string;
|
|
40
|
+
federal_rate_pct: number; max_amount: number; duration_years: number;
|
|
41
|
+
requirement: string; farm_type: string; investment_type: string; zone: string;
|
|
42
|
+
}>(sql, params);
|
|
43
|
+
|
|
44
|
+
// Group by grant
|
|
45
|
+
const grantMap = new Map<string, {
|
|
46
|
+
id: string; name: string; grant_type: string; description: string;
|
|
47
|
+
federal_rate_pct: number; max_amount: number; duration_years: number;
|
|
48
|
+
requirements: string[];
|
|
49
|
+
}>();
|
|
50
|
+
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
if (!grantMap.has(row.id)) {
|
|
53
|
+
grantMap.set(row.id, {
|
|
54
|
+
id: row.id,
|
|
55
|
+
name: row.name,
|
|
56
|
+
grant_type: row.grant_type,
|
|
57
|
+
description: row.description,
|
|
58
|
+
federal_rate_pct: row.federal_rate_pct,
|
|
59
|
+
max_amount: row.max_amount,
|
|
60
|
+
duration_years: row.duration_years,
|
|
61
|
+
requirements: [],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const entry = grantMap.get(row.id)!;
|
|
65
|
+
if (row.requirement && !entry.requirements.includes(row.requirement)) {
|
|
66
|
+
entry.requirements.push(row.requirement);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const eligible = Array.from(grantMap.values());
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
farm_type: args.farm_type,
|
|
74
|
+
investment_type: args.investment_type ?? null,
|
|
75
|
+
zone: args.zone ?? null,
|
|
76
|
+
jurisdiction: jv.jurisdiction,
|
|
77
|
+
eligible_grants_count: eligible.length,
|
|
78
|
+
eligible_grants: eligible,
|
|
79
|
+
_meta: buildMeta(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -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-grants-mcp',
|
|
40
|
+
_meta: buildMeta(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { buildMeta } from '../metadata.js';
|
|
2
|
+
import { validateJurisdiction } from '../jurisdiction.js';
|
|
3
|
+
import type { Database } from '../db.js';
|
|
4
|
+
|
|
5
|
+
interface DeadlineArgs {
|
|
6
|
+
grant_id?: string;
|
|
7
|
+
canton?: string;
|
|
8
|
+
jurisdiction?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function handleGetApplicationDeadlines(db: Database, args: DeadlineArgs) {
|
|
12
|
+
const jv = validateJurisdiction(args.jurisdiction);
|
|
13
|
+
if (!jv.valid) return jv.error;
|
|
14
|
+
|
|
15
|
+
let sql = `
|
|
16
|
+
SELECT d.*, g.name as grant_name, g.grant_type
|
|
17
|
+
FROM application_deadlines d
|
|
18
|
+
JOIN grants g ON d.grant_id = g.id
|
|
19
|
+
WHERE g.jurisdiction = ?
|
|
20
|
+
`;
|
|
21
|
+
const params: unknown[] = [jv.jurisdiction];
|
|
22
|
+
|
|
23
|
+
if (args.grant_id) {
|
|
24
|
+
sql += ' AND (d.grant_id = ? OR LOWER(g.name) = LOWER(?))';
|
|
25
|
+
params.push(args.grant_id, args.grant_id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (args.canton) {
|
|
29
|
+
sql += ' AND (UPPER(d.canton) = UPPER(?) OR d.canton IS NULL OR d.canton = \'alle\')';
|
|
30
|
+
params.push(args.canton);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
sql += ' ORDER BY d.deadline_date, g.name';
|
|
34
|
+
|
|
35
|
+
const deadlines = db.all<{
|
|
36
|
+
id: number; grant_id: string; grant_name: string; grant_type: string;
|
|
37
|
+
canton: string; deadline_date: string; notes: string;
|
|
38
|
+
}>(sql, params);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
grant_id: args.grant_id ?? null,
|
|
42
|
+
canton: args.canton ?? null,
|
|
43
|
+
jurisdiction: jv.jurisdiction,
|
|
44
|
+
deadlines_count: deadlines.length,
|
|
45
|
+
deadlines: deadlines.map(d => ({
|
|
46
|
+
grant_id: d.grant_id,
|
|
47
|
+
grant_name: d.grant_name,
|
|
48
|
+
grant_type: d.grant_type,
|
|
49
|
+
canton: d.canton,
|
|
50
|
+
deadline_date: d.deadline_date,
|
|
51
|
+
notes: d.notes,
|
|
52
|
+
})),
|
|
53
|
+
_meta: buildMeta(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -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 GrantDetailsArgs {
|
|
6
|
+
grant_id: string;
|
|
7
|
+
jurisdiction?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function handleGetGrantDetails(db: Database, args: GrantDetailsArgs) {
|
|
11
|
+
const jv = validateJurisdiction(args.jurisdiction);
|
|
12
|
+
if (!jv.valid) return jv.error;
|
|
13
|
+
|
|
14
|
+
const grant = db.get<{
|
|
15
|
+
id: string; name: string; grant_type: string; description: string;
|
|
16
|
+
federal_rate_pct: number; max_amount: number; duration_years: number;
|
|
17
|
+
zone_bonus: string; legal_basis: string; language: string; jurisdiction: string;
|
|
18
|
+
}>(
|
|
19
|
+
'SELECT * FROM grants WHERE (id = ? OR LOWER(name) = LOWER(?)) AND jurisdiction = ?',
|
|
20
|
+
[args.grant_id, args.grant_id, jv.jurisdiction]
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (!grant) {
|
|
24
|
+
return {
|
|
25
|
+
error: 'not_found',
|
|
26
|
+
message: `Grant '${args.grant_id}' not found. Use search_grants to find available grant programmes.`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const options = db.all<{
|
|
31
|
+
option_name: string; description: string; rate: string; conditions: string;
|
|
32
|
+
}>(
|
|
33
|
+
'SELECT option_name, description, rate, conditions FROM grant_options WHERE grant_id = ?',
|
|
34
|
+
[grant.id]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const eligibility = db.all<{
|
|
38
|
+
farm_type: string; investment_type: string; zone: string; requirement: string;
|
|
39
|
+
}>(
|
|
40
|
+
'SELECT farm_type, investment_type, zone, requirement FROM eligibility_rules WHERE grant_id = ?',
|
|
41
|
+
[grant.id]
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...grant,
|
|
46
|
+
zone_bonus: grant.zone_bonus ? JSON.parse(grant.zone_bonus) : null,
|
|
47
|
+
options: options.length > 0 ? options : [],
|
|
48
|
+
eligibility_rules: eligibility.length > 0 ? eligibility : [],
|
|
49
|
+
_meta: buildMeta(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { buildMeta } from '../metadata.js';
|
|
2
|
+
import { validateJurisdiction } from '../jurisdiction.js';
|
|
3
|
+
import type { Database } from '../db.js';
|
|
4
|
+
|
|
5
|
+
interface PaymentRatesArgs {
|
|
6
|
+
grant_id: string;
|
|
7
|
+
zone?: string;
|
|
8
|
+
jurisdiction?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function handleGetPaymentRates(db: Database, args: PaymentRatesArgs) {
|
|
12
|
+
const jv = validateJurisdiction(args.jurisdiction);
|
|
13
|
+
if (!jv.valid) return jv.error;
|
|
14
|
+
|
|
15
|
+
const grant = db.get<{
|
|
16
|
+
id: string; name: string; grant_type: string; federal_rate_pct: number;
|
|
17
|
+
max_amount: number; duration_years: number; zone_bonus: string;
|
|
18
|
+
}>(
|
|
19
|
+
'SELECT id, name, grant_type, federal_rate_pct, max_amount, duration_years, zone_bonus FROM grants WHERE (id = ? OR LOWER(name) = LOWER(?)) AND jurisdiction = ?',
|
|
20
|
+
[args.grant_id, args.grant_id, jv.jurisdiction]
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (!grant) {
|
|
24
|
+
return {
|
|
25
|
+
error: 'not_found',
|
|
26
|
+
message: `Grant '${args.grant_id}' not found. Use search_grants to find available grant programmes.`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const zoneBonus = grant.zone_bonus ? JSON.parse(grant.zone_bonus) as Record<string, string> : null;
|
|
31
|
+
|
|
32
|
+
let applicableZoneInfo: string | null = null;
|
|
33
|
+
if (args.zone && zoneBonus) {
|
|
34
|
+
const zoneKey = Object.keys(zoneBonus).find(k => k.toLowerCase() === args.zone!.toLowerCase());
|
|
35
|
+
applicableZoneInfo = zoneKey ? zoneBonus[zoneKey] : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const options = db.all<{
|
|
39
|
+
option_name: string; rate: string; conditions: string;
|
|
40
|
+
}>(
|
|
41
|
+
'SELECT option_name, rate, conditions FROM grant_options WHERE grant_id = ?',
|
|
42
|
+
[grant.id]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
grant_id: grant.id,
|
|
47
|
+
grant_name: grant.name,
|
|
48
|
+
grant_type: grant.grant_type,
|
|
49
|
+
federal_rate_pct: grant.federal_rate_pct,
|
|
50
|
+
max_amount_chf: grant.max_amount,
|
|
51
|
+
duration_years: grant.duration_years,
|
|
52
|
+
zone_requested: args.zone ?? null,
|
|
53
|
+
zone_bonus: applicableZoneInfo,
|
|
54
|
+
all_zone_bonuses: zoneBonus,
|
|
55
|
+
rate_options: options,
|
|
56
|
+
jurisdiction: jv.jurisdiction,
|
|
57
|
+
currency: 'CHF',
|
|
58
|
+
_meta: buildMeta(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { buildMeta } from '../metadata.js';
|
|
2
|
+
import { validateJurisdiction } from '../jurisdiction.js';
|
|
3
|
+
import type { Database } from '../db.js';
|
|
4
|
+
|
|
5
|
+
interface ListGrantOptionsArgs {
|
|
6
|
+
grant_id?: string;
|
|
7
|
+
jurisdiction?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function handleListGrantOptions(db: Database, args: ListGrantOptionsArgs) {
|
|
11
|
+
const jv = validateJurisdiction(args.jurisdiction);
|
|
12
|
+
if (!jv.valid) return jv.error;
|
|
13
|
+
|
|
14
|
+
if (args.grant_id) {
|
|
15
|
+
const grant = db.get<{ id: string; name: string }>(
|
|
16
|
+
'SELECT id, name FROM grants WHERE (id = ? OR LOWER(name) = LOWER(?)) AND jurisdiction = ?',
|
|
17
|
+
[args.grant_id, args.grant_id, jv.jurisdiction]
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (!grant) {
|
|
21
|
+
return {
|
|
22
|
+
error: 'not_found',
|
|
23
|
+
message: `Grant '${args.grant_id}' not found. Use search_grants to find available grant programmes.`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const options = db.all<{
|
|
28
|
+
id: number; option_name: string; description: string; rate: string; conditions: string;
|
|
29
|
+
}>(
|
|
30
|
+
'SELECT id, option_name, description, rate, conditions FROM grant_options WHERE grant_id = ?',
|
|
31
|
+
[grant.id]
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
grant_id: grant.id,
|
|
36
|
+
grant_name: grant.name,
|
|
37
|
+
jurisdiction: jv.jurisdiction,
|
|
38
|
+
options_count: options.length,
|
|
39
|
+
options,
|
|
40
|
+
_meta: buildMeta(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// List all grants with their option counts
|
|
45
|
+
const grants = db.all<{
|
|
46
|
+
id: string; name: string; grant_type: string; option_count: number;
|
|
47
|
+
}>(
|
|
48
|
+
`SELECT g.id, g.name, g.grant_type, COUNT(o.id) as option_count
|
|
49
|
+
FROM grants g
|
|
50
|
+
LEFT JOIN grant_options o ON g.id = o.grant_id
|
|
51
|
+
WHERE g.jurisdiction = ?
|
|
52
|
+
GROUP BY g.id
|
|
53
|
+
ORDER BY g.grant_type, g.name`,
|
|
54
|
+
[jv.jurisdiction]
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
jurisdiction: jv.jurisdiction,
|
|
59
|
+
grants_count: grants.length,
|
|
60
|
+
grants,
|
|
61
|
+
_meta: buildMeta(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -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: 'Strukturverbesserungsverordnung (SVV, SR 913.1)',
|
|
21
|
+
authority: 'Bundesamt fuer Landwirtschaft (BLW)',
|
|
22
|
+
official_url: 'https://www.blw.admin.ch/blw/de/home/instrumente/strukturverbesserungen.html',
|
|
23
|
+
retrieval_method: 'PDF_EXTRACT',
|
|
24
|
+
update_frequency: 'annual (with ordinance updates)',
|
|
25
|
+
license: 'Swiss Federal Administration — free reuse',
|
|
26
|
+
coverage: 'Investment credits, capital grants, eligibility criteria, contribution rates',
|
|
27
|
+
last_retrieved: lastIngest?.value,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'BLW Weisungen Investitionskredite und Beitraege',
|
|
31
|
+
authority: 'Bundesamt fuer Landwirtschaft (BLW)',
|
|
32
|
+
official_url: 'https://www.blw.admin.ch/blw/de/home/instrumente/strukturverbesserungen/investitionskredite.html',
|
|
33
|
+
retrieval_method: 'PDF_EXTRACT',
|
|
34
|
+
update_frequency: 'annual',
|
|
35
|
+
license: 'Swiss Federal Administration — free reuse',
|
|
36
|
+
coverage: 'Detailed rules on investment credit eligibility, loan terms, project types',
|
|
37
|
+
last_retrieved: lastIngest?.value,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Kantonale Landwirtschaftsaemter — Strukturverbesserungen',
|
|
41
|
+
authority: 'Kantonale Landwirtschaftsaemter',
|
|
42
|
+
official_url: 'https://www.agridea.ch',
|
|
43
|
+
retrieval_method: 'MANUAL_RESEARCH',
|
|
44
|
+
update_frequency: 'variable (per canton)',
|
|
45
|
+
license: 'Cantonal public information',
|
|
46
|
+
coverage: 'Cantonal contribution rates, additional cantonal programmes, deadlines',
|
|
47
|
+
last_retrieved: lastIngest?.value,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'AGRIDEA Beratungsunterlagen Strukturverbesserungen',
|
|
51
|
+
authority: 'AGRIDEA',
|
|
52
|
+
official_url: 'https://www.agridea.ch',
|
|
53
|
+
retrieval_method: 'MANUAL_RESEARCH',
|
|
54
|
+
update_frequency: 'periodic',
|
|
55
|
+
license: 'AGRIDEA — educational use',
|
|
56
|
+
coverage: 'Project planning guidance, application procedures, cost estimates',
|
|
57
|
+
last_retrieved: lastIngest?.value,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
sources,
|
|
63
|
+
_meta: buildMeta(),
|
|
64
|
+
};
|
|
65
|
+
}
|