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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formlab-mcp",
3
- "version": "0.6.25",
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.25",
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.25",
16
+ "version": "0.6.26",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"
@@ -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
- note: 'shelfLife.remainingMonths is the point-estimate months from the last test to the spec crossing; ci95Months is the conservative (ICH-style) estimate where the 95% confidence bound crosses spec. Numbers are computed here — narrate them, do not recompute.',
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,
@@ -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.