formlab-mcp 0.6.12 → 0.6.14

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/README.md CHANGED
@@ -37,7 +37,7 @@ Ask Claude (or another MCP client) things like:
37
37
  | `list_lots` | Inventory lots across ingredients (each a received batch with its own remaining balance, supplier, expiry). Filters: `ingredient_id`, `expiring_within_days`, `status` |
38
38
  | `list_inventory` | Portfolio stock rollup — one row per ingredient with on-hand qty, summed lot balance, nearest expiry, and a low-stock flag (on-hand ≤ `reorderThreshold`, or zero). Filters: `low_stock_only`, `expiring_within_days`, `family` |
39
39
  | `list_batches` | Filtered list of production / lab-prep events |
40
- | `get_batch` | Full record + actual composition, measured/derived **actual volume** (mass ÷ density, never a sum of per-ingredient volumes), the **process** method + any **process-factor values** (DOE inputs), samples + blend lineage |
40
+ | `get_batch` | Full record + actual composition, measured/derived **actual volume** (mass ÷ density, never a sum of per-ingredient volumes) and **actual mass** (yield, else summed inputs, else measured volume × density), the **process** method + any **process-factor values** (DOE inputs), samples + blend lineage |
41
41
  | `list_samples` | Filtered list of physical specimens |
42
42
  | `get_sample` | Full record + canonical variant + test reports + blend lineage |
43
43
  | `list_test_results` | Filtered list of test reports (by sample, parameter, **measured-value range** (`value_min`/`value_max`), date, or lab) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formlab-mcp",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
4
4
  "mcpName": "io.github.juliu1980/formlab-mcp",
5
5
  "description": "Read-only Model Context Protocol server for FormLab — 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.12",
5
+ "version": "0.6.14",
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.12",
16
+ "version": "0.6.14",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"
package/tools/lab.js CHANGED
@@ -26,6 +26,17 @@ function _batchActualMassKg(b) {
26
26
  for (const c of b.actualComposition) { const kg = _massToKg(parseFloat(c.amount), c.unit); if (kg != null) { t += kg; any = true; } }
27
27
  if (any) return t;
28
28
  }
29
+ // Volume-anchored batch (no weighed mass above): mass = measured volume ×
30
+ // measured density (V·ρ — always valid; summing per-ingredient volumes is
31
+ // not). Mirrors FormLab's js/views/batches.js _batchActualMassKg. The app
32
+ // also falls back to the formula's estimated density; MCP stays exact and
33
+ // reports null rather than an estimate. Recursion-safe: _batchActualVolume
34
+ // calls back into mass only when measuredVolume is ABSENT.
35
+ const mv = parseFloat(b.measuredVolume), d = parseFloat(b.measuredDensity);
36
+ if (isFinite(mv) && mv > 0 && isFinite(d) && d > 0) {
37
+ const L = _volToL(mv, b.measuredVolumeUnit || 'L');
38
+ if (L != null) return L * d; // kg (d in g/mL === kg/L)
39
+ }
29
40
  return null;
30
41
  }
31
42
  function _batchActualVolume(b) {
@@ -66,6 +77,10 @@ function _trimBatch(b, formulationsById) {
66
77
  measuredVolume: b.measuredVolume ?? null,
67
78
  measuredVolumeUnit: b.measuredVolumeUnit || '',
68
79
  actualVolume: _actualVolumeInfo(b),
80
+ // Actual MASS in kg (the conserved, additive quantity). From actual yield
81
+ // (mass-unit batch), else summed weighed inputs, else measured volume ×
82
+ // measured density. null when none is determinable.
83
+ actualMass: (function () { const m = _batchActualMassKg(b); return (m != null && isFinite(m)) ? { kg: +m.toFixed(4), unit: 'kg' } : null; })(),
69
84
  preparedDate: b.preparedDate || '',
70
85
  preparedBy: b.preparedBy || '',
71
86
  process: b.process || '',
@@ -174,6 +189,11 @@ const get_batch = {
174
189
  ..._trimBatch(b, indexes.formulationsById),
175
190
  notes: b.notes || '',
176
191
  equipmentUsed: b.equipmentUsed || '',
192
+ // Multi-select equipment: full list [{name, equipmentId}]; falls back to
193
+ // the legacy single equipmentUsed so older batches still report one item.
194
+ equipmentUsedList: (Array.isArray(b.equipmentUsedList) && b.equipmentUsedList.length)
195
+ ? b.equipmentUsedList.filter(e => e && e.name).map(e => ({ name: e.name, equipmentId: e.equipmentId || null }))
196
+ : (b.equipmentUsed ? [{ name: b.equipmentUsed, equipmentId: b.equipmentUsedId || null }] : []),
177
197
  actualCompositionRowCount: Array.isArray(b.actualComposition) ? b.actualComposition.length : 0,
178
198
  hasActualComposition: Array.isArray(b.actualComposition) && b.actualComposition.length > 0,
179
199
  sourceBlendParents: parents,