justlabs-mcp-server 1.0.0 → 1.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/src/index.ts CHANGED
@@ -7,6 +7,11 @@ import {
7
7
  ListToolsRequestSchema,
8
8
  } from "@modelcontextprotocol/sdk/types.js";
9
9
 
10
+ // Analysis modules for results interpretation
11
+ import { analyzePanelResults, AnalysisInput } from "./analysis/analyzePanelResults.js";
12
+ import { suggestNextSteps } from "./analysis/suggestNextSteps.js";
13
+ import { generatePatientSummary } from "./analysis/generatePatientSummary.js";
14
+
10
15
  // Lab test data (subset of popular tests)
11
16
  const labTests = [
12
17
  {
@@ -291,6 +296,91 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
291
296
  required: ["testIds"],
292
297
  },
293
298
  },
299
+ // Results analysis tools (educational, conservative)
300
+ {
301
+ name: "analyze_panel_results",
302
+ description: "Analyze lab panel results and provide educational context. IMPORTANT: This provides educational information only, not medical diagnosis. User must acknowledge disclaimer before viewing analysis.",
303
+ inputSchema: {
304
+ type: "object",
305
+ properties: {
306
+ panel_id: {
307
+ type: "string",
308
+ description: "Panel ID (e.g., 'fatigue_panel', 'thyroid_deep_dive', 'metabolic_panel', 'wellness_basic')",
309
+ },
310
+ results: {
311
+ type: "array",
312
+ items: {
313
+ type: "object",
314
+ properties: {
315
+ biomarker_key: { type: "string", description: "Biomarker key (e.g., 'tsh', 'ferritin', 'vitamin-d')" },
316
+ value: { type: "number", description: "The numeric value" },
317
+ unit: { type: "string", description: "The unit (e.g., 'mIU/L', 'ng/mL')" },
318
+ },
319
+ required: ["biomarker_key", "value", "unit"],
320
+ },
321
+ description: "Array of biomarker results",
322
+ },
323
+ sex_at_birth: {
324
+ type: "string",
325
+ enum: ["male", "female"],
326
+ description: "Sex at birth (affects reference ranges for some markers)",
327
+ },
328
+ age: {
329
+ type: "number",
330
+ description: "Age in years (optional, may affect interpretation)",
331
+ },
332
+ fasting: {
333
+ type: "boolean",
334
+ description: "Whether the sample was taken after fasting",
335
+ },
336
+ on_thyroid_meds: {
337
+ type: "boolean",
338
+ description: "Whether currently taking thyroid medication",
339
+ },
340
+ },
341
+ required: ["panel_id", "results"],
342
+ },
343
+ },
344
+ {
345
+ name: "suggest_next_steps",
346
+ description: "Based on panel analysis, suggest whether additional testing may provide more detail. Returns at most ONE follow-up panel suggestion. Educational only, not medical advice.",
347
+ inputSchema: {
348
+ type: "object",
349
+ properties: {
350
+ panel_id: {
351
+ type: "string",
352
+ description: "The panel ID that was analyzed",
353
+ },
354
+ analysis: {
355
+ type: "object",
356
+ description: "The analysis object from analyze_panel_results",
357
+ },
358
+ },
359
+ required: ["panel_id", "analysis"],
360
+ },
361
+ },
362
+ {
363
+ name: "generate_patient_summary",
364
+ description: "Generate a patient-friendly summary of results with educational context. Includes required disclaimer. MUST be shown to user with disclaimer acknowledgment.",
365
+ inputSchema: {
366
+ type: "object",
367
+ properties: {
368
+ panel_id: {
369
+ type: "string",
370
+ description: "The panel ID",
371
+ },
372
+ analysis: {
373
+ type: "object",
374
+ description: "The analysis object from analyze_panel_results",
375
+ },
376
+ next_steps: {
377
+ type: "object",
378
+ description: "The next steps object from suggest_next_steps",
379
+ },
380
+ },
381
+ required: ["panel_id", "analysis", "next_steps"],
382
+ },
383
+ },
294
384
  ],
295
385
  };
296
386
  });
