@dforge-core/dforge-mcp 0.2.2 → 0.2.4

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,48 +1,48 @@
1
1
  {
2
- "name": "@dforge-core/dforge-mcp",
3
- "version": "0.2.2",
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
- "license": "MIT",
6
- "homepage": "https://github.com/iash44/dForge-core",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/dforge-core/dforge-mcp.git"
10
- },
11
- "bin": {
12
- "dforge-mcp": "dist/server.js",
13
- "dforge-install-skills": "scripts/install-skills.mjs"
14
- },
15
- "files": [
16
- "dist/",
17
- "docs/",
18
- "resources/",
19
- "skills/",
20
- "scripts/install-skills.mjs",
21
- "README.md",
22
- "CHANGELOG.md"
23
- ],
24
- "main": "dist/server.js",
25
- "engines": {
26
- "node": ">=18"
27
- },
28
- "scripts": {
29
- "build": "tsup",
30
- "sync-schemas": "node scripts/vendor-schemas.cjs",
31
- "prepublishOnly": "node scripts/vendor-schemas.cjs && tsup",
32
- "typecheck": "tsc --noEmit",
33
- "test": "vitest run",
34
- "install-skills": "node scripts/install-skills.mjs"
35
- },
36
- "dependencies": {
37
- "@dforge-core/dforge-cli": "^0.2.10",
38
- "@dforge-core/metadata": "^0.0.12",
39
- "@modelcontextprotocol/sdk": "^1.29.0",
40
- "zod": "^4.4.3"
41
- },
42
- "devDependencies": {
43
- "@types/node": "^22.20.1",
44
- "tsup": "^8.5.1",
45
- "typescript": "^6.0.3",
46
- "vitest": "^3.2.7"
47
- }
2
+ "name": "@dforge-core/dforge-mcp",
3
+ "version": "0.2.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
+ "license": "MIT",
6
+ "homepage": "https://github.com/iash44/dForge-core",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/dforge-core/dforge-mcp.git"
10
+ },
11
+ "bin": {
12
+ "dforge-mcp": "dist/server.js",
13
+ "dforge-install-skills": "scripts/install-skills.mjs"
14
+ },
15
+ "files": [
16
+ "dist/",
17
+ "docs/",
18
+ "resources/",
19
+ "skills/",
20
+ "scripts/install-skills.mjs",
21
+ "README.md",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "main": "dist/server.js",
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "sync-schemas": "node scripts/vendor-schemas.cjs",
31
+ "prepublishOnly": "node scripts/vendor-schemas.cjs && tsup",
32
+ "typecheck": "tsc --noEmit",
33
+ "test": "vitest run",
34
+ "install-skills": "node scripts/install-skills.mjs"
35
+ },
36
+ "dependencies": {
37
+ "@dforge-core/dforge-cli": "^0.2.12",
38
+ "@dforge-core/metadata": "^0.0.13",
39
+ "@modelcontextprotocol/sdk": "^1.30.0",
40
+ "zod": "^4.4.3"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.20.1",
44
+ "tsup": "^8.5.1",
45
+ "typescript": "^6.0.3",
46
+ "vitest": "^3.2.7"
47
+ }
48
48
  }
@@ -288,6 +288,68 @@ Fractional-unit arithmetic.
288
288
  [token_expires_at] = addMinutes(now(), 15)
