snow-flow 10.0.1-dev.362 → 10.0.1-dev.389
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/shared/tool-registry.ts +2 -0
- package/src/servicenow/servicenow-mcp-unified/tools/flow-designer/index.ts +5 -0
- package/src/servicenow/servicenow-mcp-unified/tools/flow-designer/snow_manage_flow.ts +809 -0
- package/src/servicenow/servicenow-mcp-unified/tools/workflow/snow_workflow_manage.ts +3 -7
package/package.json
CHANGED
|
@@ -82,6 +82,7 @@ import * as integrationTools from '../tools/integration/index.js';
|
|
|
82
82
|
import * as platformTools from '../tools/platform/index.js';
|
|
83
83
|
import * as devopsTools from '../tools/devops/index.js';
|
|
84
84
|
import * as virtualAgentTools from '../tools/virtual-agent/index.js';
|
|
85
|
+
import * as flowDesignerTools from '../tools/flow-designer/index.js';
|
|
85
86
|
|
|
86
87
|
// ES Module compatible __dirname
|
|
87
88
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -148,6 +149,7 @@ const STATIC_TOOL_MODULES: Record<string, any> = {
|
|
|
148
149
|
'platform': platformTools,
|
|
149
150
|
'devops': devopsTools,
|
|
150
151
|
'virtual-agent': virtualAgentTools,
|
|
152
|
+
'flow-designer': flowDesignerTools,
|
|
151
153
|
};
|
|
152
154
|
|
|
153
155
|
export class ToolRegistry {
|
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* snow_manage_flow - Complete Flow Designer lifecycle management
|
|
3
|
+
*
|
|
4
|
+
* Create, list, get, update, activate, deactivate, delete and publish
|
|
5
|
+
* Flow Designer flows and subflows via Table API.
|
|
6
|
+
*
|
|
7
|
+
* ServiceNow does NOT expose a public Flow Designer creation API.
|
|
8
|
+
* This tool uses the Table API (sys_hub_flow) to manage flows directly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { MCPToolDefinition, ServiceNowContext, ToolResult } from '../../shared/types.js';
|
|
12
|
+
import { getAuthenticatedClient } from '../../shared/auth.js';
|
|
13
|
+
import { createSuccessResult, createErrorResult, SnowFlowError, ErrorType } from '../../shared/error-handler.js';
|
|
14
|
+
import { summary } from '../../shared/output-formatter.js';
|
|
15
|
+
|
|
16
|
+
// ── helpers ────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function sanitizeInternalName(name: string): string {
|
|
19
|
+
return name
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
22
|
+
.replace(/_+/g, '_')
|
|
23
|
+
.replace(/^_|_$/g, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isSysId(value: string): boolean {
|
|
27
|
+
return /^[a-f0-9]{32}$/.test(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a flow name to its sys_id. If the value is already a 32-char hex
|
|
32
|
+
* string it is returned as-is.
|
|
33
|
+
*/
|
|
34
|
+
async function resolveFlowId(client: any, flowId: string): Promise<string> {
|
|
35
|
+
if (isSysId(flowId)) return flowId;
|
|
36
|
+
|
|
37
|
+
var lookup = await client.get('/api/now/table/sys_hub_flow', {
|
|
38
|
+
params: {
|
|
39
|
+
sysparm_query: 'name=' + flowId,
|
|
40
|
+
sysparm_fields: 'sys_id',
|
|
41
|
+
sysparm_limit: 1
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!lookup.data.result || lookup.data.result.length === 0) {
|
|
46
|
+
throw new SnowFlowError(ErrorType.NOT_FOUND, 'Flow not found: ' + flowId);
|
|
47
|
+
}
|
|
48
|
+
return lookup.data.result[0].sys_id;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── tool definition ────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export const toolDefinition: MCPToolDefinition = {
|
|
54
|
+
name: 'snow_manage_flow',
|
|
55
|
+
description: 'Complete Flow Designer lifecycle: create flows/subflows, list, get details, update, activate, deactivate, delete and publish',
|
|
56
|
+
category: 'automation',
|
|
57
|
+
subcategory: 'flow-designer',
|
|
58
|
+
use_cases: ['flow-designer', 'automation', 'flow-management', 'subflow'],
|
|
59
|
+
complexity: 'advanced',
|
|
60
|
+
frequency: 'high',
|
|
61
|
+
permission: 'write',
|
|
62
|
+
allowedRoles: ['developer', 'admin'],
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
action: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
enum: ['create', 'create_subflow', 'list', 'get', 'update', 'activate', 'deactivate', 'delete', 'publish'],
|
|
69
|
+
description: 'Action to perform'
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// ── shared identifiers ──
|
|
73
|
+
flow_id: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description: 'Flow sys_id or name (required for get, update, activate, deactivate, delete, publish)'
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// ── create / create_subflow params ──
|
|
79
|
+
name: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'Flow name (required for create / create_subflow)'
|
|
82
|
+
},
|
|
83
|
+
description: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'Flow description'
|
|
86
|
+
},
|
|
87
|
+
trigger_type: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
enum: ['record_created', 'record_updated', 'scheduled', 'manual'],
|
|
90
|
+
description: 'Trigger type (create only, default: manual)',
|
|
91
|
+
default: 'manual'
|
|
92
|
+
},
|
|
93
|
+
table: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Table for record-based triggers (e.g. "incident") or list filter'
|
|
96
|
+
},
|
|
97
|
+
trigger_condition: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
description: 'Encoded query condition for the trigger'
|
|
100
|
+
},
|
|
101
|
+
category: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: 'Flow category',
|
|
104
|
+
default: 'custom'
|
|
105
|
+
},
|
|
106
|
+
run_as: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
enum: ['user', 'system'],
|
|
109
|
+
description: 'Run-as context (default: user)',
|
|
110
|
+
default: 'user'
|
|
111
|
+
},
|
|
112
|
+
activities: {
|
|
113
|
+
type: 'array',
|
|
114
|
+
description: 'Flow action steps',
|
|
115
|
+
items: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
name: { type: 'string', description: 'Step name' },
|
|
119
|
+
type: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
enum: ['notification', 'field_update', 'create_record', 'script', 'log', 'wait', 'approval'],
|
|
122
|
+
description: 'Action type'
|
|
123
|
+
},
|
|
124
|
+
inputs: { type: 'object', description: 'Step-specific input values' }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
inputs: {
|
|
129
|
+
type: 'array',
|
|
130
|
+
description: 'Flow input variables',
|
|
131
|
+
items: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
name: { type: 'string' },
|
|
135
|
+
label: { type: 'string' },
|
|
136
|
+
type: { type: 'string', enum: ['string', 'integer', 'boolean', 'reference', 'object', 'array'] },
|
|
137
|
+
mandatory: { type: 'boolean' },
|
|
138
|
+
default_value: { type: 'string' }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
outputs: {
|
|
143
|
+
type: 'array',
|
|
144
|
+
description: 'Flow output variables',
|
|
145
|
+
items: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {
|
|
148
|
+
name: { type: 'string' },
|
|
149
|
+
label: { type: 'string' },
|
|
150
|
+
type: { type: 'string', enum: ['string', 'integer', 'boolean', 'reference', 'object', 'array'] }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
activate: {
|
|
155
|
+
type: 'boolean',
|
|
156
|
+
description: 'Activate flow after creation (default: true)',
|
|
157
|
+
default: true
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
// ── list params ──
|
|
161
|
+
type: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
enum: ['flow', 'subflow', 'all'],
|
|
164
|
+
description: 'Filter by type (list only, default: all)',
|
|
165
|
+
default: 'all'
|
|
166
|
+
},
|
|
167
|
+
active_only: {
|
|
168
|
+
type: 'boolean',
|
|
169
|
+
description: 'Only list active flows (default: true)',
|
|
170
|
+
default: true
|
|
171
|
+
},
|
|
172
|
+
limit: {
|
|
173
|
+
type: 'number',
|
|
174
|
+
description: 'Max results for list',
|
|
175
|
+
default: 50
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// ── update params ──
|
|
179
|
+
update_fields: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
description: 'Fields to update (for update action)',
|
|
182
|
+
properties: {
|
|
183
|
+
name: { type: 'string' },
|
|
184
|
+
description: { type: 'string' },
|
|
185
|
+
category: { type: 'string' },
|
|
186
|
+
run_as: { type: 'string' }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
required: ['action']
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// ── execute ────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
export async function execute(args: any, context: ServiceNowContext): Promise<ToolResult> {
|
|
197
|
+
var action = args.action;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
var client = await getAuthenticatedClient(context);
|
|
201
|
+
|
|
202
|
+
switch (action) {
|
|
203
|
+
|
|
204
|
+
// ────────────────────────────────────────────────────────────────
|
|
205
|
+
// CREATE
|
|
206
|
+
// ────────────────────────────────────────────────────────────────
|
|
207
|
+
case 'create':
|
|
208
|
+
case 'create_subflow': {
|
|
209
|
+
var flowName = args.name;
|
|
210
|
+
if (!flowName) {
|
|
211
|
+
throw new SnowFlowError(ErrorType.VALIDATION_ERROR, 'name is required for ' + action);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
var isSubflow = action === 'create_subflow';
|
|
215
|
+
var flowDescription = args.description || flowName;
|
|
216
|
+
var triggerType = isSubflow ? 'manual' : (args.trigger_type || 'manual');
|
|
217
|
+
var flowTable = args.table || '';
|
|
218
|
+
var triggerCondition = args.trigger_condition || '';
|
|
219
|
+
var flowCategory = args.category || 'custom';
|
|
220
|
+
var flowRunAs = isSubflow ? 'user_who_calls' : (args.run_as || 'user');
|
|
221
|
+
var activitiesArg = args.activities || [];
|
|
222
|
+
var inputsArg = args.inputs || [];
|
|
223
|
+
var outputsArg = args.outputs || [];
|
|
224
|
+
var shouldActivate = args.activate !== false;
|
|
225
|
+
|
|
226
|
+
// Build flow_definition JSON
|
|
227
|
+
var flowDefinition: any = {
|
|
228
|
+
name: flowName,
|
|
229
|
+
description: flowDescription,
|
|
230
|
+
trigger: {
|
|
231
|
+
type: triggerType,
|
|
232
|
+
table: flowTable,
|
|
233
|
+
condition: triggerCondition
|
|
234
|
+
},
|
|
235
|
+
activities: activitiesArg.map(function (act: any, idx: number) {
|
|
236
|
+
return {
|
|
237
|
+
name: act.name,
|
|
238
|
+
label: act.name,
|
|
239
|
+
type: act.type || 'script',
|
|
240
|
+
inputs: act.inputs || {},
|
|
241
|
+
order: (idx + 1) * 100,
|
|
242
|
+
active: true
|
|
243
|
+
};
|
|
244
|
+
}),
|
|
245
|
+
inputs: inputsArg.map(function (inp: any) {
|
|
246
|
+
return {
|
|
247
|
+
name: inp.name,
|
|
248
|
+
label: inp.label || inp.name,
|
|
249
|
+
type: inp.type || 'string',
|
|
250
|
+
mandatory: inp.mandatory || false,
|
|
251
|
+
default_value: inp.default_value || ''
|
|
252
|
+
};
|
|
253
|
+
}),
|
|
254
|
+
outputs: outputsArg.map(function (out: any) {
|
|
255
|
+
return {
|
|
256
|
+
name: out.name,
|
|
257
|
+
label: out.label || out.name,
|
|
258
|
+
type: out.type || 'string'
|
|
259
|
+
};
|
|
260
|
+
}),
|
|
261
|
+
version: '1.0'
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Remove trigger block for subflows
|
|
265
|
+
if (isSubflow) {
|
|
266
|
+
delete flowDefinition.trigger;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
var flowData: any = {
|
|
270
|
+
name: flowName,
|
|
271
|
+
description: flowDescription,
|
|
272
|
+
active: shouldActivate,
|
|
273
|
+
internal_name: sanitizeInternalName(flowName),
|
|
274
|
+
category: flowCategory,
|
|
275
|
+
run_as: flowRunAs,
|
|
276
|
+
status: shouldActivate ? 'published' : 'draft',
|
|
277
|
+
validated: true,
|
|
278
|
+
type: isSubflow ? 'subflow' : 'flow',
|
|
279
|
+
flow_definition: JSON.stringify(flowDefinition),
|
|
280
|
+
latest_snapshot: JSON.stringify(flowDefinition)
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// 1. Create flow record via Table API
|
|
284
|
+
var flowResponse = await client.post('/api/now/table/sys_hub_flow', flowData);
|
|
285
|
+
|
|
286
|
+
var createdFlow = flowResponse.data.result;
|
|
287
|
+
var flowSysId = createdFlow.sys_id;
|
|
288
|
+
|
|
289
|
+
// 2. Create trigger instance (non-manual flows only)
|
|
290
|
+
var triggerCreated = false;
|
|
291
|
+
if (!isSubflow && triggerType !== 'manual') {
|
|
292
|
+
try {
|
|
293
|
+
// Lookup trigger action type
|
|
294
|
+
var triggerTypeLookup: Record<string, string> = {
|
|
295
|
+
'record_created': 'sn_fd.trigger.record_created',
|
|
296
|
+
'record_updated': 'sn_fd.trigger.record_updated',
|
|
297
|
+
'scheduled': 'sn_fd.trigger.scheduled'
|
|
298
|
+
};
|
|
299
|
+
var triggerInternalName = triggerTypeLookup[triggerType] || '';
|
|
300
|
+
|
|
301
|
+
if (triggerInternalName) {
|
|
302
|
+
var triggerDefResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
|
|
303
|
+
params: {
|
|
304
|
+
sysparm_query: 'internal_name=' + triggerInternalName,
|
|
305
|
+
sysparm_fields: 'sys_id',
|
|
306
|
+
sysparm_limit: 1
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
var triggerDefId = triggerDefResp.data.result?.[0]?.sys_id;
|
|
311
|
+
if (triggerDefId) {
|
|
312
|
+
var triggerData: any = {
|
|
313
|
+
flow: flowSysId,
|
|
314
|
+
action_type: triggerDefId,
|
|
315
|
+
name: triggerType,
|
|
316
|
+
order: 0,
|
|
317
|
+
active: true
|
|
318
|
+
};
|
|
319
|
+
if (flowTable) {
|
|
320
|
+
triggerData.table = flowTable;
|
|
321
|
+
}
|
|
322
|
+
if (triggerCondition) {
|
|
323
|
+
triggerData.condition = triggerCondition;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await client.post('/api/now/table/sys_hub_trigger_instance', triggerData);
|
|
327
|
+
triggerCreated = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} catch (triggerError) {
|
|
331
|
+
// Trigger creation is best-effort
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 3. Create action instances
|
|
336
|
+
var actionsCreated = 0;
|
|
337
|
+
for (var ai = 0; ai < activitiesArg.length; ai++) {
|
|
338
|
+
var activity = activitiesArg[ai];
|
|
339
|
+
try {
|
|
340
|
+
// Lookup action type definition
|
|
341
|
+
var actionTypeName = activity.type || 'script';
|
|
342
|
+
var actionTypeQuery = 'internal_nameLIKE' + actionTypeName + '^ORnameLIKE' + actionTypeName;
|
|
343
|
+
|
|
344
|
+
var actionDefResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
|
|
345
|
+
params: {
|
|
346
|
+
sysparm_query: actionTypeQuery,
|
|
347
|
+
sysparm_fields: 'sys_id,name,internal_name',
|
|
348
|
+
sysparm_limit: 1
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
var actionDefId = actionDefResp.data.result?.[0]?.sys_id;
|
|
353
|
+
var instanceData: any = {
|
|
354
|
+
flow: flowSysId,
|
|
355
|
+
name: activity.name,
|
|
356
|
+
order: (ai + 1) * 100,
|
|
357
|
+
active: true
|
|
358
|
+
};
|
|
359
|
+
if (actionDefId) {
|
|
360
|
+
instanceData.action_type = actionDefId;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
await client.post('/api/now/table/sys_hub_action_instance', instanceData);
|
|
364
|
+
actionsCreated++;
|
|
365
|
+
} catch (actError) {
|
|
366
|
+
// Action creation is best-effort
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 4. Create flow variables (for subflows with inputs/outputs)
|
|
371
|
+
var varsCreated = 0;
|
|
372
|
+
if (isSubflow) {
|
|
373
|
+
for (var vi = 0; vi < inputsArg.length; vi++) {
|
|
374
|
+
var inp = inputsArg[vi];
|
|
375
|
+
try {
|
|
376
|
+
await client.post('/api/now/table/sys_hub_flow_variable', {
|
|
377
|
+
flow: flowSysId,
|
|
378
|
+
name: inp.name,
|
|
379
|
+
label: inp.label || inp.name,
|
|
380
|
+
type: inp.type || 'string',
|
|
381
|
+
mandatory: inp.mandatory || false,
|
|
382
|
+
default_value: inp.default_value || '',
|
|
383
|
+
variable_type: 'input'
|
|
384
|
+
});
|
|
385
|
+
varsCreated++;
|
|
386
|
+
} catch (varError) {
|
|
387
|
+
// Best-effort
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (var vo = 0; vo < outputsArg.length; vo++) {
|
|
391
|
+
var out = outputsArg[vo];
|
|
392
|
+
try {
|
|
393
|
+
await client.post('/api/now/table/sys_hub_flow_variable', {
|
|
394
|
+
flow: flowSysId,
|
|
395
|
+
name: out.name,
|
|
396
|
+
label: out.label || out.name,
|
|
397
|
+
type: out.type || 'string',
|
|
398
|
+
variable_type: 'output'
|
|
399
|
+
});
|
|
400
|
+
varsCreated++;
|
|
401
|
+
} catch (varError) {
|
|
402
|
+
// Best-effort
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 5. Best-effort snapshot
|
|
408
|
+
try {
|
|
409
|
+
await client.post('/api/sn_flow_designer/flow/snapshot', {
|
|
410
|
+
flow_id: flowSysId
|
|
411
|
+
});
|
|
412
|
+
} catch (snapError) {
|
|
413
|
+
// Snapshot API may not exist in all instances
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Build summary
|
|
417
|
+
var createSummary = summary()
|
|
418
|
+
.success('Created ' + (isSubflow ? 'subflow' : 'flow') + ': ' + flowName)
|
|
419
|
+
.field('sys_id', flowSysId)
|
|
420
|
+
.field('Type', isSubflow ? 'Subflow' : 'Flow')
|
|
421
|
+
.field('Category', flowCategory)
|
|
422
|
+
.field('Status', shouldActivate ? 'Published (active)' : 'Draft')
|
|
423
|
+
.field('Method', 'Table API');
|
|
424
|
+
|
|
425
|
+
if (!isSubflow && triggerType !== 'manual') {
|
|
426
|
+
createSummary.field('Trigger', triggerType + (triggerCreated ? ' (created)' : ' (best-effort)'));
|
|
427
|
+
if (flowTable) createSummary.field('Table', flowTable);
|
|
428
|
+
}
|
|
429
|
+
if (activitiesArg.length > 0) {
|
|
430
|
+
createSummary.field('Actions', actionsCreated + '/' + activitiesArg.length + ' created');
|
|
431
|
+
}
|
|
432
|
+
if (varsCreated > 0) {
|
|
433
|
+
createSummary.field('Variables', varsCreated + ' created');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return createSuccessResult({
|
|
437
|
+
created: true,
|
|
438
|
+
method: 'table_api',
|
|
439
|
+
flow: {
|
|
440
|
+
sys_id: flowSysId,
|
|
441
|
+
name: flowName,
|
|
442
|
+
type: isSubflow ? 'subflow' : 'flow',
|
|
443
|
+
category: flowCategory,
|
|
444
|
+
active: shouldActivate,
|
|
445
|
+
status: shouldActivate ? 'published' : 'draft'
|
|
446
|
+
},
|
|
447
|
+
trigger: !isSubflow && triggerType !== 'manual' ? {
|
|
448
|
+
type: triggerType,
|
|
449
|
+
table: flowTable,
|
|
450
|
+
condition: triggerCondition,
|
|
451
|
+
created: triggerCreated
|
|
452
|
+
} : null,
|
|
453
|
+
activities_created: actionsCreated,
|
|
454
|
+
activities_requested: activitiesArg.length,
|
|
455
|
+
variables_created: varsCreated
|
|
456
|
+
}, {}, createSummary.build());
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ────────────────────────────────────────────────────────────────
|
|
460
|
+
// LIST
|
|
461
|
+
// ────────────────────────────────────────────────────────────────
|
|
462
|
+
case 'list': {
|
|
463
|
+
var listQuery = '';
|
|
464
|
+
var filterType = args.type || 'all';
|
|
465
|
+
var activeOnly = args.active_only !== false;
|
|
466
|
+
var listLimit = args.limit || 50;
|
|
467
|
+
var filterTable = args.table || '';
|
|
468
|
+
|
|
469
|
+
if (filterType !== 'all') {
|
|
470
|
+
listQuery = 'type=' + filterType;
|
|
471
|
+
}
|
|
472
|
+
if (activeOnly) {
|
|
473
|
+
listQuery += (listQuery ? '^' : '') + 'active=true';
|
|
474
|
+
}
|
|
475
|
+
if (filterTable) {
|
|
476
|
+
listQuery += (listQuery ? '^' : '') + 'flow_definitionLIKE' + filterTable;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
var listResp = await client.get('/api/now/table/sys_hub_flow', {
|
|
480
|
+
params: {
|
|
481
|
+
sysparm_query: listQuery || undefined,
|
|
482
|
+
sysparm_fields: 'sys_id,name,description,type,category,active,status,run_as,sys_created_on,sys_updated_on',
|
|
483
|
+
sysparm_limit: listLimit
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
var flows = (listResp.data.result || []).map(function (f: any) {
|
|
488
|
+
return {
|
|
489
|
+
sys_id: f.sys_id,
|
|
490
|
+
name: f.name,
|
|
491
|
+
description: f.description,
|
|
492
|
+
type: f.type,
|
|
493
|
+
category: f.category,
|
|
494
|
+
active: f.active === 'true',
|
|
495
|
+
status: f.status,
|
|
496
|
+
run_as: f.run_as,
|
|
497
|
+
created: f.sys_created_on,
|
|
498
|
+
updated: f.sys_updated_on
|
|
499
|
+
};
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
var listSummary = summary()
|
|
503
|
+
.success('Found ' + flows.length + ' flow' + (flows.length === 1 ? '' : 's'));
|
|
504
|
+
|
|
505
|
+
for (var li = 0; li < Math.min(flows.length, 15); li++) {
|
|
506
|
+
var lf = flows[li];
|
|
507
|
+
listSummary.bullet(
|
|
508
|
+
lf.name +
|
|
509
|
+
' [' + (lf.type || 'flow') + ']' +
|
|
510
|
+
(lf.active ? '' : ' (inactive)')
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
if (flows.length > 15) {
|
|
514
|
+
listSummary.indented('... and ' + (flows.length - 15) + ' more');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return createSuccessResult({
|
|
518
|
+
action: 'list',
|
|
519
|
+
count: flows.length,
|
|
520
|
+
flows: flows
|
|
521
|
+
}, {}, listSummary.build());
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ────────────────────────────────────────────────────────────────
|
|
525
|
+
// GET
|
|
526
|
+
// ────────────────────────────────────────────────────────────────
|
|
527
|
+
case 'get': {
|
|
528
|
+
if (!args.flow_id) {
|
|
529
|
+
throw new SnowFlowError(ErrorType.VALIDATION_ERROR, 'flow_id is required for get action');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
var getSysId = await resolveFlowId(client, args.flow_id);
|
|
533
|
+
|
|
534
|
+
// Fetch flow record
|
|
535
|
+
var getResp = await client.get('/api/now/table/sys_hub_flow/' + getSysId);
|
|
536
|
+
var flowRecord = getResp.data.result;
|
|
537
|
+
|
|
538
|
+
// Fetch trigger instances
|
|
539
|
+
var triggerInstances: any[] = [];
|
|
540
|
+
try {
|
|
541
|
+
var trigResp = await client.get('/api/now/table/sys_hub_trigger_instance', {
|
|
542
|
+
params: {
|
|
543
|
+
sysparm_query: 'flow=' + getSysId,
|
|
544
|
+
sysparm_fields: 'sys_id,name,action_type,table,condition,active,order',
|
|
545
|
+
sysparm_limit: 10
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
triggerInstances = trigResp.data.result || [];
|
|
549
|
+
} catch (e) { /* best-effort */ }
|
|
550
|
+
|
|
551
|
+
// Fetch action instances
|
|
552
|
+
var actionInstances: any[] = [];
|
|
553
|
+
try {
|
|
554
|
+
var actResp = await client.get('/api/now/table/sys_hub_action_instance', {
|
|
555
|
+
params: {
|
|
556
|
+
sysparm_query: 'flow=' + getSysId + '^ORDERBYorder',
|
|
557
|
+
sysparm_fields: 'sys_id,name,action_type,order,active',
|
|
558
|
+
sysparm_limit: 50
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
actionInstances = actResp.data.result || [];
|
|
562
|
+
} catch (e) { /* best-effort */ }
|
|
563
|
+
|
|
564
|
+
// Fetch flow variables
|
|
565
|
+
var flowVars: any[] = [];
|
|
566
|
+
try {
|
|
567
|
+
var varResp = await client.get('/api/now/table/sys_hub_flow_variable', {
|
|
568
|
+
params: {
|
|
569
|
+
sysparm_query: 'flow=' + getSysId,
|
|
570
|
+
sysparm_fields: 'sys_id,name,label,type,mandatory,variable_type,default_value',
|
|
571
|
+
sysparm_limit: 50
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
flowVars = varResp.data.result || [];
|
|
575
|
+
} catch (e) { /* best-effort */ }
|
|
576
|
+
|
|
577
|
+
// Fetch recent executions
|
|
578
|
+
var executions: any[] = [];
|
|
579
|
+
try {
|
|
580
|
+
var execResp = await client.get('/api/now/table/sys_hub_flow_run', {
|
|
581
|
+
params: {
|
|
582
|
+
sysparm_query: 'flow=' + getSysId + '^ORDERBYDESCsys_created_on',
|
|
583
|
+
sysparm_fields: 'sys_id,state,started,ended,duration,trigger_record_table,trigger_record_id',
|
|
584
|
+
sysparm_limit: 10
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
executions = execResp.data.result || [];
|
|
588
|
+
} catch (e) { /* best-effort */ }
|
|
589
|
+
|
|
590
|
+
var getSummary = summary()
|
|
591
|
+
.success('Flow: ' + (flowRecord.name || args.flow_id))
|
|
592
|
+
.field('sys_id', flowRecord.sys_id)
|
|
593
|
+
.field('Type', flowRecord.type)
|
|
594
|
+
.field('Category', flowRecord.category)
|
|
595
|
+
.field('Status', flowRecord.active === 'true' ? 'Active' : 'Inactive')
|
|
596
|
+
.field('Run as', flowRecord.run_as)
|
|
597
|
+
.field('Description', flowRecord.description);
|
|
598
|
+
|
|
599
|
+
if (triggerInstances.length > 0) {
|
|
600
|
+
getSummary.blank().line('Triggers: ' + triggerInstances.length);
|
|
601
|
+
for (var ti = 0; ti < triggerInstances.length; ti++) {
|
|
602
|
+
getSummary.bullet(triggerInstances[ti].name || 'trigger-' + ti);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (actionInstances.length > 0) {
|
|
606
|
+
getSummary.blank().line('Actions: ' + actionInstances.length);
|
|
607
|
+
for (var aci = 0; aci < Math.min(actionInstances.length, 10); aci++) {
|
|
608
|
+
getSummary.bullet(actionInstances[aci].name || 'action-' + aci);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (flowVars.length > 0) {
|
|
612
|
+
getSummary.blank().line('Variables: ' + flowVars.length);
|
|
613
|
+
}
|
|
614
|
+
if (executions.length > 0) {
|
|
615
|
+
getSummary.blank().line('Recent executions: ' + executions.length);
|
|
616
|
+
for (var ei = 0; ei < Math.min(executions.length, 5); ei++) {
|
|
617
|
+
var ex = executions[ei];
|
|
618
|
+
getSummary.bullet((ex.state || 'unknown') + ' - ' + (ex.started || 'pending'));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return createSuccessResult({
|
|
623
|
+
action: 'get',
|
|
624
|
+
flow: {
|
|
625
|
+
sys_id: flowRecord.sys_id,
|
|
626
|
+
name: flowRecord.name,
|
|
627
|
+
description: flowRecord.description,
|
|
628
|
+
type: flowRecord.type,
|
|
629
|
+
category: flowRecord.category,
|
|
630
|
+
active: flowRecord.active === 'true',
|
|
631
|
+
status: flowRecord.status,
|
|
632
|
+
run_as: flowRecord.run_as,
|
|
633
|
+
created: flowRecord.sys_created_on,
|
|
634
|
+
updated: flowRecord.sys_updated_on
|
|
635
|
+
},
|
|
636
|
+
triggers: triggerInstances.map(function (t: any) {
|
|
637
|
+
return {
|
|
638
|
+
sys_id: t.sys_id,
|
|
639
|
+
name: t.name,
|
|
640
|
+
action_type: typeof t.action_type === 'object' ? t.action_type.display_value : t.action_type,
|
|
641
|
+
table: t.table,
|
|
642
|
+
condition: t.condition,
|
|
643
|
+
active: t.active === 'true'
|
|
644
|
+
};
|
|
645
|
+
}),
|
|
646
|
+
actions: actionInstances.map(function (a: any) {
|
|
647
|
+
return {
|
|
648
|
+
sys_id: a.sys_id,
|
|
649
|
+
name: a.name,
|
|
650
|
+
action_type: typeof a.action_type === 'object' ? a.action_type.display_value : a.action_type,
|
|
651
|
+
order: a.order,
|
|
652
|
+
active: a.active === 'true'
|
|
653
|
+
};
|
|
654
|
+
}),
|
|
655
|
+
variables: flowVars.map(function (v: any) {
|
|
656
|
+
return {
|
|
657
|
+
sys_id: v.sys_id,
|
|
658
|
+
name: v.name,
|
|
659
|
+
label: v.label,
|
|
660
|
+
type: v.type,
|
|
661
|
+
mandatory: v.mandatory === 'true',
|
|
662
|
+
variable_type: v.variable_type,
|
|
663
|
+
default_value: v.default_value
|
|
664
|
+
};
|
|
665
|
+
}),
|
|
666
|
+
recent_executions: executions.map(function (e: any) {
|
|
667
|
+
return {
|
|
668
|
+
sys_id: e.sys_id,
|
|
669
|
+
state: e.state,
|
|
670
|
+
started: e.started,
|
|
671
|
+
ended: e.ended,
|
|
672
|
+
duration: e.duration,
|
|
673
|
+
trigger_table: e.trigger_record_table,
|
|
674
|
+
trigger_record: e.trigger_record_id
|
|
675
|
+
};
|
|
676
|
+
})
|
|
677
|
+
}, {}, getSummary.build());
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ────────────────────────────────────────────────────────────────
|
|
681
|
+
// UPDATE
|
|
682
|
+
// ────────────────────────────────────────────────────────────────
|
|
683
|
+
case 'update': {
|
|
684
|
+
if (!args.flow_id) {
|
|
685
|
+
throw new SnowFlowError(ErrorType.VALIDATION_ERROR, 'flow_id is required for update action');
|
|
686
|
+
}
|
|
687
|
+
var updateFields = args.update_fields;
|
|
688
|
+
if (!updateFields || Object.keys(updateFields).length === 0) {
|
|
689
|
+
throw new SnowFlowError(ErrorType.VALIDATION_ERROR, 'update_fields is required for update action');
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
var updateSysId = await resolveFlowId(client, args.flow_id);
|
|
693
|
+
await client.patch('/api/now/table/sys_hub_flow/' + updateSysId, updateFields);
|
|
694
|
+
|
|
695
|
+
var updateSummary = summary()
|
|
696
|
+
.success('Updated flow: ' + args.flow_id)
|
|
697
|
+
.field('sys_id', updateSysId);
|
|
698
|
+
|
|
699
|
+
var fieldNames = Object.keys(updateFields);
|
|
700
|
+
for (var fi = 0; fi < fieldNames.length; fi++) {
|
|
701
|
+
updateSummary.field(fieldNames[fi], updateFields[fieldNames[fi]]);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return createSuccessResult({
|
|
705
|
+
action: 'update',
|
|
706
|
+
flow_id: updateSysId,
|
|
707
|
+
updated_fields: fieldNames,
|
|
708
|
+
message: 'Flow updated successfully'
|
|
709
|
+
}, {}, updateSummary.build());
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// ────────────────────────────────────────────────────────────────
|
|
713
|
+
// ACTIVATE / PUBLISH
|
|
714
|
+
// ────────────────────────────────────────────────────────────────
|
|
715
|
+
case 'activate':
|
|
716
|
+
case 'publish': {
|
|
717
|
+
if (!args.flow_id) {
|
|
718
|
+
throw new SnowFlowError(ErrorType.VALIDATION_ERROR, 'flow_id is required for ' + action + ' action');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
var activateSysId = await resolveFlowId(client, args.flow_id);
|
|
722
|
+
await client.patch('/api/now/table/sys_hub_flow/' + activateSysId, {
|
|
723
|
+
active: true,
|
|
724
|
+
status: 'published',
|
|
725
|
+
validated: true
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
var activateSummary = summary()
|
|
729
|
+
.success('Flow activated and published')
|
|
730
|
+
.field('sys_id', activateSysId);
|
|
731
|
+
|
|
732
|
+
return createSuccessResult({
|
|
733
|
+
action: action,
|
|
734
|
+
flow_id: activateSysId,
|
|
735
|
+
active: true,
|
|
736
|
+
status: 'published',
|
|
737
|
+
message: 'Flow activated and published'
|
|
738
|
+
}, {}, activateSummary.build());
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ────────────────────────────────────────────────────────────────
|
|
742
|
+
// DEACTIVATE
|
|
743
|
+
// ────────────────────────────────────────────────────────────────
|
|
744
|
+
case 'deactivate': {
|
|
745
|
+
if (!args.flow_id) {
|
|
746
|
+
throw new SnowFlowError(ErrorType.VALIDATION_ERROR, 'flow_id is required for deactivate action');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
var deactivateSysId = await resolveFlowId(client, args.flow_id);
|
|
750
|
+
await client.patch('/api/now/table/sys_hub_flow/' + deactivateSysId, {
|
|
751
|
+
active: false
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
var deactivateSummary = summary()
|
|
755
|
+
.success('Flow deactivated')
|
|
756
|
+
.field('sys_id', deactivateSysId);
|
|
757
|
+
|
|
758
|
+
return createSuccessResult({
|
|
759
|
+
action: 'deactivate',
|
|
760
|
+
flow_id: deactivateSysId,
|
|
761
|
+
active: false,
|
|
762
|
+
message: 'Flow deactivated'
|
|
763
|
+
}, {}, deactivateSummary.build());
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ────────────────────────────────────────────────────────────────
|
|
767
|
+
// DELETE
|
|
768
|
+
// ────────────────────────────────────────────────────────────────
|
|
769
|
+
case 'delete': {
|
|
770
|
+
if (!args.flow_id) {
|
|
771
|
+
throw new SnowFlowError(ErrorType.VALIDATION_ERROR, 'flow_id is required for delete action');
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
var deleteSysId = await resolveFlowId(client, args.flow_id);
|
|
775
|
+
await client.delete('/api/now/table/sys_hub_flow/' + deleteSysId);
|
|
776
|
+
|
|
777
|
+
var deleteSummary = summary()
|
|
778
|
+
.success('Flow deleted')
|
|
779
|
+
.field('sys_id', deleteSysId);
|
|
780
|
+
|
|
781
|
+
return createSuccessResult({
|
|
782
|
+
action: 'delete',
|
|
783
|
+
flow_id: deleteSysId,
|
|
784
|
+
message: 'Flow deleted'
|
|
785
|
+
}, {}, deleteSummary.build());
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
default:
|
|
789
|
+
throw new SnowFlowError(ErrorType.VALIDATION_ERROR, 'Unknown action: ' + action);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
} catch (error: any) {
|
|
793
|
+
if (error instanceof SnowFlowError) {
|
|
794
|
+
return createErrorResult(error);
|
|
795
|
+
}
|
|
796
|
+
if (error.response?.status === 403) {
|
|
797
|
+
return createErrorResult(
|
|
798
|
+
'Permission denied (403): Your ServiceNow user lacks Flow Designer permissions. ' +
|
|
799
|
+
'Required roles: "flow_designer" or "admin". Contact your ServiceNow administrator.'
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
return createErrorResult(
|
|
803
|
+
new SnowFlowError(ErrorType.UNKNOWN_ERROR, error.message, { originalError: error })
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export const version = '1.0.0';
|
|
809
|
+
export const author = 'Snow-Flow Team';
|
|
@@ -31,16 +31,12 @@ const LEGACY_WARNING = `
|
|
|
31
31
|
ServiceNow Workflow (wf_workflow) is deprecated in favor of Flow Designer.
|
|
32
32
|
Flow Designer provides a modern, visual interface for building automations.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Use snow_manage_flow to create and manage Flow Designer flows programmatically.
|
|
35
35
|
|
|
36
36
|
RECOMMENDATIONS:
|
|
37
|
-
1. For NEW automations →
|
|
37
|
+
1. For NEW automations → Use snow_manage_flow (action: create)
|
|
38
38
|
2. For EXISTING workflows → This tool can manage legacy workflows
|
|
39
|
-
3.
|
|
40
|
-
specification document with the required triggers, actions, and logic.
|
|
41
|
-
|
|
42
|
-
To generate a Flow Designer specification, ask:
|
|
43
|
-
"Generate a Flow Designer specification for [describe your automation]"
|
|
39
|
+
3. For MIGRATION → Use snow_manage_flow to recreate workflows as flows
|
|
44
40
|
`;
|
|
45
41
|
|
|
46
42
|
export const toolDefinition: MCPToolDefinition = {
|