@dforge-core/dforge-mcp 0.2.28 → 0.2.29
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @dforge-core/dforge-mcp
|
|
2
2
|
|
|
3
|
-
MCP server for dForge module authoring. Exposes
|
|
3
|
+
MCP server for dForge module authoring. Exposes 35 composable tools and the canonical schemas so AI agents (Claude Code, Cursor, Zed, etc.) can drive the full module lifecycle — scaffold → entities → actions → views → security → install — through structured tool calls instead of free-form JSON generation.
|
|
4
4
|
|
|
5
5
|
Ships with **four Claude Skills** (`skills/`): a router plus one skill per lifecycle stage — design (Phase 0), build (Phases 1-5), and ship (Phase 6). The router directory carries 25 reference files (field types, flags, traits, formulas, DSL, security, etc.) and two annotated example modules.
|
|
6
6
|
|
|
@@ -74,7 +74,7 @@ claude mcp list
|
|
|
74
74
|
# Should show: dforge — npx -y @dforge-core/dforge-mcp — connected
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
Or inside a Claude Code session, type `/mcp` to see all connected servers + their tools. The
|
|
77
|
+
Or inside a Claude Code session, type `/mcp` to see all connected servers + their tools. The 35 `dforge_*` tools should be listed.
|
|
78
78
|
|
|
79
79
|
### Cursor / Zed
|
|
80
80
|
|
|
@@ -157,6 +157,7 @@ The field/entity tools validate against the `@dforge-core/metadata` registry: an
|
|
|
157
157
|
| Tool | Behavior |
|
|
158
158
|
|---|---|
|
|
159
159
|
| `dforge_dependency_add` | Add a dep on another dForge module |
|
|
160
|
+
| `dforge_diagram_add` | Create or extend a named entity diagram, `docs/diagrams/<code>.json` (design-time only) — one per functional area of a large module. Takes entity keys and/or `fromFolder` (a folder's entities plus its descendants'); appends only, keeping placed positions. Rejects a key that resolves to no own or dependency entity |
|
|
160
161
|
| `dforge_dbml_import` | **DBML front-end** to `module_import` — parses DBML (Table blocks, typed columns, inline + top-level refs) into the table-spec, drops the source PK (identity provides `{entity}_id`), and imports. Pass `module` for a greenfield import |
|
|
161
162
|
|
|
162
163
|
### Writing files: preview vs `apply`
|
|
@@ -179,7 +180,7 @@ Served over MCP with descriptions and mime types, so the agent can tell what a U
|
|
|
179
180
|
|
|
180
181
|
| URI pattern | Content |
|
|
181
182
|
|---|---|
|
|
182
|
-
| `dforge://schema/<name>` | JSON Schemas: `manifest`, `entity`, `domains`, `data-views`, `folders`, `menus`, `roles`, `jobs`, `seed-data`, `traits`, `webhooks`, `triggers`, `print-templates`, `settings`, `reports` |
|
|
183
|
+
| `dforge://schema/<name>` | JSON Schemas: `manifest`, `entity`, `domains`, `data-views`, `folders`, `diagram`, `menus`, `roles`, `jobs`, `seed-data`, `traits`, `webhooks`, `triggers`, `print-templates`, `settings`, `reports` |
|
|
183
184
|
| `dforge://reference/<name>` | 25 per-element authoring references — schema shape + worked example + common-mistakes list for one element type |
|
|
184
185
|
| `dforge://example/<path>` | Files from the canonical `simple-todo` module (mandatory structure validators) |
|
|
185
186
|
| `dforge://example/matrix-budget/<path>` | Files from the `matrix` (pivot) view example |
|
package/dist/server.js
CHANGED
|
@@ -31283,7 +31283,7 @@ function buildZedTasks() {
|
|
|
31283
31283
|
var fs = __toESM(require("fs"));
|
|
31284
31284
|
var path = __toESM(require("path"));
|
|
31285
31285
|
|
|
31286
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
31286
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.30/node_modules/@dforge-core/metadata/dist/index.js
|
|
31287
31287
|
var AggType = {
|
|
31288
31288
|
Sum: "sum",
|
|
31289
31289
|
Count: "count",
|
|
@@ -31296,9 +31296,9 @@ var AGG_TYPE_LIST = Object.values(AggType);
|
|
|
31296
31296
|
var fieldTypes = [
|
|
31297
31297
|
{ cd: "text", name: "Text", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31298
31298
|
{ cd: "avatar", name: "Avatar Name", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31299
|
-
{ cd: "email", name: "Email", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31300
|
-
{ cd: "phone", name: "Phone", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31301
|
-
{ cd: "url", name: "URL", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31299
|
+
{ cd: "email", name: "Email", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true, defParams: { pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$" } },
|
|
31300
|
+
{ cd: "phone", name: "Phone", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true, defParams: { pattern: "^\\+?[0-9\\s\\-().]+$" } },
|
|
31301
|
+
{ cd: "url", name: "URL", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true, defParams: { pattern: "^https?://.+", patternFlags: "i" } },
|
|
31302
31302
|
{ cd: "textarea", name: "Multiline Text", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31303
31303
|
{ cd: "number", name: "Number", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: null, useStringInFilter: false },
|
|
31304
31304
|
{ cd: "currency", name: "Currency", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: 2, useStringInFilter: false },
|
|
@@ -31538,6 +31538,7 @@ function modulePaths(moduleDir) {
|
|
|
31538
31538
|
translationsDir: path.join(root, "translations"),
|
|
31539
31539
|
dataViews: path.join(root, "ui", "data_views.json"),
|
|
31540
31540
|
folders: path.join(root, "ui", "folders.json"),
|
|
31541
|
+
diagramsDir: path.join(root, "docs", "diagrams"),
|
|
31541
31542
|
menus: path.join(root, "ui", "menus.json"),
|
|
31542
31543
|
actions: path.join(root, "ui", "actions.json"),
|
|
31543
31544
|
reports: path.join(root, "ui", "reports.json"),
|
|
@@ -31569,6 +31570,52 @@ function readJsonOrDefault(absPath, dflt) {
|
|
|
31569
31570
|
throw new Error(`${absPath}: ${e.message}`);
|
|
31570
31571
|
}
|
|
31571
31572
|
}
|
|
31573
|
+
function diagramFiles(diagramsDir) {
|
|
31574
|
+
if (!fs.existsSync(diagramsDir)) return [];
|
|
31575
|
+
return fs.readdirSync(diagramsDir).filter((f) => f.endsWith(".json")).sort().map((f) => path.join(diagramsDir, f));
|
|
31576
|
+
}
|
|
31577
|
+
function isPlannedDiagramKey(key, entityMap) {
|
|
31578
|
+
return !key.includes(".") && !Object.prototype.hasOwnProperty.call(entityMap, key);
|
|
31579
|
+
}
|
|
31580
|
+
function likelyMisspelledEntity(key, entityMap) {
|
|
31581
|
+
let best;
|
|
31582
|
+
for (const code2 of Object.keys(entityMap)) {
|
|
31583
|
+
if (code2.includes(".")) continue;
|
|
31584
|
+
const d = editDistance(key, code2);
|
|
31585
|
+
if (d > 2 || d >= Math.min(key.length, code2.length) / 3) continue;
|
|
31586
|
+
if (!best || d < best.d) best = { code: code2, d };
|
|
31587
|
+
}
|
|
31588
|
+
return best?.code;
|
|
31589
|
+
}
|
|
31590
|
+
function editDistance(a, b) {
|
|
31591
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
31592
|
+
for (let i = 1; i <= a.length; i++) {
|
|
31593
|
+
const cur = [i];
|
|
31594
|
+
for (let j = 1; j <= b.length; j++) {
|
|
31595
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
31596
|
+
}
|
|
31597
|
+
prev = cur;
|
|
31598
|
+
}
|
|
31599
|
+
return prev[b.length];
|
|
31600
|
+
}
|
|
31601
|
+
function diagramKeyProblem(key, entityMap, deps, root) {
|
|
31602
|
+
if (Object.prototype.hasOwnProperty.call(entityMap, key)) return void 0;
|
|
31603
|
+
const dep = [...deps].filter((d) => key.startsWith(`${d}.`) && key.length > d.length + 1).sort((a, b) => b.length - a.length)[0];
|
|
31604
|
+
if (!dep) {
|
|
31605
|
+
return key.includes(".") ? `'${key}' is not an entity of this module, and no declared dependency matches its module prefix. Declare the dependency, or drop the entity from the diagram.` : `'${key}' is not an entity in manifest.entities. Drop it from the diagram, or fix the code.`;
|
|
31606
|
+
}
|
|
31607
|
+
const contractPath = path.join(root, "deps", `${dep}.json`);
|
|
31608
|
+
if (!fs.existsSync(contractPath)) return void 0;
|
|
31609
|
+
let consumed;
|
|
31610
|
+
try {
|
|
31611
|
+
consumed = JSON.parse(fs.readFileSync(contractPath, "utf8"))?.entities;
|
|
31612
|
+
} catch {
|
|
31613
|
+
return void 0;
|
|
31614
|
+
}
|
|
31615
|
+
if (!consumed || typeof consumed !== "object") return void 0;
|
|
31616
|
+
const entity = key.slice(dep.length + 1);
|
|
31617
|
+
return Object.prototype.hasOwnProperty.call(consumed, entity) ? void 0 : `'${entity}' is not among the entities deps/${dep}.json consumes from '${dep}'. Add it to the contract if the module uses it, or drop it from the diagram.`;
|
|
31618
|
+
}
|
|
31572
31619
|
function jsonText(obj) {
|
|
31573
31620
|
return JSON.stringify(obj, null, " ") + "\n";
|
|
31574
31621
|
}
|
|
@@ -34207,6 +34254,20 @@ function entityRename(args) {
|
|
|
34207
34254
|
const entityMap = manifest.entities ?? {};
|
|
34208
34255
|
if (!(oldE in entityMap)) throw new Error(`Entity '${oldE}' is not in the manifest.`);
|
|
34209
34256
|
if (newE in entityMap) throw new Error(`Entity '${newE}' already exists in this module.`);
|
|
34257
|
+
const collisions = diagramFiles(paths.diagramsDir).filter((fp) => {
|
|
34258
|
+
let d;
|
|
34259
|
+
try {
|
|
34260
|
+
d = JSON.parse(fs6.readFileSync(fp, "utf8"));
|
|
34261
|
+
} catch {
|
|
34262
|
+
return false;
|
|
34263
|
+
}
|
|
34264
|
+
return !!d && typeof d === "object" && diagramMentions(d, oldE) && diagramMentions(d, newE);
|
|
34265
|
+
}).map((fp) => `docs/diagrams/${path8.basename(fp)}`);
|
|
34266
|
+
if (collisions.length > 0) {
|
|
34267
|
+
throw new Error(
|
|
34268
|
+
`${collisions.join(", ")} already draw${collisions.length === 1 ? "s" : ""} a '${newE}' alongside '${oldE}' \u2014 renaming would merge the two nodes. Remove or rename '${newE}' in the diagram first.`
|
|
34269
|
+
);
|
|
34270
|
+
}
|
|
34210
34271
|
const files = {};
|
|
34211
34272
|
const deletes = [];
|
|
34212
34273
|
const changes = [];
|
|
@@ -34312,6 +34373,36 @@ function entityRename(args) {
|
|
|
34312
34373
|
files[rel(paths.root, paths.folders)] = jsonText(folders);
|
|
34313
34374
|
changes.push("folders.json (entity bindings)");
|
|
34314
34375
|
}
|
|
34376
|
+
for (const fp of diagramFiles(paths.diagramsDir)) {
|
|
34377
|
+
let d;
|
|
34378
|
+
try {
|
|
34379
|
+
d = JSON.parse(fs6.readFileSync(fp, "utf8"));
|
|
34380
|
+
} catch {
|
|
34381
|
+
continue;
|
|
34382
|
+
}
|
|
34383
|
+
if (!d || typeof d !== "object") continue;
|
|
34384
|
+
const what = [];
|
|
34385
|
+
const ents = d.entities;
|
|
34386
|
+
if (ents && typeof ents === "object" && oldE in ents) {
|
|
34387
|
+
d.entities = renameKey(ents, oldE, newE);
|
|
34388
|
+
what.push("entity key");
|
|
34389
|
+
}
|
|
34390
|
+
let relTouched = false;
|
|
34391
|
+
for (const r of diagramRelations(d)) {
|
|
34392
|
+
if (r.from === oldE) {
|
|
34393
|
+
r.from = newE;
|
|
34394
|
+
relTouched = true;
|
|
34395
|
+
}
|
|
34396
|
+
if (r.to === oldE) {
|
|
34397
|
+
r.to = newE;
|
|
34398
|
+
relTouched = true;
|
|
34399
|
+
}
|
|
34400
|
+
}
|
|
34401
|
+
if (relTouched) what.push("relations");
|
|
34402
|
+
if (what.length === 0) continue;
|
|
34403
|
+
files[rel(paths.root, fp)] = jsonText(d);
|
|
34404
|
+
changes.push(`docs/diagrams/${path8.basename(fp)} (${what.join(", ")})`);
|
|
34405
|
+
}
|
|
34315
34406
|
if (fs6.existsSync(paths.seedDataDir)) {
|
|
34316
34407
|
for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
|
|
34317
34408
|
const fp = path8.join(paths.seedDataDir, file2);
|
|
@@ -34338,6 +34429,17 @@ function entityRename(args) {
|
|
|
34338
34429
|
deletes
|
|
34339
34430
|
);
|
|
34340
34431
|
}
|
|
34432
|
+
function diagramRelations(d) {
|
|
34433
|
+
if (!Array.isArray(d.relations)) return [];
|
|
34434
|
+
return d.relations.filter(
|
|
34435
|
+
(r) => !!r && typeof r === "object" && !Array.isArray(r)
|
|
34436
|
+
);
|
|
34437
|
+
}
|
|
34438
|
+
function diagramMentions(d, key) {
|
|
34439
|
+
const ents = d.entities;
|
|
34440
|
+
if (ents && typeof ents === "object" && Object.prototype.hasOwnProperty.call(ents, key)) return true;
|
|
34441
|
+
return diagramRelations(d).some((r) => r.from === key || r.to === key);
|
|
34442
|
+
}
|
|
34341
34443
|
var entityDeleteSchema = {
|
|
34342
34444
|
moduleDir: external_exports.string().describe("Path to the module root."),
|
|
34343
34445
|
entityName: code.describe("Entity code to delete.")
|
|
@@ -34405,6 +34507,29 @@ function entityDelete(args) {
|
|
|
34405
34507
|
files[rel(paths.root, paths.folders)] = jsonText(folders);
|
|
34406
34508
|
changes.push("folders.json (binding)");
|
|
34407
34509
|
}
|
|
34510
|
+
for (const fp of diagramFiles(paths.diagramsDir)) {
|
|
34511
|
+
let d;
|
|
34512
|
+
try {
|
|
34513
|
+
d = JSON.parse(fs6.readFileSync(fp, "utf8"));
|
|
34514
|
+
} catch {
|
|
34515
|
+
continue;
|
|
34516
|
+
}
|
|
34517
|
+
if (!d || typeof d !== "object") continue;
|
|
34518
|
+
const what = [];
|
|
34519
|
+
const ents = d.entities;
|
|
34520
|
+
if (ents && typeof ents === "object" && target in ents) {
|
|
34521
|
+
delete ents[target];
|
|
34522
|
+
what.push("entity key");
|
|
34523
|
+
}
|
|
34524
|
+
const doomed = new Set(diagramRelations(d).filter((r) => r.from === target || r.to === target));
|
|
34525
|
+
if (doomed.size > 0) {
|
|
34526
|
+
d.relations = d.relations.filter((r) => !doomed.has(r));
|
|
34527
|
+
what.push("relations");
|
|
34528
|
+
}
|
|
34529
|
+
if (what.length === 0) continue;
|
|
34530
|
+
files[rel(paths.root, fp)] = jsonText(d);
|
|
34531
|
+
changes.push(`docs/diagrams/${path8.basename(fp)} (${what.join(", ")})`);
|
|
34532
|
+
}
|
|
34408
34533
|
const views = readJsonOrDefault(paths.dataViews, {});
|
|
34409
34534
|
let viewsTouched = false;
|
|
34410
34535
|
const removedViews = [];
|
|
@@ -34465,7 +34590,7 @@ function entityDelete(args) {
|
|
|
34465
34590
|
// src/tools/action-add.ts
|
|
34466
34591
|
var path9 = __toESM(require("path"));
|
|
34467
34592
|
|
|
34468
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
34593
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.30/node_modules/@dforge-core/metadata/dist/dsl/index.js
|
|
34469
34594
|
var PUNCT3 = ["===", "!=="];
|
|
34470
34595
|
var PUNCT2 = [
|
|
34471
34596
|
"==",
|
|
@@ -36739,6 +36864,81 @@ function folderAdd(args) {
|
|
|
36739
36864
|
}
|
|
36740
36865
|
);
|
|
36741
36866
|
}
|
|
36867
|
+
var diagramAddSchema = {
|
|
36868
|
+
moduleDir: external_exports.string(),
|
|
36869
|
+
code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Diagram code \u2014 becomes the file name docs/diagrams/<code>.json."),
|
|
36870
|
+
label: external_exports.string().optional().describe("Display name. Defaults to the fromFolder's label, else the code."),
|
|
36871
|
+
description: external_exports.string().optional().describe("What part of the model the diagram covers."),
|
|
36872
|
+
entities: external_exports.array(external_exports.string().min(1)).default([]).describe(
|
|
36873
|
+
"Entity keys to draw: the manifest's own entity code ('invoice'), or 'module.entity' for an entity of a declared dependency ('parties.party') \u2014 the same form a field link's `entity` takes."
|
|
36874
|
+
),
|
|
36875
|
+
fromFolder: external_exports.string().optional().describe(
|
|
36876
|
+
"A folder code from ui/folders.json (anywhere in the tree). Adds the entities bound to that folder AND to all its descendants."
|
|
36877
|
+
),
|
|
36878
|
+
showBoundary: external_exports.boolean().optional().describe(
|
|
36879
|
+
"Also draw the entities outside the diagram that its entities reference or are referenced by, as header-only stubs. Default true."
|
|
36880
|
+
)
|
|
36881
|
+
};
|
|
36882
|
+
function diagramAdd(args) {
|
|
36883
|
+
if (!/^[a-z][a-z0-9_]*$/.test(args.code)) {
|
|
36884
|
+
throw new Error(`Diagram code '${args.code}' must be snake_case (^[a-z][a-z0-9_]*$) \u2014 it becomes the file name.`);
|
|
36885
|
+
}
|
|
36886
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
36887
|
+
const entityMap = manifest.entities ?? {};
|
|
36888
|
+
const deps = new Set(Object.keys(manifest.dependencies ?? {}));
|
|
36889
|
+
const keys = [...args.entities];
|
|
36890
|
+
let folderLabel;
|
|
36891
|
+
if (args.fromFolder !== void 0) {
|
|
36892
|
+
const root = readJsonOrDefault(paths.folders, {});
|
|
36893
|
+
const folder = walkFolders(root).find((f) => f.code === args.fromFolder);
|
|
36894
|
+
if (!folder) {
|
|
36895
|
+
throw new Error(
|
|
36896
|
+
`Folder '${args.fromFolder}' is not in ui/folders.json. Pass a sub-folder's code (codes are unique across the tree); for every entity in the module, list them in 'entities' instead.`
|
|
36897
|
+
);
|
|
36898
|
+
}
|
|
36899
|
+
if (typeof folder.node.label === "string") folderLabel = folder.node.label;
|
|
36900
|
+
for (const node of [folder.node, ...walkFolders(folder.node).map((f) => f.node)]) {
|
|
36901
|
+
keys.push(...Object.keys(node.entities ?? {}));
|
|
36902
|
+
}
|
|
36903
|
+
}
|
|
36904
|
+
const wanted = [...new Set(keys)];
|
|
36905
|
+
const problems = wanted.map((k) => diagramKeyProblem(k, entityMap, deps, paths.root)).filter((p) => p !== void 0);
|
|
36906
|
+
if (problems.length > 0) {
|
|
36907
|
+
throw new Error(`Nothing written \u2014 ${problems.join(" ")}`);
|
|
36908
|
+
}
|
|
36909
|
+
const file2 = path13.join(paths.diagramsDir, `${args.code}.json`);
|
|
36910
|
+
const exists = fs10.existsSync(file2);
|
|
36911
|
+
let diagram;
|
|
36912
|
+
if (exists) {
|
|
36913
|
+
const existing = readJson(file2);
|
|
36914
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing) || !existing.entities || typeof existing.entities !== "object" || Array.isArray(existing.entities)) {
|
|
36915
|
+
throw new Error(
|
|
36916
|
+
`${rel(paths.root, file2)} is not a diagram (an object with an 'entities' map) \u2014 fix or delete it first.`
|
|
36917
|
+
);
|
|
36918
|
+
}
|
|
36919
|
+
diagram = existing;
|
|
36920
|
+
if (args.label !== void 0) diagram.label = args.label;
|
|
36921
|
+
if (args.description !== void 0) diagram.description = args.description;
|
|
36922
|
+
if (args.showBoundary !== void 0) diagram.showBoundary = args.showBoundary;
|
|
36923
|
+
} else {
|
|
36924
|
+
diagram = { label: args.label ?? folderLabel ?? args.code };
|
|
36925
|
+
if (args.description !== void 0) diagram.description = args.description;
|
|
36926
|
+
diagram.entities = {};
|
|
36927
|
+
if (args.showBoundary !== void 0) diagram.showBoundary = args.showBoundary;
|
|
36928
|
+
}
|
|
36929
|
+
const entities = diagram.entities;
|
|
36930
|
+
const added = wanted.filter((k) => !Object.prototype.hasOwnProperty.call(entities, k));
|
|
36931
|
+
for (const k of added) entities[k] = {};
|
|
36932
|
+
const relPath = rel(paths.root, file2);
|
|
36933
|
+
const present = wanted.length - added.length;
|
|
36934
|
+
return makeResult(
|
|
36935
|
+
exists ? `Updated diagram '${args.code}' (${relPath}): added ${added.length} entit${added.length === 1 ? "y" : "ies"}${present ? `, ${present} already drawn` : ""}.` : `Created diagram '${args.code}' (${relPath}) with ${added.length} entit${added.length === 1 ? "y" : "ies"}.`,
|
|
36936
|
+
{
|
|
36937
|
+
[relPath]: jsonText(diagram),
|
|
36938
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
36939
|
+
}
|
|
36940
|
+
);
|
|
36941
|
+
}
|
|
36742
36942
|
var SYSTEM_MODULES = /* @__PURE__ */ new Set(["admin", "metadata", "workspace"]);
|
|
36743
36943
|
var dependencyAddSchema = {
|
|
36744
36944
|
moduleDir: external_exports.string(),
|
|
@@ -36900,6 +37100,18 @@ function moduleInspect(args) {
|
|
|
36900
37100
|
}));
|
|
36901
37101
|
const foldersTree = readJsonOrDefault(paths.folders, {});
|
|
36902
37102
|
const folderDepth = computeDepth(foldersTree);
|
|
37103
|
+
const diagramSummaries = diagramFiles(paths.diagramsDir).map((fp) => {
|
|
37104
|
+
let d = {};
|
|
37105
|
+
try {
|
|
37106
|
+
d = JSON.parse(fs11.readFileSync(fp, "utf8"));
|
|
37107
|
+
} catch {
|
|
37108
|
+
}
|
|
37109
|
+
return {
|
|
37110
|
+
code: path14.basename(fp, ".json"),
|
|
37111
|
+
label: typeof d?.label === "string" ? d.label : void 0,
|
|
37112
|
+
entities: Object.keys(d?.entities ?? {})
|
|
37113
|
+
};
|
|
37114
|
+
});
|
|
36903
37115
|
const menus = readJsonOrDefault(paths.menus, {});
|
|
36904
37116
|
const menuSummaries = Object.entries(menus).map(([code2, m]) => ({
|
|
36905
37117
|
code: code2,
|
|
@@ -36967,6 +37179,7 @@ function moduleInspect(args) {
|
|
|
36967
37179
|
entities: entitySummaries,
|
|
36968
37180
|
views: viewSummaries,
|
|
36969
37181
|
folders: { tree: foldersTree, depth: folderDepth },
|
|
37182
|
+
diagrams: diagramSummaries,
|
|
36970
37183
|
menus: menuSummaries,
|
|
36971
37184
|
roles: roleSummaries,
|
|
36972
37185
|
actions: actionSummaries,
|
|
@@ -37549,6 +37762,47 @@ function moduleValidate(args, cli) {
|
|
|
37549
37762
|
}
|
|
37550
37763
|
}
|
|
37551
37764
|
}
|
|
37765
|
+
for (const fp of diagramFiles(paths.diagramsDir)) {
|
|
37766
|
+
const where = `docs/diagrams/${path15.basename(fp)}`;
|
|
37767
|
+
let diagram;
|
|
37768
|
+
try {
|
|
37769
|
+
diagram = JSON.parse(fs12.readFileSync(fp, "utf8"));
|
|
37770
|
+
} catch (ex) {
|
|
37771
|
+
err(where, `invalid JSON: ${ex.message}`);
|
|
37772
|
+
continue;
|
|
37773
|
+
}
|
|
37774
|
+
if (!diagram || typeof diagram !== "object" || Array.isArray(diagram)) {
|
|
37775
|
+
warn(where, "must be an object \u2014 the diagram itself, with an 'entities' map.");
|
|
37776
|
+
continue;
|
|
37777
|
+
}
|
|
37778
|
+
const diagramEntities = diagram.entities;
|
|
37779
|
+
if (!diagramEntities || typeof diagramEntities !== "object" || Array.isArray(diagramEntities)) {
|
|
37780
|
+
warn(where, "has no 'entities' object \u2014 list the entities the diagram draws, keyed by entity code.");
|
|
37781
|
+
continue;
|
|
37782
|
+
}
|
|
37783
|
+
for (const [key, placement] of Object.entries(diagramEntities)) {
|
|
37784
|
+
const at = `${where} \u2192 entities.${key}`;
|
|
37785
|
+
if (isPlannedDiagramKey(key, entityMap)) {
|
|
37786
|
+
const meant = likelyMisspelledEntity(key, entityMap);
|
|
37787
|
+
if (meant) {
|
|
37788
|
+
warn(
|
|
37789
|
+
at,
|
|
37790
|
+
`'${key}' is not built yet, so the diagram draws it as a planned entity. If you meant the existing '${meant}', fix the key.`
|
|
37791
|
+
);
|
|
37792
|
+
}
|
|
37793
|
+
} else {
|
|
37794
|
+
const unresolved = diagramKeyProblem(key, entityMap, deps, paths.root);
|
|
37795
|
+
if (unresolved) warn(at, unresolved);
|
|
37796
|
+
}
|
|
37797
|
+
const p = placement && typeof placement === "object" ? placement : {};
|
|
37798
|
+
if ("x" in p !== "y" in p) {
|
|
37799
|
+
warn(
|
|
37800
|
+
at,
|
|
37801
|
+
`sets '${"x" in p ? "x" : "y"}' without '${"x" in p ? "y" : "x"}'. Give both to pin the entity, or neither to let the diagram place it.`
|
|
37802
|
+
);
|
|
37803
|
+
}
|
|
37804
|
+
}
|
|
37805
|
+
}
|
|
37552
37806
|
for (const [rcode, rawReport] of Object.entries(reports)) {
|
|
37553
37807
|
const report = rawReport ?? {};
|
|
37554
37808
|
const where = `ui/reports.json \u2192 ${rcode}`;
|
|
@@ -38022,6 +38276,11 @@ var resources = [
|
|
|
38022
38276
|
"Folders JSON schema",
|
|
38023
38277
|
"JSON Schema for ui/folders.json \u2014 root folder tree with per-entity view bindings."
|
|
38024
38278
|
),
|
|
38279
|
+
schema(
|
|
38280
|
+
"diagram",
|
|
38281
|
+
"Diagram JSON schema",
|
|
38282
|
+
"JSON Schema for docs/diagrams/<code>.json \u2014 one entity diagram per file, the file name is its code (a subset of entities + optional x/y placement each). Design-time only: install ignores it."
|
|
38283
|
+
),
|
|
38025
38284
|
schema(
|
|
38026
38285
|
"menus",
|
|
38027
38286
|
"Menus JSON schema",
|
|
@@ -38309,7 +38568,7 @@ server.registerTool(
|
|
|
38309
38568
|
"dforge_entity_rename",
|
|
38310
38569
|
{
|
|
38311
38570
|
title: "Rename an entity (refactor-safe)",
|
|
38312
|
-
description: "Refactor-safe rename of an entity code. Moves the entity file (old is listed in the response's `deletes` \u2014 delete it), renames the manifest key, cascades the identity PK {old}_id \u2192 {new}_id wherever an FK targets it, and repoints every reference: other entities' link.entity / references.to, view entityCode, role rights keys, action entity, folder bindings, and seed-data entityCode + PK keys. Reports/translations/menu labels/DSL are NOT rewritten (warned). Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
38571
|
+
description: "Refactor-safe rename of an entity code. Moves the entity file (old is listed in the response's `deletes` \u2014 delete it), renames the manifest key, cascades the identity PK {old}_id \u2192 {new}_id wherever an FK targets it, and repoints every reference: other entities' link.entity / references.to, view entityCode, role rights keys, action entity, folder bindings, diagram entity keys (docs/diagrams/*.json), and seed-data entityCode + PK keys. Reports/translations/menu labels/DSL are NOT rewritten (warned). Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
38313
38572
|
inputSchema: { ...entityRenameSchema, ...applyInput },
|
|
38314
38573
|
annotations: DESTRUCTIVE
|
|
38315
38574
|
},
|
|
@@ -38319,7 +38578,7 @@ server.registerTool(
|
|
|
38319
38578
|
"dforge_entity_delete",
|
|
38320
38579
|
{
|
|
38321
38580
|
title: "Delete an entity (refactor-safe)",
|
|
38322
|
-
description: "Refactor-safe deletion of an entity. Removes the entity file + its seed files (listed in `deletes`), drops the manifest entry, role rights key, folder binding, and data-view sources (deleting a view left with no source). Cross-entity FKs targeting it, actions on it, and menus pointing at removed views are surfaced as warnings \u2014 fix those by hand. Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
38581
|
+
description: "Refactor-safe deletion of an entity. Removes the entity file + its seed files (listed in `deletes`), drops the manifest entry, role rights key, folder binding, diagram entries (docs/diagrams/*.json), and data-view sources (deleting a view left with no source). Cross-entity FKs targeting it, actions on it, and menus pointing at removed views are surfaced as warnings \u2014 fix those by hand. Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
38323
38582
|
inputSchema: { ...entityDeleteSchema, ...applyInput },
|
|
38324
38583
|
annotations: DESTRUCTIVE
|
|
38325
38584
|
},
|
|
@@ -38534,6 +38793,15 @@ server.registerTool(
|
|
|
38534
38793
|
},
|
|
38535
38794
|
envelope(dependencyAdd)
|
|
38536
38795
|
);
|
|
38796
|
+
server.registerTool(
|
|
38797
|
+
"dforge_diagram_add",
|
|
38798
|
+
{
|
|
38799
|
+
title: "Add or extend an entity diagram",
|
|
38800
|
+
description: "Create or extend a named entity diagram, docs/diagrams/<code>.json \u2014 one file per diagram, drawing a chosen subset of entities. Design-time only (install ignores it). Use it on a LARGE module to give each functional area its own readable diagram (e.g. 'billing', 'inventory'); a small module needs none. Pass `entities` (own code, or 'module.entity' for a declared dependency) and/or `fromFolder` to take a folder's entities plus all its descendants'. On an existing diagram it only appends missing entities \u2014 positions already set are kept and nothing is removed. Leave layout to the editor: new entities get no x/y. An entity key that resolves to nothing is rejected and nothing is written.",
|
|
38801
|
+
inputSchema: { ...diagramAddSchema, ...applyInput }
|
|
38802
|
+
},
|
|
38803
|
+
envelope(diagramAdd)
|
|
38804
|
+
);
|
|
38537
38805
|
server.registerTool(
|
|
38538
38806
|
"dforge_dbml_import",
|
|
38539
38807
|
{
|
package/docs/creating-modules.md
CHANGED
|
@@ -160,5 +160,5 @@ VS Code with the [dForge extension](https://github.com/dforge-core/dforge-editor
|
|
|
160
160
|
- [dforge-module-design](../skills/dforge-module-design/SKILL.md) — Phase 0, including the intake guardrails and gap scans
|
|
161
161
|
- [dforge-module-build](../skills/dforge-module-build/SKILL.md) — Phases 1-5, the loading-policy table, core-rules cheat sheet, and the deterministic backtrack protocol
|
|
162
162
|
- [dforge-module-ship](../skills/dforge-module-ship/SKILL.md) — Phase 6 and the install-fix loop
|
|
163
|
-
- [README.md](../README.md) — full tool reference (
|
|
163
|
+
- [README.md](../README.md) — full tool reference (35 tools) + maintainer docs
|
|
164
164
|
- [iash44/dForge-core](https://github.com/iash44/dForge-core) — the platform itself: source of truth for the schemas + DSL conventions
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforge-core/dforge-mcp",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "MCP server for dForge module authoring. Exposes
|
|
3
|
+
"version": "0.2.29",
|
|
4
|
+
"description": "MCP server for dForge module authoring. Exposes 35 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/iash44/dForge-core",
|
|
7
7
|
"packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@dforge-core/dforge-cli": "^0.2.22",
|
|
39
|
-
"@dforge-core/metadata": "^0.0.
|
|
39
|
+
"@dforge-core/metadata": "^0.0.30",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
41
41
|
"zod": "^4.4.3"
|
|
42
42
|
},
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://dforge.dev/schemas/diagram.schema.json",
|
|
4
|
+
"title": "dForge Diagram",
|
|
5
|
+
"description": "One named entity diagram of a dForge module: docs/diagrams/<code>.json, the file name being the diagram's code. A diagram draws a chosen subset of entities, so a large module can be read one area at a time. Design-time only: the platform installs the module without reading it.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["entities"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string"
|
|
11
|
+
},
|
|
12
|
+
"label": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Diagram display name. Defaults to the diagram code."
|
|
15
|
+
},
|
|
16
|
+
"description": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "What part of the model the diagram covers."
|
|
19
|
+
},
|
|
20
|
+
"entities": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"description": "Entity key → placement. A key is the entity code as the module's manifest names it, or `module.entity` for an entity of a dependency — the same form a field link's `entity` takes. A key no entity has yet is a planned entity, drawn as a name only. An empty object places the entity automatically.",
|
|
23
|
+
"propertyNames": {
|
|
24
|
+
"minLength": 1
|
|
25
|
+
},
|
|
26
|
+
"additionalProperties": {
|
|
27
|
+
"$ref": "#/$defs/diagramEntity"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"showBoundary": {
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"description": "Also draw the entities outside the diagram that its entities reference or are referenced by, as header-only stubs, so relations leaving the diagram stay visible. Default: true."
|
|
33
|
+
},
|
|
34
|
+
"detail": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"enum": ["conceptual", "keys", "full"],
|
|
37
|
+
"description": "How much of each entity to draw: `conceptual` — the name only, relations drawn entity to entity; `keys` — primary and foreign key columns; `full` — every column. Default: full."
|
|
38
|
+
},
|
|
39
|
+
"relations": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"description": "Relations drawn by hand, for entities with no foreign key between them yet — a planned entity, or one not built that far. Where a real foreign key links the same two entities, the key is drawn and the relation is not.",
|
|
42
|
+
"items": {
|
|
43
|
+
"$ref": "#/$defs/diagramRelation"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"$defs": {
|
|
49
|
+
"diagramEntity": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {
|
|
52
|
+
"x": {
|
|
53
|
+
"type": "number",
|
|
54
|
+
"description": "Left edge of the entity node, in diagram units. Set together with `y`; both omitted means automatic placement."
|
|
55
|
+
},
|
|
56
|
+
"y": {
|
|
57
|
+
"type": "number",
|
|
58
|
+
"description": "Top edge of the entity node, in diagram units. Set together with `x`."
|
|
59
|
+
},
|
|
60
|
+
"label": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "Display name of a planned entity. An existing entity shows its own label, and this is ignored."
|
|
63
|
+
},
|
|
64
|
+
"description": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "What a planned entity will hold. An existing entity shows its own description, and this is ignored."
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"x": ["y"],
|
|
71
|
+
"y": ["x"]
|
|
72
|
+
},
|
|
73
|
+
"additionalProperties": false
|
|
74
|
+
},
|
|
75
|
+
"diagramRelation": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"required": ["from", "to"],
|
|
78
|
+
"properties": {
|
|
79
|
+
"from": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"minLength": 1,
|
|
82
|
+
"description": "Entity key of the many side — the entity that will carry the foreign key."
|
|
83
|
+
},
|
|
84
|
+
"to": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"minLength": 1,
|
|
87
|
+
"description": "Entity key of the one side — the entity referenced."
|
|
88
|
+
},
|
|
89
|
+
"label": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"description": "Verb read from `from` to `to`, e.g. `placed by`."
|
|
92
|
+
},
|
|
93
|
+
"required": {
|
|
94
|
+
"type": "boolean",
|
|
95
|
+
"description": "Every `from` row must have a `to` row. Default: false."
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"additionalProperties": false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"description": "SQL SELECT statement for view-backed entities (isView=true). Executed as CREATE OR REPLACE VIEW during module install."
|
|
37
37
|
},
|
|
38
38
|
"toString": {
|
|
39
|
-
"description": "Display pattern using column placeholders, e.g. \"{first_name} {last_name}\".
|
|
39
|
+
"description": "Display pattern using column placeholders, e.g. \"{first_name} {last_name}\" — the caption of a record wherever it is referenced. Every `{placeholder}` must name a field of the entity (trait columns included) and not a set; pack and install fail otherwise. Omitted, the install generates one from the fields and warns. Not declared on extension entities (with `extends`). No `type` constraint here because `toString` collides with Object.prototype.toString in JSON validators — they read the inherited function and reject it, even when the property is absent from the JSON."
|
|
40
40
|
},
|
|
41
41
|
"comments": {
|
|
42
42
|
"type": "boolean",
|
|
@@ -68,7 +68,7 @@ Full descriptions are on the tools themselves — this is the map.
|
|
|
68
68
|
| UI | `dforge_view_add`, `dforge_view_modify`, `dforge_menu_add`, `dforge_report_add` |
|
|
69
69
|
| Polish | `dforge_setting_add`, `dforge_translation_sync`, `dforge_seed_add` |
|
|
70
70
|
| Security | `dforge_role_add`, `dforge_role_right_set`, `dforge_folder_add` |
|
|
71
|
-
| Cross-cutting | `dforge_dependency_add` |
|
|
71
|
+
| Cross-cutting | `dforge_dependency_add`, `dforge_diagram_add` |
|
|
72
72
|
|
|
73
73
|
**Composite tools first.** A relation, a roll-up, and a status column each span
|
|
74
74
|
several coordinated keys, and hand-assembling them from `entity_field_add` is
|