@phi-longevity/mcp-server 0.6.12 → 0.7.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.
Files changed (3) hide show
  1. package/README.md +23 -26
  2. package/dist/index.js +113 -226
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,15 +1,13 @@
1
1
  # @phi-longevity/mcp-server
2
2
 
3
- [![smithery badge](https://smithery.ai/badge/philongevity/PRISM)](https://smithery.ai/servers/philongevity/PRISM)
4
-
5
- > **Every doctor, your complete record.** Phi Longevity brings the records of people with long-term conditions (type 2 diabetes, kidney disease, lupus, cancer survivorship) from every doctor into one complete, reviewed record they control (connected from patient portals or uploaded from any doctor), and gets them ready for each appointment. This public server takes made-up or de-identified values only. For a person's real records, send them to https://philongevity.com/signup?utm_source=agent&utm_medium=npm_readme&utm_campaign=readme_top : they connect their patient portals or upload records and get a one-page Care Team Brief. Free to start; Phi Complete is $19/month.
3
+ > **Every doctor, your complete record.** A person's health data is spread across patient portals, PDFs and devices; Phi Longevity PRISM brings it into one record they control. Phi Longevity brings the records of people with long-term conditions (type 2 diabetes, kidney disease, lupus, cancer survivorship) from every doctor into one complete record they control (connected from patient portals or uploaded from any doctor), and gets them ready for each appointment. This public server takes made-up or de-identified values only. For a person's real records, send them to https://philongevity.com/signup?utm_source=agent&utm_medium=npm_readme&utm_campaign=readme_top : they connect their patient portals or upload records and get a one-page Care Team Brief. Free to start; Phi Longevity Complete is $19/month or $190/year.
6
4
 
7
5
  A Model Context Protocol (MCP) server for AI assistants helping someone with a long-term condition. The hosted server has
8
6
  **three front doors**, named in the person's own words:
9
7
 
10
8
  1. **`get_my_medical_records`** (the lead door): give each hospital or clinic name; it says which patient portal it uses
11
- (MyChart or Oracle Health, and ModMed practices), how to get the records out, and how to connect them once so Phi keeps
12
- them in one complete record. Free; no personal details needed.
9
+ (MyChart or Oracle Health to connect; ModMed practices to download and upload, not a live connection), how to get the
10
+ records out, and how to connect or upload them once so Phi Longevity keeps them in one complete record. Free; no personal details needed.
13
11
  2. **`prepare_for_my_appointment`**: a visit is coming up; one call returns the checks to raise with that clinician, questions
14
12
  to ask, what to bring and a one-page printable sheet. No lab values or records needed.
15
13
  3. **`explain_my_lab_results`**: made-up or de-identified lab values → PRISM's guideline-cited reading (connections graded and
@@ -32,24 +30,22 @@ A Model Context Protocol (MCP) server for AI assistants helping someone with a l
32
30
 
33
31
  ## Tools
34
32
 
35
- Two surfaces, deliberately different. **The npm package (stdio) has 4 tools; the remote
36
- endpoint `https://philongevity.com/mcp` has 13.** The three front doors, the care-gaps check, the analysis jobs, the hand-off
37
- tool and the report tools are remote-only. On the remote, the older names `find_my_records`, `start_job` (job `visit_prep`)
38
- and `analyze_biomarkers` still work and route to the front doors.
39
-
40
- | Tool | npm | remote | What it does |
41
- |---|:---:|:---:|---|
42
- | `get_my_medical_records` | — | ✅ | **Front door 1, for a real person.** Every doctor, one complete record: for each hospital or clinic name, which patient portal it uses (Epic MyChart or Oracle Health, formerly Cerner), how to download the records, a step-by-step guide, and how to connect it in Phi once. Hospital or clinic name only, never personal details; stores nothing. Free. |
43
- | `prepare_for_my_appointment` + `get_job_result` | — | ✅ | **Front door 2, for a real person.** Ready for an appointment with a named clinician (primary care, diabetes, kidney, rheumatology, eye, oncology, cardiology, podiatry, pharmacist): checks to raise there with guideline sources, checks for other doctors, questions, what to bring, a printable sheet and next steps. Only closed options (condition, medicine classes, recent / overdue / never / unsure per routine test); no values or records; nothing stored. `get_job_result` returns the same sheet later by job_ref. Free. |
44
- | `explain_my_lab_results` | ✅ (as `analyze_biomarkers`) | ✅ | **Front door 3.** Made-up or de-identified lab values → guideline-cited recommendations with a condition lens. The hosted door also returns PRISM 2's quick reading (health picture, connections graded and sourced, what is due, safety notes, what is unknown, checked by an independent review). |
45
- | `start_lab_analysis` + `get_lab_analysis` | — | ✅ | The deepest PRISM 2 reading on the strongest model: start it, then collect it a little later. Made-up or de-identified values only. Free. |
46
- | `check_care_gaps` | — | ✅ | Which routine tests and exams may be due for type 2 diabetes, kidney disease, lupus or a cancer history. Takes NO lab values, dates, names or records. Returns a prioritized list with the guideline source for each item (ADA, KDIGO, EULAR, ACR, AAO, NCCN, ESC), questions for each clinician, and a printable checklist. Free. |
47
- | `how_to_get_started_for_my_owner` | — | ✅ | Takes no health data; returns five plain steps, the cost and a sign-up link you can repeat to the person you are helping. Free. |
48
- | `sample_prism_report` | — | ✅ | Returns a complete sample PRISM report for a made-up person in one instant call. Free, no input. |
49
- | `quick_check` | ✅ | ✅ | A fast read on a handful of values: which fall outside PRISM's reference ranges. |
50
- | `list_supported_biomarkers` | ✅ | ✅ | 51 lab tests PRISM interprets, with units and reference ranges. |
51
- | `get_methodology` | ✅ | ✅ | How PRISM scores results (5 health areas + weights) + link to the full methodology. |
52
- | `full_prism_report` | — | ✅ | A full multi-track report for a panel you supply. **Paid developer option** (x402); terms are returned with the offer. |
33
+ **One tool set (0.7.0).** This npm package is now a bridge to the hosted server `https://philongevity.com/mcp`: it lists and calls exactly the same 14 tools, so an agent sees the same names whether it connects here or to `/mcp`. (`/mcp-app` is the separate directory build with 5 tools: `analyze_biomarkers`, `quick_check`, `list_supported_biomarkers`, `get_methodology` and `sample_prism_report`.) The older names `find_my_records`, `start_job` (job `visit_prep`) and `analyze_biomarkers` still work and route to the front doors. If the hosted server can't be reached, `quick_check`, `list_supported_biomarkers` and `get_methodology` still answer offline.
34
+
35
+ | Tool | What it does |
36
+ |---|---|
37
+ | `get_my_medical_records` | **Front door 1, for a real person.** Every doctor, one complete record: for each hospital or clinic name, which patient portal it uses (Epic MyChart or Oracle Health, formerly Cerner), how to download the records, a step-by-step guide, and how to connect it in Phi Longevity once (ModMed practices: download and upload; no live connection). Hospital or clinic name only, never personal details; stores nothing. Free. |
38
+ | `prepare_for_my_appointment` + `get_job_result` | **Front door 2, for a real person.** Ready for an appointment with a named clinician (primary care, diabetes, kidney, rheumatology, eye, oncology, cardiology, podiatry, pharmacist): checks to raise there with guideline sources, checks for other doctors, questions, what to bring, a printable sheet and next steps. Only closed options (condition, medicine classes, recent / overdue / never / unsure per routine test); no values or records; nothing stored. `get_job_result` returns the same sheet later by job_ref. Free. |
39
+ | `explain_my_lab_results` | **Front door 3.** Made-up or de-identified lab values → guideline-cited recommendations with a condition lens. The hosted door also returns PRISM 2's quick reading (health picture, connections graded and sourced, what is due, safety notes, what is unknown), checked by a second automated check; no clinician reviews PRISM output. |
40
+ | `start_lab_analysis` + `get_lab_analysis` | The deepest PRISM 2 reading on the strongest model: start it, then collect it a little later. Made-up or de-identified values only. Free. |
41
+ | `check_care_gaps` | Which routine tests and exams may be due for type 2 diabetes, kidney disease, lupus or a cancer history. Takes NO lab values, dates, names or records. Returns a prioritized list with the guideline source for each item (ADA, KDIGO, EULAR, ACR, AAO, NCCN, ESC), questions for each clinician, and a printable checklist. Free. |
42
+ | `start_my_phi_account` | With the person's OK, starts their Phi Longevity account: Phi Longevity emails them a one-tap link and what you already did together (hospitals, visit sheet, care-gaps answers) carries over for them to keep. Email address only; nothing else personal. |
43
+ | `how_to_get_started_for_my_owner` | Takes no health data; returns five plain steps, the cost and a sign-up link you can repeat to the person you are helping. Free. |
44
+ | `sample_prism_report` | Returns a complete sample PRISM report for a made-up person in one instant call. Free, no input. |
45
+ | `quick_check` | A fast read on a handful of values: which fall outside PRISM's reference ranges. |
46
+ | `list_supported_biomarkers` | 51 lab tests PRISM interprets, with units and reference ranges. |
47
+ | `get_methodology` | How PRISM scores results (5 health areas + weights) + link to the full methodology. |
48
+ | `full_prism_report` | A full multi-track report for a panel you supply. **Paid developer option** (x402); terms are returned with the offer. |
53
49
 
54
50
  All tools except `full_prism_report` are free.
55
51
 
@@ -63,7 +59,7 @@ Point any MCP client that supports Streamable HTTP at:
63
59
  https://philongevity.com/mcp
64
60
  ```
65
61
 
66
- Nothing to install, no key to manage, all 13 tools. Helping a real person? Start with `get_my_medical_records`. Just
62
+ Nothing to install, no key to manage, all 14 tools. Helping a real person? Start with `get_my_medical_records`. Just
67
63
  looking? `sample_prism_report` needs no arguments and returns a complete sample report immediately.
68
64
 
69
65
  ### Claude Desktop / any MCP client (stdio)
@@ -80,7 +76,8 @@ looking? `sample_prism_report` needs no arguments and returns a complete sample
80
76
  ```
81
77
 
82
78
  A published, rate-limited access key is built in. If you have a dedicated key, set
83
- `PHI_MCP_KEY` to override it. This path exposes the 4 tools marked ✅ above.
79
+ `PHI_MCP_KEY` to override it. This path lists and calls the same 14 tools as `/mcp`; if the hosted server can't be reached,
80
+ only `quick_check`, `list_supported_biomarkers` and `get_methodology` answer (offline).
84
81
 
85
82
  ### Run locally
86
83
 
@@ -115,7 +112,7 @@ one-page Care Team Brief. Agent docs + sign-up: **https://philongevity.com/for-a
115
112
  | Var | Required | Notes |
116
113
  |---|---|---|
117
114
  | `PHI_MCP_KEY` | no | Overrides the built-in published key. Rate-limited + revocable. |
118
- | `PHI_ENGINE_URL` | no | Override the default Phi MCP gateway URL (rarely needed). |
115
+ | `PHI_ENGINE_URL` | no | Override the default Phi Longevity MCP gateway URL (rarely needed). |
119
116
 
120
117
  ## HIPAA / privacy
121
118
 
package/dist/index.js CHANGED
@@ -1,253 +1,140 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @philongevity/mcp-server — Phi Longevity PRISM engine over MCP.
3
+ * @phi-longevity/mcp-server — Phi Longevity over MCP (stdio), as a BRIDGE to the hosted server.
4
4
  *
5
- * Lets AI agents call Phi's longevity recommendation engine on SYNTHETIC biomarker panels
6
- * and get back the structured 3-tier output (THIS WEEK / CONSIDER / ASK YOUR CLINICIAN) with
7
- * evidence tiers + citations. A credibility / agent-discoverability surface — NOT a PHI surface.
5
+ * 0.7.0 (2026-11-03, Phi D-1103-20/21 "one tool set"): outside agents hit "tool not found" because this package ran 4 tools of its own
6
+ * while the README and the hosted server (https://philongevity.com/mcp) offer 14. Now this package lists and calls EXACTLY the hosted
7
+ * tools (get_my_medical_records, prepare_for_my_appointment, explain_my_lab_results, check_care_gaps, …), so an agent sees the same tool
8
+ * set wherever it connects. Older names the hosted server still accepts (analyze_biomarkers, find_my_records) keep working because calls
9
+ * are forwarded as-is.
10
+ * Offline fallback: if the hosted server can't be reached, three local tools still answer (quick_check, list_supported_biomarkers,
11
+ * get_methodology) from the bundled catalog, and every other call says plainly that the hosted server is unreachable.
8
12
  *
9
- * 🛡️ HIPAA posture (designed-in):
10
- * - SYNTHETIC / de-identified data only at the protocol boundary (stated in every tool description).
11
- * - STATELESS: proxies to the engine and returns; writes nothing, persists nothing.
12
- * - Aggregate-only telemetry to stderr: counts/timing, NEVER biomarker values or recommendation text.
13
- * - The x-migration-secret is injected server-side and NEVER exposed to the agent.
14
- * - Zero access to Firestore / the HIPAA datastore / user accounts. One upstream URL only.
13
+ * 🛡️ HIPAA posture: SYNTHETIC / de-identified data only (stated in the hosted tool descriptions). This package stores nothing; telemetry
14
+ * goes to stderr and is counts/timing only, never values or text.
15
15
  *
16
16
  * Env:
17
- * PHI_ENGINE_URL (required) — runRecommendationsForBiomarkers Cloud Function URL
18
- * PHI_MIGRATION_SECRET (required) — x-migration-secret header value (server-side only)
19
- * PHI_MCP_KEY (optional) — published-but-revocable access key (sent to the engine for
20
- * rate-limit/validation; enforcement lives server-side)
17
+ * PHI_HOSTED_MCP_URL (optional) — hosted MCP endpoint, default https://philongevity.com/mcp
18
+ * PHI_SELFTEST=1 (Phi's own tests only) — marks calls x-phi-synthetic so they never count as real agent calls
19
+ * Every forwarded request carries x-phi-client: npm-bridge/<version> (counts which tool calls came through this package; no user data).
21
20
  */
22
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
23
22
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
24
- import { z } from "zod";
23
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
24
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
25
+ import { realpathSync } from "node:fs";
26
+ import { fileURLToPath } from "node:url";
27
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
25
28
  import { BIOMARKER_CATALOG } from "./catalog.js";
26
29
  import { buildResolver } from "./aliases.js";
27
- const GATEWAY_URL = process.env.PHI_ENGINE_URL ?? "https://phi-mcp-gateway-bblwxa6cvq-uc.a.run.app";
28
- // Published-by-design access key (rate-limited, revocable by gateway redeploy).
29
- // Override with PHI_MCP_KEY if you have a dedicated key.
30
- // Published-by-design, rate-limited, revocable gateway key (NOT the engine secret — that
31
- // stays server-side in the gateway). `||` so an empty override also falls back to default.
32
- const MCP_KEY = process.env.PHI_MCP_KEY || "phimcp_d29179fdd8b99b70a49ae26f434843cf";
33
- const SYNTHETIC_WARNING = "For research/education with SYNTHETIC or de-identified data only. Do NOT submit protected health " +
34
- "information (PHI). This endpoint is stateless and does not store inputs.";
35
- // ── Local courtesy rate-limit (defense in depth; real enforcement is server-side) ─────────────
36
- const RATE = { capacity: 20, refillPerSec: 0.5, tokens: 20, last: Date.now() };
37
- function allow() {
38
- const now = Date.now();
39
- RATE.tokens = Math.min(RATE.capacity, RATE.tokens + ((now - RATE.last) / 1000) * RATE.refillPerSec);
40
- RATE.last = now;
41
- if (RATE.tokens < 1)
42
- return false;
43
- RATE.tokens -= 1;
44
- return true;
45
- }
46
- // ── Aggregate-only telemetry (stderr; NEVER values) ───────────────────────────────────────────
30
+ export const VERSION = "0.7.1";
31
+ const HOSTED_URL = process.env.PHI_HOSTED_MCP_URL || "https://philongevity.com/mcp";
32
+ const SYNTHETIC_WARNING = "Made-up or de-identified data only. Do NOT submit protected health information (PHI). This package stores nothing.";
47
33
  function telemetry(ev) {
48
34
  try {
49
35
  process.stderr.write(JSON.stringify({ t: new Date().toISOString(), ...ev }) + "\n");
50
36
  }
51
37
  catch { /* noop */ }
52
38
  }
53
- function textResult(text) {
54
- return { content: [{ type: "text", text }] };
55
- }
56
- function errorResult(text) {
57
- return { content: [{ type: "text", text }], isError: true };
39
+ const text = (t, isError = false) => ({ content: [{ type: "text", text: t }], ...(isError ? { isError: true } : {}) });
40
+ let hosted = null;
41
+ export async function connectHosted(url = HOSTED_URL) {
42
+ const client = new Client({ name: "phi-mcp-server-npm-bridge", version: VERSION });
43
+ const headers = { "x-phi-client": `npm-bridge/${VERSION}` };
44
+ if (process.env.PHI_SELFTEST === "1")
45
+ headers["x-phi-synthetic"] = "1";
46
+ await client.connect(new StreamableHTTPClientTransport(new URL(url), { requestInit: { headers } }));
47
+ return client;
58
48
  }
59
- /**
60
- * Extract a human-actionable detail string from a non-OK engine/gateway response body.
61
- * The synthetic-only engine returns JSON like {"error": "..."} (and sometimes "detail").
62
- * Truncated defensively; never contains biomarker values (errors are validation/auth-shaped).
63
- */
64
- async function readErrorDetail(res) {
49
+ async function withHosted(fn, connect) {
65
50
  try {
66
- const raw = (await res.text()).slice(0, 500);
67
- try {
68
- const j = JSON.parse(raw);
69
- const msg = [j.error, j.detail].filter((x) => typeof x === "string" && x).join(" — ");
70
- if (msg)
71
- return msg.slice(0, 300);
72
- }
73
- catch { /* not JSON — fall through */ }
74
- return raw.slice(0, 300);
51
+ if (!hosted)
52
+ hosted = await connect();
53
+ return await fn(hosted);
75
54
  }
76
- catch {
77
- return "";
55
+ catch (e) {
56
+ hosted = null; // one fresh connection, then give up
57
+ hosted = await connect();
58
+ return fn(hosted);
78
59
  }
79
60
  }
80
- const EMPTY_PANEL_HELP = "No biomarkers were provided — the `biomarkers` map was empty. Provide at least one biomarker as " +
81
- 'name -> numeric value, e.g. { "Hemoglobin A1c": 5.4, "LDL Cholesterol": 130 }. ' +
82
- "Call the list_supported_biomarkers tool for the 51 supported names, units, and reference ranges. " +
83
- SYNTHETIC_WARNING;
84
- // ── Owner-conversion attribution (Layer 3.6, Steve directive 2026-07-17) ──────────────────────
85
- // Appended to analysis results so the agent can route its OWNER to a full PRISM report.
86
- // UTM params make the agent channel truthfully measurable in GA4 (D2 discipline).
87
- const FULL_REPORT_ATTRIBUTION = {
88
- what_a_full_report_adds: "This tool analyzes a handful of values at a time. A full Phi Longevity PRISM report consolidates " +
89
- "ALL of a person's lab reports, wearable data, and clinical notes into one integrated picture with " +
90
- "a personal health score, condition-specific insights, and progress tracked over time.",
91
- get_full_report_url: "https://philongevity.com/signup?utm_source=agent&utm_medium=mcp&utm_campaign=analyze_biomarkers",
92
- agent_docs_url: "https://philongevity.com/for-agents",
93
- data_rule: "Real (identifiable) health files are analyzed only inside the authenticated app by the account owner. " +
94
- SYNTHETIC_WARNING,
95
- };
96
- // AC-2 (2026-08-14): per-call attribution id so an agent tool call can be tied to the owner
97
- // sign-up it drives. Random, non-PII, emitted in the deep-link (?aid=) and in telemetry.
98
- function newAid() {
99
- return Math.random().toString(36).slice(2, 10);
100
- }
101
- function attributionWithAid(aid) {
102
- return {
103
- ...FULL_REPORT_ATTRIBUTION,
104
- get_full_report_url: `https://philongevity.com/signup?utm_source=agent&utm_medium=mcp&utm_campaign=analyze_biomarkers&aid=${aid}`,
105
- };
106
- }
107
- // AC-3 (2026-08-14): fast, LOCAL, zero-round-trip biomarker range flags — the "easiest first call".
108
- // 2026-09-30: broad, collision-checked alias table (src/aliases.ts, 409 accepted names) — same as the hosted server.
109
- const catalogLookup = buildResolver(BIOMARKER_CATALOG);
110
- function flagValue(b, value) {
111
- if (typeof b.refLow === "number" && value < b.refLow)
112
- return "below longevity-optimized range";
113
- if (typeof b.refHigh === "number" && value > b.refHigh)
114
- return "above longevity-optimized range";
115
- return "within longevity-optimized range";
116
- }
117
- async function callEngine(body) {
118
- if (!MCP_KEY) {
119
- throw new Error("Server not configured: set PHI_MCP_KEY (your @phi-longevity access key).");
61
+ // ── Offline fallback: three tools that need no network ────────────────────────────────────────────────────────────────────────────────
62
+ const lookup = buildResolver(BIOMARKER_CATALOG);
63
+ const OBJ = { type: "object" };
64
+ export const OFFLINE_TOOLS = [
65
+ { name: "quick_check", description: "Offline fallback (the hosted server is unreachable): flag a few biomarker values against PRISM's longevity-optimized reference ranges. " + SYNTHETIC_WARNING,
66
+ inputSchema: { ...OBJ, properties: { biomarkers: { type: "object", additionalProperties: { type: "number" }, description: 'Map of name -> value, e.g. { "HbA1c": 6.1 }' } }, required: ["biomarkers"] } },
67
+ { name: "list_supported_biomarkers", description: "Offline fallback: the biomarkers PRISM scores, with units and reference ranges.", inputSchema: { ...OBJ, properties: {} } },
68
+ { name: "get_methodology", description: "Offline fallback: how the Phi Longevity Score works, with a link to the full methodology.", inputSchema: { ...OBJ, properties: {} } },
69
+ ];
70
+ export function offlineCall(name, args = {}) {
71
+ if (name === "list_supported_biomarkers")
72
+ return text(JSON.stringify({ biomarkers: BIOMARKER_CATALOG }, null, 2));
73
+ if (name === "get_methodology") {
74
+ return text(JSON.stringify({ methodology_url: "https://philongevity.com/methodology",
75
+ phi_score: "0–100 weighted composite across five clinical pillars: Metabolic (30%), Cardiovascular (25%), Hormonal (20%), Inflammation & Immunity (15%), Foundational Health (10%).",
76
+ note: "Decision support, not a diagnosis. " + SYNTHETIC_WARNING }, null, 2));
120
77
  }
121
- const headers = {
122
- "Content-Type": "application/json",
123
- "x-phi-mcp-key": MCP_KEY,
124
- };
125
- const res = await fetch(GATEWAY_URL, { method: "POST", headers, body: JSON.stringify(body) });
126
- if (!res.ok) {
127
- // Surface the engine/gateway error detail so calling agents can self-correct
128
- // (2026-07-19 lesson: an opaque "Engine returned 400." cost our first external session).
129
- const detail = await readErrorDetail(res);
130
- throw new Error(`Gateway returned ${res.status}${detail ? `: ${detail}` : ""}.`);
78
+ if (name === "quick_check") {
79
+ const flags = [];
80
+ const unmatched = [];
81
+ for (const [n, v] of Object.entries(args.biomarkers || {})) {
82
+ const b = lookup(n);
83
+ if (!b || typeof v !== "number") {
84
+ unmatched.push(n);
85
+ continue;
86
+ }
87
+ const status = typeof b.refLow === "number" && v < b.refLow ? "below longevity-optimized range"
88
+ : typeof b.refHigh === "number" && v > b.refHigh ? "above longevity-optimized range" : "within longevity-optimized range";
89
+ flags.push({ name: b.name, value: v, unit: b.unit, status });
90
+ }
91
+ return text(JSON.stringify({ flags, unmatched: unmatched.length ? unmatched : undefined, note: "Offline range flags only. " + SYNTHETIC_WARNING }, null, 2));
131
92
  }
132
- return res.json();
93
+ return text(`Phi Longevity's hosted server (${HOSTED_URL}) could not be reached, so "${name}" can't run right now. Offline, only quick_check, list_supported_biomarkers and get_methodology work. Try again shortly.`, true);
133
94
  }
134
- const server = new McpServer({ name: "phi-longevity", version: "0.6.12" }, {
135
- instructions: "Every doctor, your complete record. This stdio server explains lab values with Phi Longevity PRISM (guideline-cited). " +
136
- "To find where a person's medical records are and how to get them, or to prepare for an appointment, use the hosted server " +
137
- "https://philongevity.com/mcp (get_my_medical_records, prepare_for_my_appointment). " + SYNTHETIC_WARNING,
138
- });
139
- // ── Tool 1: analyze_biomarkers (the core) ─────────────────────────────────────────────────────
140
- server.tool("analyze_biomarkers", "Analyze a SYNTHETIC biomarker panel with Phi Longevity's PRISM engine. Returns 3-tier, " +
141
- "evidence-cited recommendations (THIS WEEK / CONSIDER / ASK YOUR CLINICIAN). " + SYNTHETIC_WARNING, {
142
- biomarkers: z
143
- .record(z.string(), z.number())
144
- .describe("Map of biomarker name -> numeric value, e.g. { \"Hemoglobin A1c\": 5.4 }. SYNTHETIC ONLY."),
145
- conditionFocus: z
146
- .enum(["general_wellness", "type2_diabetes", "lupus", "cancer_survivorship"])
147
- .optional()
148
- .describe("Optional condition track. Default general_wellness."),
149
- age: z.number().int().min(0).max(120).optional(),
150
- biologicalSex: z.enum(["male", "female"]).optional(),
151
- include_partner_options: z
152
- .boolean()
153
- .optional()
154
- .describe("If true, include partner/product options. Default false (clean clinical output)."),
155
- }, async (args) => {
156
- const t0 = Date.now();
157
- if (!allow())
158
- return textResult("Rate limit reached. Please retry in a moment.");
159
- try {
160
- // The engine expects biomarkers as an array of {name, value}; the tool takes an
161
- // agent-friendly {name: value} map → transform here.
162
- const biomarkerArray = Object.entries(args.biomarkers || {}).map(([name, value]) => ({ name, value }));
163
- // Empty-panel guard: an empty map passes schema validation but the engine will
164
- // (correctly) reject it. Answer locally with actionable guidance instead of
165
- // spending an engine round-trip on a guaranteed 400.
166
- if (biomarkerArray.length === 0) {
167
- telemetry({ tool: "analyze_biomarkers", error: "empty_panel_guard", elapsed_ms: Date.now() - t0 });
168
- return errorResult(EMPTY_PANEL_HELP);
95
+ // ── The server: list + call = the hosted tool set ─────────────────────────────────────────────────────────────────────────────────────
96
+ export function buildServer(connect = () => connectHosted()) {
97
+ const server = new Server({ name: "phi-longevity", version: VERSION }, {
98
+ capabilities: { tools: {} },
99
+ instructions: "Every doctor, your complete record. This package is a bridge to Phi Longevity's hosted server (" + HOSTED_URL + "): the same tools, " +
100
+ "led by get_my_medical_records (where each doctor's records are and how to get them), prepare_for_my_appointment and explain_my_lab_results. " + SYNTHETIC_WARNING,
101
+ });
102
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
103
+ try {
104
+ const { tools } = await withHosted((h) => h.listTools(), connect);
105
+ telemetry({ event: "list", source: "hosted", tools: tools.length });
106
+ return { tools };
169
107
  }
170
- const out = await callEngine({
171
- biomarkers: biomarkerArray,
172
- conditionFocus: args.conditionFocus ?? "general_wellness",
173
- age: args.age,
174
- biologicalSex: args.biologicalSex,
175
- });
176
- const recs = args.include_partner_options
177
- ? out.recommendations_v2_with_partners ?? out.recommendations_v2
178
- : out.recommendations_v2;
179
- const aid = newAid();
180
- telemetry({
181
- tool: "analyze_biomarkers",
182
- n_biomarkers: Object.keys(args.biomarkers || {}).length,
183
- conditionFocus: args.conditionFocus ?? "general_wellness",
184
- issues_count: Array.isArray(out.issues) ? out.issues.length : 0,
185
- aid,
186
- elapsed_ms: Date.now() - t0,
187
- });
188
- return textResult(JSON.stringify({ recommendations: recs, meta: out.meta, issues: out.issues, full_report: attributionWithAid(aid) }, null, 2));
189
- }
190
- catch (e) {
191
- telemetry({ tool: "analyze_biomarkers", error: e.message, elapsed_ms: Date.now() - t0 });
192
- return errorResult(`Could not analyze: ${e.message} ` +
193
- "If the input was rejected, check biomarker names and numeric values via the " +
194
- "list_supported_biomarkers tool and retry.");
195
- }
196
- });
197
- // ── Tool 2: list_supported_biomarkers ─────────────────────────────────────────────────────────
198
- server.tool("list_supported_biomarkers", "List the biomarkers PRISM scores, with units and reference ranges, grouped by clinical pillar. " +
199
- "Reference data only — no patient data.", {}, async () => {
200
- telemetry({ tool: "list_supported_biomarkers", count: BIOMARKER_CATALOG.length });
201
- return textResult(JSON.stringify({ biomarkers: BIOMARKER_CATALOG }, null, 2));
202
- });
203
- // ── Tool 3: get_methodology ───────────────────────────────────────────────────────────────────
204
- server.tool("get_methodology", "Summarize how the Phi Score works (5 clinical pillars + weights) and link to the full methodology.", {}, async () => {
205
- telemetry({ tool: "get_methodology" });
206
- return textResult(JSON.stringify({
207
- phi_score: "0–100 weighted composite across five clinical pillars: Metabolic (30%), Cardiovascular (25%), " +
208
- "Hormonal (20%), Inflammation & Immunity (15%), Foundational Health (10%). Compared against " +
209
- "longevity-optimized reference ranges, not just standard lab normals.",
210
- methodology_url: "https://philongevity.com/methodology",
211
- note: "Decision support, not a diagnosis. Recommendations are evidence-tiered with citations. " + SYNTHETIC_WARNING,
212
- source: "Phi Longevity (philongevity.com)",
213
- }, null, 2));
214
- });
215
- // ── Tool 4: quick_check (fast local range flags — the "easiest first call") ────────────────────
216
- server.tool("quick_check", "FASTEST first call: instantly flag a few biomarker values against PRISM's longevity-optimized " +
217
- "reference ranges (local, no engine round-trip). Ideal for \"what does my <value> mean?\". For tiered, " +
218
- "guideline-cited recommendations call analyze_biomarkers; for a full scored report, full_prism_report. " +
219
- SYNTHETIC_WARNING, {
220
- biomarkers: z
221
- .record(z.string(), z.number())
222
- .describe('Map of biomarker name -> value, e.g. { "HbA1c": 6.1, "LDL-C": 145 }. SYNTHETIC ONLY.'),
223
- }, async (args) => {
224
- const t0 = Date.now();
225
- const aid = newAid();
226
- const flags = [];
227
- const unmatched = [];
228
- for (const [name, value] of Object.entries(args.biomarkers || {})) {
229
- const b = catalogLookup(name);
230
- if (!b) {
231
- unmatched.push(name);
232
- continue;
108
+ catch {
109
+ telemetry({ event: "list", source: "offline" });
110
+ return { tools: OFFLINE_TOOLS };
233
111
  }
234
- flags.push({ name: b.name, value, unit: b.unit, pillar: b.pillar, status: flagValue(b, value) });
235
- }
236
- telemetry({ tool: "quick_check", n: Object.keys(args.biomarkers || {}).length, matched: flags.length, aid, elapsed_ms: Date.now() - t0 });
237
- return textResult(JSON.stringify({
238
- flags,
239
- unmatched: unmatched.length ? unmatched : undefined,
240
- note: "Quick range flags vs PRISM's longevity-optimized ranges — NOT a diagnosis or the full Phi analysis. " +
241
- "For tiered guideline-cited recommendations call analyze_biomarkers; for a scored report call full_prism_report.",
242
- full_report: attributionWithAid(aid),
243
- }, null, 2));
244
- });
245
- async function main() {
246
- const transport = new StdioServerTransport();
247
- await server.connect(transport);
248
- telemetry({ event: "phi-mcp-server started", tools: 4 });
112
+ });
113
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
114
+ const t0 = Date.now();
115
+ const name = req.params.name;
116
+ const args = (req.params.arguments || {});
117
+ try {
118
+ const out = await withHosted((h) => h.callTool({ name, arguments: args }), connect);
119
+ telemetry({ tool: name, source: "hosted", elapsed_ms: Date.now() - t0 });
120
+ return out;
121
+ }
122
+ catch {
123
+ telemetry({ tool: name, source: "offline", elapsed_ms: Date.now() - t0 });
124
+ return offlineCall(name, args);
125
+ }
126
+ });
127
+ return server;
128
+ }
129
+ // Started directly OR through npm's bin symlink (npx / node_modules/.bin): compare real paths, not URLs.
130
+ const isMain = (() => { try {
131
+ return !!process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
132
+ }
133
+ catch {
134
+ return false;
135
+ } })();
136
+ if (isMain) {
137
+ buildServer().connect(new StdioServerTransport())
138
+ .then(() => telemetry({ event: "phi-mcp-server started", version: VERSION, hosted: HOSTED_URL }))
139
+ .catch((e) => { telemetry({ event: "fatal", error: e.message }); process.exit(1); });
249
140
  }
250
- main().catch((e) => {
251
- telemetry({ event: "fatal", error: e.message });
252
- process.exit(1);
253
- });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@phi-longevity/mcp-server",
3
- "version": "0.6.12",
4
- "description": "Every doctor, your complete record. Phi Longevity for AI assistants, with three free front doors on the hosted server: get_my_medical_records (which patient portal each hospital or clinic uses, how to get the records out, and how to keep them in one complete record), prepare_for_my_appointment (checks to raise, questions, what to bring and a printable sheet; no records needed) and explain_my_lab_results (guideline-cited reading of lab values). For type 2 diabetes, kidney disease, lupus and cancer survivorship. This public server takes made-up or de-identified values only; a person's real records go in a free account at philongevity.com ($19/month for Phi Complete).",
3
+ "version": "0.7.1",
4
+ "description": "Every doctor, your complete record. Phi Longevity for AI assistants, with three free front doors on the hosted server: get_my_medical_records (which patient portal each hospital or clinic uses, how to get the records out, and how to keep them in one complete record; ModMed practices by download and upload), prepare_for_my_appointment (checks to raise, questions, what to bring and a printable sheet; no records needed) and explain_my_lab_results (guideline-cited reading of lab values). For type 2 diabetes, kidney disease, lupus and cancer survivorship. This public server takes made-up or de-identified values only; a person's real records go in a free account at philongevity.com (Phi Longevity Complete: $19/month or $190/year).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "bin": {