formlab-mcp 0.6.17 → 0.6.19
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/formulations.js +1 -0
- package/tools/ingredients.js +7 -2
- package/tools/lab.js +12 -1
package/README.md
CHANGED
|
@@ -34,10 +34,10 @@ Ask Claude (or another MCP client) things like:
|
|
|
34
34
|
| `compare_formulations` | Pairwise side-by-side composition diff |
|
|
35
35
|
| `list_ingredients` | Filtered list of raw materials. Filters: `family`, `supplier`, `name_contains`, `in_stock_only`, `ingredient_class` (small-molecule / surfactant / polymer / extract / fragrance / pigment / sequence / mixture), `sequence_contains` (e.g. `KTTKS` → Matrixyl), `taxon_contains` (e.g. `Centella`) |
|
|
36
36
|
| `get_ingredient` | Full record + supplier / **cost ($/kg)** / stock / chemical name / molecular weight / storage / formulations using it, plus **GHS safety** (pictograms, H/P codes, signal word), **per-jurisdiction regulatory status**, and **inventory lots** (balances + expiry). Returns every class-specific sub-object when present: `sequence` (peptide / oligo), `taxon` (NCBI ID + scientific name), `ingredientClass`, `extractDetails`, `sequenceDetails`, `polymerDetails`, `surfactantDetails`, `pigmentDetails`, `fragranceDetails` |
|
|
37
|
-
| `list_lots` | Inventory lots across ingredients (each a received batch with its own remaining balance, supplier, expiry). Filters: `ingredient_id`, `expiring_within_days`, `status` |
|
|
37
|
+
| `list_lots` | Inventory lots across ingredients (each a received batch with its own remaining balance, supplier, expiry, **unit cost**, and **status** — `active` / `hold` / `quarantine` / `rejected`; held lots are blocked from consumption, with a `holdReason`). Filters: `ingredient_id`, `expiring_within_days`, `status` (e.g. `hold` to find lots held for a recall) |
|
|
38
38
|
| `list_inventory` | Portfolio stock rollup — one row per ingredient with on-hand qty, summed lot balance, nearest expiry, and a low-stock flag (on-hand ≤ `reorderThreshold`, or zero). Filters: `low_stock_only`, `expiring_within_days`, `family` |
|
|
39
39
|
| `list_batches` | Filtered list of production / lab-prep events |
|
|
40
|
-
| `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 inputs, else measured volume × density), the **process** method + any **process-factor values** (DOE inputs), samples + blend lineage |
|
|
40
|
+
| `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 inputs, else measured volume × density), 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) |
|
|
41
41
|
| `list_samples` | Filtered list of physical specimens |
|
|
42
42
|
| `get_sample` | Full record + canonical variant + test reports + blend lineage |
|
|
43
43
|
| `list_test_results` | Filtered list of test reports (by sample, parameter, **measured-value range** (`value_min`/`value_max`), date, or lab) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "formlab-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.19",
|
|
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.
|
|
5
|
+
"version": "0.6.19",
|
|
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.19",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
package/tools/formulations.js
CHANGED
|
@@ -22,6 +22,7 @@ function _trimFormulation(f) {
|
|
|
22
22
|
application: f.application || '',
|
|
23
23
|
owner: f.owner || '',
|
|
24
24
|
regulatoryCategory: f.regulatoryCategory || '',
|
|
25
|
+
shelfLifeMonths: f.shelfLifeMonths ?? null,
|
|
25
26
|
tags: f.tags || [],
|
|
26
27
|
projectId: f.projectId || null,
|
|
27
28
|
compositionRowCount: (f.composition || []).length,
|
package/tools/ingredients.js
CHANGED
|
@@ -295,13 +295,13 @@ const find_by_smarts = {
|
|
|
295
295
|
const list_lots = {
|
|
296
296
|
definition: {
|
|
297
297
|
name: 'list_lots',
|
|
298
|
-
description: 'List inventory lots (received batches of an ingredient, each with its own remaining balance, supplier, and
|
|
298
|
+
description: 'List inventory lots (received batches of an ingredient, each with its own remaining balance, supplier, expiry, and unit cost). Optionally filter by ingredient, expiring-within-days, or status. A lot\'s status can be "active", "hold" (a manual QC / recall hold — see holdReason), "quarantine", or "rejected"; the last three are blocked from auto-consumption. For one ingredient\'s full lots + traceability, use get_ingredient.',
|
|
299
299
|
inputSchema: {
|
|
300
300
|
type: 'object',
|
|
301
301
|
properties: {
|
|
302
302
|
ingredient_id: { type: 'string', description: 'Only lots for this ingredient (internal id or UID, e.g. ING-001).' },
|
|
303
303
|
expiring_within_days: { type: 'number', description: 'Only lots whose expiryDate is within this many days from now (negative days = already expired are always included).' },
|
|
304
|
-
status: { type: 'string', description: 'Filter by lot status
|
|
304
|
+
status: { type: 'string', description: 'Filter by lot status: "active", "hold", "quarantine", or "rejected". Held/quarantined/rejected lots are blocked from auto-consumption (use "hold" to find lots put on hold for a QC / recall).' },
|
|
305
305
|
limit: { type: 'number', description: 'Max rows (default 200, max 1000).' },
|
|
306
306
|
},
|
|
307
307
|
},
|
|
@@ -332,6 +332,11 @@ const list_lots = {
|
|
|
332
332
|
qtyRemaining: l.qtyRemaining, qtyReceived: l.qtyReceived, unit: l.unit || '',
|
|
333
333
|
receivedDate: l.receivedDate || null, expiryDate: l.expiryDate || null,
|
|
334
334
|
supplier: l.supplier || '', status: l.status || 'active',
|
|
335
|
+
// 'hold' / 'quarantine' / 'rejected' lots are blocked from auto-consumption;
|
|
336
|
+
// holdReason explains a manual hold (QC / recall).
|
|
337
|
+
holdReason: l.holdReason || '',
|
|
338
|
+
unitCost: (l.unitCost != null && l.unitCost !== '') ? l.unitCost : null,
|
|
339
|
+
unitCostCcy: l.unitCostCcy || null,
|
|
335
340
|
}));
|
|
336
341
|
return { totalMatching: rows.length, returned: Math.min(rows.length, limit), lots: rows.slice(0, limit) };
|
|
337
342
|
},
|
package/tools/lab.js
CHANGED
|
@@ -96,6 +96,9 @@ function _trimBatch(b, formulationsById) {
|
|
|
96
96
|
actualMass: (function () { const m = _batchActualMassKg(b); return (m != null && isFinite(m)) ? { kg: +m.toFixed(4), unit: 'kg' } : null; })(),
|
|
97
97
|
preparedDate: b.preparedDate || '',
|
|
98
98
|
preparedBy: b.preparedBy || '',
|
|
99
|
+
// Finished-goods storage location for this batch's produced stock (Production
|
|
100
|
+
// Inventory: a batch is a finished-goods lot).
|
|
101
|
+
location: b.location || '',
|
|
99
102
|
process: b.process || '',
|
|
100
103
|
// Per-batch process-factor values (DOE Matrix "+ Factor" at batch grain) —
|
|
101
104
|
// recorded process conditions (cure temp, mix time, RPM, pH, …) as DOE INPUTS.
|
|
@@ -172,7 +175,7 @@ const list_batches = {
|
|
|
172
175
|
const get_batch = {
|
|
173
176
|
definition: {
|
|
174
177
|
name: 'get_batch',
|
|
175
|
-
description: 'Get full details for a single batch including its actual composition (as-prepared, when logged), child samples, lineage (parent / sub-blend parents),
|
|
178
|
+
description: 'Get full details for a single batch including its actual composition (as-prepared, when logged), child samples, lineage (parent / sub-blend parents), and — treating the batch as a finished-goods lot — its storage `location` and finished-goods `stockLedger` (shipments out + adjustments; on-hand = produced − sample pulls − sub-batch draws + Σ ledger). Plus 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.',
|
|
176
179
|
inputSchema: {
|
|
177
180
|
type: 'object',
|
|
178
181
|
properties: {
|
|
@@ -212,6 +215,14 @@ const get_batch = {
|
|
|
212
215
|
sourceBlendParents: parents,
|
|
213
216
|
samples: samples.map(s => ({ id: s.id, uid: s.uid, name: s.name || '', status: s.status })),
|
|
214
217
|
sampleCount: samples.length,
|
|
218
|
+
// Finished-goods drawdown ledger (a batch is a finished-goods lot): shipments
|
|
219
|
+
// out + manual adjustments, in the batch unit. On-hand = produced − sample
|
|
220
|
+
// pulls − sub-batch draws + Σ(ledger deltas). Sub-batches are batches whose
|
|
221
|
+
// parentBatchId === this id (see list_batches).
|
|
222
|
+
stockLedger: (Array.isArray(b.stockLedger) ? b.stockLedger : []).map(e => ({
|
|
223
|
+
date: e.ts || null, delta: e.delta ?? null, unit: e.unit || b.unit || '', kind: e.kind || '', note: e.note || '',
|
|
224
|
+
})),
|
|
225
|
+
shippedQty: (Array.isArray(b.stockLedger) ? b.stockLedger : []).reduce((s, e) => e.kind === 'shipment' ? s + Math.abs(parseFloat(e.delta) || 0) : s, 0),
|
|
215
226
|
};
|
|
216
227
|
},
|
|
217
228
|
};
|