@phi-longevity/mcp-server 0.6.2 → 0.6.4

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 +59 -5
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -52,6 +52,34 @@ function telemetry(ev) {
52
52
  function textResult(text) {
53
53
  return { content: [{ type: "text", text }] };
54
54
  }
55
+ function errorResult(text) {
56
+ return { content: [{ type: "text", text }], isError: true };
57
+ }
58
+ /**
59
+ * Extract a human-actionable detail string from a non-OK engine/gateway response body.
60
+ * The synthetic-only engine returns JSON like {"error": "..."} (and sometimes "detail").
61
+ * Truncated defensively; never contains biomarker values (errors are validation/auth-shaped).
62
+ */
63
+ async function readErrorDetail(res) {
64
+ try {
65
+ const raw = (await res.text()).slice(0, 500);
66
+ try {
67
+ const j = JSON.parse(raw);
68
+ const msg = [j.error, j.detail].filter((x) => typeof x === "string" && x).join(" — ");
69
+ if (msg)
70
+ return msg.slice(0, 300);
71
+ }
72
+ catch { /* not JSON — fall through */ }
73
+ return raw.slice(0, 300);
74
+ }
75
+ catch {
76
+ return "";
77
+ }
78
+ }
79
+ const EMPTY_PANEL_HELP = "No biomarkers were provided — the `biomarkers` map was empty. Provide at least one biomarker as " +
80
+ 'name -> numeric value, e.g. { "Hemoglobin A1c": 5.4, "LDL Cholesterol": 130 }. ' +
81
+ "Call the list_supported_biomarkers tool for the 51 supported names, units, and reference ranges. " +
82
+ SYNTHETIC_WARNING;
55
83
  // ── Owner-conversion attribution (Layer 3.6, Steve directive 2026-07-17) ──────────────────────
56
84
  // Appended to analysis results so the agent can route its OWNER to a full PRISM report.
57
85
  // UTM params make the agent channel truthfully measurable in GA4 (D2 discipline).
@@ -64,6 +92,17 @@ const FULL_REPORT_ATTRIBUTION = {
64
92
  data_rule: "Real (identifiable) health files are analyzed only inside the authenticated app by the account owner. " +
65
93
  SYNTHETIC_WARNING,
66
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
+ }
67
106
  async function callEngine(body) {
68
107
  if (!MCP_KEY) {
69
108
  throw new Error("Server not configured: set PHI_MCP_KEY (your @phi-longevity access key).");
@@ -73,11 +112,15 @@ async function callEngine(body) {
73
112
  "x-phi-mcp-key": MCP_KEY,
74
113
  };
75
114
  const res = await fetch(GATEWAY_URL, { method: "POST", headers, body: JSON.stringify(body) });
76
- if (!res.ok)
77
- throw new Error(`Gateway returned ${res.status}.`);
115
+ if (!res.ok) {
116
+ // Surface the engine/gateway error detail so calling agents can self-correct
117
+ // (2026-07-19 lesson: an opaque "Engine returned 400." cost our first external session).
118
+ const detail = await readErrorDetail(res);
119
+ throw new Error(`Gateway returned ${res.status}${detail ? `: ${detail}` : ""}.`);
120
+ }
78
121
  return res.json();
79
122
  }
80
- const server = new McpServer({ name: "phi-longevity", version: "0.6.2" }, {
123
+ const server = new McpServer({ name: "phi-longevity", version: "0.6.3" }, {
81
124
  instructions: "Phi Longevity PRISM — longevity-optimized analysis of biomarker panels. " + SYNTHETIC_WARNING,
82
125
  });
83
126
  // ── Tool 1: analyze_biomarkers (the core) ─────────────────────────────────────────────────────
@@ -104,6 +147,13 @@ server.tool("analyze_biomarkers", "Analyze a SYNTHETIC biomarker panel with Phi
104
147
  // The engine expects biomarkers as an array of {name, value}; the tool takes an
105
148
  // agent-friendly {name: value} map → transform here.
106
149
  const biomarkerArray = Object.entries(args.biomarkers || {}).map(([name, value]) => ({ name, value }));
150
+ // Empty-panel guard: an empty map passes schema validation but the engine will
151
+ // (correctly) reject it. Answer locally with actionable guidance instead of
152
+ // spending an engine round-trip on a guaranteed 400.
153
+ if (biomarkerArray.length === 0) {
154
+ telemetry({ tool: "analyze_biomarkers", error: "empty_panel_guard", elapsed_ms: Date.now() - t0 });
155
+ return errorResult(EMPTY_PANEL_HELP);
156
+ }
107
157
  const out = await callEngine({
108
158
  biomarkers: biomarkerArray,
109
159
  conditionFocus: args.conditionFocus ?? "general_wellness",
@@ -113,18 +163,22 @@ server.tool("analyze_biomarkers", "Analyze a SYNTHETIC biomarker panel with Phi
113
163
  const recs = args.include_partner_options
114
164
  ? out.recommendations_v2_with_partners ?? out.recommendations_v2
115
165
  : out.recommendations_v2;
166
+ const aid = newAid();
116
167
  telemetry({
117
168
  tool: "analyze_biomarkers",
118
169
  n_biomarkers: Object.keys(args.biomarkers || {}).length,
119
170
  conditionFocus: args.conditionFocus ?? "general_wellness",
120
171
  issues_count: Array.isArray(out.issues) ? out.issues.length : 0,
172
+ aid,
121
173
  elapsed_ms: Date.now() - t0,
122
174
  });
123
- return textResult(JSON.stringify({ recommendations: recs, meta: out.meta, issues: out.issues, full_report: FULL_REPORT_ATTRIBUTION }, null, 2));
175
+ return textResult(JSON.stringify({ recommendations: recs, meta: out.meta, issues: out.issues, full_report: attributionWithAid(aid) }, null, 2));
124
176
  }
125
177
  catch (e) {
126
178
  telemetry({ tool: "analyze_biomarkers", error: e.message, elapsed_ms: Date.now() - t0 });
127
- return textResult(`Could not analyze: ${e.message}`);
179
+ return errorResult(`Could not analyze: ${e.message} ` +
180
+ "If the input was rejected, check biomarker names and numeric values via the " +
181
+ "list_supported_biomarkers tool and retry.");
128
182
  }
129
183
  });
130
184
  // ── Tool 2: list_supported_biomarkers ─────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-longevity/mcp-server",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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",