snow-flow 10.0.197 → 10.0.199
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/servicenow/servicenow-mcp-unified/tools/catalog/snow_order_catalog_item.ts +75 -18
- package/src/servicenow/servicenow-mcp-unified/tools/platform/snow_create_ui_policy.ts +25 -4
- package/src/servicenow/servicenow-mcp-unified/tools/ui-policies/link-ui-policy.ts +84 -0
- package/src/servicenow/servicenow-mcp-unified/tools/ui-policies/snow_create_ui_policy_action.ts +7 -25
package/package.json
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* snow_order_catalog_item - Order catalog item
|
|
3
3
|
*
|
|
4
|
-
* Orders a catalog item programmatically, creating a request (RITM) with
|
|
4
|
+
* Orders a catalog item programmatically, creating a request (RITM) with
|
|
5
|
+
* specified variable values.
|
|
6
|
+
*
|
|
7
|
+
* Operation order (avoids race condition with flows):
|
|
8
|
+
* 1. Fetch variable definitions (item_option_new)
|
|
9
|
+
* 2. Create sc_request
|
|
10
|
+
* 3. Pre-create sc_item_option records in parallel (no RITM needed yet)
|
|
11
|
+
* 4. Short delay to ensure DB commit
|
|
12
|
+
* 5. Create sc_req_item (RITM) — flow triggers here with variables ready
|
|
13
|
+
* 6. Create sc_item_option_mtom links in parallel
|
|
14
|
+
* 7. Two PATCHs to trigger Business Rules that associate the flow context
|
|
15
|
+
*
|
|
16
|
+
* Based on contribution by @frodoyoraul (PR #83).
|
|
5
17
|
*/
|
|
6
18
|
|
|
7
19
|
import { MCPToolDefinition, ServiceNowContext, ToolResult } from "../../shared/types.js"
|
|
8
20
|
import { getAuthenticatedClient } from "../../shared/auth.js"
|
|
9
21
|
import { createSuccessResult, createErrorResult } from "../../shared/error-handler.js"
|
|
10
22
|
|
|
23
|
+
// Delay between sc_item_option creation and RITM insert to ensure DB commit
|
|
24
|
+
const VARIABLE_COMMIT_DELAY_MS = 1000
|
|
25
|
+
|
|
11
26
|
export const toolDefinition: MCPToolDefinition = {
|
|
12
27
|
name: "snow_order_catalog_item",
|
|
13
28
|
description: "Orders a catalog item programmatically, creating a request (RITM) with specified variable values.",
|
|
14
|
-
// Metadata for tool discovery (not sent to LLM)
|
|
15
29
|
category: "itsm",
|
|
16
30
|
subcategory: "service-catalog",
|
|
17
31
|
use_cases: ["ordering", "automation", "ritm"],
|
|
18
32
|
complexity: "intermediate",
|
|
19
33
|
frequency: "high",
|
|
20
34
|
|
|
21
|
-
// Permission enforcement
|
|
22
|
-
// Classification: WRITE - Write operation based on name pattern
|
|
23
35
|
permission: "write",
|
|
24
36
|
allowedRoles: ["developer", "admin"],
|
|
25
37
|
inputSchema: {
|
|
@@ -36,46 +48,89 @@ export const toolDefinition: MCPToolDefinition = {
|
|
|
36
48
|
},
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
52
|
+
|
|
39
53
|
export async function execute(args: any, context: ServiceNowContext): Promise<ToolResult> {
|
|
40
54
|
const { cat_item, requested_for, variables, quantity = 1, delivery_address, special_instructions } = args
|
|
41
55
|
|
|
42
56
|
try {
|
|
43
57
|
const client = await getAuthenticatedClient(context)
|
|
44
58
|
|
|
45
|
-
//
|
|
59
|
+
// 1. Fetch variable definitions before creating the RITM
|
|
60
|
+
const varDefsResponse = await client.get("/api/now/table/item_option_new", {
|
|
61
|
+
params: {
|
|
62
|
+
sysparm_query: "cat_item=" + cat_item,
|
|
63
|
+
sysparm_fields: "sys_id,name",
|
|
64
|
+
sysparm_limit: 100,
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
const varDefs = varDefsResponse.data.result || []
|
|
68
|
+
const varMap = new Map<string, string>()
|
|
69
|
+
varDefs.forEach((def: any) => {
|
|
70
|
+
if (def.name) varMap.set(def.name, def.sys_id)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// 2. Create sc_request
|
|
46
74
|
const requestData: any = {
|
|
47
75
|
requested_for,
|
|
48
76
|
opened_by: requested_for,
|
|
49
77
|
}
|
|
50
|
-
|
|
51
78
|
if (special_instructions) requestData.special_instructions = special_instructions
|
|
52
79
|
|
|
53
80
|
const requestResponse = await client.post("/api/now/table/sc_request", requestData)
|
|
54
81
|
const requestId = requestResponse.data.result.sys_id
|
|
55
82
|
|
|
56
|
-
//
|
|
83
|
+
// 3. Pre-create sc_item_option records in parallel (no RITM needed yet)
|
|
84
|
+
const varEntries = variables ? Object.entries(variables).filter(([name]) => varMap.has(name)) : []
|
|
85
|
+
const skippedVars = variables ? Object.keys(variables).filter((name) => !varMap.has(name)) : []
|
|
86
|
+
|
|
87
|
+
const optionSysIds: Array<{ optionSysId: string }> = await Promise.all(
|
|
88
|
+
varEntries.map(async ([varName, varValue]) => {
|
|
89
|
+
const defSysId = varMap.get(varName)!
|
|
90
|
+
const optResp = await client.post("/api/now/table/sc_item_option", {
|
|
91
|
+
item_option_new: defSysId,
|
|
92
|
+
value: varValue,
|
|
93
|
+
})
|
|
94
|
+
return { optionSysId: optResp.data.result.sys_id }
|
|
95
|
+
}),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// 4. Wait for sc_item_option records to be committed in DB
|
|
99
|
+
if (optionSysIds.length > 0) {
|
|
100
|
+
await sleep(VARIABLE_COMMIT_DELAY_MS)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 5. Create sc_req_item (RITM) — flow triggers here with variables already ready
|
|
57
104
|
const ritmData: any = {
|
|
58
105
|
request: requestId,
|
|
59
106
|
cat_item,
|
|
60
107
|
requested_for,
|
|
61
108
|
quantity,
|
|
62
109
|
}
|
|
63
|
-
|
|
64
110
|
if (delivery_address) ritmData.delivery_address = delivery_address
|
|
65
111
|
|
|
66
112
|
const ritmResponse = await client.post("/api/now/table/sc_req_item", ritmData)
|
|
67
113
|
const ritmId = ritmResponse.data.result.sys_id
|
|
68
114
|
const ritmNumber = ritmResponse.data.result.number
|
|
69
115
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
116
|
+
// 6. Create sc_item_option_mtom links in parallel
|
|
117
|
+
await Promise.all(
|
|
118
|
+
optionSysIds.map(({ optionSysId }) =>
|
|
119
|
+
client.post("/api/now/table/sc_item_option_mtom", {
|
|
74
120
|
request_item: ritmId,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
121
|
+
sc_item_option: optionSysId,
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
// 7. Two PATCHs to trigger Business Rules that associate the flow context.
|
|
127
|
+
// sys_mod_count is auto-incremented by ServiceNow (the value we send is
|
|
128
|
+
// ignored), but the PATCH itself is a real update that fires BR triggers.
|
|
129
|
+
// Two updates are needed: the first associates the flow, the second
|
|
130
|
+
// ensures the flow context is fully propagated.
|
|
131
|
+
if (optionSysIds.length > 0) {
|
|
132
|
+
await client.patch("/api/now/table/sc_req_item/" + ritmId, { sys_mod_count: "1" })
|
|
133
|
+
await client.patch("/api/now/table/sc_req_item/" + ritmId, { sys_mod_count: "1" })
|
|
79
134
|
}
|
|
80
135
|
|
|
81
136
|
return createSuccessResult(
|
|
@@ -85,6 +140,8 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
85
140
|
ritm_id: ritmId,
|
|
86
141
|
ritm_number: ritmNumber,
|
|
87
142
|
quantity,
|
|
143
|
+
variables_processed: varEntries.length,
|
|
144
|
+
variables_skipped: skippedVars.length > 0 ? skippedVars : undefined,
|
|
88
145
|
},
|
|
89
146
|
{
|
|
90
147
|
operation: "order_catalog_item",
|
|
@@ -97,5 +154,5 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
97
154
|
}
|
|
98
155
|
}
|
|
99
156
|
|
|
100
|
-
export const version = "
|
|
101
|
-
export const author = "Snow-Flow
|
|
157
|
+
export const version = "2.0.0"
|
|
158
|
+
export const author = "Snow-Flow"
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { MCPToolDefinition, ServiceNowContext, ToolResult } from "../../shared/types.js"
|
|
9
9
|
import { getAuthenticatedClient } from "../../shared/auth.js"
|
|
10
10
|
import { createSuccessResult, createErrorResult } from "../../shared/error-handler.js"
|
|
11
|
+
import { linkUiPolicyReference, verifyUiPolicyLink } from "../ui-policies/link-ui-policy.js"
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Extract sys_id from API response - handles both string and object formats
|
|
@@ -129,6 +130,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
129
130
|
|
|
130
131
|
// Create UI Policy Actions
|
|
131
132
|
const createdActions = []
|
|
133
|
+
const unlinkableActions: string[] = []
|
|
132
134
|
for (const action of actions) {
|
|
133
135
|
const actionData: any = {
|
|
134
136
|
ui_policy: policySysId,
|
|
@@ -140,10 +142,20 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
const actionResponse = await client.post("/api/now/table/sys_ui_policy_action", actionData)
|
|
143
|
-
|
|
145
|
+
const actionResult = actionResponse.data.result
|
|
146
|
+
const actionSysId = extractSysId(actionResult.sys_id)
|
|
147
|
+
|
|
148
|
+
// Verify ui_policy link; if POST didn't set it, use fallback chain
|
|
149
|
+
const alreadyLinked = await verifyUiPolicyLink(client, actionSysId, policySysId)
|
|
150
|
+
if (!alreadyLinked) {
|
|
151
|
+
const linked = await linkUiPolicyReference(client, actionSysId, policySysId)
|
|
152
|
+
if (!linked) unlinkableActions.push(action.field_name)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
createdActions.push(actionResult)
|
|
144
156
|
}
|
|
145
157
|
|
|
146
|
-
|
|
158
|
+
const result: Record<string, any> = {
|
|
147
159
|
created: true,
|
|
148
160
|
ui_policy: {
|
|
149
161
|
sys_id: policySysId,
|
|
@@ -161,7 +173,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
161
173
|
mandatory: action.mandatory === "true",
|
|
162
174
|
readonly: action.readonly === "true",
|
|
163
175
|
cleared: action.cleared === "true",
|
|
164
|
-
|
|
176
|
+
ui_policy_linked: !unlinkableActions.includes(action.field),
|
|
165
177
|
})),
|
|
166
178
|
total_actions: createdActions.length,
|
|
167
179
|
best_practices: [
|
|
@@ -171,7 +183,16 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
171
183
|
"Order matters when multiple policies affect same field",
|
|
172
184
|
"Test with different user roles and data",
|
|
173
185
|
],
|
|
174
|
-
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (unlinkableActions.length > 0) {
|
|
189
|
+
result.warning =
|
|
190
|
+
"Some actions could not be linked to the UI policy: " +
|
|
191
|
+
unlinkableActions.join(", ") +
|
|
192
|
+
". Link them manually in the ServiceNow UI."
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return createSuccessResult(result)
|
|
175
196
|
} catch (error: any) {
|
|
176
197
|
return createErrorResult(error.message)
|
|
177
198
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper to link a sys_ui_policy_action record to its parent UI policy.
|
|
3
|
+
*
|
|
4
|
+
* ServiceNow's REST Table API POST silently ignores the "ui_policy" reference
|
|
5
|
+
* field on sys_ui_policy_action during creation. This module provides a
|
|
6
|
+
* cascading fallback (PATCH → PUT → GlideRecord) to set the reference after
|
|
7
|
+
* the record has been created.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AxiosInstance } from "axios"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Verify that the ui_policy reference field is set on a sys_ui_policy_action record.
|
|
14
|
+
*/
|
|
15
|
+
export async function verifyUiPolicyLink(
|
|
16
|
+
client: AxiosInstance,
|
|
17
|
+
actionSysId: string,
|
|
18
|
+
expectedSysId: string,
|
|
19
|
+
): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
const res = await client.get(
|
|
22
|
+
"/api/now/table/sys_ui_policy_action/" + actionSysId + "?sysparm_fields=ui_policy",
|
|
23
|
+
)
|
|
24
|
+
const ref = res.data.result?.ui_policy
|
|
25
|
+
const val = typeof ref === "object" && ref !== null ? ref.value : ref
|
|
26
|
+
return !!val && val !== "" && val === expectedSysId
|
|
27
|
+
} catch {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Attempt to link a sys_ui_policy_action record to its parent UI policy using
|
|
34
|
+
* a cascading fallback chain:
|
|
35
|
+
* Tier 1 — PATCH (standard Table API update)
|
|
36
|
+
* Tier 2 — PUT (full record update)
|
|
37
|
+
* Tier 3 — GlideRecord via Scripted REST endpoint (last resort)
|
|
38
|
+
*
|
|
39
|
+
* Returns true if the link was successfully set and verified.
|
|
40
|
+
*/
|
|
41
|
+
export async function linkUiPolicyReference(
|
|
42
|
+
client: AxiosInstance,
|
|
43
|
+
actionSysId: string,
|
|
44
|
+
uiPolicySysId: string,
|
|
45
|
+
): Promise<boolean> {
|
|
46
|
+
const url = "/api/now/table/sys_ui_policy_action/" + actionSysId
|
|
47
|
+
const body = { ui_policy: uiPolicySysId }
|
|
48
|
+
|
|
49
|
+
// Tier 1: PATCH
|
|
50
|
+
try {
|
|
51
|
+
await client.patch(url, body)
|
|
52
|
+
if (await verifyUiPolicyLink(client, actionSysId, uiPolicySysId)) return true
|
|
53
|
+
} catch {
|
|
54
|
+
// fall through
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Tier 2: PUT
|
|
58
|
+
try {
|
|
59
|
+
await client.put(url, body)
|
|
60
|
+
if (await verifyUiPolicyLink(client, actionSysId, uiPolicySysId)) return true
|
|
61
|
+
} catch {
|
|
62
|
+
// fall through
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Tier 3: GlideRecord via Scripted REST endpoint
|
|
66
|
+
try {
|
|
67
|
+
const script =
|
|
68
|
+
"var gr = new GlideRecord('sys_ui_policy_action');\n" +
|
|
69
|
+
"gr.get('" + actionSysId + "');\n" +
|
|
70
|
+
"gr.setValue('ui_policy', '" + uiPolicySysId + "');\n" +
|
|
71
|
+
"gr.update();\n" +
|
|
72
|
+
"gs.print(gr.getValue('ui_policy'));"
|
|
73
|
+
|
|
74
|
+
await client.post("/api/snow_flow_exec/execute", {
|
|
75
|
+
script,
|
|
76
|
+
execution_id: "link_ui_policy_" + actionSysId,
|
|
77
|
+
})
|
|
78
|
+
if (await verifyUiPolicyLink(client, actionSysId, uiPolicySysId)) return true
|
|
79
|
+
} catch {
|
|
80
|
+
// all tiers exhausted
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return false
|
|
84
|
+
}
|
package/src/servicenow/servicenow-mcp-unified/tools/ui-policies/snow_create_ui_policy_action.ts
CHANGED
|
@@ -10,18 +10,15 @@
|
|
|
10
10
|
* - "disabled" is the actual column name for "Read only" in the UI
|
|
11
11
|
* - "table" is auto-derived from the parent UI policy but must be sent for field validation
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* ServiceNow platform restriction for parent-child reference fields.
|
|
17
|
-
* Workaround: after Table API creation, a direct XML POST is attempted to set
|
|
18
|
-
* the ui_policy reference. If the XML POST also fails, the action is created
|
|
19
|
-
* without the reference — it can be linked manually in the UI.
|
|
13
|
+
* The "ui_policy" reference field is set via POST during creation. If the
|
|
14
|
+
* platform silently ignores it, a cascading fallback (PATCH → PUT → GlideRecord)
|
|
15
|
+
* ensures the link is established.
|
|
20
16
|
*/
|
|
21
17
|
|
|
22
18
|
import type { MCPToolDefinition, ServiceNowContext, ToolResult } from "../../shared/types.js"
|
|
23
19
|
import { getAuthenticatedClient } from "../../shared/auth.js"
|
|
24
20
|
import { createSuccessResult, createErrorResult } from "../../shared/error-handler.js"
|
|
21
|
+
import { linkUiPolicyReference, verifyUiPolicyLink } from "./link-ui-policy.js"
|
|
25
22
|
|
|
26
23
|
export const toolDefinition: MCPToolDefinition = {
|
|
27
24
|
name: "snow_create_ui_policy_action",
|
|
@@ -113,24 +110,9 @@ export async function execute(args: Record<string, unknown>, context: ServiceNow
|
|
|
113
110
|
const action = response.data.result
|
|
114
111
|
const actionSysId = action.sys_id?.value || action.sys_id
|
|
115
112
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"<record>" + "<sys_id>" + actionSysId + "</sys_id>" + "<ui_policy>" + uid + "</ui_policy>" + "</record>"
|
|
120
|
-
await client.post("/sys_ui_policy_action.do?XML&sys_id=" + actionSysId, xmlBody, {
|
|
121
|
-
headers: { "Content-Type": "application/xml" },
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
const verify = await client.get(
|
|
125
|
-
"/api/now/table/sys_ui_policy_action/" + actionSysId + "?sysparm_fields=ui_policy",
|
|
126
|
-
)
|
|
127
|
-
const ref = verify.data.result?.ui_policy
|
|
128
|
-
const val = typeof ref === "object" && ref !== null ? ref.value : ref
|
|
129
|
-
return !!val && val !== ""
|
|
130
|
-
} catch (_e) {
|
|
131
|
-
return false
|
|
132
|
-
}
|
|
133
|
-
})()
|
|
113
|
+
// Check if the POST already set the reference; if not, use the fallback chain
|
|
114
|
+
const alreadyLinked = await verifyUiPolicyLink(client, actionSysId, uid)
|
|
115
|
+
const linked = alreadyLinked || (await linkUiPolicyReference(client, actionSysId, uid))
|
|
134
116
|
|
|
135
117
|
const refVal = action.ui_policy
|
|
136
118
|
const resolvedRef = typeof refVal === "object" && refVal !== null ? refVal.value : refVal
|