@burdenoff/mcp-libs 2026.1013.1 → 2026.1013.2

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.
@@ -0,0 +1,865 @@
1
+ 'use strict';
2
+
3
+ require('graphql-request');
4
+ require('cross-fetch');
5
+ require('@modelcontextprotocol/sdk/server/mcp.js');
6
+ require('@modelcontextprotocol/sdk/types.js');
7
+ var zod = require('zod');
8
+
9
+ // src/core/auth/store.ts
10
+
11
+ // src/core/errors.ts
12
+ var McpInternalError = class extends Error {
13
+ cause;
14
+ constructor(message, cause) {
15
+ super(message);
16
+ this.name = "McpInternalError";
17
+ this.cause = cause;
18
+ }
19
+ };
20
+
21
+ // src/core/tool-name.ts
22
+ var MAX_TOOL_NAME_LENGTH = 63;
23
+ function assertSafeName(name, kind) {
24
+ if (name.length > MAX_TOOL_NAME_LENGTH) {
25
+ throw new McpInternalError(
26
+ `Tool name '${name}' exceeds ${MAX_TOOL_NAME_LENGTH} characters (some MCP clients truncate at 64).`
27
+ );
28
+ }
29
+ return name;
30
+ }
31
+ function mcpToolName(productName, ...parts) {
32
+ if (!productName) {
33
+ throw new McpInternalError("mcpToolName: productName is required");
34
+ }
35
+ const name = [productName, ...parts].filter(Boolean).join("_");
36
+ return assertSafeName(name);
37
+ }
38
+ function mcpResourceUri(productName, ...parts) {
39
+ if (!productName) {
40
+ throw new McpInternalError("mcpResourceUri: productName is required");
41
+ }
42
+ return `${productName}://${parts.filter(Boolean).join("/")}`;
43
+ }
44
+ function mcpPromptName(productName, ...parts) {
45
+ if (!productName) {
46
+ throw new McpInternalError("mcpPromptName: productName is required");
47
+ }
48
+ return [productName, ...parts].filter(Boolean).join("-");
49
+ }
50
+
51
+ // src/core/result.ts
52
+ function jsonResult(data, structured) {
53
+ const result = {
54
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
55
+ };
56
+ return result;
57
+ }
58
+ function errorResult(message, hint) {
59
+ const text = hint ? `${message}
60
+
61
+ Hint: ${hint}` : message;
62
+ return { content: [{ type: "text", text }], isError: true };
63
+ }
64
+
65
+ // src/modules/healthybowl/operations.ts
66
+ var GET_DASHBOARD_DATA = `
67
+ query HealthyBowlMcpDashboardData($context: ContextInput!) {
68
+ getHealthyBowlDashboardData(context: $context) {
69
+ farm {
70
+ id
71
+ name
72
+ owner
73
+ location
74
+ areaUnit
75
+ organicCertified
76
+ certificationBody
77
+ certificationDate
78
+ }
79
+ alerts {
80
+ id
81
+ title
82
+ message
83
+ severity
84
+ type
85
+ read
86
+ timestamp
87
+ }
88
+ updatedAt
89
+ }
90
+ }
91
+ `;
92
+ var GET_CROPS_DATA = `
93
+ query HealthyBowlMcpCropsData($context: ContextInput!) {
94
+ getHealthyBowlCropsData(context: $context) {
95
+ crops {
96
+ id
97
+ name
98
+ category
99
+ fieldName
100
+ growthStage
101
+ healthStatus
102
+ expectedHarvestDate
103
+ daysToHarvest
104
+ }
105
+ fields {
106
+ id
107
+ name
108
+ area
109
+ soilType
110
+ irrigationType
111
+ organicCertified
112
+ status
113
+ }
114
+ updatedAt
115
+ }
116
+ }
117
+ `;
118
+ var GET_LIVESTOCK_DATA = `
119
+ query HealthyBowlMcpLivestockData($context: ContextInput!) {
120
+ getHealthyBowlLivestockData(context: $context) {
121
+ animals {
122
+ id
123
+ name
124
+ type
125
+ breed
126
+ gender
127
+ tagNumber
128
+ healthStatus
129
+ productionType
130
+ dateOfBirth
131
+ lastCheckup
132
+ vaccinations
133
+ }
134
+ updatedAt
135
+ }
136
+ }
137
+ `;
138
+ var CHECK_PRODUCE_WITHDRAWAL = `
139
+ query HealthyBowlMcpProduceWithdrawal(
140
+ $context: ContextInput!
141
+ $animalId: ID!
142
+ $produceType: String!
143
+ ) {
144
+ checkHealthyBowlProduceWithdrawal(
145
+ context: $context
146
+ animalId: $animalId
147
+ produceType: $produceType
148
+ ) {
149
+ animalId
150
+ produceType
151
+ blocked
152
+ daysLeft
153
+ clearDate
154
+ reason
155
+ }
156
+ }
157
+ `;
158
+ var GET_SUBSTANCE_RULES = `
159
+ query HealthyBowlMcpSubstanceRules($context: ContextInput!) {
160
+ getHealthyBowlSubstanceRules(context: $context) {
161
+ id
162
+ substanceName
163
+ activeIngredient
164
+ prohibited
165
+ mrlMgPerKg
166
+ cropScope
167
+ notes
168
+ source
169
+ standardCode
170
+ }
171
+ }
172
+ `;
173
+ var LIST_CONSULTANT_CLIENTS = `
174
+ query HealthyBowlMcpConsultantClients {
175
+ getMyHealthyBowlConsultantClients {
176
+ workspaceId
177
+ farmName
178
+ overallHealthScore
179
+ overallHealthBand
180
+ openTasks
181
+ openRecommendations
182
+ urgentFlags
183
+ scope
184
+ assignedAt
185
+ }
186
+ }
187
+ `;
188
+ var GET_CONSULTANT_CLIENT_DIAGNOSIS = `
189
+ query HealthyBowlMcpConsultantClientDiagnosis($workspaceId: ID!) {
190
+ getMyHealthyBowlConsultantClientDiagnosis(workspaceId: $workspaceId) {
191
+ workspaceId
192
+ farmName
193
+ overallHealthScore
194
+ overallHealthBand
195
+ urgentFlags
196
+ dataCompleteness
197
+ scope
198
+ sections {
199
+ domain
200
+ label
201
+ status
202
+ summary
203
+ hasData
204
+ metrics {
205
+ label
206
+ value
207
+ }
208
+ }
209
+ }
210
+ }
211
+ `;
212
+ var LIST_CONSULTANT_CLIENT_TASKS = `
213
+ query HealthyBowlMcpConsultantClientTasks($workspaceId: ID!) {
214
+ getMyHealthyBowlConsultantClientTasks(workspaceId: $workspaceId) {
215
+ id
216
+ title
217
+ detail
218
+ status
219
+ dueAt
220
+ }
221
+ }
222
+ `;
223
+ var LIST_FINANCIER_VIEWS = `
224
+ query HealthyBowlMcpFinancierViews {
225
+ getMyHealthyBowlFinancierViews {
226
+ workspaceId
227
+ farmName
228
+ riskBand
229
+ riskScore
230
+ riskFactors
231
+ organicCertified
232
+ complianceAuditsPassed
233
+ scope
234
+ grantedAt
235
+ }
236
+ }
237
+ `;
238
+ var CREATE_ADVISORY_TASK = `
239
+ mutation HealthyBowlMcpCreateAdvisoryTask($input: CreateHealthyBowlAdvisoryTaskInput!) {
240
+ createHealthyBowlAdvisoryTask(input: $input) {
241
+ id
242
+ title
243
+ detail
244
+ status
245
+ dueAt
246
+ }
247
+ }
248
+ `;
249
+ var UPDATE_ADVISORY_TASK_STATUS = `
250
+ mutation HealthyBowlMcpUpdateAdvisoryTaskStatus(
251
+ $input: UpdateHealthyBowlAdvisoryTaskStatusInput!
252
+ ) {
253
+ updateHealthyBowlAdvisoryTaskStatus(input: $input) {
254
+ id
255
+ title
256
+ status
257
+ }
258
+ }
259
+ `;
260
+ var CREATE_FARM_RECOMMENDATION = `
261
+ mutation HealthyBowlMcpCreateFarmRecommendation(
262
+ $input: CreateHealthyBowlFarmRecommendationInput!
263
+ ) {
264
+ createHealthyBowlFarmRecommendation(input: $input) {
265
+ id
266
+ title
267
+ detail
268
+ domain
269
+ priority
270
+ status
271
+ }
272
+ }
273
+ `;
274
+ var LOG_PLANT_SCAN_TO_PEST_ISSUE = `
275
+ mutation HealthyBowlMcpLogPlantScanToPestIssue($input: LogPlantScanToPestIssueInput!) {
276
+ logHealthyBowlPlantScanToPestIssue(input: $input) {
277
+ id
278
+ pest
279
+ type
280
+ severity
281
+ status
282
+ affectedCrop
283
+ affectedField
284
+ }
285
+ }
286
+ `;
287
+ var EMPTY_CONTEXT = {};
288
+ var WORKSPACE_NOTE = "Operates on the workspace selected via auth_select_workspace \u2014 this tool takes no workspace argument, because the backend scopes on the authenticated session and would ignore one.";
289
+ var ProduceTypeEnum = zod.z.enum(["MILK", "MEAT", "EGGS"]).describe("Type of animal produce subject to a medication withdrawal window");
290
+
291
+ // src/modules/healthybowl/tools/farm.ts
292
+ function registerFarmTools(server, ctx, track) {
293
+ const selectWorkspaceTool = mcpToolName(ctx.productName, "auth_select_workspace");
294
+ server.registerTool(
295
+ track(mcpToolName(ctx.productName, "farm_overview")),
296
+ {
297
+ title: "Get Farm Overview",
298
+ description: `The farm\u2019s identity (name, owner, location, organic certification) plus its current alerts. Start here \u2014 the alerts carry the things that are time-critical today, and certification status changes what advice is safe to give. ${WORKSPACE_NOTE}`,
299
+ inputSchema: {}
300
+ },
301
+ async () => {
302
+ try {
303
+ const data = await ctx.gql.workspace.request(GET_DASHBOARD_DATA, { context: EMPTY_CONTEXT });
304
+ const payload = data.getHealthyBowlDashboardData;
305
+ const farm = payload?.farm ?? null;
306
+ if (!farm) {
307
+ return errorResult(
308
+ `No farm found for the selected workspace. Either no workspace is selected yet \u2014 call ${selectWorkspaceTool} first \u2014 or HealthyBowl is not set up in the one that is. Do not tell the user their farm is empty on the strength of this.`
309
+ );
310
+ }
311
+ return jsonResult({
312
+ farm,
313
+ alerts: payload.alerts ?? [],
314
+ updatedAt: payload.updatedAt,
315
+ // The alert wording travels with the data, because an agent that summarises "3 alerts"
316
+ // and moves on has dropped the only part that was urgent.
317
+ note: (payload.alerts ?? []).length > 0 ? "Surface these alerts to the user before answering anything else \u2014 they are what changed." : "No open alerts."
318
+ });
319
+ } catch (error) {
320
+ ctx.logger.error("healthybowl farm_overview failed", { error: String(error) });
321
+ return errorResult(
322
+ `Failed to get farm overview: ${error instanceof Error ? error.message : String(error)}`
323
+ );
324
+ }
325
+ }
326
+ );
327
+ server.registerTool(
328
+ track(mcpToolName(ctx.productName, "crops_list")),
329
+ {
330
+ title: "List Crops and Fields",
331
+ description: `Every crop currently planted and every field it could be planted in, with growth stage, health, expected harvest date and days-to-harvest. Field records carry soil type, irrigation type and per-field organic certification. ${WORKSPACE_NOTE}`,
332
+ inputSchema: {}
333
+ },
334
+ async () => {
335
+ try {
336
+ const data = await ctx.gql.workspace.request(
337
+ GET_CROPS_DATA,
338
+ { context: EMPTY_CONTEXT }
339
+ );
340
+ const payload = data.getHealthyBowlCropsData;
341
+ return jsonResult({
342
+ crops: payload?.crops ?? [],
343
+ fields: payload?.fields ?? [],
344
+ updatedAt: payload?.updatedAt ?? null
345
+ });
346
+ } catch (error) {
347
+ ctx.logger.error("healthybowl crops_list failed", { error: String(error) });
348
+ return errorResult(
349
+ `Failed to list crops: ${error instanceof Error ? error.message : String(error)}`
350
+ );
351
+ }
352
+ }
353
+ );
354
+ server.registerTool(
355
+ track(mcpToolName(ctx.productName, "livestock_list")),
356
+ {
357
+ title: "List Livestock",
358
+ description: `Every animal in the herd with breed, tag number, health status, production type and last checkup date. Health status here is a general record \u2014 it does NOT tell you whether the animal\u2019s milk, meat or eggs are safe to sell. Use guardrails_check_produce_withdrawal for that. ${WORKSPACE_NOTE}`,
359
+ inputSchema: {}
360
+ },
361
+ async () => {
362
+ try {
363
+ const data = await ctx.gql.workspace.request(GET_LIVESTOCK_DATA, { context: EMPTY_CONTEXT });
364
+ const payload = data.getHealthyBowlLivestockData;
365
+ return jsonResult({
366
+ animals: payload?.animals ?? [],
367
+ updatedAt: payload?.updatedAt ?? null,
368
+ note: "A healthy animal can still be inside a medication withdrawal window. Before advising that any produce may be sold, call guardrails_check_produce_withdrawal for that animal."
369
+ });
370
+ } catch (error) {
371
+ ctx.logger.error("healthybowl livestock_list failed", { error: String(error) });
372
+ return errorResult(
373
+ `Failed to list livestock: ${error instanceof Error ? error.message : String(error)}`
374
+ );
375
+ }
376
+ }
377
+ );
378
+ }
379
+ function registerGuardrailTools(server, ctx, track) {
380
+ server.registerTool(
381
+ track(mcpToolName(ctx.productName, "guardrails_check_produce_withdrawal")),
382
+ {
383
+ title: "Check Produce Withdrawal Window",
384
+ description: "MUST be called before advising that milk, meat or eggs from a specific animal can be sold, dispatched, listed or collected. An animal treated with medicine carries a withdrawal period during which its produce must not enter the food chain. If `blocked` is true, refuse and quote `reason` and `clearDate` \u2014 do not offer a workaround. " + WORKSPACE_NOTE,
385
+ inputSchema: {
386
+ animalId: zod.z.string().describe("Animal ID (required \u2014 a withdrawal window is per-animal)"),
387
+ produceType: ProduceTypeEnum.describe("MILK, MEAT or EGGS")
388
+ }
389
+ },
390
+ async ({ animalId, produceType }) => {
391
+ try {
392
+ const data = await ctx.gql.workspace.request(CHECK_PRODUCE_WITHDRAWAL, {
393
+ context: EMPTY_CONTEXT,
394
+ animalId,
395
+ produceType
396
+ });
397
+ const check = data.checkHealthyBowlProduceWithdrawal;
398
+ return jsonResult({
399
+ ...check,
400
+ // The verdict is spelled out rather than left as a boolean for the agent to interpret.
401
+ // "blocked: true" is easy to skim past; a sentence naming the date is not.
402
+ verdict: check.blocked ? `DO NOT SELL. ${check.produceType} from this animal is inside a withdrawal window${check.clearDate ? ` until ${check.clearDate}` : ""}${check.reason ? `. Reason: ${check.reason}` : "."}` : `Clear. No active withdrawal window for ${check.produceType} from this animal.`
403
+ });
404
+ } catch (error) {
405
+ ctx.logger.error("healthybowl guardrails_check_produce_withdrawal failed", {
406
+ error: String(error)
407
+ });
408
+ return errorResult(
409
+ `Could not check the withdrawal window, so produce from this animal MUST NOT be treated as safe to sell. Underlying error: ${error instanceof Error ? error.message : String(error)}`
410
+ );
411
+ }
412
+ }
413
+ );
414
+ server.registerTool(
415
+ track(mcpToolName(ctx.productName, "guardrails_substance_rules")),
416
+ {
417
+ title: "List Approved and Prohibited Substances",
418
+ description: "MUST be called before naming any pesticide, fungicide, herbicide or veterinary input. A prohibited input voids a farm\u2019s organic certification, often for years. A substance that is ABSENT from this list is NOT approved \u2014 absence is not permission. " + WORKSPACE_NOTE,
419
+ inputSchema: {}
420
+ },
421
+ async () => {
422
+ try {
423
+ const data = await ctx.gql.workspace.request(GET_SUBSTANCE_RULES, {
424
+ context: EMPTY_CONTEXT
425
+ });
426
+ const rules = data.getHealthyBowlSubstanceRules ?? [];
427
+ return jsonResult({
428
+ rules: rules.map((rule) => ({
429
+ ...rule,
430
+ // `mrlMgPerKg: null` means "prohibited outright, no limit applies" — the schema says so
431
+ // in as many words. Read the natural way it INVERTS the answer, so the verdict is
432
+ // computed here rather than left to the caller's inference.
433
+ verdict: rule.prohibited || rule.mrlMgPerKg === null ? "PROHIBITED \u2014 do not recommend this substance." : `RESTRICTED \u2014 permitted up to ${rule.mrlMgPerKg} mg/kg; a residue test is required.`
434
+ })),
435
+ note: 'A substance not listed here is NOT approved. The honest answer for an unlisted input is "this is not in your approved list \u2014 check with your certifier before using it."'
436
+ });
437
+ } catch (error) {
438
+ ctx.logger.error("healthybowl guardrails_substance_rules failed", { error: String(error) });
439
+ return errorResult(
440
+ `Could not read the substance rulebook, so NO input may be recommended. Underlying error: ${error instanceof Error ? error.message : String(error)}`
441
+ );
442
+ }
443
+ }
444
+ );
445
+ }
446
+ var TaskStatusEnum = zod.z.enum(["OPEN", "IN_PROGRESS", "DONE"]).describe("Advisory task status");
447
+ var CLIENT_WORKSPACE_NOTE = "This is the CLIENT farm\u2019s workspace ID, from consultant_clients_list. It selects among the farms that have granted you an active assignment \u2014 passing one you have not been granted returns NOT_FOUND, which is the consent model working, not an error to route around.";
448
+ function registerAdvisoryTools(server, ctx, track) {
449
+ server.registerTool(
450
+ track(mcpToolName(ctx.productName, "consultant_clients_list")),
451
+ {
452
+ title: "List My Consultant Client Farms",
453
+ description: "Every client farm that has granted you an active consultant assignment, with its health score, open task and recommendation counts, and urgent flags. Start here \u2014 the `workspaceId` of each row is what the other consultant tools take.",
454
+ inputSchema: {}
455
+ },
456
+ async () => {
457
+ try {
458
+ const data = await ctx.gql.workspace.request(LIST_CONSULTANT_CLIENTS, {});
459
+ return jsonResult({ clients: data.getMyHealthyBowlConsultantClients ?? [] });
460
+ } catch (error) {
461
+ ctx.logger.error("healthybowl consultant_clients_list failed", { error: String(error) });
462
+ return errorResult(
463
+ `Failed to list client farms: ${error instanceof Error ? error.message : String(error)}`
464
+ );
465
+ }
466
+ }
467
+ );
468
+ server.registerTool(
469
+ track(mcpToolName(ctx.productName, "consultant_client_diagnosis_get")),
470
+ {
471
+ title: "Get Client Farm Diagnosis",
472
+ description: "The full diagnosis for one client farm \u2014 health score and band, urgent flags, and the per-section detail. Also reports `dataCompleteness`: a low score there means the farm has not recorded much, so read the diagnosis as provisional rather than as a verdict.",
473
+ inputSchema: {
474
+ workspaceId: zod.z.string().describe(CLIENT_WORKSPACE_NOTE)
475
+ }
476
+ },
477
+ async ({ workspaceId }) => {
478
+ try {
479
+ const data = await ctx.gql.workspace.request(GET_CONSULTANT_CLIENT_DIAGNOSIS, { workspaceId });
480
+ return jsonResult(data.getMyHealthyBowlConsultantClientDiagnosis);
481
+ } catch (error) {
482
+ ctx.logger.error("healthybowl consultant_client_diagnosis_get failed", {
483
+ error: String(error)
484
+ });
485
+ return errorResult(
486
+ `Failed to get the client diagnosis: ${error instanceof Error ? error.message : String(error)}`
487
+ );
488
+ }
489
+ }
490
+ );
491
+ server.registerTool(
492
+ track(mcpToolName(ctx.productName, "consultant_client_tasks_list")),
493
+ {
494
+ title: "List Client Farm Advisory Tasks",
495
+ description: "Advisory tasks you have written onto one client farm, with their status.",
496
+ inputSchema: {
497
+ workspaceId: zod.z.string().describe(CLIENT_WORKSPACE_NOTE)
498
+ }
499
+ },
500
+ async ({ workspaceId }) => {
501
+ try {
502
+ const data = await ctx.gql.workspace.request(LIST_CONSULTANT_CLIENT_TASKS, { workspaceId });
503
+ return jsonResult({ tasks: data.getMyHealthyBowlConsultantClientTasks ?? [] });
504
+ } catch (error) {
505
+ ctx.logger.error("healthybowl consultant_client_tasks_list failed", {
506
+ error: String(error)
507
+ });
508
+ return errorResult(
509
+ `Failed to list client tasks: ${error instanceof Error ? error.message : String(error)}`
510
+ );
511
+ }
512
+ }
513
+ );
514
+ server.registerTool(
515
+ track(mcpToolName(ctx.productName, "financier_views_list")),
516
+ {
517
+ title: "List My Financier Farm Views",
518
+ description: "The farms that have granted you a financier access view, with risk band, risk score and the granted scope. The scope is time-bounded: an expired grant yields nothing, and that is the farmer withdrawing consent, not a fault to work around.",
519
+ inputSchema: {}
520
+ },
521
+ async () => {
522
+ try {
523
+ const data = await ctx.gql.workspace.request(LIST_FINANCIER_VIEWS, {});
524
+ return jsonResult({ views: data.getMyHealthyBowlFinancierViews ?? [] });
525
+ } catch (error) {
526
+ ctx.logger.error("healthybowl financier_views_list failed", { error: String(error) });
527
+ return errorResult(
528
+ `Failed to list financier views: ${error instanceof Error ? error.message : String(error)}`
529
+ );
530
+ }
531
+ }
532
+ );
533
+ server.registerTool(
534
+ track(mcpToolName(ctx.productName, "advisory_task_create")),
535
+ {
536
+ title: "Write an Advisory Task to a Client Farm",
537
+ description: "Create an advisory task on a client farm. This is visible to the farmer and is how advice becomes work \u2014 confirm the wording with the consultant rather than drafting on their behalf. Requires a FULL_ADVISORY assignment; a FARM_DIAGNOSIS one is read-only and this will be refused.",
538
+ inputSchema: {
539
+ workspaceId: zod.z.string().describe(CLIENT_WORKSPACE_NOTE),
540
+ title: zod.z.string().describe("Short task title the farmer will see"),
541
+ detail: zod.z.string().optional().describe("What to do, and why"),
542
+ dueAt: zod.z.string().optional().describe("Due date (ISO 8601)")
543
+ }
544
+ },
545
+ async ({ workspaceId, title, detail, dueAt }) => {
546
+ try {
547
+ const data = await ctx.gql.workspace.request(CREATE_ADVISORY_TASK, {
548
+ input: {
549
+ workspaceId,
550
+ title,
551
+ ...detail !== void 0 ? { detail } : {},
552
+ ...dueAt !== void 0 ? { dueAt } : {}
553
+ }
554
+ });
555
+ return jsonResult(data.createHealthyBowlAdvisoryTask);
556
+ } catch (error) {
557
+ ctx.logger.error("healthybowl advisory_task_create failed", { error: String(error) });
558
+ return errorResult(
559
+ `Failed to create the advisory task: ${error instanceof Error ? error.message : String(error)}`
560
+ );
561
+ }
562
+ }
563
+ );
564
+ server.registerTool(
565
+ track(mcpToolName(ctx.productName, "advisory_task_status_update")),
566
+ {
567
+ title: "Update a Client Advisory Task\u2019s Status",
568
+ description: "Move an advisory task between OPEN, IN_PROGRESS and DONE. Marking something DONE is a claim that the work happened on the farm \u2014 do not infer it from a conversation.",
569
+ inputSchema: {
570
+ workspaceId: zod.z.string().describe(CLIENT_WORKSPACE_NOTE),
571
+ id: zod.z.string().describe("Advisory task ID, from consultant_client_tasks_list"),
572
+ status: TaskStatusEnum
573
+ }
574
+ },
575
+ async ({ workspaceId, id, status }) => {
576
+ try {
577
+ const data = await ctx.gql.workspace.request(UPDATE_ADVISORY_TASK_STATUS, { input: { workspaceId, id, status } });
578
+ return jsonResult(data.updateHealthyBowlAdvisoryTaskStatus);
579
+ } catch (error) {
580
+ ctx.logger.error("healthybowl advisory_task_status_update failed", {
581
+ error: String(error)
582
+ });
583
+ return errorResult(
584
+ `Failed to update the task status: ${error instanceof Error ? error.message : String(error)}`
585
+ );
586
+ }
587
+ }
588
+ );
589
+ server.registerTool(
590
+ track(mcpToolName(ctx.productName, "farm_recommendation_create")),
591
+ {
592
+ title: "Write a Recommendation to a Client Farm",
593
+ description: "Record a recommendation on a client farm. If the recommendation names ANY pesticide, fungicide, herbicide or veterinary input, call guardrails_substance_rules first and quote it \u2014 a prohibited input can void the farm\u2019s organic certification for years, and this write puts your name on the advice. Requires a FULL_ADVISORY assignment.",
594
+ inputSchema: {
595
+ workspaceId: zod.z.string().describe(CLIENT_WORKSPACE_NOTE),
596
+ domain: zod.z.string().describe('Area the recommendation covers, e.g. "SOIL", "PEST", "WATER"'),
597
+ title: zod.z.string().describe("Short recommendation title"),
598
+ detail: zod.z.string().describe("The recommendation itself, and the reasoning behind it"),
599
+ priority: zod.z.string().optional().describe("Priority, as the schema defines it. Omit rather than guessing.")
600
+ }
601
+ },
602
+ async ({ workspaceId, domain, title, detail, priority }) => {
603
+ try {
604
+ const data = await ctx.gql.workspace.request(CREATE_FARM_RECOMMENDATION, {
605
+ input: {
606
+ workspaceId,
607
+ domain,
608
+ title,
609
+ detail,
610
+ ...priority !== void 0 ? { priority } : {}
611
+ }
612
+ });
613
+ return jsonResult(data.createHealthyBowlFarmRecommendation);
614
+ } catch (error) {
615
+ ctx.logger.error("healthybowl farm_recommendation_create failed", { error: String(error) });
616
+ return errorResult(
617
+ `Failed to create the recommendation: ${error instanceof Error ? error.message : String(error)}`
618
+ );
619
+ }
620
+ }
621
+ );
622
+ }
623
+ var PestTypeEnum = zod.z.enum(["INSECT", "DISEASE", "WEED", "FUNGUS"]);
624
+ var SeverityEnum = zod.z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]);
625
+ var IssueStatusEnum = zod.z.enum(["ACTIVE", "TREATING", "RESOLVED", "MONITORING"]);
626
+ function registerPestIssueTools(server, ctx, track) {
627
+ server.registerTool(
628
+ track(mcpToolName(ctx.productName, "plant_scan_log_to_pest_issue")),
629
+ {
630
+ title: "Log a Plant Scan to Pest Issues",
631
+ description: `Promote an existing AI plant scan into a tracked pest issue, so it enters the normal open \u2192 monitoring \u2192 resolved workflow. Use the scan\u2019s OWN diagnosis rather than restating it: the scan is the evidence, and a pest issue that disagrees with the scan it came from is worse than none. Do not name a treatment unless the farmer chose it. ${WORKSPACE_NOTE}`,
632
+ inputSchema: {
633
+ scanId: zod.z.string().describe("Plant scan ID being promoted"),
634
+ fieldId: zod.z.string().describe("Field the pest issue belongs to (a PestIssue needs a real field)"),
635
+ pestName: zod.z.string().optional().describe("Pest or disease name \u2014 take it from the scan"),
636
+ type: PestTypeEnum.optional(),
637
+ severity: SeverityEnum.optional(),
638
+ status: IssueStatusEnum.optional(),
639
+ cropAffected: zod.z.string().optional(),
640
+ treatment: zod.z.string().optional().describe(
641
+ "Only if the farmer has chosen one. Check guardrails_substance_rules before naming any input here."
642
+ ),
643
+ notes: zod.z.string().optional(),
644
+ attachPhoto: zod.z.boolean().optional().describe("Link the scan photo to the issue. Defaults to true.")
645
+ }
646
+ },
647
+ async (args) => {
648
+ try {
649
+ const data = await ctx.gql.workspace.request(LOG_PLANT_SCAN_TO_PEST_ISSUE, {
650
+ // `context: {}` — NOT a workspaceId. This mutation is header-scoped.
651
+ input: { ...args, context: EMPTY_CONTEXT }
652
+ });
653
+ return jsonResult(data.logHealthyBowlPlantScanToPestIssue);
654
+ } catch (error) {
655
+ ctx.logger.error("healthybowl plant_scan_log_to_pest_issue failed", {
656
+ error: String(error)
657
+ });
658
+ return errorResult(
659
+ `Failed to log the scan to pest issues: ${error instanceof Error ? error.message : String(error)}`
660
+ );
661
+ }
662
+ }
663
+ );
664
+ }
665
+
666
+ // src/modules/healthybowl/resources.ts
667
+ function registerHealthyBowlResources(server, ctx, track) {
668
+ const schemaUri = track(mcpResourceUri(ctx.productName, "schema"));
669
+ const guardrailsUri = track(mcpResourceUri(ctx.productName, "guardrails"));
670
+ server.registerResource(
671
+ "healthybowl-schema",
672
+ schemaUri,
673
+ {
674
+ title: "HealthyBowl Domain Schema",
675
+ description: "Overview of the HealthyBowl farm-operations domain \u2014 entities, enums, and the tool surface.",
676
+ mimeType: "text/plain"
677
+ },
678
+ async () => {
679
+ ctx.logger.debug(`Resource ${schemaUri} requested`);
680
+ const schema = `# HealthyBowl Domain (Overview)
681
+
682
+ An operating system for a working farm.
683
+
684
+ ## What this MCP module exposes
685
+ A READ window into the farm, plus one write (planting a crop). It is not the
686
+ whole product: compliance filings, deliveries, finance, consultations and
687
+ medication records are reachable in the app and deliberately not here.
688
+
689
+ ## Entities you will see
690
+ - Farm \u2014 the holding itself; carries organic certification status
691
+ - Field \u2014 a parcel; soil type, irrigation type, per-field certification
692
+ - Crop \u2014 what is planted in a field; growth stage, health, days to harvest
693
+ - Animal \u2014 a member of the herd; breed, tag number, production type
694
+ - Alert \u2014 something that needs attention today
695
+ - ComplianceSubstanceRule \u2014 the approved/prohibited input rulebook
696
+
697
+ ## Enums
698
+ - Crop health: EXCELLENT | GOOD | FAIR | POOR | CRITICAL
699
+ - Crop status: PLANNING | PLANTED | GROWING | FLOWERING | FRUITING | HARVEST_READY | HARVESTED
700
+ - Season: KHARIF | RABI | ZAID | PERENNIAL
701
+ - Produce type: MILK | MEAT | EGGS
702
+
703
+ ## No tool takes a workspace \u2014 select one first
704
+ HealthyBowl scopes on the authenticated session's \`x-workspace-id\` header, and
705
+ the backend deliberately IGNORES any workspace passed in the \`context\`
706
+ argument. Call \`${ctx.productName}_auth_select_workspace\` before anything else.
707
+
708
+ Until one is selected, reads return an EMPTY farm rather than an error \u2014 so an
709
+ unscoped call looks like a farm with nothing on it.
710
+
711
+ ## Before you advise
712
+ Read ${guardrailsUri}.`;
713
+ return { contents: [{ uri: schemaUri, mimeType: "text/plain", text: schema }] };
714
+ }
715
+ );
716
+ server.registerResource(
717
+ "healthybowl-guardrails",
718
+ guardrailsUri,
719
+ {
720
+ title: "HealthyBowl Advisory Guardrails",
721
+ description: "The checks that must happen before advising on produce sales or farm inputs. Read this before answering anything.",
722
+ mimeType: "text/plain"
723
+ },
724
+ async () => {
725
+ ctx.logger.debug(`Resource ${guardrailsUri} requested`);
726
+ const text = `# Before you advise on this farm
727
+
728
+ Farm advice is not like most tool output. Four things here have consequences
729
+ that land on a real person, and none of them are recoverable by editing a
730
+ record afterwards.
731
+
732
+ ## 1. Never say produce is safe to sell without checking
733
+ An animal treated with medicine carries a withdrawal period. Milk, meat or eggs
734
+ taken during it must not enter the food chain. \`healthStatus\` on the animal
735
+ does NOT encode this \u2014 a perfectly healthy animal can be blocked.
736
+
737
+ Call \`${ctx.productName}_guardrails_check_produce_withdrawal\` first. If it says
738
+ blocked, say no and quote the clear date. Do not offer a workaround.
739
+
740
+ ## 2. Never name a farm input from memory
741
+ Recommending a prohibited pesticide or veterinary input can void a farm's
742
+ organic certification for years. Call
743
+ \`${ctx.productName}_guardrails_substance_rules\` and quote from it.
744
+
745
+ A substance ABSENT from that list is not approved. Absence is not permission.
746
+ The honest answer is "that is not on your approved list \u2014 check with your
747
+ certifier."
748
+
749
+ ## 3. Certification status changes the answer
750
+ A farm or field marked \`organicCertified\` cannot be advised the same way as one
751
+ that is not. \`farm_overview\` and \`crops_list\` both report it. Read it before
752
+ recommending any treatment.
753
+
754
+ ## 4. If a check fails, you do not know
755
+ Both guardrail tools fail closed: when the check itself errors, that is not
756
+ permission to proceed on the optimistic reading. Tell the user the check could
757
+ not be run.`;
758
+ return { contents: [{ uri: guardrailsUri, mimeType: "text/plain", text }] };
759
+ }
760
+ );
761
+ }
762
+ function registerHealthyBowlPrompts(server, ctx, track) {
763
+ const p = ctx.productName;
764
+ const briefingName = track(mcpPromptName(p, "farm-briefing"));
765
+ const saleCheckName = track(mcpPromptName(p, "produce-sale-check"));
766
+ server.registerPrompt(
767
+ briefingName,
768
+ {
769
+ title: "Daily Farm Briefing",
770
+ description: "Walk the farm: alerts first, then crops approaching harvest, then the herd \u2014 in the order that matters on the day.",
771
+ argsSchema: {}
772
+ },
773
+ () => {
774
+ ctx.logger.debug(`Prompt ${briefingName} called`);
775
+ const wsNote = `Make sure a workspace is selected first (${p}_auth_select_workspace) \u2014 no farm tool takes a workspace argument, and an unselected session returns an empty farm rather than an error.`;
776
+ return {
777
+ messages: [
778
+ {
779
+ role: "user",
780
+ content: {
781
+ type: "text",
782
+ text: `Give me today's farm briefing. ${wsNote}
783
+
784
+ 1. **Alerts first**: Call ${p}_farm_overview. Lead with the alerts \u2014 they are what changed since yesterday. If there are none, say so plainly rather than padding.
785
+ 2. **Crops**: Call ${p}_crops_list. Call out anything with \`healthStatus\` of POOR or CRITICAL, and anything with \`daysToHarvest\` under 14. Ignore the rest \u2014 a briefing that lists every crop is not a briefing.
786
+ 3. **Herd**: Call ${p}_livestock_list. Flag animals whose \`lastCheckup\` is old or whose \`healthStatus\` is poor.
787
+ 4. **Do not advise on inputs or sales here.** If something in the briefing suggests a treatment or a sale, say what you noticed and stop \u2014 those need ${p}_guardrails_substance_rules and ${p}_guardrails_check_produce_withdrawal respectively.
788
+
789
+ Keep it short. The reader is standing in a field.`
790
+ }
791
+ }
792
+ ]
793
+ };
794
+ }
795
+ );
796
+ server.registerPrompt(
797
+ saleCheckName,
798
+ {
799
+ title: "Check Produce Before Sale",
800
+ description: "The mandatory pre-sale check for milk, meat or eggs from a specific animal \u2014 run this before listing, dispatching or collecting.",
801
+ argsSchema: {
802
+ animalId: zod.z.string().optional().describe("Animal whose produce is being sold")
803
+ }
804
+ },
805
+ ({ animalId }) => {
806
+ ctx.logger.debug(`Prompt ${saleCheckName} called`, { animalId });
807
+ const wsNote = `Confirm a workspace is selected (${p}_auth_select_workspace).`;
808
+ const animalNote = animalId ? `Check animal "${animalId}".` : `Ask which animal. If the user does not know the ID, call ${p}_livestock_list and match on name or tag number \u2014 do not guess.`;
809
+ return {
810
+ messages: [
811
+ {
812
+ role: "user",
813
+ content: {
814
+ type: "text",
815
+ text: `I want to sell produce from one of my animals. ${wsNote} ${animalNote}
816
+
817
+ 1. **Check the withdrawal window**: Call ${p}_guardrails_check_produce_withdrawal with the animalId and the produceType (MILK, MEAT or EGGS).
818
+ 2. **If \`blocked\` is true**: Say no. Quote the \`clearDate\` and the \`reason\`. Do not suggest a way around it, do not suggest selling it as something else, and do not soften it \u2014 produce inside a withdrawal window must not enter the food chain.
819
+ 3. **If the check itself fails**: Say the check could not be run, and that the produce therefore cannot be treated as safe. An error is not a green light.
820
+ 4. **If clear**: Say so, and name the animal and produce type you checked so the user can confirm you checked the right one.`
821
+ }
822
+ }
823
+ ]
824
+ };
825
+ }
826
+ );
827
+ }
828
+
829
+ // src/modules/healthybowl/index.ts
830
+ var healthybowlModule = {
831
+ id: "healthybowl",
832
+ version: "0.0.1",
833
+ displayName: "HealthyBowl",
834
+ dependsOn: ["auth"],
835
+ capabilities: { tools: {}, resources: {}, prompts: {} },
836
+ register(server, ctx) {
837
+ const tools = [];
838
+ const resources = [];
839
+ const prompts = [];
840
+ const trackTool = (n) => {
841
+ tools.push(n);
842
+ return n;
843
+ };
844
+ const trackResource = (uri) => {
845
+ resources.push(uri);
846
+ return uri;
847
+ };
848
+ const trackPrompt = (n) => {
849
+ prompts.push(n);
850
+ return n;
851
+ };
852
+ registerFarmTools(server, ctx, trackTool);
853
+ registerGuardrailTools(server, ctx, trackTool);
854
+ registerAdvisoryTools(server, ctx, trackTool);
855
+ registerPestIssueTools(server, ctx, trackTool);
856
+ registerHealthyBowlResources(server, ctx, trackResource);
857
+ registerHealthyBowlPrompts(server, ctx, trackPrompt);
858
+ return { tools, resources, prompts };
859
+ }
860
+ };
861
+ var healthybowl_default = healthybowlModule;
862
+
863
+ module.exports = healthybowl_default;
864
+ //# sourceMappingURL=index.cjs.map
865
+ //# sourceMappingURL=index.cjs.map