formlab-mcp 0.6.47 → 0.6.48
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 +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/tools/analytics.js +36 -4
package/README.md
CHANGED
|
@@ -44,8 +44,8 @@ Ask Claude (or another MCP client) things like:
|
|
|
44
44
|
| `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 as-prepared inputs — mL rows through that ingredient's density — else measured volume × density), **estimatedVolume** (mass ÷ the formula's finished density when no actual exists, flagged `estimate: true`), **plannedMass** for volume-target batches (target × finished density) and **formulaFinishedDensity**, the **process** method + any **process-factor values** (DOE inputs), samples + blend lineage, and — as a finished-goods lot — its storage **`location`** + finished-goods **`stockLedger`** (shipments out + adjustments; `shippedQty` total) |
|
|
45
45
|
| `list_samples` | Filtered list of physical specimens |
|
|
46
46
|
| `get_sample` | Full record + canonical variant + test reports + blend lineage |
|
|
47
|
-
| `list_test_results` | Filtered list of test reports (by sample, parameter, **measured-value range** (`value_min`/`value_max`), date, or lab) |
|
|
48
|
-
| `get_test_result` | Full report: every measurement's value, spec, any per-measurement run **conditions**, and the **resolved instrument** (`instrumentSource`: row / method / run). Complex types carry a `representative` (time-series → final, distribution → D50); a time series adds `aggregated` (per-timestamp mean ± sd) and `pointReplicates` (repeat readings at one `t`). Report-level `seriesReplicates` lists parameters measured 2+ times as separate rows — point vs series replicates are distinct. Each measurement also gives its **effective spec** (`effectiveSpec`, `specSource`: override / template / parameter / none) and `passFail` — panel and Test Method specs apply exactly as in the app, and every pass/fail count across the tools uses the same rule |
|
|
47
|
+
| `list_test_results` | Filtered list of test reports (by sample, parameter, **measured-value range** (`value_min`/`value_max` — a curve at its final point, a distribution at its spec's statistic or D50), **grade** (`min_grade`/`max_grade` on a ranked scale), **text value** (`value_equals`), **run condition** (`condition`), date, or lab) |
|
|
48
|
+
| `get_test_result` | Full report: every measurement's value, spec, any per-measurement run **conditions**, and the **resolved instrument** (`instrumentSource`: row / method / run). Complex types carry a `representative` (time-series → final, distribution → D50; a distribution also its `bins` and, when its spec names a statistic, `judged` at it); a ranked grade carries its `scale` and `rank`; a time series adds `aggregated` (per-timestamp mean ± sd) and `pointReplicates` (repeat readings at one `t`). Report-level `seriesReplicates` lists parameters measured 2+ times as separate rows — point vs series replicates are distinct. Each measurement also gives its **effective spec** (`effectiveSpec`, `specSource`: override / template / parameter / none) and `passFail` — panel and Test Method specs apply exactly as in the app, and every pass/fail count across the tools uses the same rule |
|
|
49
49
|
| `get_doe_matrix` | Pivot matrix (CSV by default) — rows × ingredients × parameters. A parameter measured under two or more **run conditions** (storage 25 °C vs 40 °C) is one column per condition (`pH`, `pH · 40 °C`; the bare name is the base arm), never averaged together — same columns, in the same order (grouped by measurement type, the test panel's order within a group), as the app's DOE Matrix |
|
|
50
50
|
| `find_failures` | Parameters ranked by failure **rate** (failing ÷ all readings; replicates in one report count once), `lowN` flag under 5 readings, plus formulations ranked by share of failing reports — same numbers as the app's Test Analytics → Failures |
|
|
51
51
|
| `get_coverage_matrix` | TEST coverage by formulation, batch or sample (`grain`) × parameter. Each cell is the **latest** reading by test date (same-day reports averaged, any fail → fail) with its date and how many reports measured it; one column per **run condition** when a parameter was measured under two or more — same rollup as the app's Test Analytics → Coverage. A ranked scale shows its median grade (with its `rank`) when several readings share the date, a text result its most frequent value, a distribution with no D50 its bins; columns in the app's order (type groups, the test panel's order) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "formlab-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.48",
|
|
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.48",
|
|
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.48",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
package/tools/analytics.js
CHANGED
|
@@ -84,7 +84,9 @@ function _mcpTsAggregate(points) {
|
|
|
84
84
|
|
|
85
85
|
function _trimMeasurement(m, t, db) {
|
|
86
86
|
const ins = _resolveInstrument(m, t, db);
|
|
87
|
-
|
|
87
|
+
// The app's type rule (13s): a legacy valueType 'Categorical' row is text,
|
|
88
|
+
// not a scalar.
|
|
89
|
+
const type = _mcpMeasType(m);
|
|
88
90
|
// Reduce a complex type to the single representative every analysis surface
|
|
89
91
|
// uses (time_series → final point, distribution → D50), and — for a series —
|
|
90
92
|
// expose per-timestamp aggregation so an agent can see POINT replicates
|
|
@@ -101,6 +103,16 @@ function _trimMeasurement(m, t, db) {
|
|
|
101
103
|
} else if (type === 'distribution' && Array.isArray(m.bins) && m.bins.length) {
|
|
102
104
|
const d50 = m.bins.find(b => String(b.label || '').toUpperCase().replace(/\s/g, '') === 'D50');
|
|
103
105
|
complex.representative = d50 ? { value: _round(parseFloat(d50.value), 6), basis: 'D50' } : null;
|
|
106
|
+
complex.bins = m.bins.map(b => ({ label: b.label, value: b.value }));
|
|
107
|
+
// The statistic its spec names is the one it is judged on ("D90 <=30").
|
|
108
|
+
const sp = _resolveSpec(m, t && t.templateId, db).spec;
|
|
109
|
+
if (sp && sp.stat) { const v = _mcpRepNumber(m, { dist: String(sp.stat) }); complex.judged = { value: v != null ? _round(v, 6) : null, basis: String(sp.stat) }; }
|
|
110
|
+
} else if (type === 'ordinal' && Array.isArray(m.scale) && m.scale.length) {
|
|
111
|
+
// A grade's order (13s): the scale and its place on it, so a floor like
|
|
112
|
+
// "at least HB" can be reasoned about.
|
|
113
|
+
complex.scale = m.scale.slice();
|
|
114
|
+
const r = _mcpRepNumber(m, { ordinal: 'rank' });
|
|
115
|
+
if (r != null) complex.rank = r;
|
|
104
116
|
}
|
|
105
117
|
return {
|
|
106
118
|
parameter: m.parameter || '',
|
|
@@ -127,7 +139,7 @@ function _trimMeasurement(m, t, db) {
|
|
|
127
139
|
const list_test_results = {
|
|
128
140
|
definition: {
|
|
129
141
|
name: 'list_test_results',
|
|
130
|
-
description: 'List test result reports, optionally filtered by sample, parameter, measured-value range, date range, or lab. Combine parameter + value_min/value_max to find reports whose measurement is in range (the "find by results" query, e.g. parameter="Viscosity", value_min=800, value_max=3000). Returns metadata (incl. the run-level instrument and controlSample flag) + parameters tested; use get_test_result for per-measurement values and their resolved instruments.',
|
|
142
|
+
description: 'List test result reports, optionally filtered by sample, parameter, measured-value range, grade, text value, run condition, date range, or lab. Combine parameter + value_min/value_max to find reports whose measurement is in range (the "find by results" query, e.g. parameter="Viscosity", value_min=800, value_max=3000). The number a range filter reads: a scalar as recorded, a curve at its final point, a distribution at the statistic its spec names (D90) or else D50; a ranked scale or text result has no number — use min_grade / max_grade (positions on the reading\'s own scale, e.g. parameter="Pencil hardness", min_grade="HB") or value_equals (a text result, case-insensitive). condition narrows to readings under a run condition (substring of e.g. "Storage 40 °C"; "none" = readings with no condition). Returns metadata (incl. the run-level instrument and controlSample flag) + parameters tested; use get_test_result for per-measurement values and their resolved instruments.',
|
|
131
143
|
inputSchema: {
|
|
132
144
|
type: 'object',
|
|
133
145
|
properties: {
|
|
@@ -135,6 +147,10 @@ const list_test_results = {
|
|
|
135
147
|
parameter: { type: 'string', description: 'Filter to results that include this parameter (case-insensitive substring).' },
|
|
136
148
|
value_min: { type: 'number', description: 'Only reports where the parameter (above) measured >= this value. Use with `parameter`; applies to any measurement if `parameter` is omitted.' },
|
|
137
149
|
value_max: { type: 'number', description: 'Only reports where the parameter (above) measured <= this value. Use with `parameter`; applies to any measurement if `parameter` is omitted.' },
|
|
150
|
+
min_grade: { type: 'string', description: 'A ranked scale: only readings at or above this grade on the reading\'s own scale (e.g. "HB").' },
|
|
151
|
+
max_grade: { type: 'string', description: 'A ranked scale: only readings at or below this grade.' },
|
|
152
|
+
value_equals: { type: 'string', description: 'A text (or ranked) result equal to this value, case-insensitive (e.g. "pass", "slight haze").' },
|
|
153
|
+
condition: { type: 'string', description: 'Only readings under a run condition whose label contains this (e.g. "40 °C"); "none" = readings with no run condition.' },
|
|
138
154
|
since_date: { type: 'string', description: 'ISO date — only reports on/after this date.' },
|
|
139
155
|
until_date: { type: 'string', description: 'ISO date — only reports on/before this date.' },
|
|
140
156
|
lab: { type: 'string', description: 'Filter by lab name (case-insensitive substring).' },
|
|
@@ -159,16 +175,32 @@ const list_test_results = {
|
|
|
159
175
|
if (args.until_date && (t.testDate || '') > args.until_date) return false;
|
|
160
176
|
if (args.lab && !norm(t.lab).includes(norm(args.lab))) return false;
|
|
161
177
|
const hasRange = args.value_min != null || args.value_max != null;
|
|
162
|
-
|
|
178
|
+
const hasGrade = !!(args.min_grade || args.max_grade);
|
|
179
|
+
if (args.parameter || hasRange || hasGrade || args.value_equals || args.condition) {
|
|
163
180
|
const pn = norm(args.parameter);
|
|
164
181
|
const found = (t.measurements || []).some(m => {
|
|
165
182
|
if (args.parameter && !norm(m.parameter).includes(pn)) return false;
|
|
183
|
+
if (args.condition) {
|
|
184
|
+
const lbl = norm(_mcpCondLabel(m));
|
|
185
|
+
if (norm(args.condition) === 'none' ? lbl !== '' : !lbl.includes(norm(args.condition))) return false;
|
|
186
|
+
}
|
|
166
187
|
if (hasRange) {
|
|
167
|
-
|
|
188
|
+
// The value it is judged on (13s): a distribution at its spec's statistic.
|
|
189
|
+
const sp = _mcpMeasType(m) === 'distribution' ? (_resolveSpec(m, t.templateId, db).spec || null) : null;
|
|
190
|
+
const v = sp && sp.stat ? _mcpRepNumber(m, { dist: String(sp.stat) }) : _mcpRepNumber(m); // a curve's final value, not its point count (12v)
|
|
168
191
|
if (v == null) return false;
|
|
169
192
|
if (args.value_min != null && v < args.value_min) return false;
|
|
170
193
|
if (args.value_max != null && v > args.value_max) return false;
|
|
171
194
|
}
|
|
195
|
+
if (hasGrade) {
|
|
196
|
+
if (_mcpMeasType(m) !== 'ordinal' || !Array.isArray(m.scale)) return false;
|
|
197
|
+
const up = m.scale.map(g => String(g).trim().toUpperCase());
|
|
198
|
+
const r = up.indexOf(String(m.value == null ? '' : m.value).trim().toUpperCase());
|
|
199
|
+
if (r < 0) return false;
|
|
200
|
+
if (args.min_grade) { const lo = up.indexOf(String(args.min_grade).trim().toUpperCase()); if (lo < 0 || r < lo) return false; }
|
|
201
|
+
if (args.max_grade) { const hi = up.indexOf(String(args.max_grade).trim().toUpperCase()); if (hi < 0 || r > hi) return false; }
|
|
202
|
+
}
|
|
203
|
+
if (args.value_equals && norm(String(m.value == null ? '' : m.value).trim()) !== norm(String(args.value_equals).trim())) return false;
|
|
172
204
|
return true;
|
|
173
205
|
});
|
|
174
206
|
if (!found) return false;
|