@tenonhq/dovetail-servicenow 0.0.24 → 0.0.26
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 +108 -4
- package/dist/cli.js +82 -0
- package/dist/client.d.ts +31 -2
- package/dist/client.js +42 -8
- package/dist/createRecord.d.ts +2 -8
- package/dist/index.d.ts +7 -1
- package/dist/index.js +9 -1
- package/dist/invokeRest.d.ts +50 -0
- package/dist/invokeRest.js +93 -0
- package/dist/mcp/registry.d.ts +1 -1
- package/dist/mcp/registry.js +89 -1
- package/dist/mcp/schemas.d.ts +63 -0
- package/dist/mcp/schemas.js +31 -1
- package/dist/setField.d.ts +14 -5
- package/dist/setField.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -253,6 +253,105 @@ adds diagnostics (app-switch status, resolved column key, assigned sys_id) to th
|
|
|
253
253
|
result note. Ground truth (the HAR dissection) lives in the CTO repo's create-table
|
|
254
254
|
docs.
|
|
255
255
|
|
|
256
|
+
### Set a field on a record
|
|
257
|
+
|
|
258
|
+
Set scalar field value(s) on an **existing** data record, capture the change into
|
|
259
|
+
an update set, then read it back and verify.
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
# Target by sys_id
|
|
263
|
+
npx dove-sn set-field \
|
|
264
|
+
--table x_cadso_core_metric_point_type --sys-id <sys_id> \
|
|
265
|
+
--fields "order=20" --update-set <sys_id> --dry-run --json
|
|
266
|
+
|
|
267
|
+
# Or target by a query that resolves to EXACTLY one row
|
|
268
|
+
npx dove-sn set-field \
|
|
269
|
+
--table x_cadso_core_metric_point_type --query "name=send_size" \
|
|
270
|
+
--fields "order=20,label=Send Size" --update-set <sys_id>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`set-field` wraps the update-set-aware `pushWithUpdateSet` core op (update-set +
|
|
274
|
+
scope switching handled server-side, so no `sys_user_preference` is touched), then
|
|
275
|
+
re-queries the record and verifies each value landed. It **refuses** schema tables
|
|
276
|
+
(`sys_db_object` / `sys_dictionary`) — use `add-column` / `create-table` for those.
|
|
277
|
+
`--fields` is a comma-separated `key=value` map (values are sent as strings;
|
|
278
|
+
ServiceNow coerces); `--update-set` is required so the change is captured;
|
|
279
|
+
`--dry-run` reads the current values and prints the plan without writing. Exit
|
|
280
|
+
codes: `0` applied / dry-run, `1` bad args, `2` write landed but read-back did not
|
|
281
|
+
verify.
|
|
282
|
+
|
|
283
|
+
### Create a record
|
|
284
|
+
|
|
285
|
+
Insert **one** new data record, owned by an explicit scope and captured into an
|
|
286
|
+
update set, then read it back and verify.
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
npx dove-sn create-record \
|
|
290
|
+
--table x_cadso_core_metric_point_type \
|
|
291
|
+
--fields "name=avg_message_parts,label=Avg. Message Parts,order=35" \
|
|
292
|
+
--scope x_cadso_core --update-set <sys_id> \
|
|
293
|
+
--if-absent "name=avg_message_parts" --dry-run --json
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
`create-record` wraps the scope- and update-set-aware `createRecord` core op, which
|
|
297
|
+
switches the executing user's app scope + update set server-side, inserts, and
|
|
298
|
+
restores both — so the record is owned by the right app and the insert is captured
|
|
299
|
+
in the right update set. Like `set-field` it **refuses** schema tables and verifies
|
|
300
|
+
via read-back. `--scope` and `--update-set` are required; `--if-absent
|
|
301
|
+
"<encoded-query>"` makes re-runs idempotent (the insert is skipped when the query
|
|
302
|
+
already matches a row). Exit codes: `0` created / skipped-in-sync / dry-run, `1` bad
|
|
303
|
+
args, `2` write landed unverified (or skipped with drift). To **update** an existing
|
|
304
|
+
record instead, use `set-field`.
|
|
305
|
+
|
|
306
|
+
Both verbs are exported for programmatic use:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import { createClient, setField, createRecord } from "@tenonhq/dovetail-servicenow";
|
|
310
|
+
|
|
311
|
+
var client = createClient({});
|
|
312
|
+
var r = await setField({
|
|
313
|
+
client: client,
|
|
314
|
+
table: "x_cadso_core_metric_point_type",
|
|
315
|
+
sysId: "<sys_id>",
|
|
316
|
+
fields: { order: "20" },
|
|
317
|
+
updateSetSysId: "<sys_id>"
|
|
318
|
+
});
|
|
319
|
+
console.log(r.status, r.verified); // "applied" true
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Invoke an arbitrary REST operation
|
|
323
|
+
|
|
324
|
+
Invoke any authenticated ServiceNow REST operation — an application's own
|
|
325
|
+
Scripted REST endpoints (`sys_ws_operation` at `/api/<scope>/<service>/<resource>`)
|
|
326
|
+
included — with GET, POST, PUT or DELETE. This is the transport primitive for
|
|
327
|
+
operations the fixed verbs can't express, and the only surface with PUT/DELETE
|
|
328
|
+
coverage (a verification harness that cleans up after itself needs the DELETE).
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
# Dry-run — the DEFAULT: echoes method + path + body, sends NOTHING
|
|
332
|
+
npx dove-sn invoke-rest --method DELETE \
|
|
333
|
+
--path /api/x_cadso_core/testkit/resource/<sys_id>
|
|
334
|
+
|
|
335
|
+
# Send for real
|
|
336
|
+
npx dove-sn invoke-rest --method PUT \
|
|
337
|
+
--path /api/x_cadso_core/testkit/resource/<sys_id> \
|
|
338
|
+
--body '{"name":"updated"}' --confirm --json
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`invoke-rest` is **dry-run by default** — nothing is sent without `--confirm`
|
|
342
|
+
(`--dry-run` forces a dry-run even with it). On send the response passes through
|
|
343
|
+
**verbatim** as `{ httpStatus, ok, body }`: non-2xx responses are returned, not
|
|
344
|
+
thrown, so the operation's own error contract survives (the transport still
|
|
345
|
+
retries 429/5xx first). The path must be instance-relative and start with
|
|
346
|
+
`/api/`. **Bodies are never printed in human output** — request or response,
|
|
347
|
+
dry-run or sent: method, path and status only. The structured `--json` result
|
|
348
|
+
is the one channel that carries them (a dry-run's `requestBody` echo lives
|
|
349
|
+
there). Exit codes: `0` dry-run or 2xx, `1` bad args, `2` sent but non-2xx.
|
|
350
|
+
|
|
351
|
+
Programmatic: `invokeRest({ method, path, body, confirm })` is exported, and the
|
|
352
|
+
client gained `now.put` / `now.delete` / `now.invoke` (the latter returns
|
|
353
|
+
`{ status, body }` verbatim) alongside the existing `now.get` / `now.post`.
|
|
354
|
+
|
|
256
355
|
`test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
|
|
257
356
|
declared variables?) that never runs the flow; `--execute --confirm` runs it via
|
|
258
357
|
the server-side FlowAPI runner (deploy `resources/runFlow.md` first).
|
|
@@ -323,13 +422,18 @@ console.log(formatLayoutResult("form layout", result));
|
|
|
323
422
|
|
|
324
423
|
`dove-sn mcp` runs a self-contained MCP stdio server exposing the tools to
|
|
325
424
|
Claude Code and agents: `create_view`, `set_list_layout`, `set_form_layout`,
|
|
326
|
-
`set_related_lists`, `add_choices_to_field`,
|
|
327
|
-
`
|
|
425
|
+
`set_related_lists`, `add_choices_to_field`, the schema verbs `create_table` /
|
|
426
|
+
`add_column`, the record-write verbs `set_field` (update scalar fields on an
|
|
427
|
+
existing record) and `create_record` (insert one record) — both update-set-captured
|
|
428
|
+
and read-back-verified — `host_assets` (deploy a built dist/), plus the Flow Designer
|
|
429
|
+
tools `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
|
|
328
430
|
type's model), `flow_publish` (compile a flow/subflow snapshot), `flow_copy`
|
|
329
431
|
(copy a flow as an inactive draft), `flow_create` (create a NEW flow from scratch +
|
|
330
432
|
publish, grafting a template), `flow_test` (validate or run a flow), and
|
|
331
|
-
`flow_edit` (patch a flow)
|
|
332
|
-
|
|
433
|
+
`flow_edit` (patch a flow), plus `invoke_rest` (invoke an arbitrary authenticated
|
|
434
|
+
REST operation — Scripted REST included — with GET/POST/PUT/DELETE; dry-run by
|
|
435
|
+
default, response passed through verbatim, bodies never logged). It reads
|
|
436
|
+
ServiceNow credentials from the same env vars as the CLI.
|
|
333
437
|
|
|
334
438
|
```bash
|
|
335
439
|
npx dove-sn mcp --smoke # list the registered tools and exit
|
package/dist/cli.js
CHANGED
|
@@ -81,6 +81,7 @@ const testFlow_1 = require("./flowDesigner/testFlow");
|
|
|
81
81
|
const table_1 = require("./table");
|
|
82
82
|
const setField_1 = require("./setField");
|
|
83
83
|
const createRecord_1 = require("./createRecord");
|
|
84
|
+
const invokeRest_1 = require("./invokeRest");
|
|
84
85
|
const hostAssets_1 = require("./hostAssets");
|
|
85
86
|
const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
|
|
86
87
|
function parseArgs(argv) {
|
|
@@ -719,6 +720,10 @@ function printHelp() {
|
|
|
719
720
|
" create-record Create ONE NEW record in a data table, into an update set, then verify\n" +
|
|
720
721
|
" (--table <t> --fields \"k=v,k2=v2\" --scope <s> --update-set <sys_id>\n" +
|
|
721
722
|
" [--if-absent <encoded-query>] [--dry-run] [--json])\n" +
|
|
723
|
+
" invoke-rest Invoke an arbitrary authenticated REST operation (Scripted REST incl.)\n" +
|
|
724
|
+
" DRY-RUN BY DEFAULT — nothing is sent without --confirm\n" +
|
|
725
|
+
" (--method <GET|POST|PUT|DELETE> --path /api/<scope>/<service>/<resource>\n" +
|
|
726
|
+
" [--body '<json>' | --body-json <path>] [--confirm] [--dry-run] [--json])\n" +
|
|
722
727
|
" host-assets Deploy a built dist/ to ServiceNow (carrier sys_ui_script + attachment + m2m)\n" +
|
|
723
728
|
" (--dir <dist> --app <sys_id> --scope <namespace>\n" +
|
|
724
729
|
" [--update-set <sys_id>] [--max-bytes <n>] [--allow-oversize] [--dry-run] [--json])\n" +
|
|
@@ -988,6 +993,80 @@ async function runCreateRecord(flags) {
|
|
|
988
993
|
return 2;
|
|
989
994
|
return 0;
|
|
990
995
|
}
|
|
996
|
+
/**
|
|
997
|
+
* dove-sn invoke-rest:
|
|
998
|
+
* --method <GET|POST|PUT|DELETE> Required.
|
|
999
|
+
* --path </api/...> Required. Instance-relative; must start with /api/.
|
|
1000
|
+
* --body '<json>' Optional inline JSON body (or --body-json <path>).
|
|
1001
|
+
* --confirm Send for real. WITHOUT it the command is a DRY-RUN.
|
|
1002
|
+
* --dry-run Force a dry-run even with --confirm.
|
|
1003
|
+
* --json Emit the structured InvokeRestResult.
|
|
1004
|
+
*
|
|
1005
|
+
* Invoke an arbitrary authenticated REST operation (Scripted REST included).
|
|
1006
|
+
* Dry-run by default; --confirm sends and returns { httpStatus, ok, body } with
|
|
1007
|
+
* the response passed through verbatim (non-2xx included — the transport still
|
|
1008
|
+
* retries 429/5xx first). Bodies are NEVER printed in human output — request or
|
|
1009
|
+
* response, dry-run or sent: method, path and status only. The structured
|
|
1010
|
+
* --json result is the one channel that carries them (a dry-run's requestBody
|
|
1011
|
+
* echo satisfies the #212 "echo the plan" gate there).
|
|
1012
|
+
* Exit codes: 0 dry-run or 2xx, 1 bad args, 2 sent but non-2xx.
|
|
1013
|
+
*/
|
|
1014
|
+
async function runInvokeRest(flags) {
|
|
1015
|
+
if (!flags.method || !flags.path) {
|
|
1016
|
+
process.stderr.write("invoke-rest: --method <GET|POST|PUT|DELETE> and --path </api/...> are required\n");
|
|
1017
|
+
return 1;
|
|
1018
|
+
}
|
|
1019
|
+
var body;
|
|
1020
|
+
if (flags["body-json"]) {
|
|
1021
|
+
try {
|
|
1022
|
+
body = JSON.parse(fs.readFileSync(flags["body-json"], "utf8"));
|
|
1023
|
+
}
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
process.stderr.write("invoke-rest: --body-json must point to a readable JSON file: "
|
|
1026
|
+
+ (err && err.message ? err.message : String(err)) + "\n");
|
|
1027
|
+
return 1;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
else if (flags.body !== undefined) {
|
|
1031
|
+
try {
|
|
1032
|
+
body = JSON.parse(flags.body);
|
|
1033
|
+
}
|
|
1034
|
+
catch (err) {
|
|
1035
|
+
process.stderr.write("invoke-rest: --body must be valid JSON: " + err.message + "\n");
|
|
1036
|
+
return 1;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
var params = {
|
|
1040
|
+
method: flags.method,
|
|
1041
|
+
path: flags.path,
|
|
1042
|
+
confirm: flags.confirm === "true",
|
|
1043
|
+
dryRun: flags["dry-run"] === "true"
|
|
1044
|
+
};
|
|
1045
|
+
if (body !== undefined) {
|
|
1046
|
+
params.body = body;
|
|
1047
|
+
}
|
|
1048
|
+
var result = await (0, invokeRest_1.invokeRest)(params);
|
|
1049
|
+
if (flags.json === "true") {
|
|
1050
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1051
|
+
}
|
|
1052
|
+
else if (result.status === "dry-run") {
|
|
1053
|
+
process.stdout.write("[dry-run] " + result.method + " " + result.path + "\n"
|
|
1054
|
+
+ (result.requestBody !== undefined
|
|
1055
|
+
? "Request body withheld from human output — use --json to view.\n"
|
|
1056
|
+
: "")
|
|
1057
|
+
+ result.note + "\n");
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1060
|
+
// Bodies are never logged: human output is method + path + status only.
|
|
1061
|
+
process.stdout.write("[sent] " + result.method + " " + result.path + " -> HTTP " + result.httpStatus
|
|
1062
|
+
+ (result.ok ? "" : " (non-2xx)") + "\n"
|
|
1063
|
+
+ "Response body withheld from human output — use --json for { httpStatus, ok, body }.\n");
|
|
1064
|
+
}
|
|
1065
|
+
if (result.status === "sent" && result.ok !== true) {
|
|
1066
|
+
return 2;
|
|
1067
|
+
}
|
|
1068
|
+
return 0;
|
|
1069
|
+
}
|
|
991
1070
|
/**
|
|
992
1071
|
* dove-sn host-assets:
|
|
993
1072
|
* --dir <dist> Required. Path to the pre-built dist/ directory.
|
|
@@ -1070,6 +1149,9 @@ async function main() {
|
|
|
1070
1149
|
if (parsed.command === "create-record") {
|
|
1071
1150
|
return await runCreateRecord(parsed.flags);
|
|
1072
1151
|
}
|
|
1152
|
+
if (parsed.command === "invoke-rest") {
|
|
1153
|
+
return await runInvokeRest(parsed.flags);
|
|
1154
|
+
}
|
|
1073
1155
|
if (parsed.command === "host-assets") {
|
|
1074
1156
|
return await runHostAssets(parsed.flags);
|
|
1075
1157
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -43,6 +43,20 @@ export interface AttachmentMeta {
|
|
|
43
43
|
hash?: string;
|
|
44
44
|
size_bytes?: string;
|
|
45
45
|
}
|
|
46
|
+
export type NowInvokeMethod = "GET" | "POST" | "PUT" | "DELETE";
|
|
47
|
+
export interface NowInvokeParams {
|
|
48
|
+
method: NowInvokeMethod;
|
|
49
|
+
/** Instance-relative path, e.g. /api/x_cadso_core/<service>/<resource>. */
|
|
50
|
+
path: string;
|
|
51
|
+
/** JSON request body for POST/PUT/DELETE. Ignored for GET. */
|
|
52
|
+
body?: unknown;
|
|
53
|
+
}
|
|
54
|
+
export interface NowInvokeResponse {
|
|
55
|
+
/** HTTP status code of the final response (after any 429/5xx retries). */
|
|
56
|
+
status: number;
|
|
57
|
+
/** Response body, verbatim. */
|
|
58
|
+
body: unknown;
|
|
59
|
+
}
|
|
46
60
|
export interface ServiceNowClient {
|
|
47
61
|
table: {
|
|
48
62
|
/** GET /api/now/table/<t>?sysparm_query=...&sysparm_limit=N — returns result array. */
|
|
@@ -74,7 +88,7 @@ export interface ServiceNowClient {
|
|
|
74
88
|
/** POST /api/cadso/dovetail_core/createRecord (legacy: /api/cadso/dovetail/createRecord). */
|
|
75
89
|
createRecord: (params: {
|
|
76
90
|
table: string;
|
|
77
|
-
fields: Record<string,
|
|
91
|
+
fields: Record<string, unknown>;
|
|
78
92
|
scope?: string;
|
|
79
93
|
update_set_sys_id?: string;
|
|
80
94
|
sys_id?: string;
|
|
@@ -87,7 +101,7 @@ export interface ServiceNowClient {
|
|
|
87
101
|
update_set_sys_id: string;
|
|
88
102
|
table: string;
|
|
89
103
|
record_sys_id: string;
|
|
90
|
-
fields: Record<string,
|
|
104
|
+
fields: Record<string, unknown>;
|
|
91
105
|
}) => Promise<{
|
|
92
106
|
sys_id: string;
|
|
93
107
|
[k: string]: any;
|
|
@@ -121,6 +135,21 @@ export interface ServiceNowClient {
|
|
|
121
135
|
get: <T = any>(path: string) => Promise<T>;
|
|
122
136
|
/** POST an arbitrary native ServiceNow REST path with a JSON body. See `get`. */
|
|
123
137
|
post: <T = any>(path: string, body: any) => Promise<T>;
|
|
138
|
+
/** PUT an arbitrary native ServiceNow REST path with a JSON body. See `get`. */
|
|
139
|
+
put: <T = any>(path: string, body: any) => Promise<T>;
|
|
140
|
+
/** DELETE an arbitrary native ServiceNow REST path (optional JSON body). See `get`. */
|
|
141
|
+
delete: <T = any>(path: string, body?: any) => Promise<T>;
|
|
142
|
+
/**
|
|
143
|
+
* Invoke an arbitrary native ServiceNow REST path with an explicit method and
|
|
144
|
+
* get the HTTP response back verbatim as { status, body }. Unlike get/post/
|
|
145
|
+
* put/delete, non-2xx responses are RETURNED, not thrown, so callers (the
|
|
146
|
+
* dove-sn invoke-rest verb / invoke_rest MCP tool) can pass a Scripted REST
|
|
147
|
+
* operation's own error contract through faithfully. Same auth/retry/throttle
|
|
148
|
+
* transport: 429/5xx are retried per the client config and the LAST response
|
|
149
|
+
* is returned when retries are exhausted; only a network failure (no HTTP
|
|
150
|
+
* response at all) throws.
|
|
151
|
+
*/
|
|
152
|
+
invoke: (params: NowInvokeParams) => Promise<NowInvokeResponse>;
|
|
124
153
|
};
|
|
125
154
|
attachment: {
|
|
126
155
|
/**
|
package/dist/client.js
CHANGED
|
@@ -110,7 +110,15 @@ function createClient(config = {}) {
|
|
|
110
110
|
// Latch the legacy flag after the first 404 to avoid paying the round-trip
|
|
111
111
|
// cost on every subsequent call.
|
|
112
112
|
var useDovetailLegacyPath = false;
|
|
113
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Shared transport loop: throttle, send, retry 429/5xx, and surface auth/404
|
|
115
|
+
* failures as clear errors. With `passThrough`, any HTTP response that would
|
|
116
|
+
* normally throw (or that exhausted its retries) is RETURNED as
|
|
117
|
+
* { status, data } instead — only a network failure (no HTTP response at all)
|
|
118
|
+
* still throws. now.invoke uses passThrough to hand a Scripted REST
|
|
119
|
+
* operation's own error contract back verbatim.
|
|
120
|
+
*/
|
|
121
|
+
async function requestRaw(cfg, ctx, passThrough) {
|
|
114
122
|
var attempt429 = 0;
|
|
115
123
|
var attempt5xx = 0;
|
|
116
124
|
// eslint-disable-next-line no-constant-condition
|
|
@@ -132,14 +140,10 @@ function createClient(config = {}) {
|
|
|
132
140
|
await sleep(Math.pow(2, attempt5xx) * 1000);
|
|
133
141
|
continue;
|
|
134
142
|
}
|
|
135
|
-
if (res.status === 401 || res.status === 403) {
|
|
136
|
-
throw new Error("SN auth error " + res.status + " on " + ctx + " — check SN_USER/SN_PASSWORD and ACLs.");
|
|
137
|
-
}
|
|
138
|
-
if (res.status === 404) {
|
|
139
|
-
throw new Error("SN 404 on " + ctx + " — endpoint or record not found.");
|
|
140
|
-
}
|
|
141
143
|
if (res.status === 429) {
|
|
142
144
|
if (attempt429 >= max429) {
|
|
145
|
+
if (passThrough)
|
|
146
|
+
return { status: res.status, data: res.data };
|
|
143
147
|
throw new Error("SN 429 rate limit — retries exhausted on " + ctx);
|
|
144
148
|
}
|
|
145
149
|
attempt429 += 1;
|
|
@@ -148,19 +152,34 @@ function createClient(config = {}) {
|
|
|
148
152
|
}
|
|
149
153
|
if (res.status >= 500) {
|
|
150
154
|
if (attempt5xx >= max5xx) {
|
|
155
|
+
if (passThrough)
|
|
156
|
+
return { status: res.status, data: res.data };
|
|
151
157
|
throw new Error("SN " + res.status + " on " + ctx + " — retries exhausted.");
|
|
152
158
|
}
|
|
153
159
|
attempt5xx += 1;
|
|
154
160
|
await sleep(Math.pow(2, attempt5xx) * 1000);
|
|
155
161
|
continue;
|
|
156
162
|
}
|
|
163
|
+
if (passThrough) {
|
|
164
|
+
return { status: res.status, data: res.data };
|
|
165
|
+
}
|
|
166
|
+
if (res.status === 401 || res.status === 403) {
|
|
167
|
+
throw new Error("SN auth error " + res.status + " on " + ctx + " — check SN_USER/SN_PASSWORD and ACLs.");
|
|
168
|
+
}
|
|
169
|
+
if (res.status === 404) {
|
|
170
|
+
throw new Error("SN 404 on " + ctx + " — endpoint or record not found.");
|
|
171
|
+
}
|
|
157
172
|
if (res.status < 200 || res.status >= 300) {
|
|
158
173
|
var body = typeof res.data === "string" ? res.data : JSON.stringify(res.data);
|
|
159
174
|
throw new Error("SN " + res.status + " on " + ctx + ": " + body.substring(0, 400));
|
|
160
175
|
}
|
|
161
|
-
return res.data;
|
|
176
|
+
return { status: res.status, data: res.data };
|
|
162
177
|
}
|
|
163
178
|
}
|
|
179
|
+
async function request(cfg, ctx) {
|
|
180
|
+
var raw = await requestRaw(cfg, ctx);
|
|
181
|
+
return raw.data;
|
|
182
|
+
}
|
|
164
183
|
// Dovetail core Scripted REST API request: try /api/cadso/dovetail_core/<op>,
|
|
165
184
|
// fall back to the legacy /api/cadso/dovetail/<op> on 404 (one-time warning).
|
|
166
185
|
async function dovetailRequest(method, op, body, params, ctx) {
|
|
@@ -315,6 +334,21 @@ function createClient(config = {}) {
|
|
|
315
334
|
},
|
|
316
335
|
post: function (path, body) {
|
|
317
336
|
return request({ method: "POST", url: path, data: body }, "now.post(" + path + ")");
|
|
337
|
+
},
|
|
338
|
+
put: function (path, body) {
|
|
339
|
+
return request({ method: "PUT", url: path, data: body }, "now.put(" + path + ")");
|
|
340
|
+
},
|
|
341
|
+
delete: function (path, body) {
|
|
342
|
+
return request({ method: "DELETE", url: path, data: body }, "now.delete(" + path + ")");
|
|
343
|
+
},
|
|
344
|
+
invoke: async function (params) {
|
|
345
|
+
var cfg = { method: params.method, url: params.path };
|
|
346
|
+
// Per NowInvokeParams: body is ignored for GET — never attach a GET payload.
|
|
347
|
+
if (params.method !== "GET" && params.body !== undefined) {
|
|
348
|
+
cfg.data = params.body;
|
|
349
|
+
}
|
|
350
|
+
var raw = await requestRaw(cfg, "now.invoke(" + params.method + " " + params.path + ")", true);
|
|
351
|
+
return { status: raw.status, body: raw.data };
|
|
318
352
|
}
|
|
319
353
|
},
|
|
320
354
|
attachment: {
|
package/dist/createRecord.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* create-table / add-column. To UPDATE an existing record, use `set-field`.
|
|
13
13
|
*/
|
|
14
14
|
import type { ServiceNowClient } from "./client";
|
|
15
|
+
import type { RecordWriteResult } from "./setField";
|
|
15
16
|
export interface CreateRecordParams {
|
|
16
17
|
client?: ServiceNowClient;
|
|
17
18
|
table: string;
|
|
@@ -25,15 +26,8 @@ export interface CreateRecordParams {
|
|
|
25
26
|
ifAbsentQuery?: string;
|
|
26
27
|
dryRun?: boolean;
|
|
27
28
|
}
|
|
28
|
-
export interface CreateRecordResult {
|
|
29
|
+
export interface CreateRecordResult extends RecordWriteResult {
|
|
29
30
|
status: "dry-run" | "created" | "skipped" | "failed";
|
|
30
|
-
table: string;
|
|
31
|
-
sysId: string;
|
|
32
31
|
scope: string;
|
|
33
|
-
updateSetSysId: string;
|
|
34
|
-
fields: Record<string, string>;
|
|
35
|
-
after: Record<string, string>;
|
|
36
|
-
verified: boolean;
|
|
37
|
-
note: string;
|
|
38
32
|
}
|
|
39
33
|
export declare function createRecord(params: CreateRecordParams): Promise<CreateRecordResult>;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { createClient } from "./client";
|
|
8
8
|
export { createClientFromEnvFile, resolveConfigFromEnvFile } from "./createClientFromEnvFile";
|
|
9
|
-
export type { ServiceNowClient, TableQueryOptions, TableSchema, TableSchemaField, AttachmentMeta } from "./client";
|
|
9
|
+
export type { ServiceNowClient, TableQueryOptions, TableSchema, TableSchemaField, AttachmentMeta, NowInvokeMethod, NowInvokeParams, NowInvokeResponse } from "./client";
|
|
10
10
|
export { addChoicesToField } from "./choices";
|
|
11
11
|
export { hostAssets, classifyChunks, formatHostAssetsResult } from "./hostAssets";
|
|
12
12
|
export { formatAddChoicesResult } from "./formatter";
|
|
@@ -21,3 +21,9 @@ export type { TemplateRef, ListTemplatesParams, FlowKind, VerifyExpect, VerifyFo
|
|
|
21
21
|
export type { ServiceNowClientConfig, ChoiceValue, ChoiceType, AddChoicesParams, AddChoicesResult, ChoiceActionResult, DictionaryRecord, UpdateSetRecord, LayoutAction, LayoutRecordResult, LayoutResult, CreateViewParams, CreateViewResult, FormSectionSpec, SetFormLayoutParams, SetListLayoutParams, SetRelatedListsParams, ChunkRole, ChunkInfo, ChunkResult, PrunedResult, HostAssetsParams, HostAssetsResult } from "./types";
|
|
22
22
|
export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, applyAddColumnOverlay } from "./table";
|
|
23
23
|
export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec } from "./table";
|
|
24
|
+
export { setField } from "./setField";
|
|
25
|
+
export { createRecord } from "./createRecord";
|
|
26
|
+
export type { RecordWriteResult, SetFieldParams, SetFieldResult } from "./setField";
|
|
27
|
+
export type { CreateRecordParams, CreateRecordResult } from "./createRecord";
|
|
28
|
+
export { invokeRest, INVOKE_REST_METHODS } from "./invokeRest";
|
|
29
|
+
export type { InvokeRestParams, InvokeRestResult } from "./invokeRest";
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* REST API so every change lands in the target update set and scope.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.applyAddColumnOverlay = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.createClient = void 0;
|
|
9
|
+
exports.createRecord = exports.setField = exports.applyAddColumnOverlay = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.createClient = void 0;
|
|
10
|
+
exports.INVOKE_REST_METHODS = exports.invokeRest = void 0;
|
|
10
11
|
var client_1 = require("./client");
|
|
11
12
|
Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
|
|
12
13
|
var createClientFromEnvFile_1 = require("./createClientFromEnvFile");
|
|
@@ -68,3 +69,10 @@ Object.defineProperty(exports, "DEFAULT_SAVE_ACTION", { enumerable: true, get: f
|
|
|
68
69
|
Object.defineProperty(exports, "addColumn", { enumerable: true, get: function () { return table_1.addColumn; } });
|
|
69
70
|
Object.defineProperty(exports, "deriveElement", { enumerable: true, get: function () { return table_1.deriveElement; } });
|
|
70
71
|
Object.defineProperty(exports, "applyAddColumnOverlay", { enumerable: true, get: function () { return table_1.applyAddColumnOverlay; } });
|
|
72
|
+
var setField_1 = require("./setField");
|
|
73
|
+
Object.defineProperty(exports, "setField", { enumerable: true, get: function () { return setField_1.setField; } });
|
|
74
|
+
var createRecord_1 = require("./createRecord");
|
|
75
|
+
Object.defineProperty(exports, "createRecord", { enumerable: true, get: function () { return createRecord_1.createRecord; } });
|
|
76
|
+
var invokeRest_1 = require("./invokeRest");
|
|
77
|
+
Object.defineProperty(exports, "invokeRest", { enumerable: true, get: function () { return invokeRest_1.invokeRest; } });
|
|
78
|
+
Object.defineProperty(exports, "INVOKE_REST_METHODS", { enumerable: true, get: function () { return invokeRest_1.INVOKE_REST_METHODS; } });
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dove-sn invoke-rest — invoke an arbitrary authenticated ServiceNow REST
|
|
3
|
+
* operation (an application's own Scripted REST endpoints included) with
|
|
4
|
+
* GET / POST / PUT / DELETE. The transport primitive behind the invoke_rest
|
|
5
|
+
* MCP tool (TenonHQ/Dovetail#212).
|
|
6
|
+
*
|
|
7
|
+
* DRY-RUN BY DEFAULT: without confirm:true the resolved method + path + body
|
|
8
|
+
* are echoed back and nothing is sent (no client, no credentials needed).
|
|
9
|
+
* On confirm the response comes back verbatim as { httpStatus, ok, body } —
|
|
10
|
+
* non-2xx responses are returned, not thrown, so a Scripted REST operation's
|
|
11
|
+
* own error contract passes through faithfully (the transport still retries
|
|
12
|
+
* 429/5xx first).
|
|
13
|
+
*
|
|
14
|
+
* PRIVACY: these endpoints carry caller payloads. This module never logs a
|
|
15
|
+
* request or response body — no console output, no debug files; bodies exist
|
|
16
|
+
* only in the returned result. Any log line about an invocation elsewhere must
|
|
17
|
+
* carry method, path and status ONLY.
|
|
18
|
+
*/
|
|
19
|
+
import type { NowInvokeMethod, ServiceNowClient } from "./client";
|
|
20
|
+
export declare var INVOKE_REST_METHODS: Array<NowInvokeMethod>;
|
|
21
|
+
export interface InvokeRestParams {
|
|
22
|
+
/** Optional client injection (tests / reuse). Only resolved on the send path —
|
|
23
|
+
* a dry-run needs no credentials. */
|
|
24
|
+
client?: ServiceNowClient;
|
|
25
|
+
/** HTTP method — GET, POST, PUT or DELETE (case-insensitive). */
|
|
26
|
+
method: string;
|
|
27
|
+
/** Instance-relative path; must start with /api/ (e.g. /api/x_cadso_core/<service>/<resource>). */
|
|
28
|
+
path: string;
|
|
29
|
+
/** JSON request body for POST/PUT/DELETE. Rejected for GET. */
|
|
30
|
+
body?: unknown;
|
|
31
|
+
/** The send gate: the request is only sent when confirm is exactly true. */
|
|
32
|
+
confirm?: boolean;
|
|
33
|
+
/** Force a dry-run even when confirm is set. */
|
|
34
|
+
dryRun?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface InvokeRestResult {
|
|
37
|
+
status: "dry-run" | "sent";
|
|
38
|
+
method: NowInvokeMethod;
|
|
39
|
+
path: string;
|
|
40
|
+
/** Echo of the request body (present when one was supplied) so the plan is auditable. */
|
|
41
|
+
requestBody?: unknown;
|
|
42
|
+
/** HTTP status of the response (sent only). */
|
|
43
|
+
httpStatus?: number;
|
|
44
|
+
/** True when httpStatus is 2xx (sent only). */
|
|
45
|
+
ok?: boolean;
|
|
46
|
+
/** Response body, verbatim (sent only). */
|
|
47
|
+
body?: unknown;
|
|
48
|
+
note: string;
|
|
49
|
+
}
|
|
50
|
+
export declare function invokeRest(params: InvokeRestParams): Promise<InvokeRestResult>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* dove-sn invoke-rest — invoke an arbitrary authenticated ServiceNow REST
|
|
4
|
+
* operation (an application's own Scripted REST endpoints included) with
|
|
5
|
+
* GET / POST / PUT / DELETE. The transport primitive behind the invoke_rest
|
|
6
|
+
* MCP tool (TenonHQ/Dovetail#212).
|
|
7
|
+
*
|
|
8
|
+
* DRY-RUN BY DEFAULT: without confirm:true the resolved method + path + body
|
|
9
|
+
* are echoed back and nothing is sent (no client, no credentials needed).
|
|
10
|
+
* On confirm the response comes back verbatim as { httpStatus, ok, body } —
|
|
11
|
+
* non-2xx responses are returned, not thrown, so a Scripted REST operation's
|
|
12
|
+
* own error contract passes through faithfully (the transport still retries
|
|
13
|
+
* 429/5xx first).
|
|
14
|
+
*
|
|
15
|
+
* PRIVACY: these endpoints carry caller payloads. This module never logs a
|
|
16
|
+
* request or response body — no console output, no debug files; bodies exist
|
|
17
|
+
* only in the returned result. Any log line about an invocation elsewhere must
|
|
18
|
+
* carry method, path and status ONLY.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.INVOKE_REST_METHODS = void 0;
|
|
22
|
+
exports.invokeRest = invokeRest;
|
|
23
|
+
const client_1 = require("./client");
|
|
24
|
+
exports.INVOKE_REST_METHODS = ["GET", "POST", "PUT", "DELETE"];
|
|
25
|
+
function normalizeMethod(method) {
|
|
26
|
+
var raw = typeof method === "string" ? method.toUpperCase() : "";
|
|
27
|
+
if (exports.INVOKE_REST_METHODS.indexOf(raw) === -1) {
|
|
28
|
+
throw new Error("invoke-rest: method must be one of GET, POST, PUT, DELETE (got '"
|
|
29
|
+
+ String(method) + "').");
|
|
30
|
+
}
|
|
31
|
+
return raw;
|
|
32
|
+
}
|
|
33
|
+
function validatePath(path) {
|
|
34
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
35
|
+
throw new Error("invoke-rest: path is required.");
|
|
36
|
+
}
|
|
37
|
+
if (path.indexOf("/api/") !== 0) {
|
|
38
|
+
throw new Error("invoke-rest: path must be instance-relative and start with /api/ (got '"
|
|
39
|
+
+ path + "'). Absolute URLs are not accepted — the client's base URL supplies the instance.");
|
|
40
|
+
}
|
|
41
|
+
if (/\s/.test(path)) {
|
|
42
|
+
throw new Error("invoke-rest: path must not contain whitespace — URL-encode query values.");
|
|
43
|
+
}
|
|
44
|
+
return path;
|
|
45
|
+
}
|
|
46
|
+
async function invokeRest(params) {
|
|
47
|
+
if (!params || typeof params !== "object") {
|
|
48
|
+
throw new Error("invoke-rest: params object is required.");
|
|
49
|
+
}
|
|
50
|
+
var method = normalizeMethod(params.method);
|
|
51
|
+
var path = validatePath(params.path);
|
|
52
|
+
if (method === "GET" && params.body !== undefined) {
|
|
53
|
+
throw new Error("invoke-rest: a request body is not allowed with GET.");
|
|
54
|
+
}
|
|
55
|
+
var send = params.confirm === true && params.dryRun !== true;
|
|
56
|
+
if (!send) {
|
|
57
|
+
var dry = {
|
|
58
|
+
status: "dry-run",
|
|
59
|
+
method: method,
|
|
60
|
+
path: path,
|
|
61
|
+
note: "dry-run (the default): nothing was sent. Would " + method + " " + path
|
|
62
|
+
+ (params.body !== undefined ? " with the supplied JSON body" : " with no body")
|
|
63
|
+
+ ". Re-run with confirm to send."
|
|
64
|
+
};
|
|
65
|
+
if (params.body !== undefined) {
|
|
66
|
+
dry.requestBody = params.body;
|
|
67
|
+
}
|
|
68
|
+
return dry;
|
|
69
|
+
}
|
|
70
|
+
// Client is resolved only here so a dry-run never needs credentials.
|
|
71
|
+
var client = params.client || (0, client_1.createClient)({});
|
|
72
|
+
var response = await client.now.invoke({ method: method, path: path, body: params.body });
|
|
73
|
+
var ok = response.status >= 200 && response.status < 300;
|
|
74
|
+
var result = {
|
|
75
|
+
status: "sent",
|
|
76
|
+
method: method,
|
|
77
|
+
path: path,
|
|
78
|
+
httpStatus: response.status,
|
|
79
|
+
ok: ok,
|
|
80
|
+
body: response.body,
|
|
81
|
+
note: ok
|
|
82
|
+
? "Sent " + method + " " + path + " — HTTP " + response.status + "."
|
|
83
|
+
: "Sent " + method + " " + path + " — HTTP " + response.status
|
|
84
|
+
+ " (non-2xx returned verbatim"
|
|
85
|
+
+ (response.status === 401 || response.status === 403
|
|
86
|
+
? "; if unexpected, check SN_USER/SN_PASSWORD and ACLs" : "")
|
|
87
|
+
+ ")."
|
|
88
|
+
};
|
|
89
|
+
if (params.body !== undefined) {
|
|
90
|
+
result.requestBody = params.body;
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
package/dist/mcp/registry.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import type { ToolAnnotations } from "@tenonhq/dovetail-mcp-kit";
|
|
12
12
|
import type { ServiceNowClient } from "../client";
|
|
13
|
-
export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "host_assets"];
|
|
13
|
+
export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "set_field", "create_record", "host_assets", "invoke_rest"];
|
|
14
14
|
export type ToolName = typeof TOOL_NAMES[number];
|
|
15
15
|
export interface RegistryDeps {
|
|
16
16
|
/** Optional client injection for tests; defaults to createClient({}). */
|
package/dist/mcp/registry.js
CHANGED
|
@@ -27,6 +27,9 @@ const editFlow_1 = require("../flowDesigner/editFlow");
|
|
|
27
27
|
const testFlow_1 = require("../flowDesigner/testFlow");
|
|
28
28
|
const table_1 = require("../table");
|
|
29
29
|
const hostAssets_1 = require("../hostAssets");
|
|
30
|
+
const setField_1 = require("../setField");
|
|
31
|
+
const createRecord_1 = require("../createRecord");
|
|
32
|
+
const invokeRest_1 = require("../invokeRest");
|
|
30
33
|
const schemas_1 = require("./schemas");
|
|
31
34
|
exports.TOOL_NAMES = [
|
|
32
35
|
"create_view",
|
|
@@ -43,7 +46,10 @@ exports.TOOL_NAMES = [
|
|
|
43
46
|
"flow_edit",
|
|
44
47
|
"create_table",
|
|
45
48
|
"add_column",
|
|
46
|
-
"
|
|
49
|
+
"set_field",
|
|
50
|
+
"create_record",
|
|
51
|
+
"host_assets",
|
|
52
|
+
"invoke_rest"
|
|
47
53
|
];
|
|
48
54
|
// Annotation presets (READ_ONLY / WRITE_ADDITIVE_IDEMPOTENT / WRITE_CREATE /
|
|
49
55
|
// WRITE_OVERWRITE / WRITE_EXECUTE) come from @tenonhq/dovetail-mcp-kit.
|
|
@@ -296,6 +302,56 @@ function buildDescriptors(deps = {}) {
|
|
|
296
302
|
});
|
|
297
303
|
}
|
|
298
304
|
},
|
|
305
|
+
{
|
|
306
|
+
name: "set_field",
|
|
307
|
+
annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
|
|
308
|
+
description: "Set scalar field value(s) on an EXISTING ServiceNow data record, captured into a specified "
|
|
309
|
+
+ "update set, then READ BACK to verify each value landed. Wraps the update-set-aware "
|
|
310
|
+
+ "pushWithUpdateSet core op (no sys_user_preference mutation). Target the record by sysId, or "
|
|
311
|
+
+ "by a query that resolves to EXACTLY one row. REFUSES schema tables (sys_db_object / "
|
|
312
|
+
+ "sys_dictionary) — use add_column / create_table for those. fields is a flat name->string map "
|
|
313
|
+
+ "(sent as strings; ServiceNow coerces); updateSetSysId is required so the change is tracked; "
|
|
314
|
+
+ "dryRun:true reads the current values and returns the plan without writing. To INSERT a new "
|
|
315
|
+
+ "record use create_record.",
|
|
316
|
+
shape: schemas_1.setFieldSchema.shape,
|
|
317
|
+
handler: async function (args) {
|
|
318
|
+
var p = schemas_1.setFieldSchema.parse(args);
|
|
319
|
+
return (0, setField_1.setField)({
|
|
320
|
+
client: client(),
|
|
321
|
+
table: p.table,
|
|
322
|
+
sysId: p.sysId,
|
|
323
|
+
query: p.query,
|
|
324
|
+
fields: p.fields,
|
|
325
|
+
updateSetSysId: p.updateSetSysId,
|
|
326
|
+
dryRun: p.dryRun
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "create_record",
|
|
332
|
+
annotations: dovetail_mcp_kit_1.WRITE_CREATE,
|
|
333
|
+
description: "Create ONE new ServiceNow data record, owned by an explicit app scope and captured into a "
|
|
334
|
+
+ "specified update set, then READ BACK to verify. Wraps the scope- and update-set-aware "
|
|
335
|
+
+ "createRecord core op (switches app scope + update set server-side, inserts, restores both — "
|
|
336
|
+
+ "so the record lands in the right scope without sys_user_preference mutation). REFUSES schema "
|
|
337
|
+
+ "tables (sys_db_object / sys_dictionary) — use create_table / add_column for those. fields is a "
|
|
338
|
+
+ "flat name->string map; scope and updateSetSysId are required; ifAbsentQuery makes re-runs "
|
|
339
|
+
+ "idempotent (skips the insert when it already matches a row); dryRun:true returns the plan "
|
|
340
|
+
+ "without writing. To UPDATE an existing record use set_field.",
|
|
341
|
+
shape: schemas_1.createRecordSchema.shape,
|
|
342
|
+
handler: async function (args) {
|
|
343
|
+
var p = schemas_1.createRecordSchema.parse(args);
|
|
344
|
+
return (0, createRecord_1.createRecord)({
|
|
345
|
+
client: client(),
|
|
346
|
+
table: p.table,
|
|
347
|
+
fields: p.fields,
|
|
348
|
+
scope: p.scope,
|
|
349
|
+
updateSetSysId: p.updateSetSysId,
|
|
350
|
+
ifAbsentQuery: p.ifAbsentQuery,
|
|
351
|
+
dryRun: p.dryRun
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
},
|
|
299
355
|
{
|
|
300
356
|
name: "host_assets",
|
|
301
357
|
annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
|
|
@@ -313,6 +369,38 @@ function buildDescriptors(deps = {}) {
|
|
|
313
369
|
handler: async function (args) {
|
|
314
370
|
return (0, hostAssets_1.hostAssets)(client(), schemas_1.hostAssetsSchema.parse(args));
|
|
315
371
|
}
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: "invoke_rest",
|
|
375
|
+
annotations: dovetail_mcp_kit_1.WRITE_EXECUTE,
|
|
376
|
+
description: "Invoke an arbitrary authenticated ServiceNow REST operation — including an application's "
|
|
377
|
+
+ "own Scripted REST endpoints (/api/<scope>/<service>/<resource>) — with GET, POST, PUT or "
|
|
378
|
+
+ "DELETE. A transport primitive: it can drive update and DELETE operations, so it is "
|
|
379
|
+
+ "destructive-capable and non-idempotent. DRY-RUN BY DEFAULT — without confirm:true nothing "
|
|
380
|
+
+ "is sent and the resolved method + path + body are echoed back; dryRun:true forces a "
|
|
381
|
+
+ "dry-run even with confirm. On send, returns { httpStatus, ok, body } with the response "
|
|
382
|
+
+ "passed through verbatim — non-2xx responses are returned, not thrown, so the operation's "
|
|
383
|
+
+ "own error contract is preserved (429/5xx are retried by the transport first). path must "
|
|
384
|
+
+ "be instance-relative and start with /api/. Request/response bodies are never logged — "
|
|
385
|
+
+ "they exist only in this result. For sys_* / x_* record CRUD use set_field / "
|
|
386
|
+
+ "create_record instead; this tool is for operations those fixed verbs cannot express.",
|
|
387
|
+
shape: schemas_1.invokeRestSchema.shape,
|
|
388
|
+
handler: async function (args) {
|
|
389
|
+
var p = schemas_1.invokeRestSchema.parse(args);
|
|
390
|
+
var params = {
|
|
391
|
+
method: p.method,
|
|
392
|
+
path: p.path,
|
|
393
|
+
body: p.body,
|
|
394
|
+
confirm: p.confirm,
|
|
395
|
+
dryRun: p.dryRun
|
|
396
|
+
};
|
|
397
|
+
// Client resolution is lazy: a dry-run needs no credentials, so only
|
|
398
|
+
// attach one when injected (tests) — invokeRest creates its own on send.
|
|
399
|
+
if (deps.client) {
|
|
400
|
+
params.client = deps.client;
|
|
401
|
+
}
|
|
402
|
+
return (0, invokeRest_1.invokeRest)(params);
|
|
403
|
+
}
|
|
316
404
|
}
|
|
317
405
|
];
|
|
318
406
|
}
|
package/dist/mcp/schemas.d.ts
CHANGED
|
@@ -563,3 +563,66 @@ export declare var addColumnSchema: z.ZodObject<{
|
|
|
563
563
|
saveActionSysId?: string | undefined;
|
|
564
564
|
columnsRelId?: string | undefined;
|
|
565
565
|
}>;
|
|
566
|
+
export declare var setFieldSchema: z.ZodObject<{
|
|
567
|
+
table: z.ZodString;
|
|
568
|
+
sysId: z.ZodOptional<z.ZodString>;
|
|
569
|
+
query: z.ZodOptional<z.ZodString>;
|
|
570
|
+
fields: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
571
|
+
updateSetSysId: z.ZodString;
|
|
572
|
+
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
573
|
+
}, "strip", z.ZodTypeAny, {
|
|
574
|
+
table: string;
|
|
575
|
+
fields: Record<string, string>;
|
|
576
|
+
updateSetSysId: string;
|
|
577
|
+
query?: string | undefined;
|
|
578
|
+
sysId?: string | undefined;
|
|
579
|
+
dryRun?: boolean | undefined;
|
|
580
|
+
}, {
|
|
581
|
+
table: string;
|
|
582
|
+
fields: Record<string, string>;
|
|
583
|
+
updateSetSysId: string;
|
|
584
|
+
query?: string | undefined;
|
|
585
|
+
sysId?: string | undefined;
|
|
586
|
+
dryRun?: boolean | undefined;
|
|
587
|
+
}>;
|
|
588
|
+
export declare var createRecordSchema: z.ZodObject<{
|
|
589
|
+
table: z.ZodString;
|
|
590
|
+
fields: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
591
|
+
scope: z.ZodString;
|
|
592
|
+
updateSetSysId: z.ZodString;
|
|
593
|
+
ifAbsentQuery: z.ZodOptional<z.ZodString>;
|
|
594
|
+
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
595
|
+
}, "strip", z.ZodTypeAny, {
|
|
596
|
+
table: string;
|
|
597
|
+
fields: Record<string, string>;
|
|
598
|
+
scope: string;
|
|
599
|
+
updateSetSysId: string;
|
|
600
|
+
dryRun?: boolean | undefined;
|
|
601
|
+
ifAbsentQuery?: string | undefined;
|
|
602
|
+
}, {
|
|
603
|
+
table: string;
|
|
604
|
+
fields: Record<string, string>;
|
|
605
|
+
scope: string;
|
|
606
|
+
updateSetSysId: string;
|
|
607
|
+
dryRun?: boolean | undefined;
|
|
608
|
+
ifAbsentQuery?: string | undefined;
|
|
609
|
+
}>;
|
|
610
|
+
export declare var invokeRestSchema: z.ZodObject<{
|
|
611
|
+
method: z.ZodEffects<z.ZodEnum<["GET", "POST", "PUT", "DELETE"]>, "GET" | "DELETE" | "POST" | "PUT", unknown>;
|
|
612
|
+
path: z.ZodString;
|
|
613
|
+
body: z.ZodOptional<z.ZodUnknown>;
|
|
614
|
+
confirm: z.ZodOptional<z.ZodBoolean>;
|
|
615
|
+
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
616
|
+
}, "strip", z.ZodTypeAny, {
|
|
617
|
+
path: string;
|
|
618
|
+
method: "GET" | "DELETE" | "POST" | "PUT";
|
|
619
|
+
body?: unknown;
|
|
620
|
+
confirm?: boolean | undefined;
|
|
621
|
+
dryRun?: boolean | undefined;
|
|
622
|
+
}, {
|
|
623
|
+
path: string;
|
|
624
|
+
body?: unknown;
|
|
625
|
+
method?: unknown;
|
|
626
|
+
confirm?: boolean | undefined;
|
|
627
|
+
dryRun?: boolean | undefined;
|
|
628
|
+
}>;
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* own file so registry.ts stays focused on wiring.
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
|
|
7
|
+
exports.invokeRestSchema = exports.createRecordSchema = exports.setFieldSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
|
|
8
8
|
const zod_1 = require("zod");
|
|
9
9
|
exports.createViewSchema = zod_1.z.object({
|
|
10
10
|
name: zod_1.z.string().min(1),
|
|
@@ -156,3 +156,33 @@ exports.addColumnSchema = zod_1.z.object({
|
|
|
156
156
|
dryRun: zod_1.z.boolean().optional(),
|
|
157
157
|
debug: zod_1.z.boolean().optional()
|
|
158
158
|
});
|
|
159
|
+
// Data-record write verbs. Kept as plain z.object (no .refine wrapper) so
|
|
160
|
+
// registry.ts can read `.shape`; the deeper rules — one of sysId/query, at
|
|
161
|
+
// least one field, the schema-table refusal — are enforced by the core
|
|
162
|
+
// setField / createRecord functions, which throw clear errors.
|
|
163
|
+
exports.setFieldSchema = zod_1.z.object({
|
|
164
|
+
table: zod_1.z.string().min(1),
|
|
165
|
+
sysId: zod_1.z.string().optional(),
|
|
166
|
+
query: zod_1.z.string().optional(),
|
|
167
|
+
fields: zod_1.z.record(zod_1.z.string()),
|
|
168
|
+
updateSetSysId: zod_1.z.string().min(1),
|
|
169
|
+
dryRun: zod_1.z.boolean().optional()
|
|
170
|
+
});
|
|
171
|
+
exports.createRecordSchema = zod_1.z.object({
|
|
172
|
+
table: zod_1.z.string().min(1),
|
|
173
|
+
fields: zod_1.z.record(zod_1.z.string()),
|
|
174
|
+
scope: zod_1.z.string().min(1),
|
|
175
|
+
updateSetSysId: zod_1.z.string().min(1),
|
|
176
|
+
ifAbsentQuery: zod_1.z.string().optional(),
|
|
177
|
+
dryRun: zod_1.z.boolean().optional()
|
|
178
|
+
});
|
|
179
|
+
// invoke_rest: transport primitive for arbitrary authenticated REST operations
|
|
180
|
+
// (Scripted REST included). The dry-run-unless-confirm gate lives in invokeRest
|
|
181
|
+
// itself; the regex here rejects absolute URLs and non-/api/ paths early.
|
|
182
|
+
exports.invokeRestSchema = zod_1.z.object({
|
|
183
|
+
method: zod_1.z.preprocess(function (v) { return typeof v === "string" ? v.toUpperCase() : v; }, zod_1.z.enum(["GET", "POST", "PUT", "DELETE"])),
|
|
184
|
+
path: zod_1.z.string().min(1).regex(/^\/api\//, "path must be instance-relative and start with /api/"),
|
|
185
|
+
body: zod_1.z.unknown().optional(),
|
|
186
|
+
confirm: zod_1.z.boolean().optional(),
|
|
187
|
+
dryRun: zod_1.z.boolean().optional()
|
|
188
|
+
});
|
package/dist/setField.d.ts
CHANGED
|
@@ -23,19 +23,28 @@ export interface SetFieldParams {
|
|
|
23
23
|
updateSetSysId?: string;
|
|
24
24
|
dryRun?: boolean;
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Fields common to every record-write verb's result: the target, the update set
|
|
28
|
+
* the change was captured into, the requested field map, the read-back values,
|
|
29
|
+
* whether the read-back matched, and a human note. `set-field` and
|
|
30
|
+
* `create-record` each extend this with their own `status` union (and
|
|
31
|
+
* set-field's `before` snapshot / create-record's owning `scope`).
|
|
32
|
+
*/
|
|
33
|
+
export interface RecordWriteResult {
|
|
28
34
|
table: string;
|
|
29
35
|
sysId: string;
|
|
30
36
|
updateSetSysId: string;
|
|
31
37
|
fields: Record<string, string>;
|
|
32
|
-
before: Record<string, string>;
|
|
33
38
|
after: Record<string, string>;
|
|
34
39
|
verified: boolean;
|
|
35
40
|
note: string;
|
|
36
41
|
}
|
|
42
|
+
export interface SetFieldResult extends RecordWriteResult {
|
|
43
|
+
status: "dry-run" | "applied" | "failed";
|
|
44
|
+
before: Record<string, string>;
|
|
45
|
+
}
|
|
37
46
|
/** Coerce a Table-API field value to a comparable string. Reference/display
|
|
38
47
|
* fields (sysparm_display_value=false) come back as { link, value } objects. */
|
|
39
|
-
export declare function fieldToString(value:
|
|
40
|
-
export declare function pickFields(row: Record<string,
|
|
48
|
+
export declare function fieldToString(value: unknown): string;
|
|
49
|
+
export declare function pickFields(row: Record<string, unknown>, names: Array<string>): Record<string, string>;
|
|
41
50
|
export declare function setField(params: SetFieldParams): Promise<SetFieldResult>;
|
package/dist/setField.js
CHANGED
|
@@ -25,7 +25,10 @@ function fieldToString(value) {
|
|
|
25
25
|
if (value === undefined || value === null)
|
|
26
26
|
return "";
|
|
27
27
|
if (typeof value === "object") {
|
|
28
|
-
|
|
28
|
+
if ("value" in value && value.value !== undefined && value.value !== null) {
|
|
29
|
+
return String(value.value);
|
|
30
|
+
}
|
|
31
|
+
return "";
|
|
29
32
|
}
|
|
30
33
|
return String(value);
|
|
31
34
|
}
|