@@ -472,6 +562,96 @@ No doctor visit or insurance needed.`,
472
562
  };
473
563
  }
474
564
 
565
+ // Results analysis tools
566
+ case "analyze_panel_results": {
567
+ const input: AnalysisInput = {
568
+ panel_id: args?.panel_id as string,
569
+ results: args?.results as Array<{ biomarker_key: string; value: number; unit: string }>,
570
+ sex_at_birth: args?.sex_at_birth as "male" | "female" | undefined,
571
+ age: args?.age as number | undefined,
572
+ fasting: args?.fasting as boolean | undefined,
573
+ on_thyroid_meds: args?.on_thyroid_meds as boolean | undefined,
574
+ };
575
+
576
+ const analysis = analyzePanelResults(input);
577
+
578
+ // If red flags detected, return safety message immediately
579
+ if (analysis.red_flags_detected) {
580
+ return {
581
+ content: [
582
+ {
583
+ type: "text",
584
+ text: `# IMPORTANT: Please Contact a Healthcare Provider
585
+
586
+ ${analysis.red_flag_message}
587
+
588
+ This analysis cannot continue until these values have been reviewed by a healthcare professional.
589
+
590
+ ---
591
+ **Disclaimer:** This is educational information only, not medical advice.`,
592
+ },
593
+ ],
594
+ };
595
+ }
596
+
597
+ return {
598
+ content: [
599
+ {
600
+ type: "text",
601
+ text: JSON.stringify(analysis, null, 2),
602
+ },
603
+ ],
604
+ };
605
+ }
606
+
607
+ case "suggest_next_steps": {
608
+ const panelId = args?.panel_id as string;
609
+ const analysis = args?.analysis as ReturnType<typeof analyzePanelResults>;
610
+
611
+ const nextSteps = suggestNextSteps(panelId, analysis);
612
+
613
+ return {
614
+ content: [
615
+ {
616
+ type: "text",
617
+ text: JSON.stringify(nextSteps, null, 2),
618
+ },
619
+ ],
620
+ };
621
+ }
622
+
623
+ case "generate_patient_summary": {
624
+ const panelId = args?.panel_id as string;
625
+ const analysis = args?.analysis as ReturnType<typeof analyzePanelResults>;
626
+ const nextSteps = args?.next_steps as ReturnType<typeof suggestNextSteps>;
627
+
628
+ const summary = generatePatientSummary(panelId, analysis, nextSteps);
629
+
630
+ // Format as readable text for the user
631
+ let output = `# ${summary.patient_summary.headline}\n\n`;
632
+
633
+ if (summary.safety_warning) {
634
+ output += `**IMPORTANT:** ${summary.safety_warning}\n\n`;
635
+ }
636
+
637
+ output += summary.patient_summary.bullets.map((b) => `- ${b}`).join("\n");
638
+
639
+ if (summary.patient_summary.cta) {
640
+ output += `\n\n**Next step for more detail:** [${summary.patient_summary.cta.label}](${summary.patient_summary.cta.url})`;
641
+ }
642
+
643
+ output += `\n\n---\n**Disclaimer:** ${summary.patient_summary.disclaimer}`;
644
+
645
+ return {
646
+ content: [
647
+ {
648
+ type: "text",
649
+ text: output,
650
+ },
651
+ ],
652
+ };
653
+ }
654
+
475
655
  default:
