@phi-longevity/mcp-server 0.6.3 → 0.6.5

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 (2) hide show
  1. package/dist/index.js +62 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -92,6 +92,34 @@ const FULL_REPORT_ATTRIBUTION = {
92
92
  data_rule: "Real (identifiable) health files are analyzed only inside the authenticated app by the account owner. " +
93
93
  SYNTHETIC_WARNING,
94
94
  };
95
+ // AC-2 (2026-08-14): per-call attribution id so an agent tool call can be tied to the owner
96
+ // sign-up it drives. Random, non-PII, emitted in the deep-link (?aid=) and in telemetry.
97
+ function newAid() {
98
+ return Math.random().toString(36).slice(2, 10);
99
+ }
100
+ function attributionWithAid(aid) {
101
+ return {
102
+ ...FULL_REPORT_ATTRIBUTION,
103
+ get_full_report_url: `https://philongevity.com/signup?utm_source=agent&utm_medium=mcp&utm_campaign=analyze_biomarkers&aid=${aid}`,
104
+ };
105
+ }
106
+ // AC-3 (2026-08-14): fast, LOCAL, zero-round-trip biomarker range flags — the "easiest first call".
107
+ const _norm = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, "");
108
+ const _ALIASES = { hemoglobina1c: "hba1c", a1c: "hba1c", ldl: "ldlc",
109
+ ldlcholesterol: "ldlc", hdl: "hdlc", hdlcholesterol: "hdlc", crp: "hscrp", hscrp: "hscrp",
110
+ testosterone: "totaltestosterone", vitd: "vitamind", vitamind3: "vitamind", "25ohvitamind": "vitamind" };
111
+ const CATALOG_INDEX = new Map(BIOMARKER_CATALOG.map((b) => [_norm(b.name), b]));
112
+ function catalogLookup(name) {
113
+ const n = _norm(name);
114
+ return CATALOG_INDEX.get(n) || CATALOG_INDEX.get(_ALIASES[n] || "") || null;
115
+ }
116
+ function flagValue(b, value) {
117
+ if (typeof b.refLow === "number" && value < b.refLow)
118
+ return "below longevity-optimized range";
119
+ if (typeof b.refHigh === "number" && value > b.refHigh)
120
+ return "above longevity-optimized range";
121
+ return "within longevity-optimized range";
122
+ }
95
123
  async function callEngine(body) {
96
124
  if (!MCP_KEY) {
97
125
  throw new Error("Server not configured: set PHI_MCP_KEY (your @phi-longevity access key).");
@@ -152,14 +180,16 @@ server.tool("analyze_biomarkers", "Analyze a SYNTHETIC biomarker panel with Phi
152
180
  const recs = args.include_partner_options
153
181
  ? out.recommendations_v2_with_partners ?? out.recommendations_v2
154
182
  : out.recommendations_v2;
183
+ const aid = newAid();
155
184
  telemetry({
156
185
  tool: "analyze_biomarkers",
157
186
  n_biomarkers: Object.keys(args.biomarkers || {}).length,
158
187
  conditionFocus: args.conditionFocus ?? "general_wellness",
159
188
  issues_count: Array.isArray(out.issues) ? out.issues.length : 0,
189
+ aid,
160
190
  elapsed_ms: Date.now() - t0,
161
191
  });
162
- return textResult(JSON.stringify({ recommendations: recs, meta: out.meta, issues: out.issues, full_report: FULL_REPORT_ATTRIBUTION }, null, 2));
192
+ return textResult(JSON.stringify({ recommendations: recs, meta: out.meta, issues: out.issues, full_report: attributionWithAid(aid) }, null, 2));
163
193
  }
164
194
  catch (e) {
165
195
  telemetry({ tool: "analyze_biomarkers", error: e.message, elapsed_ms: Date.now() - t0 });
@@ -186,10 +216,40 @@ server.tool("get_methodology", "Summarize how the Phi Score works (5 clinical pi
186
216
  source: "Phi Longevity (philongevity.com)",
187
217
  }, null, 2));
188
218
  });
219
+ // ── Tool 4: quick_check (fast local range flags — the "easiest first call") ────────────────────
220
+ server.tool("quick_check", "FASTEST first call: instantly flag a few biomarker values against PRISM's longevity-optimized " +
221
+ "reference ranges (local, no engine round-trip). Ideal for \"what does my <value> mean?\". For tiered, " +
222
+ "guideline-cited recommendations call analyze_biomarkers; for a full scored report, full_prism_report. " +
223
+ SYNTHETIC_WARNING, {
224
+ biomarkers: z
225
+ .record(z.string(), z.number())
226
+ .describe('Map of biomarker name -> value, e.g. { "HbA1c": 6.1, "LDL-C": 145 }. SYNTHETIC ONLY.'),
227
+ }, async (args) => {
228
+ const t0 = Date.now();
229
+ const aid = newAid();
230
+ const flags = [];
231
+ const unmatched = [];
232
+ for (const [name, value] of Object.entries(args.biomarkers || {})) {
233
+ const b = catalogLookup(name);
234
+ if (!b) {
235
+ unmatched.push(name);
236
+ continue;
237
+ }
238
+ flags.push({ name: b.name, value, unit: b.unit, pillar: b.pillar, status: flagValue(b, value) });
239
+ }
240
+ telemetry({ tool: "quick_check", n: Object.keys(args.biomarkers || {}).length, matched: flags.length, aid, elapsed_ms: Date.now() - t0 });
241
+ return textResult(JSON.stringify({
242
+ flags,
243
+ unmatched: unmatched.length ? unmatched : undefined,
244
+ note: "Quick range flags vs PRISM's longevity-optimized ranges — NOT a diagnosis or the full Phi analysis. " +
245
+ "For tiered guideline-cited recommendations call analyze_biomarkers; for a scored report call full_prism_report.",
246
+ full_report: attributionWithAid(aid),
247
+ }, null, 2));
248
+ });
189
249
  async function main() {
190
250
  const transport = new StdioServerTransport();
191
251
  await server.connect(transport);
192
- telemetry({ event: "phi-mcp-server started", tools: 3 });
252
+ telemetry({ event: "phi-mcp-server started", tools: 4 });
193
253
  }
194
254
  main().catch((e) => {
195
255
  telemetry({ event: "fatal", error: e.message });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-longevity/mcp-server",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "Model Context Protocol server for Phi Longevity \u2014 agents can call the PRISM longevity recommendation engine on SYNTHETIC biomarker panels. Synthetic/de-identified data only; no PHI; stateless.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",