289
289
  ```
290
290
 
291
+ ### Current user
292
+
293
+ #### `currentUserId() → long`, `userId → long`
294
+ The acting user's tenant user ID. Two spellings for the same value — `userId` is
295
+ a **bare identifier**, `currentUserId()` is the call form. Both are always
296
+ available; use whichever reads better.
297
+
298
+ ```dsl
299
+ [approved_by_id] = userId
300
+ [created_by_id] = currentUserId()
301
+ notify(userId, 'Done')
302
+ ```
303
+
304
+ `userId()` — the value spelled as a call — is **rejected at compile time**:
305
+
306
+ ```
307
+ 'userId' is a value, not a function — write 'userId' without parentheses, or 'currentUserId()'
308
+ ```
309
+
310
+ `CURRENT_USER_ID()` is **not** available in `execute:` — it is a *formula* function,
311
+ valid in `canExecute:`, formula columns, filters and reports, exactly like
312
+ `TODAY()`/`NOW()` vs `now()`. Same split, same reason: `execute:` is JavaScript,
313
+ the rest is the formula engine.
314
+
315
+ #### 64-bit ids are BigInt
316
+
317
+ JS numbers are doubles (53 mantissa bits); dForge ids are 58-bit snowflakes. Any
318
+ `long` outside ±2^53 — user ids, PKs, FKs — reaches the script as a JS **BigInt**,
319
+ so ids stay exact through reads, writes and comparisons. Smaller `int8` values
320
+ (counters, quantities) stay ordinary numbers, so arithmetic on them is unchanged.
321
+
322
+ ```dsl
323
+ [copy] = [owner_id] # exact round-trip
324
+ [is_mine] = currentUserId() == [last_updated_by] # exact — both sides BigInt
325
+ [label] = '' + [owner_id] # "200650220910936067"
326
+ ```
327
+
328
+ Two operations throw instead of rounding silently:
329
+
330
+ ```dsl
331
+ [x] = [owner_id] + 1 # Cannot mix BigInt and other types
332
+ [x] = Number([owner_id]) + 1 # explicit — and accepts the rounding
333
+
334
+ [x] = JSON.stringify([owner_id]) # Do not know how to serialize a BigInt
335
+ [payload] = { id: [owner_id] } # assign the object; jsonb writes the number
336
+ ```
337
+
338
+ Never write a large id as a bare numeric literal — the literal itself rounds
339
+ before the comparison runs:
340
+
341
+ | Comparison | Result |
342
+ |---|---|
343
+ | `[owner_id] == 200650220910936067` | ❌ literal rounded to `…064` |
344
+ | `[owner_id] == '200650220910936067'` | ✅ string coerces to BigInt |
345
+ | `[owner_id] == 200650220910936067n` | ✅ BigInt literal |
346
+ | `[owner_id] === '200650220910936067'` | ❌ strict equality, different types |
347
+
348
+ For "who did this", prefer the platform audit columns (`created_by` /
349
+ `last_updated_by`) — filled server-side, no script involved. For ownership
350
+ predicates in `canExecute:` or a formula column, use `CURRENT_USER_ID()`, which
351
+ the server translates to an exact `::bigint` SQL literal.
352
+
291
353
  ### Messaging & notifications
292
354
 
293
355
  #### `info(message, opts?)`, `warn(message, opts?)`
@@ -459,6 +521,16 @@ var det = detectDocument(data.raw_text) // { profile: 'proc.bill_of_lading',
459
521
  if (det != null) { var r = ocrExtract([doc_file], getSetting('ocr_endpoint_url'), null, { profile: det.profile }) }
460
522
  ```
461
523
 
524
+ #### `applyProfile(json, profile) → object`
525
+ Map an already-extracted JSON payload through a registered extraction profile's
526
+ field schema, without re-running OCR. Use when the raw JSON came from somewhere
527
+ other than `ocrExtract()` — a webhook payload, a stored response, a re-process of
528
+ an earlier extraction.
529
+
530
+ ```dsl
531
+ var mapped = applyProfile(tryParseJson([raw_payload]), 'proc.bill_of_lading')
532
+ ```
533
+
462
534
  ### Utility
463
535
 
464
536
  #### `entityLink(entityCd, record, description?) → link value`
@@ -492,7 +492,20 @@
492
492
  },
