@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 34 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.
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 34 `dforge_*` tools should be listed.
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.29/node_modules/@dforge-core/metadata/dist/index.js
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.29/node_modules/@dforge-core/metadata/dist/dsl/index.js
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
  {
@@ -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 (34 tools) + maintainer docs
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.28",
4
- "description": "MCP server for dForge module authoring. Exposes 34 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
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.29",
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}\". Optional in 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."
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