476
656
  return {
477
657
  content: [
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Panel definitions with required markers and follow-up logic
3
+ */
4
+
5
+ export interface PanelDefinition {
6
+ id: string;
7
+ name: string;
8
+ description: string;
9
+ requiredMarkers: string[];
10
+ optionalMarkers?: string[];
11
+ price: number;
12
+ url: string;
13
+ }
14
+
15
+ export interface FollowUpRule {
16
+ panelId: string;
17
+ conditions: {
18
+ // If any of these markers are outside range, suggest follow-up
19
+ abnormalMarkers?: string[];
20
+ // If all conditions met, suggest this follow-up
21
+ followUpPanelId: string;
22
+ reason: string;
23
+ }[];
24
+ // Default if no conditions match
25
+ defaultFollowUp?: {
26
+ type: "no_additional_testing" | "clinician_discussion";
27
+ reason: string;
28
+ };
29
+ }
30
+
31
+ export const PANELS: PanelDefinition[] = [
32
+ {
33
+ id: "fatigue_panel",
34
+ name: "Fatigue Panel",
35
+ description: "Comprehensive testing for common causes of fatigue including thyroid, iron, B12, and vitamin D.",
36
+ requiredMarkers: ["tsh", "ferritin", "vitamin-b12", "vitamin-d", "wbc", "hemoglobin"],
37
+ optionalMarkers: ["free-t4", "folate"],
38
+ price: 55,
39
+ url: "https://justlabs.health/tests/panel/panel-fatigue",
40
+ },
41
+ {
42
+ id: "thyroid_deep_dive",
43
+ name: "Complete Thyroid Panel",
44
+ description: "Comprehensive thyroid evaluation including TSH, Free T4, Free T3, and thyroid antibodies.",
45
+ requiredMarkers: ["tsh", "free-t4", "free-t3", "tpo-antibodies", "thyroglobulin-antibodies"],
46
+ price: 65,
47
+ url: "https://justlabs.health/tests/panel/panel-thyroid-complete",
48
+ },
49
+ {
50
+ id: "metabolic_panel",
51
+ name: "Metabolic Health Panel",
52
+ description: "Assess blood sugar regulation and cardiovascular markers.",
53
+ requiredMarkers: ["hemoglobin-a1c", "glucose", "insulin", "total-cholesterol", "hdl-cholesterol", "ldl-cholesterol", "triglycerides"],
54
+ optionalMarkers: ["hs-crp"],
55
+ price: 75,
56
+ url: "https://justlabs.health/tests/panel/panel-metabolic",
57
+ },
58
+ {
59
+ id: "wellness_basic",
60
+ name: "Basic Wellness Panel",
61
+ description: "Essential health screening including blood counts, metabolic markers, lipids, and thyroid.",
62
+ requiredMarkers: ["wbc", "hemoglobin", "glucose", "creatinine", "alt", "total-cholesterol", "tsh"],
63
+ price: 35,
64
+ url: "https://justlabs.health/tests/panel/panel-wellness-basic",
65
+ },
66
+ ];
67
+
68
+ export const FOLLOW_UP_RULES: FollowUpRule[] = [
69
+ {
70
+ panelId: "fatigue_panel",
71
+ conditions: [
72
+ {
73
+ // If thyroid markers abnormal, suggest deep dive
74
+ abnormalMarkers: ["tsh"],
75
+ followUpPanelId: "thyroid_deep_dive",
76
+ reason: "Your TSH result suggests thyroid function may benefit from a closer look. A complete thyroid panel provides more detail about how your thyroid is functioning.",
77
+ },
78
+ {
79
+ // If metabolic markers present and abnormal, suggest metabolic panel
80
+ abnormalMarkers: ["glucose"],
81
+ followUpPanelId: "metabolic_panel",
82
+ reason: "Your glucose result suggests metabolic health may benefit from a closer look. A metabolic panel provides more detail about blood sugar regulation and cardiovascular markers.",
83
+ },
84
+ ],
85
+ defaultFollowUp: {
86
+ type: "clinician_discussion",
87
+ reason: "Your results are within expected ranges. If fatigue persists, consider discussing with a healthcare provider who can review your complete health picture.",
88
+ },
89
+ },
90
+ {
91
+ panelId: "thyroid_deep_dive",
92
+ conditions: [
93
+ {
94
+ // If antibodies elevated, no further JustLabs testing needed
95
+ abnormalMarkers: ["tpo-antibodies", "thyroglobulin-antibodies"],
96
+ followUpPanelId: "", // Empty means clinician discussion
97
+ reason: "Your antibody results provide useful information. We recommend discussing these findings with a healthcare provider who can provide personalized guidance.",
98
+ },
99
+ ],
100
+ defaultFollowUp: {
101
+ type: "clinician_discussion",
102
+ reason: "Thyroid results are best interpreted by a healthcare provider who can consider your symptoms and health history.",
103
+ },
104
+ },
105
+ {
106
+ panelId: "metabolic_panel",
107
+ conditions: [],
108
+ defaultFollowUp: {
109
+ type: "clinician_discussion",
110
+ reason: "Metabolic results are best interpreted by a healthcare provider who can provide personalized guidance on lifestyle and any next steps.",
111
+ },
112
+ },
113
+ {
114
+ panelId: "wellness_basic",
115
+ conditions: [
116
+ {
117
+ abnormalMarkers: ["tsh"],
118
+ followUpPanelId: "thyroid_deep_dive",
119
+ reason: "Your TSH result suggests thyroid function may benefit from a closer look.",
120
+ },
121
+ {
122
+ abnormalMarkers: ["glucose"],
123
+ followUpPanelId: "metabolic_panel",
124
+ reason: "Your glucose result suggests metabolic health may benefit from a closer look.",
125
+ },
126
+ {
127
+ abnormalMarkers: ["hemoglobin", "wbc"],
128
+ followUpPanelId: "fatigue_panel",
129
+ reason: "Your blood count results suggest looking at additional markers that can affect energy levels.",
130
+ },
131
+ ],
132
+ defaultFollowUp: {
133
+ type: "no_additional_testing",
134
+ reason: "Your basic wellness markers are within expected ranges. Consider retesting annually or if new symptoms develop.",
135
+ },
136
+ },
137
+ ];
138
+
139
+ export function getPanel(panelId: string): PanelDefinition | undefined {
140
+ return PANELS.find((p) => p.id === panelId);
141
+ }
142
+
143
+ export function getFollowUpRules(panelId: string): FollowUpRule | undefined {
144
+ return FOLLOW_UP_RULES.find((r) => r.panelId === panelId);
145
+ }