formlab-mcp 0.1.0
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 +136 -0
- package/data.js +163 -0
- package/index.js +122 -0
- package/package.json +37 -0
- package/tools/analytics.js +426 -0
- package/tools/formulations.js +214 -0
- package/tools/ingredients.js +103 -0
- package/tools/lab.js +234 -0
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# formlab-mcp
|
|
2
|
+
|
|
3
|
+
A **read-only Model Context Protocol server** for [FormLab](https://formvix.com). Lets Claude (or any MCP-compatible AI assistant) read and analyze your local FormLab database — your formulations, ingredients, batches, samples and test results — **without your data ever leaving your machine**.
|
|
4
|
+
|
|
5
|
+
> Local-first + AI-native. Your proprietary recipes stay on your laptop; only the LLM's answer to your question travels.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
Ask Claude (or another MCP client) things like:
|
|
10
|
+
|
|
11
|
+
- _"Which of my formulations use silicone fluid at 5% or more?"_
|
|
12
|
+
- _"Compare the composition of Formula FORM-001 and FORM-004 side by side."_
|
|
13
|
+
- _"What parameters fail most often in Q2 testing?"_
|
|
14
|
+
- _"Show me the DOE matrix at sample grain for all 'Anti-aging serum' family formulas."_
|
|
15
|
+
- _"What's untested? Which of my approved formulas have no measurements yet?"_
|
|
16
|
+
|
|
17
|
+
## Tools exposed
|
|
18
|
+
|
|
19
|
+
| Tool | Purpose |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `list_formulations` | Filtered list of recipes |
|
|
22
|
+
| `get_formulation` | Full record + flattened wt-% composition (sub-formulas expanded) |
|
|
23
|
+
| `find_similar_formulations` | Find formulas using a given ingredient ≥ threshold % |
|
|
24
|
+
| `compare_formulations` | Pairwise side-by-side composition diff |
|
|
25
|
+
| `list_ingredients` | Filtered list of raw materials |
|
|
26
|
+
| `get_ingredient` | Full record + supplier / stock / formulations using it |
|
|
27
|
+
| `list_batches` | Filtered list of production / lab-prep events |
|
|
28
|
+
| `get_batch` | Full record + actual composition + samples + blend lineage |
|
|
29
|
+
| `list_samples` | Filtered list of physical specimens |
|
|
30
|
+
| `get_sample` | Full record + canonical variant + test results + blend lineage |
|
|
31
|
+
| `list_test_results` | Filtered list of test reports |
|
|
32
|
+
| `get_test_result` | Full record + every measurement value |
|
|
33
|
+
| `get_doe_matrix` | Pivot matrix (CSV by default) — rows × ingredients × parameters |
|
|
34
|
+
| `find_failures` | Pareto-style: parameters that fail acceptance most often |
|
|
35
|
+
| `get_coverage_matrix` | Which formulations × parameters have been measured |
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# From npm (once published):
|
|
41
|
+
npm install -g formlab-mcp
|
|
42
|
+
|
|
43
|
+
# Or run directly without installing:
|
|
44
|
+
npx formlab-mcp /path/to/formlab-export.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For local development from this repo:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cd mcp
|
|
51
|
+
npm install
|
|
52
|
+
node index.js /path/to/formlab-export.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Requires **Node 18+**.
|
|
56
|
+
|
|
57
|
+
## Get your FormLab export
|
|
58
|
+
|
|
59
|
+
1. Open FormLab
|
|
60
|
+
2. Sidebar → **⇅ Import / Export**
|
|
61
|
+
3. Click **Export** — saves `formlab-export-YYYY-MM-DD.json` to your downloads folder
|
|
62
|
+
4. Point this MCP at the downloaded file
|
|
63
|
+
|
|
64
|
+
The MCP server watches the file — re-export from FormLab and the next tool call sees the fresh data without restarting the server.
|
|
65
|
+
|
|
66
|
+
## Wire it up to Claude Desktop
|
|
67
|
+
|
|
68
|
+
Add this to your `claude_desktop_config.json` (on macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"formlab": {
|
|
74
|
+
"command": "npx",
|
|
75
|
+
"args": ["formlab-mcp", "/Users/you/Downloads/formlab-export-2026-05-30.json"]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Or for local-dev (from the repo):
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"mcpServers": {
|
|
86
|
+
"formlab": {
|
|
87
|
+
"command": "node",
|
|
88
|
+
"args": [
|
|
89
|
+
"/Users/you/Documents/GitHub/formlab/mcp/index.js",
|
|
90
|
+
"/Users/you/Downloads/formlab-export-2026-05-30.json"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Restart Claude Desktop. You should see a hammer icon indicating tools are available, and FormLab's 15 tools become callable in any conversation.
|
|
98
|
+
|
|
99
|
+
## Wire it up to Claude Code
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
claude mcp add formlab npx formlab-mcp /Users/you/Downloads/formlab-export.json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Or with a stored env var:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
export FORMLAB_EXPORT=/Users/you/Downloads/formlab-export-2026-05-30.json
|
|
109
|
+
claude mcp add formlab npx formlab-mcp
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Privacy and architecture
|
|
113
|
+
|
|
114
|
+
- **No network calls.** The server runs on stdio between Claude and your local Node process. Your data file never leaves your disk.
|
|
115
|
+
- **No FormLab cloud.** FormLab itself is a local-first app; the MCP follows the same model.
|
|
116
|
+
- **Hot-reload on file change.** Re-export from FormLab while the server is running — the next tool call picks up fresh data.
|
|
117
|
+
- **Read-only.** Tier 1 cannot mutate your FormLab data. (Write tools are planned for a future paid "Pro" tier — register / batch / log-test mutations with cascade-safe confirmation.)
|
|
118
|
+
|
|
119
|
+
## Data shape
|
|
120
|
+
|
|
121
|
+
The server accepts both FormLab export shapes:
|
|
122
|
+
|
|
123
|
+
- **Wrapped FAIR export**: `{ "fair": {...metadata...}, "data": {...db...} }`
|
|
124
|
+
- **Legacy bare-db JSON**: `{...db...}` directly
|
|
125
|
+
|
|
126
|
+
The wrapped form is the default since 2026; the legacy form is supported for older exports.
|
|
127
|
+
|
|
128
|
+
## Tier 2 (planned, paid)
|
|
129
|
+
|
|
130
|
+
Live file-sync with write tools: `create_formulation`, `update_formulation`, `log_test_result`, `create_batch`, etc. Mutations from Claude write back to the same JSON FormLab reads. Conflict detection, cascade-safe deletes, schema versioning.
|
|
131
|
+
|
|
132
|
+
Not yet shipped — see the FormLab roadmap.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
package/data.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// DATA LOADER — accepts both FormLab export shapes
|
|
3
|
+
// 1. Wrapped FAIR export: { fair: {...}, data: {...db...} }
|
|
4
|
+
// 2. Legacy bare-db JSON: {...db...} directly
|
|
5
|
+
// And exposes a single normalized "store" object plus helpers
|
|
6
|
+
// for the tools to query against.
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
import { readFileSync, watch } from 'node:fs';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
// Module-level mutable store — re-assigned on hot-reload when the
|
|
13
|
+
// watched file changes. Tools read from getStore() to always see
|
|
14
|
+
// the latest data without holding a stale reference.
|
|
15
|
+
let _store = null;
|
|
16
|
+
let _path = null;
|
|
17
|
+
|
|
18
|
+
export function loadStore(filePath) {
|
|
19
|
+
_path = resolve(filePath);
|
|
20
|
+
const raw = readFileSync(_path, 'utf8');
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
// Detect wrapped vs legacy shape. A wrapped export has top-level
|
|
23
|
+
// `fair` metadata + `data` payload. Legacy is just the db object
|
|
24
|
+
// with ingredients/formulations/etc. at the root.
|
|
25
|
+
const db = (parsed && typeof parsed === 'object' && parsed.fair && parsed.data)
|
|
26
|
+
? parsed.data
|
|
27
|
+
: parsed;
|
|
28
|
+
const meta = (parsed && parsed.fair) || null;
|
|
29
|
+
_store = {
|
|
30
|
+
db,
|
|
31
|
+
meta,
|
|
32
|
+
loadedAt: new Date().toISOString(),
|
|
33
|
+
sourcePath: _path,
|
|
34
|
+
// Pre-compute commonly-used indexes for O(1) lookups instead
|
|
35
|
+
// of repeated linear scans across every tool call.
|
|
36
|
+
indexes: _buildIndexes(db),
|
|
37
|
+
};
|
|
38
|
+
return _store;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getStore() {
|
|
42
|
+
if (!_store) throw new Error('Data store not loaded. Call loadStore(path) first.');
|
|
43
|
+
return _store;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Re-read the file on disk-change. Useful when the user re-exports
|
|
47
|
+
// FormLab while the MCP server is running — the next tool call sees
|
|
48
|
+
// fresh data without restarting the server.
|
|
49
|
+
export function watchStore(onReload) {
|
|
50
|
+
if (!_path) throw new Error('Cannot watch — no path loaded.');
|
|
51
|
+
let timer = null;
|
|
52
|
+
watch(_path, (eventType) => {
|
|
53
|
+
if (eventType !== 'change') return;
|
|
54
|
+
// Debounce: editors often fire multiple change events per save
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
timer = setTimeout(() => {
|
|
57
|
+
try {
|
|
58
|
+
loadStore(_path);
|
|
59
|
+
if (onReload) onReload(_store);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
// Don't crash the server on a malformed re-export — keep the
|
|
62
|
+
// previous good state in memory and surface the error on the
|
|
63
|
+
// next tool call.
|
|
64
|
+
console.error('[formlab-mcp] reload failed:', e.message);
|
|
65
|
+
}
|
|
66
|
+
}, 150);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _buildIndexes(db) {
|
|
71
|
+
const byId = (arr) => {
|
|
72
|
+
const m = new Map();
|
|
73
|
+
(arr || []).forEach(x => { if (x && x.id) m.set(x.id, x); });
|
|
74
|
+
return m;
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
formulationsById: byId(db.formulations),
|
|
78
|
+
ingredientsById: byId(db.ingredients),
|
|
79
|
+
batchesById: byId(db.batches),
|
|
80
|
+
samplesById: byId(db.samples),
|
|
81
|
+
testResultsById: byId(db.testResults),
|
|
82
|
+
projectsById: byId(db.projects),
|
|
83
|
+
templatesById: byId(db.templates),
|
|
84
|
+
parametersById: byId(db.parameters),
|
|
85
|
+
equipmentById: byId(db.equipment),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============================================================
|
|
90
|
+
// Shared helpers used across tools
|
|
91
|
+
// ============================================================
|
|
92
|
+
|
|
93
|
+
// Resolve a record by either UID (FORM-001) or internal id. Lets
|
|
94
|
+
// users reference entities by the human-readable UID they see in
|
|
95
|
+
// FormLab without having to dig out the opaque internal id.
|
|
96
|
+
export function resolveById(collection, idOrUid) {
|
|
97
|
+
const db = getStore().db;
|
|
98
|
+
const arr = db[collection] || [];
|
|
99
|
+
if (!idOrUid) return null;
|
|
100
|
+
return arr.find(x => x && (x.id === idOrUid || x.uid === idOrUid)) || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Flatten a formulation's composition into leaf ingredient wt-%.
|
|
104
|
+
// Walks sub-formula rows recursively (cap depth to break cycles).
|
|
105
|
+
// Mirrors the algorithm in FormLab's js/nesting.js _flattenComposition.
|
|
106
|
+
export function flattenComposition(formulation, depth = 0) {
|
|
107
|
+
const MAX_DEPTH = 5;
|
|
108
|
+
if (!formulation || !Array.isArray(formulation.composition) || depth > MAX_DEPTH) {
|
|
109
|
+
return { rows: [] };
|
|
110
|
+
}
|
|
111
|
+
const { db } = getStore();
|
|
112
|
+
const rows = [];
|
|
113
|
+
const allPct = formulation.composition.every(c => {
|
|
114
|
+
const u = String(c.unit || 'wt%').trim().toLowerCase();
|
|
115
|
+
return u.startsWith('wt') || u === '%';
|
|
116
|
+
});
|
|
117
|
+
// Total mass denominator for renormalization when sub-formulas are present
|
|
118
|
+
let totalWeight = 0;
|
|
119
|
+
formulation.composition.forEach(c => {
|
|
120
|
+
if (!c) return;
|
|
121
|
+
const amt = parseFloat(c.amount);
|
|
122
|
+
if (!isFinite(amt)) return;
|
|
123
|
+
totalWeight += amt;
|
|
124
|
+
});
|
|
125
|
+
if (totalWeight <= 0) return { rows };
|
|
126
|
+
|
|
127
|
+
formulation.composition.forEach(c => {
|
|
128
|
+
if (!c) return;
|
|
129
|
+
const amt = parseFloat(c.amount);
|
|
130
|
+
if (!isFinite(amt)) return;
|
|
131
|
+
const fraction = amt / totalWeight;
|
|
132
|
+
if (c.formulationId) {
|
|
133
|
+
const sub = db.formulations.find(g => g.id === c.formulationId);
|
|
134
|
+
if (!sub) return;
|
|
135
|
+
const sub_flat = flattenComposition(sub, depth + 1);
|
|
136
|
+
sub_flat.rows.forEach(r => {
|
|
137
|
+
rows.push({
|
|
138
|
+
ingredientId: r.ingredientId,
|
|
139
|
+
ingredientName: r.ingredientName,
|
|
140
|
+
weightFraction: r.weightFraction * fraction,
|
|
141
|
+
role: r.role || c.role || '',
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
} else if (c.ingredientId) {
|
|
145
|
+
const ing = db.ingredients.find(i => i.id === c.ingredientId);
|
|
146
|
+
rows.push({
|
|
147
|
+
ingredientId: c.ingredientId,
|
|
148
|
+
ingredientName: ing?.name || c.ingredientId,
|
|
149
|
+
weightFraction: allPct ? amt / 100 : fraction,
|
|
150
|
+
role: c.role || '',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
// Coalesce duplicate leaves (same ingredient reachable via multiple
|
|
155
|
+
// sub-formulas) by summing their weight fractions.
|
|
156
|
+
const merged = new Map();
|
|
157
|
+
rows.forEach(r => {
|
|
158
|
+
const prev = merged.get(r.ingredientId);
|
|
159
|
+
if (prev) prev.weightFraction += r.weightFraction;
|
|
160
|
+
else merged.set(r.ingredientId, { ...r });
|
|
161
|
+
});
|
|
162
|
+
return { rows: [...merged.values()] };
|
|
163
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ============================================================
|
|
3
|
+
// FormLab MCP server — read-only entry point.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// node index.js /path/to/formlab-export-2026-05-30.json
|
|
7
|
+
// FORMLAB_EXPORT=/path/to/export.json node index.js
|
|
8
|
+
// formlab-mcp /path/to/export.json (when installed via npm)
|
|
9
|
+
//
|
|
10
|
+
// Exposes 13 tools (see ./tools/) over stdio. The MCP host (Claude
|
|
11
|
+
// Desktop, Claude Code, etc.) handles tool discovery, invocation
|
|
12
|
+
// and response formatting. We just register handlers and stay out
|
|
13
|
+
// of the way.
|
|
14
|
+
//
|
|
15
|
+
// Hot-reload: if the export file changes on disk while the server
|
|
16
|
+
// is running, the store is re-read automatically so the next tool
|
|
17
|
+
// call sees fresh data. No restart needed.
|
|
18
|
+
// ============================================================
|
|
19
|
+
|
|
20
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
21
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
22
|
+
import {
|
|
23
|
+
CallToolRequestSchema,
|
|
24
|
+
ListToolsRequestSchema,
|
|
25
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
26
|
+
|
|
27
|
+
import { loadStore, watchStore, getStore } from './data.js';
|
|
28
|
+
import * as formulations from './tools/formulations.js';
|
|
29
|
+
import * as ingredients from './tools/ingredients.js';
|
|
30
|
+
import * as lab from './tools/lab.js';
|
|
31
|
+
import * as analytics from './tools/analytics.js';
|
|
32
|
+
|
|
33
|
+
const filePath = process.argv[2] || process.env.FORMLAB_EXPORT;
|
|
34
|
+
if (!filePath) {
|
|
35
|
+
process.stderr.write([
|
|
36
|
+
'Usage: formlab-mcp <path-to-export.json>',
|
|
37
|
+
'Or set FORMLAB_EXPORT=<path> and call without args.',
|
|
38
|
+
'',
|
|
39
|
+
'Export your FormLab database via Settings → Import / Export → Export,',
|
|
40
|
+
'then point this server at the downloaded formlab-export-YYYY-MM-DD.json.',
|
|
41
|
+
'',
|
|
42
|
+
].join('\n'));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
loadStore(filePath);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
process.stderr.write(`[formlab-mcp] failed to load export: ${e.message}\n`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const store = getStore();
|
|
54
|
+
process.stderr.write([
|
|
55
|
+
`[formlab-mcp] loaded ${store.sourcePath}`,
|
|
56
|
+
store.meta
|
|
57
|
+
? ` schema=${store.meta.schemaName} v${store.meta.schemaVersion} exportedAt=${store.meta.exportedAt}`
|
|
58
|
+
: ` (legacy bare-db shape — no FAIR metadata)`,
|
|
59
|
+
` records: ${
|
|
60
|
+
[
|
|
61
|
+
`${(store.db.ingredients || []).length} ingredients`,
|
|
62
|
+
`${(store.db.formulations || []).length} formulas`,
|
|
63
|
+
`${(store.db.batches || []).length} batches`,
|
|
64
|
+
`${(store.db.samples || []).length} samples`,
|
|
65
|
+
`${(store.db.testResults || []).length} test results`,
|
|
66
|
+
].join(' · ')
|
|
67
|
+
}`,
|
|
68
|
+
'',
|
|
69
|
+
].join('\n'));
|
|
70
|
+
|
|
71
|
+
watchStore((s) => {
|
|
72
|
+
process.stderr.write(`[formlab-mcp] reloaded — ${(s.db.formulations||[]).length} formulas now\n`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// All tools live in tools/*.js as a flat { definition, handler } pair.
|
|
76
|
+
// Concatenate the four modules' exports into a single registry the
|
|
77
|
+
// MCP server can use for both list_tools and call_tool dispatch.
|
|
78
|
+
const TOOLS = [
|
|
79
|
+
...Object.values(formulations.tools),
|
|
80
|
+
...Object.values(ingredients.tools),
|
|
81
|
+
...Object.values(lab.tools),
|
|
82
|
+
...Object.values(analytics.tools),
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const server = new Server(
|
|
86
|
+
{ name: 'formlab-mcp', version: '0.1.0' },
|
|
87
|
+
{ capabilities: { tools: {} } }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
91
|
+
tools: TOOLS.map(t => t.definition),
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
95
|
+
const { name, arguments: args } = req.params;
|
|
96
|
+
const tool = TOOLS.find(t => t.definition.name === name);
|
|
97
|
+
if (!tool) {
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
100
|
+
isError: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const result = await tool.handler(args || {});
|
|
105
|
+
// All tool handlers return either a plain object/array (auto-
|
|
106
|
+
// serialized as JSON text) or a pre-formatted string. The MCP
|
|
107
|
+
// protocol wants content as { type: 'text', text: '...' }.
|
|
108
|
+
const text = typeof result === 'string'
|
|
109
|
+
? result
|
|
110
|
+
: JSON.stringify(result, null, 2);
|
|
111
|
+
return { content: [{ type: 'text', text }] };
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: 'text', text: `Error: ${e.message}` }],
|
|
115
|
+
isError: true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const transport = new StdioServerTransport();
|
|
121
|
+
await server.connect(transport);
|
|
122
|
+
process.stderr.write('[formlab-mcp] ready — listening on stdio\n');
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "formlab-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Read-only Model Context Protocol server for FormLab — lets Claude (and other MCP clients) read and analyze your local FormLab export. Your recipes never leave your machine.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"formlab-mcp": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"dev": "node --watch index.js"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.0.4"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"formlab",
|
|
24
|
+
"chemistry",
|
|
25
|
+
"formulation",
|
|
26
|
+
"lab-notebook",
|
|
27
|
+
"doe",
|
|
28
|
+
"design-of-experiments",
|
|
29
|
+
"claude"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/juliu1980/FormLab",
|
|
35
|
+
"directory": "mcp"
|
|
36
|
+
}
|
|
37
|
+
}
|