@hormonaly/mcp-server 1.0.1

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/dist/tools.js ADDED
@@ -0,0 +1,766 @@
1
+ /**
2
+ * All MCP tool definitions and handlers for the Hormonaly platform.
3
+ */
4
+ import { HoromnalyClient, loadConfig } from "./client.js";
5
+ // ─── Tool result helpers ──────────────────────────────────────────────────────
6
+ function ok(data) {
7
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
8
+ }
9
+ function err(message) {
10
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
11
+ }
12
+ // ─── Tool schema definitions ──────────────────────────────────────────────────
13
+ export const TOOL_DEFINITIONS = [
14
+ // ── Helix Tools ──────────────────────────────────────────────────────────
15
+ {
16
+ name: "helix_query",
17
+ description: "Query the Helix AI engine with a clinical question about peptides, hormones, longevity, or aesthetics. Returns an evidence-based answer with GRADE rating, confidence score, citations, and related protocols. Requires a Helix API key.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ question: {
22
+ type: "string",
23
+ description: "The clinical question to answer (e.g. 'What is the optimal BPC-157 dose for gut repair?')",
24
+ maxLength: 10000,
25
+ },
26
+ language: {
27
+ type: "string",
28
+ enum: ["en", "ar"],
29
+ default: "en",
30
+ description: "Response language",
31
+ },
32
+ detail_level: {
33
+ type: "string",
34
+ enum: ["clinical", "summary"],
35
+ default: "clinical",
36
+ description: "Level of detail: 'clinical' for full analysis, 'summary' for a concise overview",
37
+ },
38
+ include_citations: {
39
+ type: "boolean",
40
+ default: true,
41
+ description: "Include PubMed citation list in the response",
42
+ },
43
+ include_three_lens: {
44
+ type: "boolean",
45
+ default: false,
46
+ description: "Include Three-Lens analysis (longevity / health-disease / performance scoring)",
47
+ },
48
+ api_key: {
49
+ type: "string",
50
+ description: "Helix API key (overrides HORMONALY_API_KEY env var)",
51
+ },
52
+ },
53
+ required: ["question"],
54
+ },
55
+ },
56
+ {
57
+ name: "helix_compare",
58
+ description: "Compare 2–3 compounds head-to-head using the Helix AI engine. Returns a structured comparison with key differences, best-for use cases, and a recommendation. Requires Professional or Enterprise tier Helix API key — Starter keys are blocked with 403 tier_insufficient.",
59
+ inputSchema: {
60
+ type: "object",
61
+ properties: {
62
+ compounds: {
63
+ type: "array",
64
+ items: { type: "string" },
65
+ minItems: 2,
66
+ maxItems: 3,
67
+ description: "List of 2 or 3 compound names to compare (e.g. ['semaglutide', 'tirzepatide'])",
68
+ },
69
+ indication: {
70
+ type: "string",
71
+ default: "General comparison",
72
+ description: "Clinical indication or context for the comparison",
73
+ },
74
+ language: {
75
+ type: "string",
76
+ enum: ["en", "ar"],
77
+ default: "en",
78
+ },
79
+ api_key: {
80
+ type: "string",
81
+ description: "Helix API key (overrides HORMONALY_API_KEY env var)",
82
+ },
83
+ },
84
+ required: ["compounds"],
85
+ },
86
+ },
87
+ {
88
+ name: "helix_protocol",
89
+ description: "Retrieve all protocols for a specific compound from the Helix API. Returns titles, evidence grades, FDA status, and summaries. Requires a Helix API key.",
90
+ inputSchema: {
91
+ type: "object",
92
+ properties: {
93
+ compound: {
94
+ type: "string",
95
+ description: "Compound name (e.g. 'semaglutide', 'BPC-157', 'testosterone')",
96
+ maxLength: 200,
97
+ },
98
+ language: {
99
+ type: "string",
100
+ enum: ["en", "ar"],
101
+ default: "en",
102
+ },
103
+ api_key: {
104
+ type: "string",
105
+ description: "Helix API key (overrides HORMONALY_API_KEY env var)",
106
+ },
107
+ },
108
+ required: ["compound"],
109
+ },
110
+ },
111
+ {
112
+ name: "helix_dossier_start",
113
+ description: "Start generating a full evidence dossier for a compound (async job). Returns a job_id to poll with helix_dossier_status. Requires Professional or Enterprise tier — Starter keys are blocked with 403 tier_insufficient.",
114
+ inputSchema: {
115
+ type: "object",
116
+ properties: {
117
+ compound: {
118
+ type: "string",
119
+ description: "Compound name",
120
+ maxLength: 500,
121
+ },
122
+ language: {
123
+ type: "string",
124
+ enum: ["en", "ar"],
125
+ default: "en",
126
+ },
127
+ api_key: {
128
+ type: "string",
129
+ description: "Helix API key (overrides HORMONALY_API_KEY env var)",
130
+ },
131
+ },
132
+ required: ["compound"],
133
+ },
134
+ },
135
+ {
136
+ name: "helix_dossier_status",
137
+ description: "Check the status of a dossier generation job started with helix_dossier_start. Returns status (processing/completed/failed) and the dossier data when complete.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {
141
+ job_id: {
142
+ type: "string",
143
+ description: "Job ID returned by helix_dossier_start",
144
+ },
145
+ api_key: {
146
+ type: "string",
147
+ description: "Helix API key (overrides HORMONALY_API_KEY env var)",
148
+ },
149
+ },
150
+ required: ["job_id"],
151
+ },
152
+ },
153
+ // ── Agentic Workflow Tools ────────────────────────────────────────────────
154
+ {
155
+ name: "run_clinical_workflow",
156
+ description: "Run a full multi-agent clinical reasoning workflow on a complex question. This triggers the supervisor/worker pattern: the supervisor decomposes the question, spawns specialized sub-agents (evidence, safety, dosing, regulatory), then synthesizes all findings. Best for complex queries involving multiple compounds, stacking protocols, or high-stakes clinical decisions. Returns the synthesized answer plus sub-agent contributions and orchestration steps.",
157
+ inputSchema: {
158
+ type: "object",
159
+ properties: {
160
+ question: {
161
+ type: "string",
162
+ description: "The complex clinical question (e.g. 'What is the optimal BPC-157 + TB-500 stack for accelerated tendon repair in athletes, accounting for safety and interactions?')",
163
+ maxLength: 10000,
164
+ },
165
+ patient_context: {
166
+ type: "string",
167
+ description: "Optional patient context (age, sex, conditions, medications) to personalize the workflow",
168
+ },
169
+ language: {
170
+ type: "string",
171
+ enum: ["en", "ar"],
172
+ default: "en",
173
+ },
174
+ api_key: {
175
+ type: "string",
176
+ description: "Helix API key (overrides HORMONALY_API_KEY env var)",
177
+ },
178
+ },
179
+ required: ["question"],
180
+ },
181
+ },
182
+ {
183
+ name: "helix_deep_analysis",
184
+ description: "Run an extended deep analysis on a clinical topic using the highest-capability model with full RAG pipeline, three-lens scoring, and citation verification. Returns a comprehensive clinical synthesis with evidence grade, confidence score, PMID citations, and three-lens analysis (longevity / health-disease / performance). Use this for research synthesis, systematic review preparation, or comprehensive protocol evaluation. Requires Enterprise Helix API key.",
185
+ inputSchema: {
186
+ type: "object",
187
+ properties: {
188
+ topic: {
189
+ type: "string",
190
+ description: "Clinical topic or question for deep analysis (e.g. 'Comprehensive analysis of semaglutide for longevity and metabolic optimization')",
191
+ maxLength: 10000,
192
+ },
193
+ language: {
194
+ type: "string",
195
+ enum: ["en", "ar"],
196
+ default: "en",
197
+ },
198
+ api_key: {
199
+ type: "string",
200
+ description: "Helix API key (overrides HORMONALY_API_KEY env var)",
201
+ },
202
+ },
203
+ required: ["topic"],
204
+ },
205
+ },
206
+ {
207
+ name: "monitor_protocol_updates",
208
+ description: "Check if any of the user's saved protocols have new evidence updates or recommendations available. Returns a list of protocols with their last-updated date, current evidence grade, and whether a review is recommended. Requires user session token.",
209
+ inputSchema: {
210
+ type: "object",
211
+ properties: {
212
+ session_token: {
213
+ type: "string",
214
+ description: "User session token (from /api/auth/session or Settings → API Keys)",
215
+ },
216
+ compound_filter: {
217
+ type: "array",
218
+ items: { type: "string" },
219
+ description: "Optional: filter to specific compounds (e.g. ['BPC-157', 'semaglutide'])",
220
+ },
221
+ },
222
+ required: [],
223
+ },
224
+ },
225
+ // ── Protocol Tools ────────────────────────────────────────────────────────
226
+ {
227
+ name: "protocol_search",
228
+ description: "Search the Hormonaly protocol library by compound name, category, or condition. Returns a list of matching protocols with titles, evidence grades, and categories. No authentication required.",
229
+ inputSchema: {
230
+ type: "object",
231
+ properties: {
232
+ query: {
233
+ type: "string",
234
+ description: "Search query (compound name, condition, or category)",
235
+ },
236
+ category: {
237
+ type: "string",
238
+ description: "Filter by category slug (e.g. 'weight-loss', 'longevity', 'hormones')",
239
+ },
240
+ limit: {
241
+ type: "number",
242
+ default: 10,
243
+ description: "Maximum number of results (max 20)",
244
+ },
245
+ },
246
+ required: ["query"],
247
+ },
248
+ },
249
+ {
250
+ name: "protocol_get",
251
+ description: "Get the full details of a protocol by its ID or slug. Returns dosing tables, mechanism steps, safety considerations, citations, and all enriched data.",
252
+ inputSchema: {
253
+ type: "object",
254
+ properties: {
255
+ id: {
256
+ type: "string",
257
+ description: "Protocol ID or slug (e.g. 'semaglutide-glp1', 'bpc-157-gut')",
258
+ },
259
+ },
260
+ required: ["id"],
261
+ },
262
+ },
263
+ {
264
+ name: "protocol_list_categories",
265
+ description: "List all protocol categories available in the Hormonaly library with their slugs and protocol counts.",
266
+ inputSchema: {
267
+ type: "object",
268
+ properties: {},
269
+ },
270
+ },
271
+ {
272
+ name: "protocol_get_interactions",
273
+ description: "Check for known drug/compound interactions between a set of compounds. Returns severity, mechanism, clinical effect, and recommendations.",
274
+ inputSchema: {
275
+ type: "object",
276
+ properties: {
277
+ compounds: {
278
+ type: "array",
279
+ items: { type: "string" },
280
+ minItems: 2,
281
+ description: "List of compound slugs to check interactions for (e.g. ['semaglutide', 'metformin'])",
282
+ },
283
+ },
284
+ required: ["compounds"],
285
+ },
286
+ },
287
+ // ── Evidence Tools ────────────────────────────────────────────────────────
288
+ {
289
+ name: "evidence_search",
290
+ description: "Search PubMed for research papers on a compound or condition. Returns paper titles, PMIDs, abstracts, and publication dates.",
291
+ inputSchema: {
292
+ type: "object",
293
+ properties: {
294
+ compound: {
295
+ type: "string",
296
+ description: "Compound or peptide name to search for",
297
+ },
298
+ max_results: {
299
+ type: "number",
300
+ default: 10,
301
+ description: "Maximum number of papers to return",
302
+ },
303
+ },
304
+ required: ["compound"],
305
+ },
306
+ },
307
+ {
308
+ name: "evidence_get",
309
+ description: "Get full evidence details for a specific evidence record by ID.",
310
+ inputSchema: {
311
+ type: "object",
312
+ properties: {
313
+ id: {
314
+ type: "string",
315
+ description: "Evidence record ID",
316
+ },
317
+ },
318
+ required: ["id"],
319
+ },
320
+ },
321
+ {
322
+ name: "evidence_grade",
323
+ description: "Fetch GRADE-methodology evidence ratings for a set of evidence record IDs. Returns each record's study type, evidence grade (A/B/C/D), confidence level, PMID, and clinical relevance summary. Use this to programmatically evaluate the strength of evidence behind a set of references.",
324
+ inputSchema: {
325
+ type: "object",
326
+ properties: {
327
+ ids: {
328
+ type: "array",
329
+ items: { type: "string" },
330
+ minItems: 1,
331
+ maxItems: 20,
332
+ description: "Evidence record IDs to grade (returned by evidence_search or evidence_get)",
333
+ },
334
+ },
335
+ required: ["ids"],
336
+ },
337
+ },
338
+ // ── Compound Tools ────────────────────────────────────────────────────────
339
+ {
340
+ name: "compound_search",
341
+ description: "Search the Hormonaly compound database by name or category. Returns compound names, categories, and slugs.",
342
+ inputSchema: {
343
+ type: "object",
344
+ properties: {
345
+ query: {
346
+ type: "string",
347
+ description: "Compound name or partial name to search for",
348
+ },
349
+ category: {
350
+ type: "string",
351
+ description: "Filter by compound category",
352
+ },
353
+ },
354
+ required: ["query"],
355
+ },
356
+ },
357
+ {
358
+ name: "compound_get_interactions",
359
+ description: "Get all known interactions for a specific compound.",
360
+ inputSchema: {
361
+ type: "object",
362
+ properties: {
363
+ slug: {
364
+ type: "string",
365
+ description: "Compound slug (e.g. 'semaglutide', 'bpc-157')",
366
+ },
367
+ },
368
+ required: ["slug"],
369
+ },
370
+ },
371
+ {
372
+ name: "compound_get_dosing",
373
+ description: "Get evidence-based dosing ranges, administration routes, frequency, and cycle guidance for a compound. Returns the full dosing table from the protocol library including dose ranges, units, frequency, and clinical notes. No authentication required.",
374
+ inputSchema: {
375
+ type: "object",
376
+ properties: {
377
+ slug: {
378
+ type: "string",
379
+ description: "Compound slug or protocol ID (e.g. 'semaglutide', 'bpc-157', 'testosterone-cypionate')",
380
+ },
381
+ },
382
+ required: ["slug"],
383
+ },
384
+ },
385
+ // ── User Tools ────────────────────────────────────────────────────────────
386
+ {
387
+ name: "user_get_profile",
388
+ description: "Get the current authenticated user's profile including name, role, subscription status, and onboarding state. Requires session authentication.",
389
+ inputSchema: {
390
+ type: "object",
391
+ properties: {
392
+ session_token: {
393
+ type: "string",
394
+ description: "Session token (overrides HORMONALY_SESSION_TOKEN env var)",
395
+ },
396
+ },
397
+ },
398
+ },
399
+ {
400
+ name: "user_get_usage",
401
+ description: "Get AI usage statistics for the current user (tokens consumed, cost breakdown, request counts). Requires session authentication.",
402
+ inputSchema: {
403
+ type: "object",
404
+ properties: {
405
+ session_token: {
406
+ type: "string",
407
+ description: "Session token (overrides HORMONALY_SESSION_TOKEN env var)",
408
+ },
409
+ },
410
+ },
411
+ },
412
+ {
413
+ name: "user_get_saved_protocols",
414
+ description: "Get the list of protocols saved by the current user. Requires session authentication.",
415
+ inputSchema: {
416
+ type: "object",
417
+ properties: {
418
+ session_token: {
419
+ type: "string",
420
+ description: "Session token (overrides HORMONALY_SESSION_TOKEN env var)",
421
+ },
422
+ },
423
+ },
424
+ },
425
+ // ── Admin Tools ───────────────────────────────────────────────────────────
426
+ {
427
+ name: "admin_get_stats",
428
+ description: "Get platform-wide statistics: user counts, protocol counts, AI usage totals, revenue metrics. Requires admin session.",
429
+ inputSchema: {
430
+ type: "object",
431
+ properties: {
432
+ admin_session_token: {
433
+ type: "string",
434
+ description: "Admin session token (overrides HORMONALY_ADMIN_SESSION_TOKEN env var)",
435
+ },
436
+ },
437
+ },
438
+ },
439
+ {
440
+ name: "admin_list_users",
441
+ description: "List platform users with optional filters. Returns user emails, signup dates, subscription status, and usage stats. Requires admin session.",
442
+ inputSchema: {
443
+ type: "object",
444
+ properties: {
445
+ limit: {
446
+ type: "number",
447
+ default: 20,
448
+ description: "Number of users to return",
449
+ },
450
+ offset: {
451
+ type: "number",
452
+ default: 0,
453
+ description: "Pagination offset",
454
+ },
455
+ search: {
456
+ type: "string",
457
+ description: "Search by email or name",
458
+ },
459
+ admin_session_token: {
460
+ type: "string",
461
+ description: "Admin session token (overrides HORMONALY_ADMIN_SESSION_TOKEN env var)",
462
+ },
463
+ },
464
+ },
465
+ },
466
+ {
467
+ name: "admin_get_ai_costs",
468
+ description: "Get AI cost breakdown by model, endpoint, and time period. Requires admin session.",
469
+ inputSchema: {
470
+ type: "object",
471
+ properties: {
472
+ days: {
473
+ type: "number",
474
+ default: 30,
475
+ description: "Number of days to include in the report",
476
+ },
477
+ admin_session_token: {
478
+ type: "string",
479
+ description: "Admin session token (overrides HORMONALY_ADMIN_SESSION_TOKEN env var)",
480
+ },
481
+ },
482
+ },
483
+ },
484
+ ];
485
+ // ─── Tool handlers ────────────────────────────────────────────────────────────
486
+ export async function handleTool(name, args, client) {
487
+ try {
488
+ switch (name) {
489
+ // ── Helix ──────────────────────────────────────────────────────────────
490
+ case "helix_query": {
491
+ const effectiveClient = withApiKey(client, args["api_key"]);
492
+ const result = await effectiveClient.post("/api/v1/helix/query", {
493
+ question: args["question"],
494
+ language: args["language"] ?? "en",
495
+ detail_level: args["detail_level"] ?? "clinical",
496
+ include_citations: args["include_citations"] ?? true,
497
+ include_three_lens: args["include_three_lens"] ?? false,
498
+ }, "helix");
499
+ return ok(result);
500
+ }
501
+ case "helix_compare": {
502
+ const effectiveClient = withApiKey(client, args["api_key"]);
503
+ const result = await effectiveClient.post("/api/v1/helix/compare", {
504
+ compounds: args["compounds"],
505
+ indication: args["indication"] ?? "General comparison",
506
+ language: args["language"] ?? "en",
507
+ }, "helix");
508
+ return ok(result);
509
+ }
510
+ case "helix_protocol": {
511
+ const effectiveClient = withApiKey(client, args["api_key"]);
512
+ const compound = encodeURIComponent(String(args["compound"]));
513
+ const lang = args["language"] ?? "en";
514
+ const result = await effectiveClient.get(`/api/v1/helix/protocols/${compound}?language=${lang}`, "helix");
515
+ return ok(result);
516
+ }
517
+ case "helix_dossier_start": {
518
+ const effectiveClient = withApiKey(client, args["api_key"]);
519
+ const result = await effectiveClient.post("/api/v1/helix/dossier", {
520
+ compound: args["compound"],
521
+ language: args["language"] ?? "en",
522
+ }, "helix");
523
+ return ok(result);
524
+ }
525
+ case "helix_dossier_status": {
526
+ const effectiveClient = withApiKey(client, args["api_key"]);
527
+ const jobId = encodeURIComponent(String(args["job_id"]));
528
+ const result = await effectiveClient.get(`/api/v1/helix/dossier/${jobId}`, "helix");
529
+ return ok(result);
530
+ }
531
+ // ── Protocols ─────────────────────────────────────────────────────────
532
+ case "protocol_search": {
533
+ const q = encodeURIComponent(String(args["query"]));
534
+ const limit = Math.min(Number(args["limit"] ?? 10), 20);
535
+ const cat = args["category"] ? `&category=${encodeURIComponent(String(args["category"]))}` : "";
536
+ const result = await client.get(`/api/protocols/search?q=${q}&limit=${limit}${cat}`, "public");
537
+ return ok(result);
538
+ }
539
+ case "protocol_get": {
540
+ const id = encodeURIComponent(String(args["id"]));
541
+ const result = await client.get(`/api/protocols/${id}`, "public");
542
+ return ok(result);
543
+ }
544
+ case "protocol_list_categories": {
545
+ // The protocols endpoint supports category listing via list-all
546
+ const protocols = await client.get("/api/protocols/list-all", "public");
547
+ // Aggregate unique categories from the full list
548
+ const categories = new Map();
549
+ if (Array.isArray(protocols)) {
550
+ for (const p of protocols) {
551
+ const slug = p.categoryId ?? p.category;
552
+ if (slug) {
553
+ const existing = categories.get(slug);
554
+ if (existing) {
555
+ existing.count++;
556
+ }
557
+ else {
558
+ categories.set(slug, { name: p.category, slug, count: 1 });
559
+ }
560
+ }
561
+ }
562
+ }
563
+ return ok(Array.from(categories.values()).sort((a, b) => b.count - a.count));
564
+ }
565
+ case "protocol_get_interactions": {
566
+ const compounds = args["compounds"] ?? [];
567
+ const allInteractions = await Promise.all(compounds.map((slug) => client.get(`/api/interactions/for/${slug}`, "public")));
568
+ const result = { compounds, interactions: allInteractions.flatMap((r) => r?.interactions ?? []) };
569
+ return ok(result);
570
+ }
571
+ // ── Evidence ──────────────────────────────────────────────────────────
572
+ case "evidence_search": {
573
+ const compound = encodeURIComponent(String(args["compound"]));
574
+ const maxResults = args["max_results"] ?? 10;
575
+ const result = await client.get(`/api/pubmed/search?peptide=${compound}&maxResults=${maxResults}`, "public");
576
+ return ok(result);
577
+ }
578
+ case "evidence_get": {
579
+ const id = encodeURIComponent(String(args["id"]));
580
+ const result = await client.get(`/api/evidence/${id}`, "public");
581
+ return ok(result);
582
+ }
583
+ case "evidence_grade": {
584
+ const ids = args["ids"];
585
+ const result = await client.post("/api/evidence/batch", { ids }, "public");
586
+ const records = Array.isArray(result) ? result : result?.evidence ?? [];
587
+ const graded = records.map((e) => ({
588
+ id: e.id,
589
+ title: e.title ?? e.name ?? null,
590
+ pmid: e.pmid ?? e.pubmedId ?? null,
591
+ study_type: e.studyType ?? e.study_type ?? null,
592
+ grade: e.gradeRating ?? e.grade ?? null,
593
+ confidence: e.confidenceLevel ?? e.confidence ?? null,
594
+ summary: e.summary ?? e.clinicalRelevance ?? null,
595
+ }));
596
+ return ok({ count: graded.length, evidence: graded });
597
+ }
598
+ // ── Compounds ─────────────────────────────────────────────────────────
599
+ case "compound_search": {
600
+ const params = new URLSearchParams();
601
+ params.set("search", String(args["query"]));
602
+ if (args["category"])
603
+ params.set("category", String(args["category"]));
604
+ const result = await client.get(`/api/compounds?${params.toString()}`, "public");
605
+ return ok(result);
606
+ }
607
+ case "compound_get_interactions": {
608
+ const slug = encodeURIComponent(String(args["slug"]));
609
+ const result = await client.get(`/api/interactions/for/${slug}`, "public");
610
+ return ok(result);
611
+ }
612
+ case "compound_get_dosing": {
613
+ const slug = encodeURIComponent(String(args["slug"]));
614
+ const protocol = await client.get(`/api/protocols/${slug}`, "public");
615
+ if (!protocol)
616
+ return err(`No protocol found for compound: ${args["slug"]}`);
617
+ return ok({
618
+ compound: protocol.title ?? protocol.name ?? args["slug"],
619
+ slug: protocol.slug ?? args["slug"],
620
+ evidence_grade: protocol.gradeRating ?? protocol.evidenceGrade ?? null,
621
+ dosing_table: protocol.dosingTable ?? protocol.dosing?.table ?? [],
622
+ dosing_notes: protocol.dosingNotes ?? protocol.dosing?.notes ?? [],
623
+ administration_routes: protocol.administrationRoutes ?? null,
624
+ cycle_guidance: protocol.cycleGuidance ?? null,
625
+ fda_status: protocol.fdaStatus ?? null,
626
+ });
627
+ }
628
+ // ── User ──────────────────────────────────────────────────────────────
629
+ case "user_get_profile": {
630
+ const effectiveClient = withSessionToken(client, args["session_token"]);
631
+ const result = await effectiveClient.get("/api/auth/user", "session");
632
+ return ok(result);
633
+ }
634
+ case "user_get_usage": {
635
+ const effectiveClient = withSessionToken(client, args["session_token"]);
636
+ const result = await effectiveClient.get("/api/user/ai-usage", "session");
637
+ return ok(result);
638
+ }
639
+ case "user_get_saved_protocols": {
640
+ const effectiveClient = withSessionToken(client, args["session_token"]);
641
+ const result = await effectiveClient.get("/api/user/saved-protocols", "session");
642
+ return ok(result);
643
+ }
644
+ // ── Admin ─────────────────────────────────────────────────────────────
645
+ case "admin_get_stats": {
646
+ const effectiveClient = withAdminToken(client, args["admin_session_token"]);
647
+ const result = await effectiveClient.get("/api/admin/stats", "admin");
648
+ return ok(result);
649
+ }
650
+ case "admin_list_users": {
651
+ const effectiveClient = withAdminToken(client, args["admin_session_token"]);
652
+ const params = new URLSearchParams();
653
+ params.set("limit", String(args["limit"] ?? 20));
654
+ params.set("offset", String(args["offset"] ?? 0));
655
+ if (args["search"])
656
+ params.set("search", String(args["search"]));
657
+ const result = await effectiveClient.get(`/api/admin/users?${params.toString()}`, "admin");
658
+ return ok(result);
659
+ }
660
+ case "admin_get_ai_costs": {
661
+ const effectiveClient = withAdminToken(client, args["admin_session_token"]);
662
+ const days = args["days"] ?? 30;
663
+ const result = await effectiveClient.get(`/api/admin/ai-costs?days=${days}`, "admin");
664
+ return ok(result);
665
+ }
666
+ // ── Agentic Workflow ──────────────────────────────────────────────────
667
+ case "run_clinical_workflow": {
668
+ const effectiveClient = withApiKey(client, args["api_key"]);
669
+ const question = args["question"];
670
+ const patientContext = args["patient_context"];
671
+ const language = args["language"] || "en";
672
+ const fullQuestion = patientContext
673
+ ? `${question}\n\nPatient context: ${patientContext}`
674
+ : question;
675
+ // Use helix /query with clinical detail level — complex queries get supervisor treatment
676
+ const result = await effectiveClient.post("/api/v1/helix/query", "apiKey", {
677
+ question: fullQuestion,
678
+ language,
679
+ detail_level: "clinical",
680
+ include_citations: true,
681
+ include_three_lens: true,
682
+ });
683
+ // Format the result to highlight the multi-agent nature
684
+ const formatted = {
685
+ answer: result?.data?.answer,
686
+ evidence_grade: result?.data?.grade,
687
+ confidence: result?.data?.confidence,
688
+ agents_used: result?.data?.agents_used ?? [],
689
+ agent_attributions: result?.data?.agent_attributions ?? [],
690
+ orchestration_steps: result?.data?.orchestration_steps ?? [],
691
+ citations: result?.data?.citations ?? [],
692
+ three_lens: result?.data?.three_lens ?? null,
693
+ usage: result?.usage,
694
+ };
695
+ return ok(formatted);
696
+ }
697
+ case "helix_deep_analysis": {
698
+ const effectiveClient = withApiKey(client, args["api_key"]);
699
+ const topic = args["topic"];
700
+ const language = args["language"] || "en";
701
+ const result = await effectiveClient.post("/api/v1/helix/query", "apiKey", {
702
+ question: topic,
703
+ language,
704
+ detail_level: "clinical",
705
+ include_citations: true,
706
+ include_three_lens: true,
707
+ });
708
+ return ok({
709
+ analysis: result?.data?.answer,
710
+ evidence_grade: result?.data?.grade,
711
+ confidence: result?.data?.confidence,
712
+ three_lens: result?.data?.three_lens ?? null,
713
+ citations: result?.data?.citations ?? [],
714
+ related_protocols: result?.data?.related_protocols ?? [],
715
+ orchestration_steps: result?.data?.orchestration_steps ?? [],
716
+ usage: result?.usage,
717
+ });
718
+ }
719
+ case "monitor_protocol_updates": {
720
+ const effectiveClient = withSessionToken(client, args["session_token"]);
721
+ const compoundFilter = args["compound_filter"];
722
+ // Get user's saved protocols
723
+ const protocols = await effectiveClient.get("/api/user/saved-protocols", "session");
724
+ let filtered = protocols ?? [];
725
+ if (compoundFilter && compoundFilter.length > 0) {
726
+ const filterLower = compoundFilter.map((c) => c.toLowerCase());
727
+ filtered = filtered.filter((p) => filterLower.some((f) => p.title?.toLowerCase().includes(f) || p.compound?.toLowerCase().includes(f)));
728
+ }
729
+ return ok({
730
+ protocols_monitored: filtered.length,
731
+ protocols: filtered.slice(0, 20).map((p) => ({
732
+ id: p.id,
733
+ title: p.title,
734
+ evidence_grade: p.evidenceGrade ?? p.evidence_grade ?? null,
735
+ last_updated: p.updatedAt ?? p.updated_at ?? null,
736
+ review_recommended: !p.updatedAt || (Date.now() - new Date(p.updatedAt).getTime()) > 90 * 24 * 60 * 60 * 1000,
737
+ })),
738
+ note: "Protocols not updated in 90+ days are flagged for review",
739
+ });
740
+ }
741
+ default:
742
+ return err(`Unknown tool: ${name}`);
743
+ }
744
+ }
745
+ catch (e) {
746
+ const message = e instanceof Error ? e.message : String(e);
747
+ return err(message);
748
+ }
749
+ }
750
+ // ─── Auth override helpers ────────────────────────────────────────────────────
751
+ function withApiKey(base, apiKey) {
752
+ if (!apiKey)
753
+ return base;
754
+ return new HoromnalyClient(loadConfig({ apiUrl: base.apiUrl, helixApiKey: apiKey }));
755
+ }
756
+ function withSessionToken(base, token) {
757
+ if (!token)
758
+ return base;
759
+ return new HoromnalyClient(loadConfig({ apiUrl: base.apiUrl, sessionToken: token }));
760
+ }
761
+ function withAdminToken(base, token) {
762
+ if (!token)
763
+ return base;
764
+ return new HoromnalyClient(loadConfig({ apiUrl: base.apiUrl, adminSessionToken: token }));
765
+ }
766
+ //# sourceMappingURL=tools.js.map