formlab-mcp 0.6.29 → 0.6.31
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/package.json +1 -1
- package/server.json +2 -2
- package/tools/analytics.js +26 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "formlab-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.31",
|
|
4
4
|
"mcpName": "io.github.juliu1980/formlab-mcp",
|
|
5
5
|
"description": "Read-only Model Context Protocol server for FormLab \u2014 lets Claude (and other MCP clients) read and analyze your FormLab data, from a local export file OR your live cloud workspace.",
|
|
6
6
|
"type": "module",
|
package/server.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.juliu1980/formlab-mcp",
|
|
4
4
|
"description": "Read-only MCP for FormLab — let Claude query your formulation lab. Free reads a local export; Pro connects to your live cloud workspace with a dedicated read-only token.",
|
|
5
|
-
"version": "0.6.
|
|
5
|
+
"version": "0.6.31",
|
|
6
6
|
"websiteUrl": "https://formvix.com/mcp",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/juliu1980/FormLab",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "formlab-mcp",
|
|
16
|
-
"version": "0.6.
|
|
16
|
+
"version": "0.6.31",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
package/tools/analytics.js
CHANGED
|
@@ -1002,11 +1002,12 @@ function _shelfLifeAssess(sl, targetMonths, now) {
|
|
|
1002
1002
|
if (targetMonths) {
|
|
1003
1003
|
const verdict = pointTotal < targetMonths ? 'short' : (ciTotal != null && ciTotal < targetMonths) ? 'marginal' : 'meets';
|
|
1004
1004
|
if (verdict === 'meets' && sl.remainingMonths > 60) return null;
|
|
1005
|
-
|
|
1005
|
+
// Mirrors the app: a projection never reaches 'high' (reserved for a measured out-of-spec value).
|
|
1006
|
+
const level = (overdue && verdict !== 'meets') || verdict === 'short' ? 'medium' : 'info';
|
|
1006
1007
|
return { verdict, level, pointTotal, ciTotal, overdue, crossDate };
|
|
1007
1008
|
}
|
|
1008
1009
|
if (sl.remainingMonths > 60) return null;
|
|
1009
|
-
return { verdict: null, level: (overdue || sl.remainingMonths < 3) ? '
|
|
1010
|
+
return { verdict: null, level: (overdue || sl.remainingMonths < 3) ? 'medium' : 'info', pointTotal, ciTotal, overdue, crossDate };
|
|
1010
1011
|
}
|
|
1011
1012
|
|
|
1012
1013
|
const get_stability = {
|
|
@@ -1039,6 +1040,10 @@ const get_stability = {
|
|
|
1039
1040
|
if (!paramLc) return { error: 'parameter is required.' };
|
|
1040
1041
|
const points = [];
|
|
1041
1042
|
let specMin = null, specMax = null, unit = '';
|
|
1043
|
+
// Each reading keeps the spec it was judged by (reading → panel row →
|
|
1044
|
+
// method). Mirrors the app: one band only when they all agree; 2+ distinct
|
|
1045
|
+
// specs (two panels) → no band + `specVaries`, never an intersection.
|
|
1046
|
+
const specByKey = new Map();
|
|
1042
1047
|
(db.testResults || []).filter(t => sampleIds.has(t.sampleId)).forEach(t => {
|
|
1043
1048
|
const dStr = t.testDate || (t.createdAt ? String(t.createdAt).slice(0, 10) : null);
|
|
1044
1049
|
if (!dStr) return;
|
|
@@ -1052,15 +1057,25 @@ const get_stability = {
|
|
|
1052
1057
|
}
|
|
1053
1058
|
if (!Number.isFinite(value)) return;
|
|
1054
1059
|
if (m.unit && !unit) unit = m.unit;
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1060
|
+
const rs = _resolveSpec(m, t.templateId), ac = rs.spec;
|
|
1061
|
+
const sp = (ac && (Number.isFinite(ac.min) || Number.isFinite(ac.max))) ? { min: Number.isFinite(ac.min) ? ac.min : null, max: Number.isFinite(ac.max) ? ac.max : null } : null;
|
|
1062
|
+
if (sp) {
|
|
1063
|
+
const k = `${sp.min}|${sp.max}`;
|
|
1064
|
+
if (!specByKey.has(k)) specByKey.set(k, { ...sp, sources: new Set(), readings: 0 });
|
|
1065
|
+
const e = specByKey.get(k); e.readings++;
|
|
1066
|
+
const tmpl = rs.source === 'template' ? (db.templates || []).find(x => x.id === t.templateId) : null;
|
|
1067
|
+
e.sources.add(tmpl ? (tmpl.name || tmpl.uid || 'panel') : rs.source === 'parameter' ? 'Test Method default' : 'set on the reading');
|
|
1068
|
+
}
|
|
1057
1069
|
const cond = (m.conditions && Object.keys(m.conditions).length)
|
|
1058
1070
|
? Object.entries(m.conditions).map(([k, v]) => `${k} ${v}`).join(' · ') : '';
|
|
1059
|
-
points.push({ date, value, cond });
|
|
1071
|
+
points.push({ date, value, cond, spec: sp });
|
|
1060
1072
|
});
|
|
1061
1073
|
});
|
|
1074
|
+
const specList = [...specByKey.values()].map(e => ({ min: e.min, max: e.max, readings: e.readings, sources: [...e.sources] }));
|
|
1075
|
+
if (specList.length === 1) { specMin = specList[0].min; specMax = specList[0].max; }
|
|
1076
|
+
const specVaries = specList.length > 1 ? specList : null;
|
|
1062
1077
|
// Spec fallback to the Test Method library entry when no measurement carried one.
|
|
1063
|
-
if (specMin == null && specMax == null) {
|
|
1078
|
+
if (specMin == null && specMax == null && !specVaries) {
|
|
1064
1079
|
const pdef = (db.parameters || []).find(p => (p.name || '').trim().toLowerCase() === paramLc);
|
|
1065
1080
|
if (pdef && pdef.acceptanceCriteria) { specMin = pdef.acceptanceCriteria.min ?? null; specMax = pdef.acceptanceCriteria.max ?? null; }
|
|
1066
1081
|
}
|
|
@@ -1098,6 +1113,10 @@ const get_stability = {
|
|
|
1098
1113
|
const ic = _iChart(ys);
|
|
1099
1114
|
if (ic) out.controlChart = { mean: _round(ic.mean), ucl: _round(ic.ucl), lcl: _round(ic.lcl), sigma: _round(ic.sigma), outOfControlPoints: ic.ooc };
|
|
1100
1115
|
if (spec.min != null || spec.max != null) out.spec = { ...spec, latestInSpec: (spec.min == null || ys[n - 1] >= spec.min) && (spec.max == null || ys[n - 1] <= spec.max) };
|
|
1116
|
+
else if (specVaries) {
|
|
1117
|
+
const ls = [...pts].reverse().find(p => p.spec);
|
|
1118
|
+
if (ls) out.spec = { ...ls.spec, judgedBy: 'the latest reading\'s own panel spec (specs differ by panel)', latestInSpec: (ls.spec.min == null || ys[n - 1] >= ls.spec.min) && (ls.spec.max == null || ys[n - 1] <= ls.spec.max) };
|
|
1119
|
+
}
|
|
1101
1120
|
const sl = _shelfLife(pts, spec);
|
|
1102
1121
|
const as = _shelfLifeAssess(sl, target && target.months);
|
|
1103
1122
|
if (sl) out.shelfLife = {
|
|
@@ -1128,7 +1147,7 @@ const get_stability = {
|
|
|
1128
1147
|
|
|
1129
1148
|
return {
|
|
1130
1149
|
entity: { type: entityType, uid: entity.uid, name: entity.name || entity.uid },
|
|
1131
|
-
parameter: args.parameter, unit, spec, pointCount: points.length, byCondition: split, series,
|
|
1150
|
+
parameter: args.parameter, unit, spec, ...(specVaries ? { specVaries, specNote: 'Readings were judged against different specs (different panels): no single band, no shelf-life projection; each reading keeps its own pass/fail.' } : {}), pointCount: points.length, byCondition: split, series,
|
|
1132
1151
|
targetShelfLife: target ? { months: target.months, source: target.source } : null,
|
|
1133
1152
|
note: 'shelfLife.fromT0Months / ci95FromT0Months are the projected shelf life counted from the first test (T0): point estimate and the conservative (ICH Q1E) 95%-bound crossing. remainingMonths / ci95Months are the same, counted from the last test. verdict compares against targetShelfLife: "short" = the trend crosses spec before the target, "marginal" = the trend clears it but the 95% bound does not, "meets" = both clear it; null = no target set (then concern is by time left: high <3 mo, medium <12 mo). overdue=true means projectedCrossDate is already past today (time left runs from the last test), so the sample is probably out of spec now — recommend a re-test. reportable=false means the app shows no projection (R² < 0.3, crossing already past, or beyond 5 years). Numbers are computed here — narrate them, do not recompute.',
|
|
1134
1153
|
};
|