493
493
  "o": {
494
494
  "type": "string",
495
- "description": "Operator: eq, neq, =, !=, gt, gtEq, grEq, lt, ltEq, in, nIn, like, isNull, isNotNull, etc."
495
+ "enum": [
496
+ "=", "eq", "!=", "notEq",
497
+ ">", "gr", ">=", "grEq", "<", "less", "<=", "lessEq",
498
+ "eqRef",
499
+ "contains", "!contains", "nContain",
500
+ "start", "!start", "nStart",
501
+ "end", "!end", "nEnd",
502
+ "mask", "!mask", "nMask",
503
+ "null", "empty", "!null", "nEmpty",
504
+ "in", "!in", "nIn",
505
+ "between", "btw", "!between", "nBetween",
506
+ "f", "func"
507
+ ],
508
+ "description": "Operator. Closed set — this is exactly what the server's FilterBuilder translates, and an operator outside it is rejected at install. Symbolic forms and their word aliases are equivalent ('=' / 'eq', '!=' / 'notEq', '>' / 'gr', '>=' / 'grEq', '<' / 'less', '<=' / 'lessEq'); a leading '!' negates, with 'n'-prefixed spellings as the alias ('!in' / 'nIn'). Null checks are 'null' / 'empty' and '!null' / 'nEmpty' (NOT 'isNull' / 'isNotNull'); substring matching is 'contains' (NOT 'like'). 'eqRef' compares against another column (v = column code), 'f' / 'func' takes v = {fn: 'isToday' | 'isYesterday' | 'isThisWeek' | 'isThisMonth' | 'isThisYear' | 'isPast' | 'isFuture' | 'isCurrentUser'}."
496
509
  },
