formlab-mcp 0.6.7 → 0.6.9

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
@@ -145,7 +145,7 @@ Or for local-dev (from the repo):
145
145
  }
146
146
  ```
147
147
 
148
- Restart Claude Desktop. You should see a hammer icon indicating tools are available, and FormLab's 33 tools become callable in any conversation.
148
+ Restart Claude Desktop. You should see a hammer icon indicating tools are available, and FormLab's 34 tools become callable in any conversation.
149
149
 
150
150
  ## Wire it up to Claude Code
151
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formlab-mcp",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
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.7",
5
+ "version": "0.6.9",
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.7",
16
+ "version": "0.6.9",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"
@@ -612,7 +612,7 @@ const list_doe_designs = {
612
612
  const get_doe_design = {
613
613
  definition: {
614
614
  name: 'get_doe_design',
615
- description: 'Get one saved DOE design in full: factors and ranges, constraints, the model it was optimised for, generation settings (run budget, replicates, centre points, seed), D-efficiency, and the complete run matrix with the formulation each run became. Everything needed to reproduce or audit the design.',
615
+ description: 'Get one saved DOE design in full: factors and ranges, constraints, the model it was optimized for, generation settings (run budget, replicates, center points, seed), D-efficiency, and the complete run matrix with the formulation each run became. Everything needed to reproduce or audit the design.',
616
616
  inputSchema: {
617
617
  type: 'object',
618
618
  properties: {
@@ -107,6 +107,19 @@ const get_formulation = {
107
107
  })),
108
108
  compositionFlattenedWtPct: flatRows,
109
109
  stepCount: (f.steps || []).length,
110
+ // The actual procedure — the tool description promises "step procedure",
111
+ // so return the ordered steps, not just the count. Keep only the
112
+ // meaningful fields; drop empties so simple recipes stay clean.
113
+ steps: (f.steps || []).map(s => ({
114
+ description: (s && (s.description || s.action || s.type)) || '',
115
+ ...(s && s.duration != null && s.duration !== '' ? { duration: s.duration } : {}),
116
+ ...(s && s.temperature != null && s.temperature !== '' ? { temperature: s.temperature } : {}),
117
+ ...(s && s.rpm != null && s.rpm !== '' ? { rpm: s.rpm } : {}),
118
+ ...(s && s.equipment ? { equipment: s.equipment } : {}),
119
+ ...(s && s.notes ? { notes: s.notes } : {}),
120
+ })),
121
+ // User-defined fields (incl. the "DOE factor"/"DOE value" convention).
122
+ customFields: (f.customFields || []).filter(c => c && c.name).map(c => ({ name: c.name, value: c.value != null ? c.value : '' })),
110
123
  batchCount: batches.length,
111
124
  sampleCount: samples.length,
112
125
  };
@@ -43,12 +43,14 @@ function _trimIngredient(i) {
43
43
  id: i.id,
44
44
  uid: i.uid,
45
45
  name: i.name,
46
+ chemicalName: i.chemicalName || '',
46
47
  family: i.family || '',
47
48
  supplier: i.supplier || '',
48
49
  casNumber: i.cas || i.casNumber || '',
49
50
  grade: i.grade || '',
50
51
  state: i.state || '',
51
52
  function: i.function || '',
53
+ storage: i.storage || '',
52
54
  mw: i.mw != null && i.mw !== '' ? Number(i.mw) : null,
53
55
  // % active for a technical/impure raw material (drives purity-corrected
54
56
  // weigh-out). null when full-strength / unspecified.
package/tools/lab.js CHANGED
@@ -8,6 +8,44 @@
8
8
 
9
9
  import { getStore, resolveById } from '../data.js';
10
10
 
11
+ // --- Self-contained unit conversion + actual-volume derivation ---------------
12
+ // The MCP server has no access to the app's cost.js helpers, so a small local
13
+ // factor table covers the common units. Mirrors js/views/batches.js:
14
+ // _batchActualMassKg / _batchActualVolume — actual volume is measured directly
15
+ // when logged, else actual mass ÷ measured finished-product density. Never a
16
+ // sum of per-ingredient volumes (volume isn't additive; mass is).
17
+ const _MASS_TO_KG = { mg: 1e-6, cg: 1e-5, dg: 1e-4, g: 1e-3, hg: 0.1, kg: 1, oz: 0.0283495, lb: 0.453592, stone: 6.35029, t: 1000, ton: 1000, tonne: 1000, grain: 6.47989e-5, 'µg': 1e-9, ug: 1e-9, mcg: 1e-9 };
18
+ const _VOL_TO_L = { 'µl': 1e-6, ul: 1e-6, ml: 1e-3, cl: 1e-2, dl: 0.1, l: 1, gal: 3.78541, fl_oz: 0.0295735, floz: 0.0295735, pt: 0.473176, qt: 0.946353, tsp: 0.00492892, tbsp: 0.0147868, cup: 0.236588 };
19
+ function _massToKg(v, u) { const f = _MASS_TO_KG[String(u || '').trim().toLowerCase()]; return (f != null && isFinite(v)) ? v * f : null; }
20
+ function _volToL(v, u) { const f = _VOL_TO_L[String(u || '').trim().toLowerCase()]; return (f != null && isFinite(v)) ? v * f : null; }
21
+ function _batchActualMassKg(b) {
22
+ const y = parseFloat(b.actualYield);
23
+ if (isFinite(y)) { const kg = _massToKg(y, b.unit); if (kg != null) return kg; }
24
+ if (Array.isArray(b.actualComposition)) {
25
+ let t = 0, any = false;
26
+ for (const c of b.actualComposition) { const kg = _massToKg(parseFloat(c.amount), c.unit); if (kg != null) { t += kg; any = true; } }
27
+ if (any) return t;
28
+ }
29
+ return null;
30
+ }
31
+ function _batchActualVolume(b) {
32
+ const mv = parseFloat(b.measuredVolume);
33
+ if (isFinite(mv) && mv > 0) { const L = _volToL(mv, b.measuredVolumeUnit || 'L'); if (L != null) return { liters: L, source: 'measured' }; }
34
+ const d = parseFloat(b.measuredDensity), m = _batchActualMassKg(b);
35
+ if (isFinite(d) && d > 0 && m != null) return { liters: m / d, source: 'derived' };
36
+ return null;
37
+ }
38
+ // Public shape for a batch's actual volume (or null). Includes the density used
39
+ // (derived case) or implied by the measurement (measured case) for QC.
40
+ function _actualVolumeInfo(b) {
41
+ const av = _batchActualVolume(b);
42
+ if (!av) return null;
43
+ const out = { liters: +av.liters.toFixed(4), unit: 'L', source: av.source };
44
+ if (av.source === 'derived') { const d = parseFloat(b.measuredDensity); if (isFinite(d)) out.densityUsed = d; }
45
+ else { const m = _batchActualMassKg(b); if (m != null && av.liters > 0) out.impliedDensity = +(m / av.liters).toFixed(4); }
46
+ return out;
47
+ }
48
+
11
49
  function _trimBatch(b, formulationsById) {
12
50
  if (!b) return null;
13
51
  const f = b.formulationId ? formulationsById.get(b.formulationId) : null;
@@ -20,6 +58,14 @@ function _trimBatch(b, formulationsById) {
20
58
  targetQuantity: b.targetQuantity ?? null,
21
59
  unit: b.unit || '',
22
60
  actualYield: b.actualYield ?? null,
61
+ // Finished-product density (g/mL === kg/L), measured on the blend, + the
62
+ // derived/measured actual VOLUME (mass is the additive quantity; volume is
63
+ // measured or derived from density, never summed across ingredients).
64
+ measuredDensity: b.measuredDensity ?? null,
65
+ densityTempC: (b.densityTemp != null && b.densityTemp !== '') ? b.densityTemp : null,
66
+ measuredVolume: b.measuredVolume ?? null,
67
+ measuredVolumeUnit: b.measuredVolumeUnit || '',
68
+ actualVolume: _actualVolumeInfo(b),
23
69
  preparedDate: b.preparedDate || '',
24
70
  preparedBy: b.preparedBy || '',
25
71
  process: b.process || '',
@@ -95,7 +141,7 @@ const list_batches = {
95
141
  const get_batch = {
96
142
  definition: {
97
143
  name: 'get_batch',
98
- description: 'Get full details for a single batch including its actual composition (as-prepared, when logged), child samples, lineage (parent / sub-blend parents), and inventory consumption note. Accepts internal id or UID.',
144
+ description: 'Get full details for a single batch including its actual composition (as-prepared, when logged), child samples, lineage (parent / sub-blend parents), inventory consumption note, and finished-product density + actual volume (measured directly or derived as actual mass ÷ measured density — never summed from per-ingredient volumes). Accepts internal id or UID.',
99
145
  inputSchema: {
100
146
  type: 'object',
101
147
  properties: {