formlab-mcp 0.6.34 → 0.6.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formlab-mcp",
3
- "version": "0.6.34",
3
+ "version": "0.6.35",
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.34",
5
+ "version": "0.6.35",
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.34",
16
+ "version": "0.6.35",
17
17
  "runtimeHint": "npx",
18
18
  "transport": {
19
19
  "type": "stdio"
@@ -226,7 +226,7 @@ const get_test_result = {
226
226
  const get_doe_matrix = {
227
227
  definition: {
228
228
  name: 'get_doe_matrix',
229
- description: 'Build a Design-of-Experiments style pivot matrix: rows = formulations (or samples / batches / tests), columns = composition wt-% per ingredient + aggregated test-parameter values. Same shape as the FormLab DOE Matrix view. A parameter measured under two or more RUN CONDITIONS (e.g. storage 25 °C vs 40 °C) is one column per condition — "pH · 40 °C" — and the bare parameter column is the base arm (readings with no condition, else the condition with the most readings); they are different responses, never averaged together. Returns CSV when format=csv (default) for compact LLM consumption, or structured JSON when format=json.',
229
+ description: 'Build a Design-of-Experiments style pivot matrix: rows = formulations (or samples / batches / tests), columns = composition wt-% per ingredient + aggregated test-parameter values. Same shape as the FormLab DOE Matrix view. A parameter measured under two or more RUN CONDITIONS (e.g. storage 25 °C vs 40 °C) is one column per condition — "pH · 40 °C" — and the bare parameter column is the base arm (readings with no condition, else the condition with the most readings, on a tie the one declared as the default on the Test Method / Test Panel); they are different responses, never averaged together. Returns CSV when format=csv (default) for compact LLM consumption, or structured JSON when format=json.',
230
230
  inputSchema: {
231
231
  type: 'object',
232
232
  properties: {
@@ -453,7 +453,34 @@ function _mcpCondTags(conds) {
453
453
  const lead = tags.map(t => t.split(/\s+[\/·]\s+/)[0]);
454
454
  return (new Set(lead).size === lead.length) ? lead : tags;
455
455
  }
456
- // items: [{ parameter, m }] → Map(parameter → Map(condSig → column name)),
456
+ // Is this reading taken under the DEFAULT run condition its Test Method or
457
+ // Test Panel declares? Port of the app's flCondDefaultScore (js/utils.js):
458
+ // the tiebreak for the primary arm when two arms have equally long records,
459
+ // instead of the condition's name ("25 °C" sorting before "40 °C").
460
+ function _mcpCondDefaultScore(m, t, db) {
461
+ const c = m && m.conditions;
462
+ if (!c || typeof c !== 'object') return 0;
463
+ const decls = [];
464
+ const ps = db.parameters || [];
465
+ const nm = String(m.parameter || '').trim().toLowerCase();
466
+ const method = (m.parameterId && ps.find(p => p && p.id === m.parameterId)) || (nm ? ps.find(p => p && String(p.name || '').trim().toLowerCase() === nm) : null);
467
+ if (method && Array.isArray(method.conditions)) decls.push(...method.conditions);
468
+ const tmpl = t && t.templateId ? (db.templates || []).find(x => x && x.id === t.templateId) : null;
469
+ if (tmpl && Array.isArray(tmpl.conditions)) decls.push(...tmpl.conditions);
470
+ const lc = {};
471
+ Object.keys(c).forEach(k => { lc[k.toLowerCase()] = c[k]; });
472
+ let matched = 0;
473
+ for (const d of decls) {
474
+ if (!d || !d.key || d.default == null || d.default === '') continue;
475
+ const v = lc[String(d.key).toLowerCase()];
476
+ if (v == null || v === '') continue;
477
+ if (String(v).trim() !== String(d.default).trim()) return 0;
478
+ matched++;
479
+ }
480
+ return matched ? 1 : 0;
481
+ }
482
+
483
+ // items: [{ parameter, m, test? }] → Map(parameter → Map(condSig → column name)),
457
484
  // only for parameters with two or more arms.
458
485
  function _mcpArmNames(items) {
459
486
  const arms = new Map();
@@ -461,14 +488,26 @@ function _mcpArmNames(items) {
461
488
  const sig = _mcpCondSig(it.m);
462
489
  if (!arms.has(it.parameter)) arms.set(it.parameter, new Map());
463
490
  const a = arms.get(it.parameter);
464
- const cur = a.get(sig) || { n: 0, m: it.m };
491
+ const cur = a.get(sig) || { n: 0, m: it.m, t: it.test || null };
465
492
  cur.n++; a.set(sig, cur);
466
493
  }
467
494
  const out = new Map();
495
+ let testOf = null; // measurement → its report, built only if a tie needs a panel default
496
+ const score = (arm) => {
497
+ const { db } = getStore();
498
+ let t = arm.t;
499
+ if (!t) {
500
+ if (!testOf) { testOf = new Map(); (db.testResults || []).forEach(r => (r.measurements || []).forEach(x => testOf.set(x, r))); }
501
+ t = testOf.get(arm.m) || null;
502
+ }
503
+ return _mcpCondDefaultScore(arm.m, t, db);
504
+ };
468
505
  arms.forEach((a, param) => {
469
506
  if (a.size < 2) return;
470
507
  const keys = [...a.keys()];
471
- const base = a.has('') ? '' : keys.slice().sort((x, y) => (a.get(y).n - a.get(x).n) || x.localeCompare(y))[0];
508
+ // No condition wins outright; among conditioned arms, most readings, then
509
+ // the declared default condition, then by name — as in the app.
510
+ const base = a.has('') ? '' : keys.slice().sort((x, y) => (a.get(y).n - a.get(x).n) || (score(a.get(y)) - score(a.get(x))) || x.localeCompare(y))[0];
472
511
  const ordered = [base, ...keys.filter(k => k !== base).sort((x, y) => x.localeCompare(y))];
473
512
  const tags = _mcpCondTags(ordered.map(k => k ? _mcpCondLabel(a.get(k).m) : ''));
474
513
  out.set(param, new Map(ordered.map((k, i) => [k, i === 0 ? param : `${param} · ${tags[i]}`])));
@@ -1243,7 +1282,7 @@ const get_stability = {
1243
1282
  },
1244
1283
  };
1245
1284
 
1246
- export { _evalPassFail, _resolveSpec, _shelfLife, _shelfLifeAssess, _mcpCondTags, _mcpArmNames };
1285
+ export { _evalPassFail, _resolveSpec, _shelfLife, _shelfLifeAssess, _mcpCondTags, _mcpArmNames, _mcpCondDefaultScore };
1247
1286
  export const tools = {
1248
1287
  list_test_results,
1249
1288
  get_test_result,