formlab-mcp 0.5.1 → 0.5.2

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
@@ -40,7 +40,7 @@ Ask Claude (or another MCP client) things like:
40
40
  | `list_samples` | Filtered list of physical specimens |
41
41
  | `get_sample` | Full record + canonical variant + test reports + blend lineage |
42
42
  | `list_test_results` | Filtered list of test reports (by sample, parameter, **measured-value range** (`value_min`/`value_max`), date, or lab) |
43
- | `get_test_result` | Full record + every measurement value |
43
+ | `get_test_result` | Full report: every measurement's value, spec, and the **resolved instrument** (`instrumentSource`: row / method / run) |
44
44
  | `get_doe_matrix` | Pivot matrix (CSV by default) — rows × ingredients × parameters |
45
45
  | `find_failures` | Pareto-style: parameters that fail acceptance most often |
46
46
  | `get_coverage_matrix` | Which formulations × parameters have been measured |
package/index.js CHANGED
@@ -114,7 +114,7 @@ const TOOLS = [
114
114
  ];
115
115
 
116
116
  const server = new Server(
117
- { name: 'formlab-mcp', version: '0.5.1' },
117
+ { name: 'formlab-mcp', version: '0.5.2' },
118
118
  { capabilities: { tools: {} } }
119
119
  );
120
120
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formlab-mcp",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
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.5.1",
5
+ "version": "0.5.2",
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.5.1",
16
+ "version": "0.5.2",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"
@@ -9,6 +9,34 @@
9
9
 
10
10
  import { getStore, resolveById, flattenComposition } from '../data.js';
11
11
 
12
+ // Which instrument produced a measurement. Mirrors the app's _trMeasInstrument:
13
+ // a measurement's raw `instrument` is usually EMPTY because blank means inherit,
14
+ // so returning the stored field alone would be near-useless here. Resolve the
15
+ // same cascade the UI shows, and report the source so a consumer can tell a
16
+ // recorded fact from an inherited default:
17
+ // 'row' — the measurement names its own instrument
18
+ // 'method' — from the parameter's Test Method equipment (the usual case)
19
+ // 'run' — the report-level default (an assumption, not evidence)
20
+ function _resolveInstrument(m, t, db) {
21
+ if (m && (m.instrument || m.equipmentId)) {
22
+ return { instrument: m.instrument || '', equipmentId: m.equipmentId || null, instrumentSource: 'row' };
23
+ }
24
+ const params = (db && db.parameters) || [];
25
+ let pm = null;
26
+ if (m && m.parameterId) pm = params.find(p => p && p.id === m.parameterId) || null;
27
+ if (!pm && m) {
28
+ const nm = String(m.parameter || '').trim().toLowerCase();
29
+ if (nm) pm = params.find(p => p && String(p.name || '').trim().toLowerCase() === nm) || null;
30
+ }
31
+ if (pm && (pm.equipment || pm.equipmentId)) {
32
+ return { instrument: pm.equipment || '', equipmentId: pm.equipmentId || null, instrumentSource: 'method' };
33
+ }
34
+ if (t && (t.instrument || t.equipmentId)) {
35
+ return { instrument: t.instrument || '', equipmentId: t.equipmentId || null, instrumentSource: 'run' };
36
+ }
37
+ return { instrument: '', equipmentId: null, instrumentSource: null };
38
+ }
39
+
12
40
  function _trimTest(t, samplesById) {
13
41
  if (!t) return null;
14
42
  const s = t.sampleId ? samplesById.get(t.sampleId) : null;
@@ -20,12 +48,16 @@ function _trimTest(t, samplesById) {
20
48
  testDate: t.testDate || '',
21
49
  performedBy: t.performedBy || '',
22
50
  lab: t.lab || '',
51
+ instrument: t.instrument || '', // run-level default; measurements may override
52
+ equipmentId: t.equipmentId || null,
53
+ controlSample: !!t.controlSample, // a re-measured reference — the valid basis for drift
23
54
  measurementCount: (t.measurements || []).length,
24
55
  parameters: [...new Set((t.measurements || []).map(m => m.parameter).filter(Boolean))],
25
56
  };
26
57
  }
27
58
 
28
- function _trimMeasurement(m) {
59
+ function _trimMeasurement(m, t, db) {
60
+ const ins = _resolveInstrument(m, t, db);
29
61
  return {
30
62
  parameter: m.parameter || '',
31
63
  value: m.value ?? null,
@@ -34,13 +66,14 @@ function _trimMeasurement(m) {
34
66
  pointCount: Array.isArray(m.points) ? m.points.length : 0,
35
67
  binCount: Array.isArray(m.bins) ? m.bins.length : 0,
36
68
  acceptanceCriteria: m.acceptanceCriteria || null,
69
+ ...ins, // instrument, equipmentId, instrumentSource
37
70
  };
38
71
  }
39
72
 
40
73
  const list_test_results = {
41
74
  definition: {
42
75
  name: 'list_test_results',
43
- 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 + parameters tested; use get_test_result for full measurement values.',
76
+ 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.',
44
77
  inputSchema: {
45
78
  type: 'object',
46
79
  properties: {
@@ -99,7 +132,7 @@ const list_test_results = {
99
132
  const get_test_result = {
100
133
  definition: {
101
134
  name: 'get_test_result',
102
- description: 'Get full details for a single test result report including every measurement\'s value, unit, type, and acceptance criteria. Accepts internal id or UID.',
135
+ description: 'Get full details for a single test result report: every measurement\'s value, unit, type, acceptance criteria, and the INSTRUMENT that produced it. instrument is RESOLVED (measurement override → the parameter\'s Test Method equipment → the report default) with instrumentSource telling you which — treat source "run" as an inherited assumption, not evidence. Accepts internal id or UID.',
103
136
  inputSchema: {
104
137
  type: 'object',
105
138
  properties: {
@@ -115,7 +148,7 @@ const get_test_result = {
115
148
  return {
116
149
  ..._trimTest(t, indexes.samplesById),
117
150
  notes: t.notes || '',
118
- measurements: (t.measurements || []).map(_trimMeasurement),
151
+ measurements: (t.measurements || []).map(m => _trimMeasurement(m, t, getStore().db)),
119
152
  };
120
153
  },
121
154
  };