formlab-mcp 0.6.52 → 0.6.53

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
@@ -29,7 +29,7 @@ Ask Claude (or another MCP client) things like:
29
29
 
30
30
  | Tool | Purpose |
31
31
  |---|---|
32
- | `list_formulations` | Filtered list of recipes |
32
+ | `list_formulations` | Filtered list of recipes, each with exact `batchCount` / `sampleCount` / `testReportCount`; `sort` (`batches`, `samples`, `test_reports`, `name`, `updated`) ranks every match before the limit — "which formulas have the most batch runs?" in one call |
33
33
  | `get_formulation` | Full record (incl. `versionLabel`, the label the user types, e.g. 1.0) + **version history** (`versionHistory`: one snapshot per save, v1, v2…, newest first, with the label each save had and its note) + flattened wt-% composition (sub-formulas expanded), the ordered **step procedure**, user **custom fields**, any **process-factor values** (DOE inputs — cure temp, mix time, RPM, pH…), the declared **finishedDensity** (g/mL of the mixed product; volume-targeted batches scale by it), and its **theoretical parameters computed** the way the app's Params tab does (Energy kcal/100g, NV %, Cost/kg…: value, unit, formula, spec pass/fail, and a `warning` when ingredients lack a value or carry mixed units) — the app's own engine, vendored in `mcp/vendor/` and run in an isolated `node:vm` context |
34
34
  | `find_similar_formulations` | Find formulas using a given ingredient ≥ threshold % |
35
35
  | `compare_formulations` | Pairwise side-by-side composition diff |
package/data.js CHANGED
@@ -97,9 +97,35 @@ function _buildIndexes(db) {
97
97
  templatesById: byId(db.templates),
98
98
  parametersById: byId(db.parameters),
99
99
  equipmentById: byId(db.equipment),
100
+ formulationCounts: _formulationCounts(db),
100
101
  };
101
102
  }
102
103
 
104
+ // Exact batch / sample / test-report counts per formula id, built once per load so
105
+ // list_formulations can rank every formula in O(n). Same rule as the app's AI context
106
+ // (js/gemini.js _fCounts, build 22m): a sample counts toward its own formulationId, or
107
+ // its batch's formula; a test report through its sample, else its own formulationId.
108
+ function _formulationCounts(db) {
109
+ const counts = new Map();
110
+ const bump = (fid, k) => {
111
+ if (!fid) return;
112
+ let c = counts.get(fid);
113
+ if (!c) counts.set(fid, c = { batches: 0, samples: 0, testReports: 0 });
114
+ c[k]++;
115
+ };
116
+ const batchFid = new Map();
117
+ (db.batches || []).forEach(b => { if (b) { bump(b.formulationId, 'batches'); batchFid.set(b.id, b.formulationId); } });
118
+ const sampleFid = new Map();
119
+ (db.samples || []).forEach(s => {
120
+ if (!s) return;
121
+ const fid = s.formulationId || batchFid.get(s.batchId);
122
+ sampleFid.set(s.id, fid);
123
+ bump(fid, 'samples');
124
+ });
125
+ (db.testResults || []).forEach(t => { if (t) bump(sampleFid.get(t.sampleId) || t.formulationId, 'testReports'); });
126
+ return counts;
127
+ }
128
+
103
129
  // ============================================================
104
130
  // Shared helpers used across tools
105
131
  // ============================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formlab-mcp",
3
- "version": "0.6.52",
3
+ "version": "0.6.53",
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.52",
5
+ "version": "0.6.53",
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.52",
16
+ "version": "0.6.53",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"
@@ -13,6 +13,7 @@ import { theoreticalParams } from '../theoretical.js';
13
13
  // while keeping the human-facing fields the user cares about.
