@tenonhq/dovetail-servicenow 0.0.25 → 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 +37 -2
- package/dist/cli.js +82 -0
- package/dist/client.d.ts +29 -0
- package/dist/client.js +42 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -0
- 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 +35 -1
- package/dist/mcp/schemas.d.ts +19 -0
- package/dist/mcp/schemas.js +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -319,6 +319,39 @@ var r = await setField({
|
|
|
319
319
|
console.log(r.status, r.verified); // "applied" true
|
|
320
320
|
```
|
|
321
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
|
+
|
|
322
355
|
`test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
|
|
323
356
|
declared variables?) that never runs the flow; `--execute --confirm` runs it via
|
|
324
357
|
the server-side FlowAPI runner (deploy `resources/runFlow.md` first).
|
|
@@ -397,8 +430,10 @@ tools `flow_view` (read a flow/subflow's step graph), `action_view` (read an act
|
|
|
397
430
|
type's model), `flow_publish` (compile a flow/subflow snapshot), `flow_copy`
|
|
398
431
|
(copy a flow as an inactive draft), `flow_create` (create a NEW flow from scratch +
|
|
399
432
|
publish, grafting a template), `flow_test` (validate or run a flow), and
|
|
400
|
-
`flow_edit` (patch a flow)
|
|
401
|
-
|
|
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.
|
|
402
437
|
|
|
403
438
|
```bash
|
|
404
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. */
|
|
@@ -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/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";
|
|
@@ -25,3 +25,5 @@ export { setField } from "./setField";
|
|
|
25
25
|
export { createRecord } from "./createRecord";
|
|
26
26
|
export type { RecordWriteResult, SetFieldParams, SetFieldResult } from "./setField";
|
|
27
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
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
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");
|
|
@@ -72,3 +73,6 @@ var setField_1 = require("./setField");
|
|
|
72
73
|
Object.defineProperty(exports, "setField", { enumerable: true, get: function () { return setField_1.setField; } });
|
|
73
74
|
var createRecord_1 = require("./createRecord");
|
|
74
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", "set_field", "create_record", "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
|
@@ -29,6 +29,7 @@ const table_1 = require("../table");
|
|
|
29
29
|
const hostAssets_1 = require("../hostAssets");
|
|
30
30
|
const setField_1 = require("../setField");
|
|
31
31
|
const createRecord_1 = require("../createRecord");
|
|
32
|
+
const invokeRest_1 = require("../invokeRest");
|
|
32
33
|
const schemas_1 = require("./schemas");
|
|
33
34
|
exports.TOOL_NAMES = [
|
|
34
35
|
"create_view",
|
|
@@ -47,7 +48,8 @@ exports.TOOL_NAMES = [
|
|
|
47
48
|
"add_column",
|
|
48
49
|
"set_field",
|
|
49
50
|
"create_record",
|
|
50
|
-
"host_assets"
|
|
51
|
+
"host_assets",
|
|
52
|
+
"invoke_rest"
|
|
51
53
|
];
|
|
52
54
|
// Annotation presets (READ_ONLY / WRITE_ADDITIVE_IDEMPOTENT / WRITE_CREATE /
|
|
53
55
|
// WRITE_OVERWRITE / WRITE_EXECUTE) come from @tenonhq/dovetail-mcp-kit.
|
|
@@ -367,6 +369,38 @@ function buildDescriptors(deps = {}) {
|
|
|
367
369
|
handler: async function (args) {
|
|
368
370
|
return (0, hostAssets_1.hostAssets)(client(), schemas_1.hostAssetsSchema.parse(args));
|
|
369
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
|
+
}
|
|
370
404
|
}
|
|
371
405
|
];
|
|
372
406
|
}
|
package/dist/mcp/schemas.d.ts
CHANGED
|
@@ -607,3 +607,22 @@ export declare var createRecordSchema: z.ZodObject<{
|
|
|
607
607
|
dryRun?: boolean | undefined;
|
|
608
608
|
ifAbsentQuery?: string | undefined;
|
|
609
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.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;
|
|
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),
|
|
@@ -176,3 +176,13 @@ exports.createRecordSchema = zod_1.z.object({
|
|
|
176
176
|
ifAbsentQuery: zod_1.z.string().optional(),
|
|
177
177
|
dryRun: zod_1.z.boolean().optional()
|
|
178
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
|
+
});
|