@dforge-core/dforge-mcp 0.2.22 → 0.2.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/dforge-mcp",
3
- "version": "0.2.22",
3
+ "version": "0.2.23",
4
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.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/iash44/dForge-core",
@@ -35,8 +35,8 @@
35
35
  "install-skills": "node scripts/install-skills.mjs"
36
36
  },
37
37
  "dependencies": {
38
- "@dforge-core/dforge-cli": "^0.2.17",
39
- "@dforge-core/metadata": "^0.0.22",
38
+ "@dforge-core/dforge-cli": "^0.2.19",
39
+ "@dforge-core/metadata": "^0.0.26",
40
40
  "@modelcontextprotocol/sdk": "^1.30.0",
41
41
  "zod": "^4.4.3"
42
42
  },
@@ -10,22 +10,25 @@ A `.dsl` file consists of four ordered, optional blocks (only `execute:` is requ
10
10
 
11
11
  ```dsl
12
12
  params:
13
- # Parameter declarations (one per line)
13
+ // Parameter declarations (one per line)
14
14
 
15
15
  canExecute:
16
- # Single-expression formula returning boolean
17
- # Enables/disables the button AND is re-checked server-side on execute
18
- # (see "canExecute is enforced, with a fail-open subset")
16
+ // Single-expression formula returning boolean
17
+ // Enables/disables the button AND is re-checked server-side on execute
18
+ // (see "canExecute is enforced, with a fail-open subset")
19
19
 
20
20
  onBeforeStart:
21
- # Pre-queue initialization (async / background actions only)
21
+ // Pre-queue initialization (async / background actions only)
22
22
 
23
23
  execute:
24
- # Main logic — required
24
+ // Main logic — required
25
25
  ```
26
26
 
27
27
  Block markers are case-insensitive but conventionally lowercase. Whitespace-indented bodies follow each marker. Blocks must appear in the order above when present.
28
28
 
29
+ Comments are `//` and `/* … */`, as in JavaScript. **`#` is not a comment** — the
30
+ compiler passes the line through and Jint rejects it as a syntax error at install.
31
+
29
32
  ---
30
33
 
31
34
  ## Execution modes
@@ -141,11 +144,11 @@ execute:
141
144
  - `old[fieldName]` — the field's value *before* the change, in an action invoked by an event trigger. Read-only; reads `null` on `insert` events and outside triggers
142
145
 
143
146
  ```dsl
144
- # Read
147
+ // Read
145
148
  if ([quantity] > [available_qty]) { error("Not enough stock") }
146
149
  if ([supplier].[is_active] == false) { error("Supplier disabled") }
147
150
 
148
- # Write
151
+ // Write
149
152
  [status] = 'closed'
150
153
  [closed_date] = now()
151
154
  ```
@@ -381,19 +384,19 @@ so ids stay exact through reads, writes and comparisons. Smaller `int8` values
381
384
  (counters, quantities) stay ordinary numbers, so arithmetic on them is unchanged.
382
385
 
383
386
  ```dsl
384
- [copy] = [owner_id] # exact round-trip
385
- [is_mine] = currentUserId() == [last_updated_by] # exact — both sides BigInt
386
- [label] = '' + [owner_id] # "200650220910936067"
387
+ [copy] = [owner_id] // exact round-trip
388
+ [is_mine] = currentUserId() == [last_updated_by] // exact — both sides BigInt
389
+ [label] = '' + [owner_id] // "200650220910936067"
387
390
  ```
388
391
 
389
392
  Two operations throw instead of rounding silently:
390
393
 
391
394
  ```dsl
392
- [x] = [owner_id] + 1 # Cannot mix BigInt and other types
393
- [x] = Number([owner_id]) + 1 # explicit — and accepts the rounding
395
+ [x] = [owner_id] + 1 // Cannot mix BigInt and other types
396
+ [x] = Number([owner_id]) + 1 // explicit — and accepts the rounding
394
397
 
395
- [x] = JSON.stringify([owner_id]) # Do not know how to serialize a BigInt
396
- [payload] = { id: [owner_id] } # assign the object; jsonb writes the number
398
+ [x] = JSON.stringify([owner_id]) // Do not know how to serialize a BigInt
399
+ [payload] = { id: [owner_id] } // assign the object; jsonb writes the number
397
400
  ```
398
401
 
399
402
  Never write a large id as a bare numeric literal — the literal itself rounds
@@ -492,10 +495,10 @@ notify([approver_id], 'New purchase request from ' + [requester_name] + ' awaiti
492
495
  Two modes:
493
496
 
494
497
  ```dsl
495
- # Raw mode: (to, subject, body)
498
+ // Raw mode: (to, subject, body)
496
499
  sendEmail('ap@acme.com', 'Bill ' + [bill_number] + ' approved', 'Amount: $' + [amount])
497
500
 
498
- # Template mode: (to, templateCd, dataObject) — template defined in module's email_templates/
501
+ // Template mode: (to, templateCd, dataObject) — template defined in module's email_templates/
499
502
  sendEmail([customer_id].[email], 'order_confirmation', {
500
503
  orderNumber: [order_number],
501
504
  total: [total],
@@ -564,8 +567,8 @@ execute:
564
567
  Folder-scoped module setting — walks the folder inheritance chain up to the module default.
565
568
 
566
569
  ```dsl
567
- var prefix = getSetting('invoice_number_prefix') # e.g. "INV-"
568
- var maxDays = getSetting('payment_due_days') # e.g. 30
570
+ var prefix = getSetting('invoice_number_prefix') // e.g. "INV-"
571
+ var maxDays = getSetting('payment_due_days') // e.g. 30
569
572
  [due_date] = addDays(now(), maxDays)
570
573
  ```
571
574
 
@@ -666,7 +669,7 @@ An explicit `description` still wins.
666
669
 
667
670
  ```dsl
668
671
  var task = insert('workspace.task', { title: 'Follow up' })
669
- # Captioned by workspace.task's toString — no third argument needed.
672
+ // Captioned by workspace.task's toString — no third argument needed.
670
673
  insert('workspace.task_note', { link: entityLink('workspace.task', task) })
671
674
  ```
672
675
 
@@ -674,14 +677,14 @@ insert('workspace.task_note', { link: entityLink('workspace.task', task) })
674
677
  Commit pending changes mid-script AND broadcast SSE updates to connected clients. **Async / background actions only** — in sync actions, everything commits at the end automatically.
675
678
 
676
679
  ```dsl
677
- # Long-running background job that wants to surface progress
680
+ // Long-running background job that wants to surface progress
678
681
  execute:
679
682
  var total = records.count()
680
683
  var done = 0
681
684
  for rec in records {
682
685
  rec[processed_at] = now()
683
686
  done = done + 1
684
- if (done % 100 == 0) { flush() } # progress update every 100 records
687
+ if (done % 100 == 0) { flush() } // progress update every 100 records
685
688
  }
686
689
  ```
687
690
 
@@ -788,7 +791,7 @@ execute:
788
791
 
789
792
  ```dsl
790
793
  execute:
791
- var threshold = getSetting('high_value_threshold') # e.g. 10000
794
+ var threshold = getSetting('high_value_threshold') // e.g. 10000
792
795
  var ccApprover = getSetting('cc_approver_user_id')
793
796
  if ([amount] >= threshold && ccApprover != null) {
794
797
  notify(ccApprover, 'Large order requires your CC approval: $' + [amount])
@@ -928,15 +931,15 @@ execute:
928
931
  ### ❌ Using `[field]` in batch mode
929
932
 
930
933
  ```dsl
931
- # WRONG — batch mode has no "current record"
934
+ // WRONG — batch mode has no "current record"
932
935
  execute:
933
936
  for rec in records {
934
- [status] = 'closed' # ← this references the dispatch context, not rec
937
+ [status] = 'closed' // ← this references the dispatch context, not rec
935
938
  }
936
939
  ```
937
940
 
938
941
  ```dsl
939
- # RIGHT
942
+ // RIGHT
940
943
  execute:
941
944
  for rec in records {
942
945
  rec[status] = 'closed'
@@ -948,13 +951,13 @@ execute:
948
951
  Jobs run as system user with no record context. The action must be entity-agnostic.
949
952
 
950
953
  ```dsl
951
- # WRONG — fails at runtime with "no record context"
954
+ // WRONG — fails at runtime with "no record context"
952
955
  execute:
953
956
  if ([overdue_count] > 0) { notify([owner_id], 'Overdue') }
954
957
  ```
955
958
 
956
959
  ```dsl
957
- # RIGHT — fetch what you need via query
960
+ // RIGHT — fetch what you need via query
958
961
  execute:
959
962
  var overdue = query('SELECT owner_id, COUNT(*) AS n FROM invoice WHERE due_date < CURRENT_DATE GROUP BY owner_id', {})
960
963
  for (var i = 0; i < overdue.length; i++) {
@@ -967,14 +970,14 @@ execute:
967
970
  The DSL body is not wrapped in an IIFE — `return` at top level is a parse error.
968
971
 
969
972
  ```dsl
970
- # WRONG
973
+ // WRONG
971
974
  execute:
972
975
  if ([status] == 'closed') { return }
973
976
  [last_touched] = now()
974
977
  ```
975
978
 
976
979
  ```dsl
977
- # RIGHT — use exit() for early success, error() for early failure
980
+ // RIGHT — use exit() for early success, error() for early failure
978
981
  execute:
979
982
  if ([status] == 'closed') { exit('Already closed', 'info') }
980
983
  [last_touched] = now()
@@ -988,9 +991,9 @@ execution path. The same script keeps the row on one path and discards it on
988
991
  another, and the difference isn't visible in the action.
989
992
 
990
993
  ```dsl
991
- # WRONG — is this insert kept or discarded? Depends on the path.
994
+ // WRONG — is this insert kept or discarded? Depends on the path.
992
995
  execute:
993
- insert('audit_log', { event: 'attempted_close', record_id: [id] }) # unreliable!
996
+ insert('audit_log', { event: 'attempted_close', record_id: [id] }) // unreliable!
994
997
  if ([balance_due] > 0) { error('Cannot close — balance owing') }
995
998
  ```
996
999
 
@@ -1003,7 +1006,7 @@ to be atomic — so don't design around it either way. Write the script so the
1003
1006
  question never arises: validate first.
1004
1007
 
1005
1008
  ```dsl
1006
- # RIGHT — validate first, then perform side-effects
1009
+ // RIGHT — validate first, then perform side-effects
1007
1010
  execute:
1008
1011
  if ([balance_due] > 0) { error('Cannot close — balance owing') }
1009
1012
  insert('audit_log', { event: 'closed', record_id: [id] })
@@ -1015,17 +1018,17 @@ execute:
1015
1018
  The platform auto-generates the number on INSERT when the target column is empty. Manual `nextNumber()` + manual field-set risks double-allocating sequence values if you forget to leave the field blank.
1016
1019
 
1017
1020
  ```dsl
1018
- # WRONG — wastes a sequence value
1021
+ // WRONG — wastes a sequence value
1019
1022
  execute:
1020
1023
  var n = nextNumber('purchase_order')
1021
- var po = insert('purchase_order', { po_number: n, ... }) # gets a DIFFERENT auto-number, n is wasted
1024
+ var po = insert('purchase_order', { po_number: n, ... }) // gets a DIFFERENT auto-number, n is wasted
1022
1025
  ```
1023
1026
 
1024
1027
  ```dsl
1025
- # RIGHT — let INSERT generate it
1028
+ // RIGHT — let INSERT generate it
1026
1029
  execute:
1027
1030
  var po = insert('purchase_order', { supplier_id: params[supplier].supplier_id })
1028
- info('Created PO ' + po.po_number) # po_number was auto-populated
1031
+ info('Created PO ' + po.po_number) // po_number was auto-populated
1029
1032
  ```
1030
1033
 
1031
1034
  Only use `nextNumber()` explicitly when you need the number BEFORE insert (e.g. to mention in a notification or message).
@@ -1039,12 +1042,12 @@ Sync actions block the UI until they finish. A 30-second action freezes the user
1039
1042
  ### ❌ String-concatenating into SQL
1040
1043
 
1041
1044
  ```dsl
1042
- # WRONG — injection-vulnerable + breaks on quotes
1045
+ // WRONG — injection-vulnerable + breaks on quotes
1043
1046
  var q = query('SELECT * FROM customer WHERE name = ' + "'" + params[name] + "'", {})
1044
1047
  ```
1045
1048
 
1046
1049
  ```dsl
1047
- # RIGHT — use named placeholders
1050
+ // RIGHT — use named placeholders
1048
1051
  var q = query('SELECT * FROM customer WHERE name = @n', { n: params[name] })
1049
1052
  ```
1050
1053
 
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "params": {
36
36
  "type": "object",
37
- "description": "Static parameters passed to the action as __params. Folder-scoped overrides go through module settings, not here.",
37
+ "description": "Static parameters passed to the action as __params. Manifest-fixed — an admin cannot change them. For tenant-configurable values use module settings, which resolve from the job's bound folder (scheduled_job.folder_id, defaulting to the module root).",
38
38
  "additionalProperties": true
39
39
  },
40
40
  "timeout": {
@@ -72,7 +72,7 @@
72
72
  "paused": {
73
73
  "type": "boolean",
74
74
  "default": false,
75
- "description": "Soft pause. Scheduler skips firing, but admin 'trigger now' still works."
75
+ "description": "Soft pause. Scheduler skips firing, but admin 'trigger now' still works. Seeds the initial value only — an upgrade never overwrites it, because an admin owns it at runtime."
76
76
  }
77
77
  },
78
78
  "additionalProperties": false
@@ -19,12 +19,5 @@
19
19
  "entities": {
20
20
  "budget_category": "./entities/budget_category.json",
21
21
  "budget_line": "./entities/budget_line.json"
22
- },
23
- "dataViews": "./ui/data_views.json",
24
- "security": {
25
- "roles": "./security/roles.json"
26
- },
27
- "seedData": [
28
- "./seed-data/01-categories.json"
29
- ]
22
+ }
30
23
  }