@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.
- package/dist/index.js +59 -5
- 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
|
-
|
|
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.
|
|
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:
|
|
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
|
|
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.
|
|
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",
|