14
14
  function _trimFormulation(f) {
15
15
  if (!f) return null;
16
+ const counts = getStore().indexes.formulationCounts.get(f.id);
16
17
  return {
17
18
  id: f.id,
18
19
  uid: f.uid,
@@ -39,13 +40,23 @@ function _trimFormulation(f) {
39
40
  compositionRowCount: (f.composition || []).length,
40
41
  hasSubFormulas: (f.composition || []).some(c => c && c.formulationId),
41
42
  updatedAt: f.updatedAt || null,
43
+ // Exact counts (a sample through its batch, a test report through its sample).
44
+ batchCount: counts ? counts.batches : 0,
45
+ sampleCount: counts ? counts.samples : 0,
46
+ testReportCount: counts ? counts.testReports : 0,
42
47
  };
43
48
  }
44
49
 
50
+ const _SORTS = {
51
+ batches: (c) => (c ? c.batches : 0),
52
+ samples: (c) => (c ? c.samples : 0),
53
+ test_reports: (c) => (c ? c.testReports : 0),
54
+ };
55
+
45
56
  const list_formulations = {
46
57
  definition: {
47
58
  name: 'list_formulations',
48
- description: 'List formulations (recipes) in the FormLab database, optionally filtered by status, project, family, tag, or name pattern. Returns one row per formula with key metadata. Use get_formulation for the full composition.',
59
+ description: 'List formulations (recipes) in the FormLab database, optionally filtered by status, project, family, tag, or name pattern. Returns one row per formula with key metadata and exact batchCount / sampleCount / testReportCount (a sample counts toward its batch\'s formula, a test report toward its sample\'s). To rank formulas (e.g. "which formulas have the most batch runs?") pass sort — it orders every matching formula before the limit is applied, so do not page through the list or call get_formulation per formula. Use get_formulation for the full composition.',
49
60
  inputSchema: {
50
61
  type: 'object',
51
62
  properties: {
@@ -54,6 +65,7 @@ const list_formulations = {
54
65
  family: { type: 'string', description: 'Filter by formula family (e.g. "Acrylic Coating", "Skin Serum"). Case-insensitive substring.' },
55
66
  tag: { type: 'string', description: 'Filter to formulas containing this tag.' },
56
67
  name_contains: { type: 'string', description: 'Case-insensitive substring match on formula name.' },
68
+ sort: { type: 'string', enum: ['batches', 'samples', 'test_reports', 'name', 'updated'], description: 'Order before the limit: batches / samples / test_reports = most first; name = A–Z; updated = most recently updated first. Default: database order.' },
57
69
  limit: { type: 'number', description: 'Max rows to return (default 50, max 500).' },
58
70
  },
59
71
  },
@@ -75,9 +87,22 @@ const list_formulations = {
75
87
  if (args.name_contains && !norm(f.name).includes(norm(args.name_contains))) return false;
76
88
  return true;
77
89
  });
90
+ if (args.sort && Object.hasOwn(_SORTS, args.sort)) {
91
+ const key = _SORTS[args.sort];
92
+ const val = (f) => key(indexes.formulationCounts.get(f.id));
93
+ filtered.sort((a, b) => val(b) - val(a));
94
+ } else if (args.sort === 'name') {
95
+ filtered.sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), undefined, { numeric: true, sensitivity: 'base' }));
96
+ } else if (args.sort === 'updated') {
97
+ const ts = (v) => (typeof v === 'number' ? v : Date.parse(v) || 0);
98
+ filtered.sort((a, b) => ts(b.updatedAt) - ts(a.updatedAt));
99
+ } else if (args.sort) {
100
+ return { error: `Unknown sort "${args.sort}". Use one of: batches, samples, test_reports, name, updated.` };
101
+ }
78
102
  return {
79
103
  totalMatching: filtered.length,
80
104
  returned: Math.min(filtered.length, limit),
105
+ ...(args.sort ? { sortedBy: args.sort } : {}),
81
106
  formulations: filtered.slice(0, limit).map(_trimFormulation),
82
107
  };
83
108
  },