@dforge-core/dforge-mcp 0.1.0-rc.9 → 0.1.1

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +84 -27
  3. package/dist/server.js +2957 -616
  4. package/docs/creating-modules.md +11 -4
  5. package/package.json +11 -6
  6. package/resources/docs/conventions.md +22 -16
  7. package/resources/docs/dsl-reference.md +767 -0
  8. package/resources/schemas/entity.schema.json +8 -1
  9. package/resources/schemas/print-templates.schema.json +82 -0
  10. package/resources/schemas/reports.schema.json +4 -0
  11. package/resources/schemas/triggers.schema.json +59 -0
  12. package/skills/dforge-mcp-author/SKILL.md +269 -69
  13. package/skills/dforge-mcp-author/examples/matrix-budget/README.md +43 -0
  14. package/skills/dforge-mcp-author/examples/matrix-budget/entities/budget_category.json +24 -0
  15. package/skills/dforge-mcp-author/examples/matrix-budget/entities/budget_line.json +56 -0
  16. package/skills/dforge-mcp-author/examples/matrix-budget/manifest.json +30 -0
  17. package/skills/dforge-mcp-author/examples/matrix-budget/security/roles.json +9 -0
  18. package/skills/dforge-mcp-author/examples/matrix-budget/seed-data/01-categories.json +8 -0
  19. package/skills/dforge-mcp-author/examples/matrix-budget/ui/data_views.json +42 -0
  20. package/skills/dforge-mcp-author/examples/simple-todo/README.md +38 -0
  21. package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_item.json +83 -0
  22. package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_list.json +43 -0
  23. package/skills/dforge-mcp-author/examples/simple-todo/logic/actions/mark_done.dsl +6 -0
  24. package/skills/dforge-mcp-author/examples/simple-todo/manifest.json +32 -0
  25. package/skills/dforge-mcp-author/examples/simple-todo/security/roles.json +10 -0
  26. package/skills/dforge-mcp-author/examples/simple-todo/seed-data/01-lists.json +17 -0
  27. package/skills/dforge-mcp-author/examples/simple-todo/ui/actions.json +11 -0
  28. package/skills/dforge-mcp-author/examples/simple-todo/ui/data_views.json +35 -0
  29. package/skills/dforge-mcp-author/examples/simple-todo/ui/menus.json +28 -0
  30. package/skills/dforge-mcp-author/references/action-dsl.md +397 -0
  31. package/skills/dforge-mcp-author/references/column-types.md +168 -0
  32. package/skills/dforge-mcp-author/references/conventions.md +177 -0
  33. package/skills/dforge-mcp-author/references/data-migration.md +270 -0
  34. package/skills/dforge-mcp-author/references/data-views.md +243 -0
  35. package/skills/dforge-mcp-author/references/excel-import.md +61 -0
  36. package/skills/dforge-mcp-author/references/field-types.md +144 -0
  37. package/skills/dforge-mcp-author/references/filters.md +326 -0
  38. package/skills/dforge-mcp-author/references/flags.md +73 -0
  39. package/skills/dforge-mcp-author/references/formulas.md +206 -0
  40. package/skills/dforge-mcp-author/references/jobs.md +149 -0
  41. package/skills/dforge-mcp-author/references/manifest.md +123 -0
  42. package/skills/dforge-mcp-author/references/menus.md +164 -0
  43. package/skills/dforge-mcp-author/references/number-sequences.md +117 -0
  44. package/skills/dforge-mcp-author/references/print-templates.md +159 -0
  45. package/skills/dforge-mcp-author/references/queries.md +312 -0
  46. package/skills/dforge-mcp-author/references/reports.md +398 -0
  47. package/skills/dforge-mcp-author/references/schema-import.md +331 -0
  48. package/skills/dforge-mcp-author/references/security.md +244 -0
  49. package/skills/dforge-mcp-author/references/settings.md +120 -0
  50. package/skills/dforge-mcp-author/references/traits.md +153 -0
  51. package/skills/dforge-mcp-author/references/translations.md +158 -0
  52. package/skills/dforge-mcp-author/references/validation-checklist.md +183 -0
  53. package/skills/dforge-mcp-author/scripts/xlsx_to_model.py +198 -0
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env python3
2
+ """Extract sheets/tables from an .xlsx into a JSON "model" for dForge import.
3
+
4
+ Pure standard library (zipfile + xml.etree) — NO `pip install` needed, runs on
5
+ any Python 3. An .xlsx is a zip of XML; this reads each worksheet grid (a capped
6
+ sample of rows) and resolves cell strings from the shared-string table, then
7
+ prints, per sheet, the header row + a sample of data rows. The AI then turns that
8
+ model into a dForge table-spec and calls `dforge_module_import`.
9
+
10
+ Memory-bounded by design: it streams each worksheet (clearing elements) and only
11
+ materialises the FIRST `max_data_rows` rows; it then loads ONLY the shared
12
+ strings those sampled cells actually reference — never the whole string table —
13
+ so there is no cap that could silently drop values.
14
+
15
+ Usage: python3 xlsx_to_model.py <file.xlsx> [max_data_rows]
16
+ Output: JSON on stdout — {"sheets":[{"name","headers":[...],"rows":[[...],...]}]}
17
+ (on failure: {"error": "..."} with a non-zero exit)
18
+
19
+ Caveat: date cells are stored as serial numbers in xlsx; they come through as
20
+ numbers here. Recognise date columns by header name when building the spec.
21
+ """
22
+ import sys
23
+ import json
24
+ import re
25
+ import zipfile
26
+ import xml.etree.ElementTree as ET
27
+
28
+ REL_NS = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id"
29
+
30
+
31
+ def localname(tag):
32
+ return tag.rsplit("}", 1)[-1]
33
+
34
+
35
+ def col_index(ref):
36
+ """'B7' -> 1 (0-based column index)."""
37
+ m = re.match(r"([A-Z]+)", ref or "")
38
+ if not m:
39
+ return 0
40
+ n = 0
41
+ for ch in m.group(1):
42
+ n = n * 26 + (ord(ch) - 64)
43
+ return n - 1
44
+
45
+
46
+ def sheet_paths(z):
47
+ wb = ET.fromstring(z.read("xl/workbook.xml"))
48
+ rels = ET.fromstring(z.read("xl/_rels/workbook.xml.rels"))
49
+ rid_target = {r.get("Id"): r.get("Target") for r in rels}
50
+ sheets = []
51
+ for sh in wb.iter():
52
+ if localname(sh.tag) != "sheet":
53
+ continue
54
+ rid = sh.get(REL_NS) or sh.get("id")
55
+ target = (rid_target.get(rid) or "").lstrip("/")
56
+ if target and not target.startswith("xl/"):
57
+ target = "xl/" + target
58
+ sheets.append((sh.get("name"), target))
59
+ return sheets
60
+
61
+
62
+ def raw_cell(c):
63
+ """Raw cell payload: the <v> text, or the concatenated <is> inline string."""
64
+ for ch in c:
65
+ name = localname(ch.tag)
66
+ if name == "v":
67
+ return ch.text
68
+ if name == "is":
69
+ return "".join(x.text or "" for x in ch.iter() if localname(x.tag) == "t")
70
+ return None
71
+
72
+
73
+ def parse_sheet_raw(z, path, max_rows):
74
+ """Stream a worksheet into raw rows of (cell_type, raw_value) tuples, capped
75
+ at max_rows+1 non-empty rows. Strings are NOT resolved yet (pass 2 does that)."""
76
+ rows = []
77
+ cells, maxc = {}, -1
78
+ with z.open(path) as f:
79
+ for event, el in ET.iterparse(f, events=("end",)):
80
+ name = localname(el.tag)
81
+ if name == "c":
82
+ i = col_index(el.get("r"))
83
+ cells[i] = (el.get("t"), raw_cell(el))
84
+ if i > maxc:
85
+ maxc = i
86
+ el.clear()
87
+ elif name == "row":
88
+ row = [cells.get(i) for i in range(maxc + 1)]
89
+ # A row counts only if a cell carries an actual VALUE — styled /
90
+ # bordered but empty cells still emit a <c> node (no <v>), and
91
+ # must not consume a sample slot before the real data is reached.
92
+ if any(c is not None and c[1] not in (None, "") for c in row):
93
+ rows.append(row)
94
+ cells, maxc = {}, -1
95
+ el.clear()
96
+ if len(rows) >= max_rows + 1:
97
+ break
98
+ return rows
99
+
100
+
101
+ def needed_string_indices(sheets_raw):
102
+ need = set()
103
+ for rows in sheets_raw:
104
+ for row in rows:
105
+ for cell in row:
106
+ if cell and cell[0] == "s" and cell[1] is not None:
107
+ try:
108
+ need.add(int(cell[1]))
109
+ except ValueError:
110
+ pass
111
+ return need
112
+
113
+
114
+ def load_shared_subset(z, needed):
115
+ """Stream the shared-string table, keeping ONLY the indices we need (bounded
116
+ by the sampled cells — never the whole table). Stops once the highest needed
117
+ index is read."""
118
+ result = {}
119
+ if not needed:
120
+ return result
121
+ max_needed = max(needed)
122
+ try:
123
+ handle = z.open("xl/sharedStrings.xml")
124
+ except KeyError:
125
+ return result
126
+ idx = -1
127
+ parts = []
128
+ with handle as f:
129
+ for event, el in ET.iterparse(f, events=("start", "end")):
130
+ name = localname(el.tag)
131
+ if event == "start":
132
+ if name == "si":
133
+ idx += 1
134
+ parts = []
135
+ elif name == "t":
136
+ parts.append(el.text or "")
137
+ elif name == "si":
138
+ if idx in needed:
139
+ result[idx] = "".join(parts)
140
+ el.clear()
141
+ if idx >= max_needed:
142
+ break
143
+ return result
144
+
145
+
146
+ def resolve(cell, shared):
147
+ if cell is None:
148
+ return None
149
+ t, raw = cell
150
+ if raw is None:
151
+ return None
152
+ if t == "s":
153
+ try:
154
+ return shared.get(int(raw))
155
+ except ValueError:
156
+ return raw
157
+ if t in ("inlineStr", "str"):
158
+ return raw
159
+ try:
160
+ f = float(raw)
161
+ return int(f) if f.is_integer() else f
162
+ except ValueError:
163
+ return raw
164
+
165
+
166
+ def main():
167
+ if len(sys.argv) < 2:
168
+ print(json.dumps({"error": "usage: xlsx_to_model.py <file.xlsx> [max_data_rows]"}))
169
+ sys.exit(2)
170
+ max_rows = int(sys.argv[2]) if len(sys.argv) > 2 else 15
171
+ try:
172
+ with zipfile.ZipFile(sys.argv[1]) as z:
173
+ raw = []
174
+ for name, path in sheet_paths(z):
175
+ if not path:
176
+ raw.append((name, []))
177
+ continue
178
+ try:
179
+ raw.append((name, parse_sheet_raw(z, path, max_rows)))
180
+ except KeyError:
181
+ raw.append((name, []))
182
+ shared = load_shared_subset(z, needed_string_indices([r for _, r in raw]))
183
+ sheets = []
184
+ for name, raw_rows in raw:
185
+ rows = [[resolve(c, shared) for c in row] for row in raw_rows]
186
+ rows = [r for r in rows if any(v is not None and v != "" for v in r)]
187
+ if not rows:
188
+ continue
189
+ headers = [str(h) if h not in (None, "") else "col%d" % (i + 1) for i, h in enumerate(rows[0])]
190
+ sheets.append({"name": name, "headers": headers, "rows": rows[1:max_rows + 1]})
191
+ print(json.dumps({"sheets": sheets}, default=str))
192
+ except Exception as exc: # noqa: BLE001 — report any failure as JSON
193
+ print(json.dumps({"error": "%s: %s" % (type(exc).__name__, exc)}))
194
+ sys.exit(1)
195
+
196
+
197
+ if __name__ == "__main__":
198
+ main()