formlab-mcp 0.6.25 → 0.6.27
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 +43 -3
- package/tools/formulations.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "formlab-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.27",
|
|
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.27",
|
|
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.27",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
package/tools/analytics.js
CHANGED
|
@@ -983,7 +983,30 @@ function _shelfLife(points, spec) {
|
|
|
983
983
|
for (let tt = lastX; tt <= lastX + 120; tt += 0.25) {
|
|
984
984
|
if (fit.slope < 0 ? bound(tt) <= limit : bound(tt) >= limit) { ciRemaining = tt - lastX; break; }
|
|
985
985
|
}
|
|
986
|
-
return { edge, limit, remainingMonths: remaining, ciRemainingMonths: ciRemaining, r2: fit.r2 };
|
|
986
|
+
return { edge, limit, remainingMonths: remaining, ciRemainingMonths: ciRemaining, r2: fit.r2, elapsedMonths: lastX, lastDate: points[points.length - 1].date };
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Same verdict rule as the app (js/views/stability.js stabShelfLifeAssess):
|
|
990
|
+
// months from T0 vs the target shelf life — short (trend crosses first),
|
|
991
|
+
// marginal (only the 95% bound does), meets; no target → by time left
|
|
992
|
+
// (<3 mo high, <12 mo medium). Nothing past the 5-year horizon is reported.
|
|
993
|
+
function _shelfLifeAssess(sl, targetMonths, now) {
|
|
994
|
+
if (!sl || !(sl.r2 >= 0.3) || !(sl.remainingMonths > 0.05)) return null;
|
|
995
|
+
const pointTotal = sl.elapsedMonths + sl.remainingMonths;
|
|
996
|
+
const ciTotal = sl.ciRemainingMonths != null ? sl.elapsedMonths + sl.ciRemainingMonths : null;
|
|
997
|
+
// Overdue: the projected crossing (last test + remaining) is already past.
|
|
998
|
+
const crossMs = sl.lastDate ? sl.lastDate.getTime() + sl.remainingMonths * _MS_PER_MONTH : null;
|
|
999
|
+
const nowMs = now != null ? +now : Date.now();
|
|
1000
|
+
const overdue = crossMs != null && crossMs < nowMs;
|
|
1001
|
+
const crossDate = crossMs != null ? new Date(crossMs).toISOString().slice(0, 10) : null;
|
|
1002
|
+
if (targetMonths) {
|
|
1003
|
+
const verdict = pointTotal < targetMonths ? 'short' : (ciTotal != null && ciTotal < targetMonths) ? 'marginal' : 'meets';
|
|
1004
|
+
if (verdict === 'meets' && sl.remainingMonths > 60) return null;
|
|
1005
|
+
const level = (overdue && verdict !== 'meets') ? 'high' : verdict === 'short' ? 'high' : verdict === 'marginal' ? 'medium' : 'info';
|
|
1006
|
+
return { verdict, level, pointTotal, ciTotal, overdue, crossDate };
|
|
1007
|
+
}
|
|
1008
|
+
if (sl.remainingMonths > 60) return null;
|
|
1009
|
+
return { verdict: null, level: (overdue || sl.remainingMonths < 3) ? 'high' : sl.remainingMonths < 12 ? 'medium' : 'info', pointTotal, ciTotal, overdue, crossDate };
|
|
987
1010
|
}
|
|
988
1011
|
|
|
989
1012
|
const get_stability = {
|
|
@@ -1045,6 +1068,14 @@ const get_stability = {
|
|
|
1045
1068
|
if (!points.length) return { error: `No dated "${args.parameter}" measurements on ${entity.uid || entity.id}.` };
|
|
1046
1069
|
|
|
1047
1070
|
const spec = { min: specMin, max: specMax };
|
|
1071
|
+
// Target shelf life: the formula's own Shelf life, else the workspace
|
|
1072
|
+
// default when this store carries it (the app's Settings → Stability).
|
|
1073
|
+
const formula = entityType === 'formulation' ? entity
|
|
1074
|
+
: (db.formulations || []).find(f => f.id === entity.formulationId) || null;
|
|
1075
|
+
const ownT = formula ? parseFloat(formula.shelfLifeMonths) : NaN;
|
|
1076
|
+
const defT = parseFloat(db.defaultShelfLifeMonths);
|
|
1077
|
+
const target = (Number.isFinite(ownT) && ownT > 0) ? { months: ownT, source: 'formula' }
|
|
1078
|
+
: (Number.isFinite(defT) && defT > 0) ? { months: defT, source: 'workspace default' } : null;
|
|
1048
1079
|
const analyze = (pts) => {
|
|
1049
1080
|
const ys = pts.map(p => p.value);
|
|
1050
1081
|
const n = ys.length;
|
|
@@ -1060,11 +1091,19 @@ const get_stability = {
|
|
|
1060
1091
|
if (ic) out.controlChart = { mean: _round(ic.mean), ucl: _round(ic.ucl), lcl: _round(ic.lcl), sigma: _round(ic.sigma), outOfControlPoints: ic.ooc };
|
|
1061
1092
|
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) };
|
|
1062
1093
|
const sl = _shelfLife(pts, spec);
|
|
1094
|
+
const as = _shelfLifeAssess(sl, target && target.months);
|
|
1063
1095
|
if (sl) out.shelfLife = {
|
|
1064
1096
|
headingToward: `${sl.edge} spec (${_round(sl.limit)})`,
|
|
1065
1097
|
remainingMonths: _round(sl.remainingMonths, 1),
|
|
1066
1098
|
ci95Months: sl.ciRemainingMonths != null ? _round(sl.ciRemainingMonths, 1) : null,
|
|
1099
|
+
fromT0Months: _round(sl.elapsedMonths + sl.remainingMonths, 1),
|
|
1100
|
+
ci95FromT0Months: sl.ciRemainingMonths != null ? _round(sl.elapsedMonths + sl.ciRemainingMonths, 1) : null,
|
|
1067
1101
|
r2: _round(sl.r2, 2), basis: 'linear extrapolation to spec crossing',
|
|
1102
|
+
verdict: as ? as.verdict : null,
|
|
1103
|
+
concern: as ? as.level : null,
|
|
1104
|
+
projectedCrossDate: as ? as.crossDate : null,
|
|
1105
|
+
overdue: as ? as.overdue : false,
|
|
1106
|
+
reportable: !!as,
|
|
1068
1107
|
};
|
|
1069
1108
|
return out;
|
|
1070
1109
|
};
|
|
@@ -1082,12 +1121,13 @@ const get_stability = {
|
|
|
1082
1121
|
return {
|
|
1083
1122
|
entity: { type: entityType, uid: entity.uid, name: entity.name || entity.uid },
|
|
1084
1123
|
parameter: args.parameter, unit, spec, pointCount: points.length, byCondition: split, series,
|
|
1085
|
-
|
|
1124
|
+
targetShelfLife: target ? { months: target.months, source: target.source } : null,
|
|
1125
|
+
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.',
|
|
1086
1126
|
};
|
|
1087
1127
|
},
|
|
1088
1128
|
};
|
|
1089
1129
|
|
|
1090
|
-
export { _evalPassFail, _resolveSpec };
|
|
1130
|
+
export { _evalPassFail, _resolveSpec, _shelfLife, _shelfLifeAssess };
|
|
1091
1131
|
export const tools = {
|
|
1092
1132
|
list_test_results,
|
|
1093
1133
|
get_test_result,
|
package/tools/formulations.js
CHANGED
|
@@ -22,6 +22,8 @@ function _trimFormulation(f) {
|
|
|
22
22
|
application: f.application || '',
|
|
23
23
|
owner: f.owner || '',
|
|
24
24
|
regulatoryCategory: f.regulatoryCategory || '',
|
|
25
|
+
// Target shelf life (months): sets finished-goods expiry AND is what
|
|
26
|
+
// get_stability judges the projected shelf life against.
|
|
25
27
|
shelfLifeMonths: f.shelfLifeMonths ?? null,
|
|
26
28
|
// Density of the MIXED product (g/mL), when declared — volume-targeted
|
|
27
29
|
// batches scale by it instead of the ideal-mix estimate. null = not set.
|