formlab-mcp 0.6.25 → 0.6.26
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 +35 -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.26",
|
|
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.26",
|
|
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.26",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
package/tools/analytics.js
CHANGED
|
@@ -983,7 +983,24 @@ 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 };
|
|
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) {
|
|
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
|
+
if (targetMonths) {
|
|
998
|
+
const verdict = pointTotal < targetMonths ? 'short' : (ciTotal != null && ciTotal < targetMonths) ? 'marginal' : 'meets';
|
|
999
|
+
if (verdict === 'meets' && sl.remainingMonths > 60) return null;
|
|
1000
|
+
return { verdict, level: verdict === 'short' ? 'high' : verdict === 'marginal' ? 'medium' : 'info', pointTotal, ciTotal };
|
|
1001
|
+
}
|
|
1002
|
+
if (sl.remainingMonths > 60) return null;
|
|
1003
|
+
return { verdict: null, level: sl.remainingMonths < 3 ? 'high' : sl.remainingMonths < 12 ? 'medium' : 'info', pointTotal, ciTotal };
|
|
987
1004
|
}
|
|
988
1005
|
|
|
989
1006
|
const get_stability = {
|
|
@@ -1045,6 +1062,14 @@ const get_stability = {
|
|
|
1045
1062
|
if (!points.length) return { error: `No dated "${args.parameter}" measurements on ${entity.uid || entity.id}.` };
|
|
1046
1063
|
|
|
1047
1064
|
const spec = { min: specMin, max: specMax };
|
|
1065
|
+
// Target shelf life: the formula's own Shelf life, else the workspace
|
|
1066
|
+
// default when this store carries it (the app's Settings → Stability).
|
|
1067
|
+
const formula = entityType === 'formulation' ? entity
|
|
1068
|
+
: (db.formulations || []).find(f => f.id === entity.formulationId) || null;
|
|
1069
|
+
const ownT = formula ? parseFloat(formula.shelfLifeMonths) : NaN;
|
|
1070
|
+
const defT = parseFloat(db.defaultShelfLifeMonths);
|
|
1071
|
+
const target = (Number.isFinite(ownT) && ownT > 0) ? { months: ownT, source: 'formula' }
|
|
1072
|
+
: (Number.isFinite(defT) && defT > 0) ? { months: defT, source: 'workspace default' } : null;
|
|
1048
1073
|
const analyze = (pts) => {
|
|
1049
1074
|
const ys = pts.map(p => p.value);
|
|
1050
1075
|
const n = ys.length;
|
|
@@ -1060,11 +1085,17 @@ const get_stability = {
|
|
|
1060
1085
|
if (ic) out.controlChart = { mean: _round(ic.mean), ucl: _round(ic.ucl), lcl: _round(ic.lcl), sigma: _round(ic.sigma), outOfControlPoints: ic.ooc };
|
|
1061
1086
|
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
1087
|
const sl = _shelfLife(pts, spec);
|
|
1088
|
+
const as = _shelfLifeAssess(sl, target && target.months);
|
|
1063
1089
|
if (sl) out.shelfLife = {
|
|
1064
1090
|
headingToward: `${sl.edge} spec (${_round(sl.limit)})`,
|
|
1065
1091
|
remainingMonths: _round(sl.remainingMonths, 1),
|
|
1066
1092
|
ci95Months: sl.ciRemainingMonths != null ? _round(sl.ciRemainingMonths, 1) : null,
|
|
1093
|
+
fromT0Months: _round(sl.elapsedMonths + sl.remainingMonths, 1),
|
|
1094
|
+
ci95FromT0Months: sl.ciRemainingMonths != null ? _round(sl.elapsedMonths + sl.ciRemainingMonths, 1) : null,
|
|
1067
1095
|
r2: _round(sl.r2, 2), basis: 'linear extrapolation to spec crossing',
|
|
1096
|
+
verdict: as ? as.verdict : null,
|
|
1097
|
+
concern: as ? as.level : null,
|
|
1098
|
+
reportable: !!as,
|
|
1068
1099
|
};
|
|
1069
1100
|
return out;
|
|
1070
1101
|
};
|
|
@@ -1082,12 +1113,13 @@ const get_stability = {
|
|
|
1082
1113
|
return {
|
|
1083
1114
|
entity: { type: entityType, uid: entity.uid, name: entity.name || entity.uid },
|
|
1084
1115
|
parameter: args.parameter, unit, spec, pointCount: points.length, byCondition: split, series,
|
|
1085
|
-
|
|
1116
|
+
targetShelfLife: target ? { months: target.months, source: target.source } : null,
|
|
1117
|
+
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). 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
1118
|
};
|
|
1087
1119
|
},
|
|
1088
1120
|
};
|
|
1089
1121
|
|
|
1090
|
-
export { _evalPassFail, _resolveSpec };
|
|
1122
|
+
export { _evalPassFail, _resolveSpec, _shelfLife, _shelfLifeAssess };
|
|
1091
1123
|
export const tools = {
|
|
1092
1124
|
list_test_results,
|
|
1093
1125
|
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.
|