@ansvar/ch-livestock-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 (113) hide show
  1. package/.github/workflows/check-freshness.yml +52 -0
  2. package/.github/workflows/ci.yml +21 -0
  3. package/.github/workflows/codeql.yml +31 -0
  4. package/.github/workflows/ghcr-build.yml +45 -0
  5. package/.github/workflows/gitleaks.yml +23 -0
  6. package/.github/workflows/ingest.yml +58 -0
  7. package/.github/workflows/publish.yml +24 -0
  8. package/CHANGELOG.md +24 -0
  9. package/CODEOWNERS +1 -0
  10. package/COVERAGE.md +62 -0
  11. package/DISCLAIMER.md +41 -0
  12. package/Dockerfile +26 -0
  13. package/LICENSE +17 -0
  14. package/PRIVACY.md +28 -0
  15. package/README.md +145 -0
  16. package/SECURITY.md +33 -0
  17. package/TOOLS.md +261 -0
  18. package/data/coverage.json +23 -0
  19. package/data/database.db +0 -0
  20. package/data/sources.yml +29 -0
  21. package/dist/db.d.ts +26 -0
  22. package/dist/db.d.ts.map +1 -0
  23. package/dist/db.js +220 -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 +294 -0
  28. package/dist/http-server.js.map +1 -0
  29. package/dist/jurisdiction.d.ts +18 -0
  30. package/dist/jurisdiction.d.ts.map +1 -0
  31. package/dist/jurisdiction.js +16 -0
  32. package/dist/jurisdiction.js.map +1 -0
  33. package/dist/metadata.d.ts +10 -0
  34. package/dist/metadata.d.ts.map +1 -0
  35. package/dist/metadata.js +21 -0
  36. package/dist/metadata.js.map +1 -0
  37. package/dist/server.d.ts +3 -0
  38. package/dist/server.d.ts.map +1 -0
  39. package/dist/server.js +240 -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-breeding-guidance.d.ts +37 -0
  50. package/dist/tools/get-breeding-guidance.d.ts.map +1 -0
  51. package/dist/tools/get-breeding-guidance.js +39 -0
  52. package/dist/tools/get-breeding-guidance.js.map +1 -0
  53. package/dist/tools/get-feed-requirements.d.ts +40 -0
  54. package/dist/tools/get-feed-requirements.d.ts.map +1 -0
  55. package/dist/tools/get-feed-requirements.js +35 -0
  56. package/dist/tools/get-feed-requirements.js.map +1 -0
  57. package/dist/tools/get-housing-requirements.d.ts +39 -0
  58. package/dist/tools/get-housing-requirements.d.ts.map +1 -0
  59. package/dist/tools/get-housing-requirements.js +35 -0
  60. package/dist/tools/get-housing-requirements.js.map +1 -0
  61. package/dist/tools/get-movement-rules.d.ts +36 -0
  62. package/dist/tools/get-movement-rules.d.ts.map +1 -0
  63. package/dist/tools/get-movement-rules.js +31 -0
  64. package/dist/tools/get-movement-rules.js.map +1 -0
  65. package/dist/tools/get-stocking-density.d.ts +37 -0
  66. package/dist/tools/get-stocking-density.d.ts.map +1 -0
  67. package/dist/tools/get-stocking-density.js +35 -0
  68. package/dist/tools/get-stocking-density.js.map +1 -0
  69. package/dist/tools/get-welfare-standards.d.ts +38 -0
  70. package/dist/tools/get-welfare-standards.d.ts.map +1 -0
  71. package/dist/tools/get-welfare-standards.js +32 -0
  72. package/dist/tools/get-welfare-standards.js.map +1 -0
  73. package/dist/tools/list-sources.d.ts +18 -0
  74. package/dist/tools/list-sources.d.ts.map +1 -0
  75. package/dist/tools/list-sources.js +51 -0
  76. package/dist/tools/list-sources.js.map +1 -0
  77. package/dist/tools/search-animal-health.d.ts +28 -0
  78. package/dist/tools/search-animal-health.d.ts.map +1 -0
  79. package/dist/tools/search-animal-health.js +33 -0
  80. package/dist/tools/search-animal-health.js.map +1 -0
  81. package/dist/tools/search-livestock-guidance.d.ts +27 -0
  82. package/dist/tools/search-livestock-guidance.d.ts.map +1 -0
  83. package/dist/tools/search-livestock-guidance.js +25 -0
  84. package/dist/tools/search-livestock-guidance.js.map +1 -0
  85. package/docker-compose.yml +12 -0
  86. package/eslint.config.js +27 -0
  87. package/package.json +54 -0
  88. package/scripts/ingest.ts +553 -0
  89. package/server.json +10 -0
  90. package/src/db.ts +268 -0
  91. package/src/http-server.ts +327 -0
  92. package/src/jurisdiction.ts +30 -0
  93. package/src/metadata.ts +31 -0
  94. package/src/server.ts +264 -0
  95. package/src/tools/about.ts +28 -0
  96. package/src/tools/check-freshness.ts +42 -0
  97. package/src/tools/get-breeding-guidance.ts +53 -0
  98. package/src/tools/get-feed-requirements.ts +53 -0
  99. package/src/tools/get-housing-requirements.ts +52 -0
  100. package/src/tools/get-movement-rules.ts +45 -0
  101. package/src/tools/get-stocking-density.ts +52 -0
  102. package/src/tools/get-welfare-standards.ts +47 -0
  103. package/src/tools/list-sources.ts +65 -0
  104. package/src/tools/search-animal-health.ts +48 -0
  105. package/src/tools/search-livestock-guidance.ts +33 -0
  106. package/tests/db.test.ts +96 -0
  107. package/tests/helpers/seed-db.ts +97 -0
  108. package/tests/jurisdiction.test.ts +41 -0
  109. package/tests/tools/about.test.ts +32 -0
  110. package/tests/tools/check-freshness.test.ts +55 -0
  111. package/tests/tools/list-sources.test.ts +56 -0
  112. package/tests/tools/search-livestock-guidance.test.ts +63 -0
  113. package/tsconfig.json +19 -0
