snow-flow 10.0.1-dev.424 → 10.0.1-dev.426
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/project/agents-template.txt +6 -0
- package/src/servicenow/servicenow-mcp-unified/shared/tool-registry.ts +2 -0
- package/src/servicenow/servicenow-mcp-unified/tools/flow-designer/snow_manage_flow.ts +195 -76
- package/src/servicenow/servicenow-mcp-unified/tools/index.ts +1 -1
- package/src/servicenow/servicenow-mcp-unified/tools/plugins/index.ts +1 -3
- package/src/servicenow/servicenow-mcp-unified/tools/plugins/snow_plugin_manage.ts +295 -0
- package/src/servicenow/servicenow-mcp-unified/tools/plugins/snow_activate_plugin.ts +0 -47
- package/src/servicenow/servicenow-mcp-unified/tools/plugins/snow_custom_plugin.ts +0 -46
- package/src/servicenow/servicenow-mcp-unified/tools/plugins/snow_list_plugins.ts +0 -52
package/package.json
CHANGED
|
@@ -135,6 +135,12 @@ await activity_start({
|
|
|
135
135
|
updateSetUrl: "https://instance.service-now.com/sys_update_set.do?sys_id=abc123..."
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
await activity_update({
|
|
139
|
+
activityId: activityId,
|
|
140
|
+
status: "in_progress", // or "review", "failed", "cancelled"
|
|
141
|
+
summary: "Progress update: completed widget creation, starting business rule"
|
|
142
|
+
});
|
|
143
|
+
|
|
138
144
|
await activity_add_artifact({
|
|
139
145
|
activityId: activityId,
|
|
140
146
|
artifactType: "widget",
|
|
@@ -83,6 +83,7 @@ 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
85
|
import * as flowDesignerTools from '../tools/flow-designer/index.js';
|
|
86
|
+
import * as pluginsTools from '../tools/plugins/index.js';
|
|
86
87
|
|
|
87
88
|
// ES Module compatible __dirname
|
|
88
89
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -150,6 +151,7 @@ const STATIC_TOOL_MODULES: Record<string, any> = {
|
|
|
150
151
|
'devops': devopsTools,
|
|
151
152
|
'virtual-agent': virtualAgentTools,
|
|
152
153
|
'flow-designer': flowDesignerTools,
|
|
154
|
+
'plugins': pluginsTools,
|
|
153
155
|
};
|
|
154
156
|
|
|
155
157
|
export class ToolRegistry {
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* Create, list, get, update, activate, deactivate, delete and publish
|
|
5
5
|
* Flow Designer flows and subflows.
|
|
6
6
|
*
|
|
7
|
-
* For create/create_subflow: uses
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* For create/create_subflow: uses the ProcessFlow REST API
|
|
8
|
+
* (/api/now/processflow/flow) — the same endpoint the Flow Designer UI
|
|
9
|
+
* uses. This creates engine-registered flows that can be opened in Flow
|
|
10
|
+
* Designer without "corrupted flow" errors. Falls back to Table API.
|
|
11
11
|
*
|
|
12
12
|
* All other actions (list, get, update, activate, etc.) use the Table API.
|
|
13
13
|
*/
|
|
@@ -36,6 +36,94 @@ function escForScript(s: string): string {
|
|
|
36
36
|
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '');
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Create a flow via the ProcessFlow REST API — the same endpoint the Flow Designer UI uses.
|
|
41
|
+
*
|
|
42
|
+
* Uses /api/now/processflow/flow to create engine-registered flows, then
|
|
43
|
+
* /api/now/processflow/versioning/create_version for versioning.
|
|
44
|
+
*
|
|
45
|
+
* This is the only method that produces flows openable in Flow Designer
|
|
46
|
+
* without "corrupted flow" errors, because it sets:
|
|
47
|
+
* - flowEngineVersion
|
|
48
|
+
* - label_cache / attributes
|
|
49
|
+
* - flowCatalogVariableModelId
|
|
50
|
+
* - proper internal_name
|
|
51
|
+
*/
|
|
52
|
+
async function createFlowViaProcessFlowAPI(
|
|
53
|
+
client: any,
|
|
54
|
+
params: {
|
|
55
|
+
name: string;
|
|
56
|
+
description: string;
|
|
57
|
+
isSubflow: boolean;
|
|
58
|
+
runAs: string;
|
|
59
|
+
shouldActivate: boolean;
|
|
60
|
+
}
|
|
61
|
+
): Promise<{
|
|
62
|
+
success: boolean;
|
|
63
|
+
flowSysId?: string;
|
|
64
|
+
versionCreated?: boolean;
|
|
65
|
+
flowData?: any;
|
|
66
|
+
error?: string;
|
|
67
|
+
}> {
|
|
68
|
+
try {
|
|
69
|
+
// Step 1: Create flow via ProcessFlow API (same as Flow Designer UI)
|
|
70
|
+
var flowResp = await client.post(
|
|
71
|
+
'/api/now/processflow/flow',
|
|
72
|
+
{
|
|
73
|
+
access: 'public',
|
|
74
|
+
description: params.description || '',
|
|
75
|
+
flowPriority: 'MEDIUM',
|
|
76
|
+
name: params.name,
|
|
77
|
+
protection: '',
|
|
78
|
+
runAs: params.runAs || 'user',
|
|
79
|
+
runWithRoles: { value: '', displayValue: '' },
|
|
80
|
+
scope: 'global',
|
|
81
|
+
scopeDisplayName: '',
|
|
82
|
+
scopeName: '',
|
|
83
|
+
security: { can_read: true, can_write: true },
|
|
84
|
+
status: 'draft',
|
|
85
|
+
type: params.isSubflow ? 'subflow' : 'flow',
|
|
86
|
+
userHasRolesAssignedToFlow: true,
|
|
87
|
+
active: false,
|
|
88
|
+
deleted: false
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
params: {
|
|
92
|
+
param_only_properties: 'true',
|
|
93
|
+
sysparm_transaction_scope: 'global'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
var flowResult = flowResp.data?.result?.data;
|
|
99
|
+
if (!flowResult?.id) {
|
|
100
|
+
var errDetail = flowResp.data?.result?.errorMessage || 'no flow id returned';
|
|
101
|
+
return { success: false, error: 'ProcessFlow API: ' + errDetail };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var flowSysId = flowResult.id;
|
|
105
|
+
|
|
106
|
+
// Step 2: Create version via ProcessFlow versioning API
|
|
107
|
+
var versionCreated = false;
|
|
108
|
+
try {
|
|
109
|
+
await client.post(
|
|
110
|
+
'/api/now/processflow/versioning/create_version',
|
|
111
|
+
{ item_sys_id: flowSysId, type: 'Autosave', annotation: '', favorite: false },
|
|
112
|
+
{ params: { sysparm_transaction_scope: 'global' } }
|
|
113
|
+
);
|
|
114
|
+
versionCreated = true;
|
|
115
|
+
} catch (_) {
|
|
116
|
+
// Non-fatal: flow was created, version creation is best-effort
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { success: true, flowSysId, versionCreated, flowData: flowResult };
|
|
120
|
+
} catch (e: any) {
|
|
121
|
+
var msg = e.message || '';
|
|
122
|
+
try { msg += ' — ' + JSON.stringify(e.response?.data || '').substring(0, 200); } catch (_) {}
|
|
123
|
+
return { success: false, error: 'ProcessFlow API: ' + msg };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
39
127
|
/**
|
|
40
128
|
* Create a flow via a ServiceNow Scheduled Job (sysauto_script).
|
|
41
129
|
*
|
|
@@ -1405,7 +1493,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
1405
1493
|
switch (action) {
|
|
1406
1494
|
|
|
1407
1495
|
// ────────────────────────────────────────────────────────────────
|
|
1408
|
-
// CREATE (
|
|
1496
|
+
// CREATE (ProcessFlow API → Table API fallback)
|
|
1409
1497
|
// ────────────────────────────────────────────────────────────────
|
|
1410
1498
|
case 'create':
|
|
1411
1499
|
case 'create_subflow': {
|
|
@@ -1468,7 +1556,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
1468
1556
|
delete flowDefinition.trigger;
|
|
1469
1557
|
}
|
|
1470
1558
|
|
|
1471
|
-
// ── Pipeline:
|
|
1559
|
+
// ── Pipeline: ProcessFlow API (primary) → Table API (fallback) ──
|
|
1472
1560
|
var flowSysId: string | null = null;
|
|
1473
1561
|
var usedMethod = 'table_api';
|
|
1474
1562
|
var versionCreated = false;
|
|
@@ -1477,82 +1565,119 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
1477
1565
|
var actionsCreated = 0;
|
|
1478
1566
|
var varsCreated = 0;
|
|
1479
1567
|
|
|
1480
|
-
// Diagnostics
|
|
1568
|
+
// Diagnostics
|
|
1481
1569
|
var diagnostics: any = {
|
|
1570
|
+
processflow_api: null,
|
|
1482
1571
|
table_api_used: false,
|
|
1483
1572
|
version_created: false,
|
|
1484
1573
|
version_method: null,
|
|
1485
1574
|
post_verify: null
|
|
1486
1575
|
};
|
|
1487
1576
|
|
|
1488
|
-
// ──
|
|
1489
|
-
//
|
|
1490
|
-
//
|
|
1491
|
-
// Also attempts sn_fd engine registration server-side.
|
|
1577
|
+
// ── ProcessFlow API (primary — same REST endpoint as Flow Designer UI) ──
|
|
1578
|
+
// Uses /api/now/processflow/flow to create engine-registered flows,
|
|
1579
|
+
// then /api/now/processflow/versioning/create_version for versioning.
|
|
1492
1580
|
try {
|
|
1493
|
-
var
|
|
1581
|
+
var pfResult = await createFlowViaProcessFlowAPI(client, {
|
|
1494
1582
|
name: flowName,
|
|
1495
1583
|
description: flowDescription,
|
|
1496
|
-
internalName: sanitizeInternalName(flowName),
|
|
1497
1584
|
isSubflow: isSubflow,
|
|
1498
|
-
category: flowCategory,
|
|
1499
1585
|
runAs: flowRunAs,
|
|
1500
|
-
shouldActivate: shouldActivate
|
|
1501
|
-
triggerType: triggerType,
|
|
1502
|
-
triggerTable: flowTable,
|
|
1503
|
-
triggerCondition: triggerCondition,
|
|
1504
|
-
activities: activitiesArg.map(function (act: any, idx: number) {
|
|
1505
|
-
return { name: act.name, type: act.type || 'script', inputs: act.inputs || {} };
|
|
1506
|
-
}),
|
|
1507
|
-
inputs: inputsArg,
|
|
1508
|
-
outputs: outputsArg,
|
|
1509
|
-
flowDefinition: flowDefinition
|
|
1586
|
+
shouldActivate: shouldActivate
|
|
1510
1587
|
});
|
|
1511
|
-
diagnostics.
|
|
1512
|
-
success:
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
latestVersionValue: scheduledResult.latestVersionValue,
|
|
1516
|
-
steps: scheduledResult.steps,
|
|
1517
|
-
error: scheduledResult.error
|
|
1588
|
+
diagnostics.processflow_api = {
|
|
1589
|
+
success: pfResult.success,
|
|
1590
|
+
versionCreated: pfResult.versionCreated,
|
|
1591
|
+
error: pfResult.error
|
|
1518
1592
|
};
|
|
1519
|
-
if (
|
|
1520
|
-
flowSysId =
|
|
1521
|
-
usedMethod = '
|
|
1522
|
-
versionCreated = !!
|
|
1593
|
+
if (pfResult.success && pfResult.flowSysId) {
|
|
1594
|
+
flowSysId = pfResult.flowSysId;
|
|
1595
|
+
usedMethod = 'processflow_api';
|
|
1596
|
+
versionCreated = !!pfResult.versionCreated;
|
|
1523
1597
|
diagnostics.version_created = versionCreated;
|
|
1524
|
-
diagnostics.version_method = '
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1598
|
+
diagnostics.version_method = 'processflow_api';
|
|
1599
|
+
}
|
|
1600
|
+
} catch (pfErr: any) {
|
|
1601
|
+
diagnostics.processflow_api = { error: pfErr.message || 'unknown' };
|
|
1602
|
+
factoryWarnings.push('ProcessFlow API failed: ' + (pfErr.message || pfErr));
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// ── Triggers, actions, variables for ProcessFlow-created flows ──
|
|
1606
|
+
// ProcessFlow API creates empty flows; add components via Table API
|
|
1607
|
+
if (flowSysId && usedMethod === 'processflow_api') {
|
|
1608
|
+
if (!isSubflow && triggerType !== 'manual') {
|
|
1609
|
+
try {
|
|
1610
|
+
var pfTrigDefId: string | null = null;
|
|
1611
|
+
var pfExactNames: Record<string, string[]> = {
|
|
1612
|
+
'record_created': ['sn_fd.trigger.record_created', 'global.sn_fd.trigger.record_created'],
|
|
1613
|
+
'record_updated': ['sn_fd.trigger.record_updated', 'global.sn_fd.trigger.record_updated'],
|
|
1614
|
+
'scheduled': ['sn_fd.trigger.scheduled', 'global.sn_fd.trigger.scheduled']
|
|
1615
|
+
};
|
|
1616
|
+
var pfCands = pfExactNames[triggerType] || [];
|
|
1617
|
+
for (var pfi = 0; pfi < pfCands.length && !pfTrigDefId; pfi++) {
|
|
1618
|
+
var pfExResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
|
|
1619
|
+
params: { sysparm_query: 'internal_name=' + pfCands[pfi], sysparm_fields: 'sys_id', sysparm_limit: 1 }
|
|
1620
|
+
});
|
|
1621
|
+
pfTrigDefId = pfExResp.data.result?.[0]?.sys_id || null;
|
|
1535
1622
|
}
|
|
1623
|
+
if (!pfTrigDefId) {
|
|
1624
|
+
var pfPfxResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
|
|
1625
|
+
params: { sysparm_query: 'internal_nameSTARTSWITHsn_fd.trigger', sysparm_fields: 'sys_id,internal_name', sysparm_limit: 10 }
|
|
1626
|
+
});
|
|
1627
|
+
var pfPfxResults = pfPfxResp.data.result || [];
|
|
1628
|
+
for (var pfpi = 0; pfpi < pfPfxResults.length && !pfTrigDefId; pfpi++) {
|
|
1629
|
+
if ((pfPfxResults[pfpi].internal_name || '').indexOf(triggerType.replace('record_', '')) > -1) {
|
|
1630
|
+
pfTrigDefId = pfPfxResults[pfpi].sys_id;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
var pfTrigData: any = { flow: flowSysId, name: triggerType, order: 0, active: true };
|
|
1635
|
+
if (pfTrigDefId) pfTrigData.action_type = pfTrigDefId;
|
|
1636
|
+
if (flowTable) pfTrigData.table = flowTable;
|
|
1637
|
+
if (triggerCondition) pfTrigData.condition = triggerCondition;
|
|
1638
|
+
await client.post('/api/now/table/sys_hub_trigger_instance', pfTrigData);
|
|
1639
|
+
triggerCreated = true;
|
|
1640
|
+
} catch (_) { /* best-effort */ }
|
|
1641
|
+
}
|
|
1642
|
+
for (var pfai = 0; pfai < activitiesArg.length; pfai++) {
|
|
1643
|
+
try {
|
|
1644
|
+
var pfAct = activitiesArg[pfai];
|
|
1645
|
+
var pfActQ = 'internal_nameLIKE' + (pfAct.type || 'script') + '^ORnameLIKE' + (pfAct.type || 'script');
|
|
1646
|
+
var pfActDefResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
|
|
1647
|
+
params: { sysparm_query: pfActQ, sysparm_fields: 'sys_id', sysparm_limit: 1 }
|
|
1648
|
+
});
|
|
1649
|
+
var pfActDef = pfActDefResp.data.result?.[0]?.sys_id;
|
|
1650
|
+
var pfInstData: any = { flow: flowSysId, name: pfAct.name, order: (pfai + 1) * 100, active: true };
|
|
1651
|
+
if (pfActDef) pfInstData.action_type = pfActDef;
|
|
1652
|
+
await client.post('/api/now/table/sys_hub_action_instance', pfInstData);
|
|
1653
|
+
actionsCreated++;
|
|
1654
|
+
} catch (_) { /* best-effort */ }
|
|
1655
|
+
}
|
|
1656
|
+
if (isSubflow) {
|
|
1657
|
+
for (var pfvi = 0; pfvi < inputsArg.length; pfvi++) {
|
|
1658
|
+
try {
|
|
1659
|
+
var pfInp = inputsArg[pfvi];
|
|
1660
|
+
await client.post('/api/now/table/sys_hub_flow_variable', {
|
|
1661
|
+
flow: flowSysId, name: pfInp.name, label: pfInp.label || pfInp.name,
|
|
1662
|
+
type: pfInp.type || 'string', mandatory: pfInp.mandatory || false,
|
|
1663
|
+
default_value: pfInp.default_value || '', variable_type: 'input'
|
|
1664
|
+
});
|
|
1665
|
+
varsCreated++;
|
|
1666
|
+
} catch (_) {}
|
|
1536
1667
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1668
|
+
for (var pfvo = 0; pfvo < outputsArg.length; pfvo++) {
|
|
1669
|
+
try {
|
|
1670
|
+
var pfOut = outputsArg[pfvo];
|
|
1671
|
+
await client.post('/api/now/table/sys_hub_flow_variable', {
|
|
1672
|
+
flow: flowSysId, name: pfOut.name, label: pfOut.label || pfOut.name,
|
|
1673
|
+
type: pfOut.type || 'string', variable_type: 'output'
|
|
1674
|
+
});
|
|
1675
|
+
varsCreated++;
|
|
1676
|
+
} catch (_) {}
|
|
1542
1677
|
}
|
|
1543
1678
|
}
|
|
1544
|
-
} catch (schedErr: any) {
|
|
1545
|
-
diagnostics.scheduled_job = { error: schedErr.message || 'unknown' };
|
|
1546
|
-
factoryWarnings.push('Scheduled job failed: ' + (schedErr.message || schedErr));
|
|
1547
1679
|
}
|
|
1548
1680
|
|
|
1549
|
-
// BR compile + engine REST registration skipped:
|
|
1550
|
-
// - BR: FlowDesigner unavailable in all contexts, FlowAPI.compile returns error,
|
|
1551
|
-
// all Script Includes (FlowDesignerScriptable etc.) don't exist on PDI instances
|
|
1552
|
-
// - REST: all sn_fd endpoints return 400 "Requested URI does not represent any resource"
|
|
1553
|
-
// Both add ~20s combined and provide zero value. Flow records are functional
|
|
1554
|
-
// (listable, queryable, deletable) but cannot be opened in Flow Designer UI.
|
|
1555
|
-
|
|
1556
1681
|
// ── Table API fallback (last resort) ─────────────────────────
|
|
1557
1682
|
if (!flowSysId) {
|
|
1558
1683
|
diagnostics.table_api_used = true;
|
|
@@ -1815,11 +1940,9 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
1815
1940
|
}
|
|
1816
1941
|
|
|
1817
1942
|
// ── Build summary ───────────────────────────────────────────
|
|
1818
|
-
var methodLabel = usedMethod
|
|
1819
|
-
? '
|
|
1820
|
-
:
|
|
1821
|
-
? 'Scripted REST API (GlideRecord)'
|
|
1822
|
-
: 'Table API' + (factoryWarnings.length > 0 ? ' (fallback)' : '');
|
|
1943
|
+
var methodLabel = usedMethod === 'processflow_api'
|
|
1944
|
+
? 'ProcessFlow API (Flow Designer engine)'
|
|
1945
|
+
: 'Table API' + (factoryWarnings.length > 0 ? ' (fallback)' : '');
|
|
1823
1946
|
|
|
1824
1947
|
var createSummary = summary()
|
|
1825
1948
|
.success('Created ' + (isSubflow ? 'subflow' : 'flow') + ': ' + flowName)
|
|
@@ -1855,17 +1978,13 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
1855
1978
|
|
|
1856
1979
|
// Diagnostics section
|
|
1857
1980
|
createSummary.blank().line('Diagnostics:');
|
|
1858
|
-
if (diagnostics.
|
|
1859
|
-
var
|
|
1860
|
-
createSummary.indented('
|
|
1861
|
-
if (
|
|
1981
|
+
if (diagnostics.processflow_api) {
|
|
1982
|
+
var pf = diagnostics.processflow_api;
|
|
1983
|
+
createSummary.indented('ProcessFlow API: ' + (pf.success ? 'success' : 'failed') + (pf.versionCreated ? ' (version created)' : ''));
|
|
1984
|
+
if (pf.error) createSummary.indented(' Error: ' + pf.error);
|
|
1862
1985
|
}
|
|
1863
|
-
createSummary.indented('Table API used: ' + diagnostics.table_api_used);
|
|
1986
|
+
createSummary.indented('Table API fallback used: ' + diagnostics.table_api_used);
|
|
1864
1987
|
createSummary.indented('Version created: ' + diagnostics.version_created + (diagnostics.version_method ? ' (' + diagnostics.version_method + ')' : ''));
|
|
1865
|
-
createSummary.indented('Engine compile: skipped (not available on this instance — FlowDesigner unavailable, REST 400s, no Script Includes)');
|
|
1866
|
-
if (diagnostics.latest_version_auto_set !== undefined) {
|
|
1867
|
-
createSummary.indented('latest_version: ' + (diagnostics.latest_version_auto_set ? 'set' : 'null'));
|
|
1868
|
-
}
|
|
1869
1988
|
if (diagnostics.post_verify) {
|
|
1870
1989
|
if (diagnostics.post_verify.error) {
|
|
1871
1990
|
createSummary.indented('Post-verify: error — ' + diagnostics.post_verify.error);
|
|
@@ -2251,5 +2370,5 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
2251
2370
|
}
|
|
2252
2371
|
}
|
|
2253
2372
|
|
|
2254
|
-
export const version = '
|
|
2373
|
+
export const version = '6.0.0';
|
|
2255
2374
|
export const author = 'Snow-Flow Team';
|
|
@@ -1,3 +1 @@
|
|
|
1
|
-
export { toolDefinition as
|
|
2
|
-
export { toolDefinition as snow_activate_plugin_def, execute as snow_activate_plugin_exec } from './snow_activate_plugin.js';
|
|
3
|
-
export { toolDefinition as snow_list_plugins_def, execute as snow_list_plugins_exec } from './snow_list_plugins.js';
|
|
1
|
+
export { toolDefinition as snow_plugin_manage_def, execute as snow_plugin_manage_exec } from './snow_plugin_manage.js'
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* snow_plugin_manage - Unified Plugin Management
|
|
3
|
+
*
|
|
4
|
+
* Single tool for all ServiceNow plugin operations:
|
|
5
|
+
* list, check, activate, deactivate.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { MCPToolDefinition, ServiceNowContext, ToolResult } from '../../shared/types.js'
|
|
9
|
+
import { getAuthenticatedClient } from '../../shared/auth.js'
|
|
10
|
+
import { createSuccessResult, createErrorResult } from '../../shared/error-handler.js'
|
|
11
|
+
|
|
12
|
+
export const toolDefinition: MCPToolDefinition = {
|
|
13
|
+
name: 'snow_plugin_manage',
|
|
14
|
+
description: `Manage ServiceNow plugins: list, check status, activate, or deactivate.
|
|
15
|
+
|
|
16
|
+
Actions:
|
|
17
|
+
• list — Search and list plugins. Filter by name/ID, show only active.
|
|
18
|
+
• check — Get detailed info on a specific plugin (status, version, dependencies).
|
|
19
|
+
• activate — Activate a plugin (admin only). Uses CICD API with table fallback.
|
|
20
|
+
• deactivate — Deactivate a plugin (admin only). Uses CICD API with table fallback.
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
• { action: "list", search: "incident" }
|
|
24
|
+
• { action: "check", plugin_id: "com.snc.incident" }
|
|
25
|
+
• { action: "activate", plugin_id: "com.snc.incident" }`,
|
|
26
|
+
category: 'advanced',
|
|
27
|
+
subcategory: 'administration',
|
|
28
|
+
use_cases: ['plugin-discovery', 'plugin-management', 'plugin-status', 'activation', 'deactivation'],
|
|
29
|
+
complexity: 'intermediate',
|
|
30
|
+
frequency: 'low',
|
|
31
|
+
permission: 'write',
|
|
32
|
+
allowedRoles: ['developer', 'admin'],
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
action: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Operation to perform',
|
|
39
|
+
enum: ['list', 'check', 'activate', 'deactivate'],
|
|
40
|
+
},
|
|
41
|
+
plugin_id: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: '[check/activate/deactivate] Plugin identifier (e.g. "com.snc.incident") or sys_id',
|
|
44
|
+
},
|
|
45
|
+
search: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: '[list] Filter by plugin name or ID (e.g. "incident", "com.snc")',
|
|
48
|
+
},
|
|
49
|
+
active_only: {
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
description: '[list] Only show active plugins',
|
|
52
|
+
default: false,
|
|
53
|
+
},
|
|
54
|
+
limit: {
|
|
55
|
+
type: 'number',
|
|
56
|
+
description: '[list] Max results to return (default 50)',
|
|
57
|
+
default: 50,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
required: ['action'],
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function execute(args: any, context: ServiceNowContext): Promise<ToolResult> {
|
|
65
|
+
const { action } = args
|
|
66
|
+
try {
|
|
67
|
+
switch (action) {
|
|
68
|
+
case 'list':
|
|
69
|
+
return await executeList(args, context)
|
|
70
|
+
case 'check':
|
|
71
|
+
return await executeCheck(args, context)
|
|
72
|
+
case 'activate':
|
|
73
|
+
return await executeActivate(args, context)
|
|
74
|
+
case 'deactivate':
|
|
75
|
+
return await executeDeactivate(args, context)
|
|
76
|
+
default:
|
|
77
|
+
return createErrorResult(
|
|
78
|
+
'Unknown action: ' + action + '. Valid actions: list, check, activate, deactivate'
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
} catch (error: any) {
|
|
82
|
+
return createErrorResult(error.message)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ==================== LIST ====================
|
|
87
|
+
async function executeList(args: any, context: ServiceNowContext): Promise<ToolResult> {
|
|
88
|
+
const { search, active_only = false, limit = 50 } = args
|
|
89
|
+
const client = await getAuthenticatedClient(context)
|
|
90
|
+
|
|
91
|
+
const queryParts: string[] = []
|
|
92
|
+
if (search) {
|
|
93
|
+
queryParts.push('nameLIKE' + search + '^ORidLIKE' + search)
|
|
94
|
+
}
|
|
95
|
+
if (active_only) {
|
|
96
|
+
queryParts.push('active=true')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const response = await client.get('/api/now/table/v_plugin', {
|
|
100
|
+
params: {
|
|
101
|
+
sysparm_query: queryParts.join('^'),
|
|
102
|
+
sysparm_fields: 'sys_id,id,name,active,version,description',
|
|
103
|
+
sysparm_limit: limit,
|
|
104
|
+
sysparm_display_value: 'true',
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const plugins = response.data.result || []
|
|
109
|
+
const activeCount = plugins.filter((p: any) => p.active === 'true' || p.active === true).length
|
|
110
|
+
const inactiveCount = plugins.length - activeCount
|
|
111
|
+
|
|
112
|
+
const summary =
|
|
113
|
+
'Found ' +
|
|
114
|
+
plugins.length +
|
|
115
|
+
' plugin(s)' +
|
|
116
|
+
(search ? ' matching "' + search + '"' : '') +
|
|
117
|
+
'. Active: ' +
|
|
118
|
+
activeCount +
|
|
119
|
+
', Inactive: ' +
|
|
120
|
+
inactiveCount +
|
|
121
|
+
'.'
|
|
122
|
+
|
|
123
|
+
return createSuccessResult(
|
|
124
|
+
{ action: 'list', plugins, count: plugins.length },
|
|
125
|
+
{ active_count: activeCount, inactive_count: inactiveCount },
|
|
126
|
+
summary
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ==================== CHECK ====================
|
|
131
|
+
async function executeCheck(args: any, context: ServiceNowContext): Promise<ToolResult> {
|
|
132
|
+
const { plugin_id } = args
|
|
133
|
+
if (!plugin_id) return createErrorResult('plugin_id is required for check action')
|
|
134
|
+
|
|
135
|
+
const client = await getAuthenticatedClient(context)
|
|
136
|
+
|
|
137
|
+
const response = await client.get('/api/now/table/v_plugin', {
|
|
138
|
+
params: {
|
|
139
|
+
sysparm_query: 'id=' + plugin_id + '^ORsys_id=' + plugin_id,
|
|
140
|
+
sysparm_fields: 'sys_id,id,name,active,version,description,parent,optional,licensable',
|
|
141
|
+
sysparm_limit: 1,
|
|
142
|
+
sysparm_display_value: 'true',
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const results = response.data.result || []
|
|
147
|
+
if (results.length === 0) {
|
|
148
|
+
return createErrorResult('Plugin not found: ' + plugin_id)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const plugin = results[0]
|
|
152
|
+
const active = plugin.active === 'true' || plugin.active === true
|
|
153
|
+
const summary =
|
|
154
|
+
'Plugin "' +
|
|
155
|
+
plugin.name +
|
|
156
|
+
'" (' +
|
|
157
|
+
plugin.id +
|
|
158
|
+
') is ' +
|
|
159
|
+
(active ? 'ACTIVE' : 'INACTIVE') +
|
|
160
|
+
'. Version: ' +
|
|
161
|
+
(plugin.version || 'unknown') +
|
|
162
|
+
'.'
|
|
163
|
+
|
|
164
|
+
return createSuccessResult(
|
|
165
|
+
{ action: 'check', plugin },
|
|
166
|
+
{ status: active ? 'active' : 'inactive' },
|
|
167
|
+
summary
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ==================== ACTIVATE ====================
|
|
172
|
+
async function executeActivate(args: any, context: ServiceNowContext): Promise<ToolResult> {
|
|
173
|
+
const { plugin_id } = args
|
|
174
|
+
if (!plugin_id) return createErrorResult('plugin_id is required for activate action')
|
|
175
|
+
|
|
176
|
+
const client = await getAuthenticatedClient(context)
|
|
177
|
+
const plugin = await lookupPlugin(client, plugin_id)
|
|
178
|
+
if (!plugin) return createErrorResult('Plugin not found: ' + plugin_id)
|
|
179
|
+
|
|
180
|
+
if (plugin.active === 'true' || plugin.active === true) {
|
|
181
|
+
return createSuccessResult(
|
|
182
|
+
{ action: 'activate', plugin, activated: false, already_active: true },
|
|
183
|
+
{},
|
|
184
|
+
'Plugin "' + plugin.name + '" (' + plugin.id + ') is already active.'
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { success, method, error } = await togglePlugin(client, plugin, 'activate')
|
|
189
|
+
if (!success) return createErrorResult(error!)
|
|
190
|
+
|
|
191
|
+
const verified = await verifyPluginState(client, plugin.sys_id, true)
|
|
192
|
+
const summary = verified
|
|
193
|
+
? 'Plugin "' + plugin.name + '" (' + plugin.id + ') activated successfully via ' + method + '.'
|
|
194
|
+
: 'Activation request sent for "' + plugin.name + '" via ' + method + '. May take a moment to fully activate.'
|
|
195
|
+
|
|
196
|
+
return createSuccessResult({ action: 'activate', plugin, activated: true, verified, method }, {}, summary)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ==================== DEACTIVATE ====================
|
|
200
|
+
async function executeDeactivate(args: any, context: ServiceNowContext): Promise<ToolResult> {
|
|
201
|
+
const { plugin_id } = args
|
|
202
|
+
if (!plugin_id) return createErrorResult('plugin_id is required for deactivate action')
|
|
203
|
+
|
|
204
|
+
const client = await getAuthenticatedClient(context)
|
|
205
|
+
const plugin = await lookupPlugin(client, plugin_id)
|
|
206
|
+
if (!plugin) return createErrorResult('Plugin not found: ' + plugin_id)
|
|
207
|
+
|
|
208
|
+
if (plugin.active !== 'true' && plugin.active !== true) {
|
|
209
|
+
return createSuccessResult(
|
|
210
|
+
{ action: 'deactivate', plugin, deactivated: false, already_inactive: true },
|
|
211
|
+
{},
|
|
212
|
+
'Plugin "' + plugin.name + '" (' + plugin.id + ') is already inactive.'
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const { success, method, error } = await togglePlugin(client, plugin, 'deactivate')
|
|
217
|
+
if (!success) return createErrorResult(error!)
|
|
218
|
+
|
|
219
|
+
const verified = await verifyPluginState(client, plugin.sys_id, false)
|
|
220
|
+
const summary = verified
|
|
221
|
+
? 'Plugin "' + plugin.name + '" (' + plugin.id + ') deactivated successfully via ' + method + '.'
|
|
222
|
+
: 'Deactivation request sent for "' + plugin.name + '" via ' + method + '. May take a moment to fully deactivate.'
|
|
223
|
+
|
|
224
|
+
return createSuccessResult({ action: 'deactivate', plugin, deactivated: true, verified, method }, {}, summary)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ==================== HELPERS ====================
|
|
228
|
+
|
|
229
|
+
async function lookupPlugin(client: any, pluginId: string): Promise<any | null> {
|
|
230
|
+
const response = await client.get('/api/now/table/v_plugin', {
|
|
231
|
+
params: {
|
|
232
|
+
sysparm_query: 'id=' + pluginId + '^ORsys_id=' + pluginId,
|
|
233
|
+
sysparm_fields: 'sys_id,id,name,active',
|
|
234
|
+
sysparm_limit: 1,
|
|
235
|
+
},
|
|
236
|
+
})
|
|
237
|
+
const results = response.data.result || []
|
|
238
|
+
return results.length > 0 ? results[0] : null
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function togglePlugin(
|
|
242
|
+
client: any,
|
|
243
|
+
plugin: any,
|
|
244
|
+
operation: 'activate' | 'deactivate'
|
|
245
|
+
): Promise<{ success: boolean; method: string; error?: string }> {
|
|
246
|
+
// Try CICD Plugin API first
|
|
247
|
+
try {
|
|
248
|
+
await client.post('/api/sn_cicd/plugin/' + encodeURIComponent(plugin.id) + '/' + operation)
|
|
249
|
+
return { success: true, method: 'cicd_api' }
|
|
250
|
+
} catch (cicdError: any) {
|
|
251
|
+
const status = cicdError.response ? cicdError.response.status : 0
|
|
252
|
+
if (status === 404 || status === 400) {
|
|
253
|
+
// Fallback to sys_plugins PATCH
|
|
254
|
+
try {
|
|
255
|
+
const activeValue = operation === 'activate' ? 'true' : 'false'
|
|
256
|
+
await client.patch('/api/now/table/sys_plugins/' + plugin.sys_id, { active: activeValue })
|
|
257
|
+
return { success: true, method: 'table_api' }
|
|
258
|
+
} catch (patchError: any) {
|
|
259
|
+
return {
|
|
260
|
+
success: false,
|
|
261
|
+
method: '',
|
|
262
|
+
error:
|
|
263
|
+
'Failed to ' +
|
|
264
|
+
operation +
|
|
265
|
+
' plugin via both CICD API and Table API. CICD: ' +
|
|
266
|
+
(cicdError.message || 'unknown') +
|
|
267
|
+
'. Table API: ' +
|
|
268
|
+
(patchError.message || 'unknown'),
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { success: false, method: '', error: 'CICD Plugin API error: ' + (cicdError.message || 'unknown') }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function verifyPluginState(client: any, sysId: string, expectActive: boolean): Promise<boolean> {
|
|
277
|
+
try {
|
|
278
|
+
const response = await client.get('/api/now/table/v_plugin', {
|
|
279
|
+
params: {
|
|
280
|
+
sysparm_query: 'sys_id=' + sysId,
|
|
281
|
+
sysparm_fields: 'active',
|
|
282
|
+
sysparm_limit: 1,
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
const results = response.data.result || []
|
|
286
|
+
if (results.length === 0) return false
|
|
287
|
+
const isActive = results[0].active === 'true' || results[0].active === true
|
|
288
|
+
return expectActive ? isActive : !isActive
|
|
289
|
+
} catch {
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export const version = '1.0.0'
|
|
295
|
+
export const author = 'Snow-Flow'
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* snow_activate_plugin
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { MCPToolDefinition, ServiceNowContext, ToolResult } from '../../shared/types.js';
|
|
6
|
-
import { getAuthenticatedClient } from '../../shared/auth.js';
|
|
7
|
-
import { createSuccessResult, createErrorResult } from '../../shared/error-handler.js';
|
|
8
|
-
|
|
9
|
-
export const toolDefinition: MCPToolDefinition = {
|
|
10
|
-
name: 'snow_activate_plugin',
|
|
11
|
-
description: 'Activate ServiceNow plugin',
|
|
12
|
-
// Metadata for tool discovery (not sent to LLM)
|
|
13
|
-
category: 'advanced',
|
|
14
|
-
subcategory: 'administration',
|
|
15
|
-
use_cases: ['plugin-management', 'activation', 'configuration'],
|
|
16
|
-
complexity: 'intermediate',
|
|
17
|
-
frequency: 'low',
|
|
18
|
-
|
|
19
|
-
// Permission enforcement
|
|
20
|
-
// Classification: WRITE - Write operation based on name pattern
|
|
21
|
-
permission: 'write',
|
|
22
|
-
allowedRoles: ['developer', 'admin'],
|
|
23
|
-
inputSchema: {
|
|
24
|
-
type: 'object',
|
|
25
|
-
properties: {
|
|
26
|
-
plugin_id: { type: 'string', description: 'Plugin ID' }
|
|
27
|
-
},
|
|
28
|
-
required: ['plugin_id']
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export async function execute(args: any, context: ServiceNowContext): Promise<ToolResult> {
|
|
33
|
-
const { plugin_id } = args;
|
|
34
|
-
try {
|
|
35
|
-
const client = await getAuthenticatedClient(context);
|
|
36
|
-
const response = await client.post('/api/now/table/v_plugin', {
|
|
37
|
-
id: plugin_id,
|
|
38
|
-
active: true
|
|
39
|
-
});
|
|
40
|
-
return createSuccessResult({ activated: true, plugin: response.data.result });
|
|
41
|
-
} catch (error: any) {
|
|
42
|
-
return createErrorResult(error.message);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const version = '1.0.0';
|
|
47
|
-
export const author = 'Snow-Flow SDK Migration';
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* snow_custom_plugin
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { MCPToolDefinition, ServiceNowContext, ToolResult } from '../../shared/types.js';
|
|
6
|
-
import { createSuccessResult, createErrorResult } from '../../shared/error-handler.js';
|
|
7
|
-
|
|
8
|
-
export const toolDefinition: MCPToolDefinition = {
|
|
9
|
-
name: 'snow_custom_plugin',
|
|
10
|
-
description: 'Execute custom plugin logic',
|
|
11
|
-
// Metadata for tool discovery (not sent to LLM)
|
|
12
|
-
category: 'advanced',
|
|
13
|
-
subcategory: 'administration',
|
|
14
|
-
use_cases: ['plugin-customization', 'extensions', 'custom-logic'],
|
|
15
|
-
complexity: 'advanced',
|
|
16
|
-
frequency: 'low',
|
|
17
|
-
|
|
18
|
-
// Permission enforcement
|
|
19
|
-
// Classification: WRITE - Plugin function - executes custom plugin which can modify data
|
|
20
|
-
permission: 'write',
|
|
21
|
-
allowedRoles: ['developer', 'admin'],
|
|
22
|
-
inputSchema: {
|
|
23
|
-
type: 'object',
|
|
24
|
-
properties: {
|
|
25
|
-
plugin_name: { type: 'string', description: 'Plugin name' },
|
|
26
|
-
parameters: { type: 'object', description: 'Plugin parameters' }
|
|
27
|
-
},
|
|
28
|
-
required: ['plugin_name']
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export async function execute(args: any, context: ServiceNowContext): Promise<ToolResult> {
|
|
33
|
-
const { plugin_name, parameters = {} } = args;
|
|
34
|
-
try {
|
|
35
|
-
return createSuccessResult({
|
|
36
|
-
executed: true,
|
|
37
|
-
plugin_name,
|
|
38
|
-
parameters
|
|
39
|
-
});
|
|
40
|
-
} catch (error: any) {
|
|
41
|
-
return createErrorResult(error.message);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const version = '1.0.0';
|
|
46
|
-
export const author = 'Snow-Flow SDK Migration';
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* snow_list_plugins
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { MCPToolDefinition, ServiceNowContext, ToolResult } from '../../shared/types.js';
|
|
6
|
-
import { getAuthenticatedClient } from '../../shared/auth.js';
|
|
7
|
-
import { createSuccessResult, createErrorResult } from '../../shared/error-handler.js';
|
|
8
|
-
|
|
9
|
-
export const toolDefinition: MCPToolDefinition = {
|
|
10
|
-
name: 'snow_list_plugins',
|
|
11
|
-
description: 'List installed plugins',
|
|
12
|
-
// Metadata for tool discovery (not sent to LLM)
|
|
13
|
-
category: 'advanced',
|
|
14
|
-
subcategory: 'administration',
|
|
15
|
-
use_cases: ['plugin-discovery', 'plugin-management', 'administration'],
|
|
16
|
-
complexity: 'beginner',
|
|
17
|
-
frequency: 'low',
|
|
18
|
-
|
|
19
|
-
// Permission enforcement
|
|
20
|
-
// Classification: READ - Read-only operation based on name pattern
|
|
21
|
-
permission: 'read',
|
|
22
|
-
allowedRoles: ['developer', 'stakeholder', 'admin'],
|
|
23
|
-
inputSchema: {
|
|
24
|
-
type: 'object',
|
|
25
|
-
properties: {
|
|
26
|
-
active_only: { type: 'boolean', default: false }
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export async function execute(args: any, context: ServiceNowContext): Promise<ToolResult> {
|
|
32
|
-
const { active_only = false } = args;
|
|
33
|
-
try {
|
|
34
|
-
const client = await getAuthenticatedClient(context);
|
|
35
|
-
const query = active_only ? 'active=true' : '';
|
|
36
|
-
const response = await client.get('/api/now/table/v_plugin', {
|
|
37
|
-
params: {
|
|
38
|
-
sysparm_query: query,
|
|
39
|
-
sysparm_limit: 100
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
return createSuccessResult({
|
|
43
|
-
plugins: response.data.result,
|
|
44
|
-
count: response.data.result.length
|
|
45
|
-
});
|
|
46
|
-
} catch (error: any) {
|
|
47
|
-
return createErrorResult(error.message);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export const version = '1.0.0';
|
|
52
|
-
export const author = 'Snow-Flow SDK Migration';
|