497
510
  "v": {
498
511
  "description": "Comparison value. Scalars, arrays (for in/nIn), or parameter references like '@param_code'."
@@ -506,8 +519,8 @@
506
519
  "properties": {
507
520
  "g": {
508
521
  "type": "string",
509
- "enum": ["and", "or"],
510
- "description": "Group combinator"
522
+ "enum": ["and", "or", "!and", "nAnd", "!or", "nOr"],
523
+ "description": "Group combinator. '!and' / '!or' negate the whole group ('nAnd' / 'nOr' are the alias spellings). Closed set — an unknown combinator is rejected at install (at query time it would silently fall back to AND)."
511
524
  },
512
525
  "i": {
513
526
  "type": "array",
@@ -87,7 +87,23 @@
87
87
  "required": ["c", "o"],
88
88
  "properties": {
89
89
  "c": { "type": "string" },
90
- "o": { "type": "string" },
90
+ "o": {
91
+ "type": "string",
92
+ "enum": [
93
+ "=", "eq", "!=", "notEq",
94
+ ">", "gr", ">=", "grEq", "<", "less", "<=", "lessEq",
95
+ "eqRef",
96
+ "contains", "!contains", "nContain",
97
+ "start", "!start", "nStart",
98
+ "end", "!end", "nEnd",
99
+ "mask", "!mask", "nMask",
100
+ "null", "empty", "!null", "nEmpty",
101
+ "in", "!in", "nIn",
102
+ "between", "btw", "!between", "nBetween",
103
+ "f", "func"
104
+ ],
105
+ "description": "Operator — same closed set as data_views.json. Null checks are 'null' / '!null' (NOT 'isNull' / 'isNotNull'); substring matching is 'contains' (NOT 'like'). An operator outside this set is rejected at install; at query time it would be dropped, widening the row filter it was meant to enforce."
106
+ },
91
107
  "v": {}
92
108
  },
93
109
  "additionalProperties": false
@@ -96,7 +112,7 @@
96
112
  "type": "object",
97
113
  "required": ["g", "i"],
98
114
  "properties": {
99
- "g": { "type": "string", "enum": ["and", "or"] },
115
+ "g": { "type": "string", "enum": ["and", "or", "!and", "nAnd", "!or", "nOr"] },
100
116
  "i": {
101
117
  "type": "array",
102
118
  "items": { "$ref": "#/$defs/filter" }
@@ -366,7 +366,23 @@
366
366
  "required": ["c", "o"],
367
367
  "properties": {
368
368
  "c": { "type": "string" },
369
- "o": { "type": "string" },
369
+ "o": {
370
+ "type": "string",
371
+ "enum": [
372
+ "=", "eq", "!=", "notEq",
373
+ ">", "gr", ">=", "grEq", "<", "less", "<=", "lessEq",
374
+ "eqRef",
375
+ "contains", "!contains", "nContain",
376
+ "start", "!start", "nStart",
377
+ "end", "!end", "nEnd",
378
+ "mask", "!mask", "nMask",
379
+ "null", "empty", "!null", "nEmpty",
380
+ "in", "!in", "nIn",
381
+ "between", "btw", "!between", "nBetween",
382
+ "f", "func"
383
+ ],
384
+ "description": "Operator — same closed set as data_views.json. Null checks are 'null' / '!null' (NOT 'isNull' / 'isNotNull'); substring matching is 'contains' (NOT 'like'). An operator outside this set is rejected at install; at query time it would be dropped, silently widening the dataset."
385
+ },
370
386
  "v": {}
371
387
  },
372
388
  "additionalProperties": false
@@ -375,7 +391,7 @@
375
391
  "type": "object",
376
392
  "required": ["g", "i"],
377
393
  "properties": {
378
- "g": { "type": "string", "enum": ["and", "or"] },
394
+ "g": { "type": "string", "enum": ["and", "or", "!and", "nAnd", "!or", "nOr"] },
379
395
  "i": {
380
396
  "type": "array",
381
397
  "items": { "$ref": "#/$defs/filter" }
@@ -224,14 +224,31 @@ var queued = select('comm.message', {
224
224
  | `getFileBase64(fileFieldValue)` | String | Read a file from storage and return its content as a base64-encoded string. Use with `callApi()` to send files to AI APIs. Max 10 MB. Example: `getFileBase64([document_file])`. |
225
225
  | `getFileUrl(fileFieldValue)` | String | Generate a temporary signed download URL (relative path) for a file field. Use in email templates or notifications — not for `callApi()`. |
226
226
  | `getSecret('secret_cd')` | String | Retrieve a decrypted secret value (API keys, tokens). Secrets are managed in the admin UI. |
227
+ | `getFileInfo(fileFieldValue)` | Object \| null | File metadata for a file field. |
228
+ | `download(url, fileName?)` | — | Fetch a URL into tenant storage. |
229
+ | `callService('name', { args })` | Any | Invoke a registered host service. |
230
+
231
+ ### Files, settings, document extraction
232
+
233
+ | Function | Returns | Description |
234
+ |---|---|---|
235
+ | `getSetting('setting_cd')` | Any | Read a module setting, resolved through the folder chain (current folder → parents → module default). |
236
+ | `ocrExtract(fileField, endpointBaseUrl, schema?, opts?)` | Object | Run OCR extraction against a document. `opts.profile` selects a registered extraction profile, e.g. `{ profile: 'proc.bill_of_lading' }`. |
237
+ | `detectDocument(rawText)` | Object \| null | Score text against reachable extraction profiles; returns the best match `{ profile, docType, score }` or `null`. |
238
+ | `applyProfile(json, 'module.profile_cd')` | Object | Map already-extracted JSON through a profile's field schema, without re-running OCR. |
227
239
 
228
240
  ### Utilities
229
241
 
230
242
  | Function | Returns | Description |
231
243
  |---|---|---|
232
244
  | `now()` | DateTime | Current date/time. Use for `date`, `datetime`, and `timestamp` fields. **Lowercase** — this is the execute-block date function. |
233
- | `IF(condition, trueVal, falseVal)` | Any | Ternary helper. |
234
245
  | `addDays(date, n)` | Date | Add `n` days to a date, e.g. `addDays(now(), 30)`. |
246
+ | `addMinutes(date, n)` | DateTime | Add `n` minutes, e.g. `addMinutes(now(), 15)`. |
247
+ | `addSeconds(date, n)` | DateTime | Add `n` seconds. |
248
+ | `currentUserId()` | Long | The acting user's tenant user ID. |
249
+ | `userId` | Long | Same value as `currentUserId()`, as a **bare identifier — no parentheses**. `userId()` is a compile error; there is no `current_user_id()`. |
250
+ | `flush()` | — | Flush pending writes so a following read sees them. |
251
+ | `tryParseJson(value)` | Any \| null | Parse JSON, returning `null` on failure instead of throwing. |
235
252
  | `nextNumber('entity')` | String | Generate next value from a number sequence. Usually not needed — platform auto-fills on `insert()`. Use for pre-generating numbers. |
236
253
  | `callProc('proc_name', { args })` | Result | Call a stored procedure. Args are passed as named parameters. |
237
254
  | `entityLink('entityCd', record, description?)` | Link value | Build a clickable link to any entity record, for storing in an `entitylink` (jsonb) column. `record` is a row object (e.g. the result of `insert()` or `getRecord()`); its PK columns are read and stored as **strings**, so snowflake/cuid ids (> 2^53) stay exact. The entity is resolved at build time and stored **by id** — `entity_cd` is only unique per module, so always pass a qualified code (`'fin.invoice'`). Omit `description` and the link is captioned with the entity's `toString` against `record`; an explicit `description` wins. |
@@ -243,6 +260,32 @@ var queued = select('comm.message', {
243
260
  > columns, and other formula contexts; use `now()` everywhere inside `execute:`. (Uppercase `NOW()`
244
261
  > is also fine *inside a raw SQL string* passed to `query()`, because that's SQL, not JS.)
245
262
 
263
+ > **`IF()` is formula-only too.** It was previously listed here as a ternary helper — that was
264
+ > wrong. `IF(cond, a, b)` is a **formula-engine** function, valid in `canExecute:` and formula
265
+ > columns, and **undefined in `execute:`** (install fails with `'IF' is not defined`). Inside
266
+ > `execute:` use the JavaScript conditional operator: `var x = cond ? a : b`.
267
+
268
+ > **64-bit ids are BigInt in `execute:`.** JS numbers are doubles (53 mantissa bits) and dForge
269
+ > ids are 58-bit snowflakes, so any `long` outside ±2^53 — user ids, PKs, FKs — reaches the script
270
+ > as a JS **BigInt**. Ids stay exact through reads, writes and comparisons, including
271
+ > `currentUserId() == [last_updated_by]`. Smaller `int8` values (counters, quantities) stay
272
+ > ordinary numbers, so arithmetic on them is unchanged.
273
+ >
274
+ > Two operations throw rather than round silently, each with a working form:
275
+ > `[owner_id] + 1` → use `Number([owner_id]) + 1` (explicit, and accepts the rounding);
276
+ > `JSON.stringify([owner_id])` → assign the object instead (`[payload] = { id: [owner_id] }`),
277
+ > which serializes correctly on the write path.
278
+ >
279
+ > Never write a large id as a bare numeric literal — the literal itself rounds before the
280
+ > comparison runs. `[owner_id] == 2006…067` is **false**; quote it (`== '2006…067'`) or suffix
281
+ > it with `n` (`== 2006…067n`). Comparing two id columns needs neither.
282
+ >
283
+ > For "who did this", the platform audit columns (`created_by` / `last_updated_by`) remain the
284
+ > better answer — filled server-side, no script involved. For ownership predicates use
285
+ > **`CURRENT_USER_ID()`** in `canExecute:` or a formula column — a *formula-engine* function
286
+ > (undefined in `execute:`) that the server translates to an exact `::bigint` SQL literal.
287
+
288
+
246
289
  ## Real examples from the codebase
247
290
 
248
291
  ### Simple field update (status transition)
@@ -61,7 +61,7 @@ A report has `description`, a `datasets` map, a `layout` object (`{ panels: [...
61
61
  "query": {
62
62
  "entityCd": "opportunity",
63
63
  "columns": ["stage", "amount", "close_date", "customer.name"],
64
- "filter": { "c": "stage", "o": "ne", "v": "Closed Lost" },
64
+ "filter": { "c": "stage", "o": "!=", "v": "Closed Lost" },
65
65
  "sort": [ { "c": "close_date", "d": "desc" } ]
66
66
  },
67
67
  "columnsDef": { "amount": { "label": "Deal Value" }, "customer.name": { "label": "Account" } }