package/src/server.ts ADDED
@@ -0,0 +1,264 @@
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 { handleSearchLivestockGuidance } from './tools/search-livestock-guidance.js';
15
+ import { handleGetWelfareStandards } from './tools/get-welfare-standards.js';
16
+ import { handleGetStockingDensity } from './tools/get-stocking-density.js';
17
+ import { handleGetFeedRequirements } from './tools/get-feed-requirements.js';
18
+ import { handleSearchAnimalHealth } from './tools/search-animal-health.js';
19
+ import { handleGetHousingRequirements } from './tools/get-housing-requirements.js';
20
+ import { handleGetMovementRules } from './tools/get-movement-rules.js';
21
+ import { handleGetBreedingGuidance } from './tools/get-breeding-guidance.js';
22
+
23
+ const SERVER_NAME = 'ch-livestock-mcp';
24
+ const SERVER_VERSION = '0.1.0';
25
+
26
+ const TOOLS = [
27
+ {
28
+ name: 'about',
29
+ description: 'Get server metadata: name, version, coverage, data sources, and links.',
30
+ inputSchema: { type: 'object' as const, properties: {} },
31
+ },
32
+ {
33
+ name: 'list_sources',
34
+ description: 'List all data sources with authority, URL, license, and freshness info.',
35
+ inputSchema: { type: 'object' as const, properties: {} },
36
+ },
37
+ {
38
+ name: 'check_data_freshness',
39
+ description: 'Check when data was last ingested, staleness status, and how to trigger a refresh.',
40
+ inputSchema: { type: 'object' as const, properties: {} },
41
+ },
42
+ {
43
+ name: 'search_livestock_guidance',
44
+ description: 'Search across all Swiss livestock topics: welfare, housing, feeding, health, transport, breeds. Use for broad queries about Swiss livestock regulations and guidance.',
45
+ inputSchema: {
46
+ type: 'object' as const,
47
+ properties: {
48
+ query: { type: 'string', description: 'Free-text search query (German or English)' },
49
+ species: { type: 'string', description: 'Filter by species (e.g. Rinder, Schweine, Gefluegel, Schafe, Ziegen, Pferde)' },
50
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
51
+ limit: { type: 'number', description: 'Max results (default: 20, max: 50)' },
52
+ },
53
+ required: ['query'],
54
+ },
55
+ },
56
+ {
57
+ name: 'get_welfare_standards',
58
+ description: 'Get legal minimum welfare requirements and RAUS/BTS programme standards for a species. Based on TSchV and DZV.',
59
+ inputSchema: {
60
+ type: 'object' as const,
61
+ properties: {
62
+ species: { type: 'string', description: 'Species: Rinder, Schweine, Gefluegel, Schafe, Ziegen, Pferde' },
63
+ production_system: { type: 'string', description: 'Production system filter (e.g. TSchV-Minimum, RAUS, BTS)' },
64
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
65
+ },
66
+ required: ['species'],
67
+ },
68
+ },
69
+ {
70
+ name: 'get_stocking_density',
71
+ description: 'Get animals per m2 and space requirements by species, age class, and housing type. Based on TSchV Anhang 1.',
72
+ inputSchema: {
73
+ type: 'object' as const,
74
+ properties: {
75
+ species: { type: 'string', description: 'Species: Rinder, Schweine, Gefluegel, Schafe, Ziegen, Pferde' },
76
+ age_class: { type: 'string', description: 'Age/weight class (e.g. Milchkuh, Kalb, Mastschwein >60kg, Legehenne)' },
77
+ housing_type: { type: 'string', description: 'Housing type (e.g. Laufstall, Anbindestall, Voliere, Freilandhaltung)' },
78
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
79
+ },
80
+ required: ['species'],
81
+ },
82
+ },
83
+ {
84
+ name: 'get_feed_requirements',
85
+ description: 'Get nutritional requirements per species and production stage. Includes GMF (graslandbasierte Milch- und Fleischproduktion) programme details.',
86
+ inputSchema: {
87
+ type: 'object' as const,
88
+ properties: {
89
+ species: { type: 'string', description: 'Species: Rinder, Schweine, Gefluegel, Schafe, Ziegen, Pferde' },
90
+ age_class: { type: 'string', description: 'Age class (e.g. Milchkuh, Aufzuchtrind, Mastschwein, Legehenne)' },
91
+ production_stage: { type: 'string', description: 'Production stage (e.g. Laktation, Trockenstehend, Mast, Aufzucht)' },
92
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
93
+ },
94
+ required: ['species'],
95
+ },
96
+ },
97
+ {
98
+ name: 'search_animal_health',
99
+ description: 'Search animal health topics: diseases, symptoms, prevention, regulatory reporting requirements.',
100
+ inputSchema: {
101
+ type: 'object' as const,
102
+ properties: {
103
+ query: { type: 'string', description: 'Search query (e.g. Salmonellen, BVD, Moderhinke, Mastitits)' },
104
+ species: { type: 'string', description: 'Filter by species' },
105
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
106
+ },
107
+ required: ['query'],
108
+ },
109
+ },
110
+ {
111
+ name: 'get_housing_requirements',
112
+ description: 'Get detailed housing specifications: space, ventilation, flooring, temperature. Compares TSchV minimum vs. BTS standard.',
113
+ inputSchema: {
114
+ type: 'object' as const,
115
+ properties: {
116
+ species: { type: 'string', description: 'Species: Rinder, Schweine, Gefluegel, Schafe, Ziegen, Pferde' },
117
+ age_class: { type: 'string', description: 'Age class (e.g. Milchkuh, Mastschwein, Legehenne)' },
118
+ system: { type: 'string', description: 'Housing system (e.g. Laufstall, Anbindestall, Voliere, BTS)' },
119
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
120
+ },
121
+ required: ['species'],
122
+ },
123
+ },
124
+ {
125
+ name: 'get_movement_rules',
126
+ description: 'Get TVD registration, transport regulations, standstill rules, and Soemmerung/Alpung requirements per species.',
127
+ inputSchema: {
128
+ type: 'object' as const,
129
+ properties: {
130
+ species: { type: 'string', description: 'Species: Rinder, Schweine, Gefluegel, Schafe, Ziegen, Pferde' },
131
+ rule_type: { type: 'string', description: 'Rule type: TVD, Transport, Soemmerung, Schlachtung' },
132
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
133
+ },
134
+ required: ['species'],
135
+ },
136
+ },
137
+ {
138
+ name: 'get_breeding_guidance',
139
+ description: 'Get Swiss breed information, breeding calendars, AI (kuenstliche Besamung), genetics, and Soemmerung guidance.',
140
+ inputSchema: {
141
+ type: 'object' as const,
142
+ properties: {
143
+ species: { type: 'string', description: 'Species: Rinder, Schweine, Schafe, Ziegen, Pferde' },
144
+ topic: { type: 'string', description: 'Topic filter (e.g. Zweinutzung, Milch, Fleisch, Alp)' },
145
+ jurisdiction: { type: 'string', description: 'ISO 3166-1 alpha-2 code (default: CH)' },
146
+ },
147
+ required: ['species'],
148
+ },
149
+ },
150
+ ];
151
+
152
+ const SearchArgsSchema = z.object({
153
+ query: z.string(),
154
+ species: z.string().optional(),
155
+ jurisdiction: z.string().optional(),
156
+ limit: z.number().optional(),
157
+ });
158
+
159
+ const WelfareArgsSchema = z.object({
160
+ species: z.string(),
161
+ production_system: z.string().optional(),
162
+ jurisdiction: z.string().optional(),
163
+ });
164
+
165
+ const StockingArgsSchema = z.object({
166
+ species: z.string(),
167
+ age_class: z.string().optional(),
168
+ housing_type: z.string().optional(),
169
+ jurisdiction: z.string().optional(),
170
+ });
171
+
172
+ const FeedArgsSchema = z.object({
173
+ species: z.string(),
174
+ age_class: z.string().optional(),
175
+ production_stage: z.string().optional(),
176
+ jurisdiction: z.string().optional(),
177
+ });
178
+
179
+ const HealthSearchArgsSchema = z.object({
180
+ query: z.string(),
181
+ species: z.string().optional(),
182
+ jurisdiction: z.string().optional(),
183
+ });
184
+
185
+ const HousingArgsSchema = z.object({
186
+ species: z.string(),
187
+ age_class: z.string().optional(),
188
+ system: z.string().optional(),
189
+ jurisdiction: z.string().optional(),
190
+ });
191
+
192
+ const MovementArgsSchema = z.object({
193
+ species: z.string(),
194
+ rule_type: z.string().optional(),
195
+ jurisdiction: z.string().optional(),
196
+ });
197
+
198
+ const BreedingArgsSchema = z.object({
199
+ species: z.string(),
200
+ topic: z.string().optional(),
201
+ jurisdiction: z.string().optional(),
202
+ });
203
+
204
+ function textResult(data: unknown) {
205
+ return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };
206
+ }
207
+
208
+ function errorResult(message: string) {
209
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ error: message }) }], isError: true };
210
+ }
211
+
212
+ const db = createDatabase();
213
+
214
+ const server = new Server(
215
+ { name: SERVER_NAME, version: SERVER_VERSION },
216
+ { capabilities: { tools: {} } }
217
+ );
218
+
219
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
220
+
221
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
222
+ const { name, arguments: args = {} } = request.params;
223
+
224
+ try {
225
+ switch (name) {
226
+ case 'about':
227
+ return textResult(handleAbout());
228
+ case 'list_sources':
229
+ return textResult(handleListSources(db));
230
+ case 'check_data_freshness':
231
+ return textResult(handleCheckFreshness(db));
232
+ case 'search_livestock_guidance':
233
+ return textResult(handleSearchLivestockGuidance(db, SearchArgsSchema.parse(args)));
234
+ case 'get_welfare_standards':
235
+ return textResult(handleGetWelfareStandards(db, WelfareArgsSchema.parse(args)));
236
+ case 'get_stocking_density':
237
+ return textResult(handleGetStockingDensity(db, StockingArgsSchema.parse(args)));
238
+ case 'get_feed_requirements':
239
+ return textResult(handleGetFeedRequirements(db, FeedArgsSchema.parse(args)));
240
+ case 'search_animal_health':
241
+ return textResult(handleSearchAnimalHealth(db, HealthSearchArgsSchema.parse(args)));
242
+ case 'get_housing_requirements':
243
+ return textResult(handleGetHousingRequirements(db, HousingArgsSchema.parse(args)));
244
+ case 'get_movement_rules':
245
+ return textResult(handleGetMovementRules(db, MovementArgsSchema.parse(args)));
246
+ case 'get_breeding_guidance':
247
+ return textResult(handleGetBreedingGuidance(db, BreedingArgsSchema.parse(args)));
248
+ default:
249
+ return errorResult(`Unknown tool: ${name}`);
250
+ }
251
+ } catch (err) {
252
+ return errorResult(err instanceof Error ? err.message : String(err));
253
+ }
254
+ });
255
+
256
+ async function main(): Promise<void> {
257
+ const transport = new StdioServerTransport();
258
+ await server.connect(transport);
259
+ }
260
+
261
+ main().catch((err) => {
262
+ process.stderr.write(`Fatal error: ${err.message}\n`);
263
+ process.exit(1);
264
+ });
@@ -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 Livestock MCP',
7
+ description:
8
+ 'Swiss livestock regulations based on the Tierschutzverordnung (TSchV), RAUS/BTS programmes (DZV), ' +
9
+ 'TVD animal movement database, and breed registry data. Covers welfare standards, space requirements, ' +
10
+ 'housing specifications, transport rules, feed requirements, animal health, and breeding guidance ' +
11
+ 'for cattle, pigs, poultry, sheep, goats, and horses in Switzerland.',
12
+ version: '0.1.0',
13
+ jurisdiction: [...SUPPORTED_JURISDICTIONS],
14
+ data_sources: [
15
+ 'Tierschutzverordnung TSchV (BLV)',
16
+ 'Direktzahlungsverordnung DZV — RAUS/BTS (BLW)',
17
+ 'Tierverkehrsdatenbank TVD (Identitas)',
18
+ 'Zuchtorganisationen (Braunvieh Schweiz, swissherdbook, Mutterkuh Schweiz, Suisseporcs)',
19
+ ],
20
+ tools_count: 11,
21
+ links: {
22
+ homepage: 'https://ansvar.eu/open-agriculture',
23
+ repository: 'https://github.com/ansvar-systems/ch-livestock-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-livestock-mcp',
40
+ _meta: buildMeta(),
41
+ };
42
+ }
@@ -0,0 +1,53 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import { validateJurisdiction } from '../jurisdiction.js';
3
+ import type { Database } from '../db.js';
4
+
5
+ interface BreedingArgs {
6
+ species: string;
7
+ topic?: string;
8
+ jurisdiction?: string;
9
+ }
10
+
11
+ export function handleGetBreedingGuidance(db: Database, args: BreedingArgs) {
12
+ const jv = validateJurisdiction(args.jurisdiction);
13
+ if (!jv.valid) return jv.error;
14
+
15
+ let sql = 'SELECT * FROM breeds WHERE LOWER(species) = LOWER(?) AND jurisdiction = ?';
16
+ const params: unknown[] = [args.species, jv.jurisdiction];
17
+
18
+ sql += ' ORDER BY name';
19
+
20
+ const breeds = db.all<{
21
+ id: number; species: string; name: string; purpose: string; notes: string;
22
+ }>(sql, params);
23
+
24
+ if (breeds.length === 0) {
25
+ return {
26
+ error: 'not_found',
27
+ message: `No breed data found for species '${args.species}'. Available species: Rinder, Schweine, Schafe, Ziegen, Pferde.`,
28
+ };
29
+ }
30
+
31
+ // If topic filter is given, filter breeds/notes by topic keyword
32
+ let filtered = breeds;
33
+ if (args.topic) {
34
+ const topicLower = args.topic.toLowerCase();
35
+ filtered = breeds.filter(b => {
36
+ const text = [b.name, b.purpose, b.notes].filter(Boolean).join(' ').toLowerCase();
37
+ return text.includes(topicLower);
38
+ });
39
+ // If topic filter yields nothing, return all breeds with a note
40
+ if (filtered.length === 0) {
41
+ filtered = breeds;
42
+ }
43
+ }
44
+
45
+ return {
46
+ species: args.species,
47
+ topic_filter: args.topic ?? null,
48
+ jurisdiction: jv.jurisdiction,
49
+ results_count: filtered.length,
50
+ results: filtered,
51
+ _meta: buildMeta(),
52
+ };
53
+ }
@@ -0,0 +1,53 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import { validateJurisdiction } from '../jurisdiction.js';
3
+ import type { Database } from '../db.js';
4
+
5
+ interface FeedArgs {
6
+ species: string;
7
+ age_class?: string;
8
+ production_stage?: string;
9
+ jurisdiction?: string;
10
+ }
11
+
12
+ export function handleGetFeedRequirements(db: Database, args: FeedArgs) {
13
+ const jv = validateJurisdiction(args.jurisdiction);
14
+ if (!jv.valid) return jv.error;
15
+
16
+ let sql = 'SELECT * FROM feed_requirements WHERE LOWER(species) = LOWER(?) AND jurisdiction = ?';
17
+ const params: unknown[] = [args.species, jv.jurisdiction];
18
+
19
+ if (args.age_class) {
20
+ sql += ' AND LOWER(age_class) = LOWER(?)';
21
+ params.push(args.age_class);
22
+ }
23
+
24
+ if (args.production_stage) {
25
+ sql += ' AND LOWER(production_stage) = LOWER(?)';
26
+ params.push(args.production_stage);
27
+ }
28
+
29
+ sql += ' ORDER BY age_class, production_stage';
30
+
31
+ const results = db.all<{
32
+ id: number; species: string; age_class: string; production_stage: string;
33
+ feed_type: string; quantity_kg_day: number | null; energy_mj: number | null;
34
+ protein_g: number | null; notes: string;
35
+ }>(sql, params);
36
+
37
+ if (results.length === 0) {
38
+ return {
39
+ error: 'not_found',
40
+ message: `No feed requirement data found for species '${args.species}'` +
41
+ (args.age_class ? ` age class '${args.age_class}'` : '') +
42
+ (args.production_stage ? ` production stage '${args.production_stage}'` : '') + '.',
43
+ };
44
+ }
45
+
46
+ return {
47
+ species: args.species,
48
+ jurisdiction: jv.jurisdiction,
49
+ results_count: results.length,
50
+ results,
51
+ _meta: buildMeta(),
52
+ };
53
+ }
@@ -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 HousingArgs {
6
+ species: string;
7
+ age_class?: string;
8
+ system?: string;
9
+ jurisdiction?: string;
10
+ }
11
+
12
+ export function handleGetHousingRequirements(db: Database, args: HousingArgs) {
13
+ const jv = validateJurisdiction(args.jurisdiction);
14
+ if (!jv.valid) return jv.error;
15
+
16
+ let sql = 'SELECT * FROM housing_requirements WHERE LOWER(species) = LOWER(?) AND jurisdiction = ?';
17
+ const params: unknown[] = [args.species, jv.jurisdiction];
18
+
19
+ if (args.age_class) {
20
+ sql += ' AND LOWER(age_class) = LOWER(?)';
21
+ params.push(args.age_class);
22
+ }
23
+
24
+ if (args.system) {
25
+ sql += ' AND LOWER(system) = LOWER(?)';
26
+ params.push(args.system);
27
+ }
28
+
29
+ sql += ' ORDER BY age_class, system';
30
+
31
+ const results = db.all<{
32
+ id: number; species: string; age_class: string; system: string;
33
+ space: string; ventilation: string; flooring: string; temperature: string;
34
+ }>(sql, params);
35
+
36
+ if (results.length === 0) {
37
+ return {
38
+ error: 'not_found',
39
+ message: `No housing requirements found for species '${args.species}'` +
40
+ (args.age_class ? ` age class '${args.age_class}'` : '') +
41
+ (args.system ? ` system '${args.system}'` : '') + '.',
42
+ };
43
+ }
44
+
45
+ return {
46
+ species: args.species,
47
+ jurisdiction: jv.jurisdiction,
48
+ results_count: results.length,
49
+ results,
50
+ _meta: buildMeta(),
51
+ };
52
+ }
@@ -0,0 +1,45 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import { validateJurisdiction } from '../jurisdiction.js';
3
+ import type { Database } from '../db.js';
4
+
5
+ interface MovementArgs {
6
+ species: string;
7
+ rule_type?: string;
8
+ jurisdiction?: string;
9
+ }
10
+
11
+ export function handleGetMovementRules(db: Database, args: MovementArgs) {
12
+ const jv = validateJurisdiction(args.jurisdiction);
13
+ if (!jv.valid) return jv.error;
14
+
15
+ let sql = 'SELECT * FROM movement_rules WHERE LOWER(species) = LOWER(?) AND jurisdiction = ?';
16
+ const params: unknown[] = [args.species, jv.jurisdiction];
17
+
18
+ if (args.rule_type) {
19
+ sql += ' AND LOWER(rule_type) = LOWER(?)';
20
+ params.push(args.rule_type);
21
+ }
22
+
23
+ sql += ' ORDER BY rule_type';
24
+
25
+ const results = db.all<{
26
+ id: number; species: string; rule_type: string; description: string;
27
+ }>(sql, params);
28
+
29
+ if (results.length === 0) {
30
+ return {
31
+ error: 'not_found',
32
+ message: `No movement rules found for species '${args.species}'` +
33
+ (args.rule_type ? ` rule type '${args.rule_type}'` : '') + '.',
34
+ };
35
+ }
36
+
37
+ return {
38
+ species: args.species,
39
+ rule_type_filter: args.rule_type ?? null,
40
+ jurisdiction: jv.jurisdiction,
41
+ results_count: results.length,
42
+ results,
43
+ _meta: buildMeta(),
44
+ };
45
+ }
@@ -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 StockingArgs {
6
+ species: string;
7
+ age_class?: string;
8
+ housing_type?: string;
9
+ jurisdiction?: string;
10
+ }
11
+
12
+ export function handleGetStockingDensity(db: Database, args: StockingArgs) {
13
+ const jv = validateJurisdiction(args.jurisdiction);
14
+ if (!jv.valid) return jv.error;
15
+
16
+ let sql = 'SELECT * FROM stocking_densities WHERE LOWER(species) = LOWER(?) AND jurisdiction = ?';
17
+ const params: unknown[] = [args.species, jv.jurisdiction];
18
+
19
+ if (args.age_class) {
20
+ sql += ' AND LOWER(age_class) = LOWER(?)';
21
+ params.push(args.age_class);
22
+ }
23
+
24
+ if (args.housing_type) {
25
+ sql += ' AND LOWER(housing_type) = LOWER(?)';
26
+ params.push(args.housing_type);
27
+ }
28
+
29
+ sql += ' ORDER BY age_class, housing_type';
30
+
31
+ const results = db.all<{
32
+ id: number; species: string; age_class: string; housing_type: string;
33
+ animals_per_m2: number | null; regulatory_minimum: string;
34
+ }>(sql, params);
35
+
36
+ if (results.length === 0) {
37
+ return {
38
+ error: 'not_found',
39
+ message: `No stocking density data found for species '${args.species}'` +
40
+ (args.age_class ? ` age class '${args.age_class}'` : '') +
41
+ (args.housing_type ? ` housing type '${args.housing_type}'` : '') + '.',
42
+ };
43
+ }
44
+
45
+ return {
46
+ species: args.species,
47
+ jurisdiction: jv.jurisdiction,
48
+ results_count: results.length,
49
+ results,
50
+ _meta: buildMeta(),
51
+ };
52
+ }
@@ -0,0 +1,47 @@
1
+ import { buildMeta } from '../metadata.js';
2
+ import { validateJurisdiction } from '../jurisdiction.js';
3
+ import type { Database } from '../db.js';
4
+
5
+ interface WelfareArgs {
6
+ species: string;
7
+ production_system?: string;
8
+ jurisdiction?: string;
9
+ }
10
+
11
+ export function handleGetWelfareStandards(db: Database, args: WelfareArgs) {
12
+ const jv = validateJurisdiction(args.jurisdiction);
13
+ if (!jv.valid) return jv.error;
14
+
15
+ let sql = 'SELECT * FROM welfare_standards WHERE LOWER(species) = LOWER(?) AND jurisdiction = ?';
16
+ const params: unknown[] = [args.species, jv.jurisdiction];
17
+
18
+ if (args.production_system) {
19
+ sql += ' AND LOWER(production_system) = LOWER(?)';
20
+ params.push(args.production_system);
21
+ }
22
+
23
+ sql += ' ORDER BY production_system, requirement';
24
+
25
+ const results = db.all<{
26
+ id: number; species: string; production_system: string;
27
+ requirement: string; min_space_m2: number | null; details: string;
28
+ }>(sql, params);
29
+
30
+ if (results.length === 0) {
31
+ return {
32
+ error: 'not_found',
33
+ message: `No welfare standards found for species '${args.species}'` +
34
+ (args.production_system ? ` with production system '${args.production_system}'` : '') +
35
+ '. Available species: Rinder, Schweine, Gefluegel, Schafe, Ziegen, Pferde.',
36
+ };
37
+ }
38
+
39
+ return {
40
+ species: args.species,
41
+ production_system_filter: args.production_system ?? null,
42
+ jurisdiction: jv.jurisdiction,
43
+ results_count: results.length,
44
+ results,
45
+ _meta: buildMeta(),
46
+ };
47
+ }