@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/CHANGELOG.md +38 -0
- package/dist/server.js +383 -336
- package/package.json +46 -46
- package/resources/docs/dsl-reference.md +72 -0
- package/resources/schemas/data-views.schema.json +16 -3
- package/resources/schemas/folders.schema.json +18 -2
- package/resources/schemas/reports.schema.json +18 -2
- package/skills/dforge-mcp-author/references/action-dsl.md +44 -1
- package/skills/dforge-mcp-author/references/reports.md +1 -1
package/package.json
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
"
|
|
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": {
|
|
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": {
|
|
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": "
|
|
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" } }
|